supabase-py icon indicating copy to clipboard operation
supabase-py copied to clipboard

Create a client with Auth context of a user

Open vlebert opened this issue 1 year ago • 6 comments

Hi everyone

Is your feature request related to a problem? Please describe.

I am trying to write python cloud function (instead of supabase edge function). I wan't to get caller's identity do proceed database read/write with his RLS context. In JS, this is possible as described in the documentation.

https://supabase.com/docs/guides/functions/auth

import { serve } from 'https://deno.land/[email protected]/http/server.ts'
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2'

serve(async (req: Request) => {
  try {
    // Create a Supabase client with the Auth context of the logged in user.
    const supabaseClient = createClient(
      // Supabase API URL - env var exported by default.
      Deno.env.get('SUPABASE_URL') ?? '',
      // Supabase API ANON KEY - env var exported by default.
      Deno.env.get('SUPABASE_ANON_KEY') ?? '',
      // Create client with Auth context of the user that called the function.
      // This way your row-level-security (RLS) policies are applied.
      { global: { headers: { Authorization: req.headers.get('Authorization')! } } }
    )

With Python client, I couldn't reproduce. I tried:

supa_client = create_client("https://****.supabase.co",
                            "***anon_api_key***",
                            ClientOptions().replace(headers={"authorization":"Bearer ***user_session_token***"
}))

I also tried

supa_client = create_client("https://****.supabase.co",
                            "***anon_api_key***",
}))
supa_client.auth.set_session("***user_session_token***","")

None of this works. After studying the code a bit, I think this may be the problem:

https://github.com/supabase-community/supabase-py/blob/2bba842449ccd0b5f933198c343f54c5a67db7ed/supabase/client.py#L61

https://github.com/supabase-community/supabase-py/blob/2bba842449ccd0b5f933198c343f54c5a67db7ed/supabase/client.py#L208

Authorization token is always overwritten with anon API KEY

options.headers.update(self._get_auth_headers())
def _get_auth_headers(self) -> Dict[str, str]:
        """Helper method to get auth headers."""
        # What's the corresponding method to get the token
        return {
            "apiKey": self.supabase_key,
            "Authorization": f"Bearer {self.supabase_key}",
        }

Describe the solution you'd like

It should be possible to reproduce JS behavior to create client with Auth context of the user that called the function (logged in user's JWT).

Am I missing something ?

vlebert avatar Apr 24 '23 15:04 vlebert

I also did the following tests

url = "*****"
anon_key="*****"
supa_client = create_client(url,
                            anon_key)
user = supa_client.auth.get_user()
HTTPStatusError                           Traceback (most recent call last)
File [~/Python/tableclone/.env/lib/python3.11/site-packages/gotrue/_sync/gotrue_base_api.py:113](https://file+.vscode-resource.vscode-cdn.net/Users/vlebert/Python/tableclone/~/Python/tableclone/.env/lib/python3.11/site-packages/gotrue/_sync/gotrue_base_api.py:113), in SyncGoTrueBaseAPI._request(self, method, path, jwt, redirect_to, headers, query, body, no_resolve_json, xform)
    106 response = self._http_client.request(
    107     method,
    108     url,
   (...)
    111     json=body.dict() if isinstance(body, BaseModel) else body,
    112 )
--> 113 response.raise_for_status()
    114 result = response if no_resolve_json else response.json()

File [~/Python/tableclone/.env/lib/python3.11/site-packages/httpx/_models.py:749](https://file+.vscode-resource.vscode-cdn.net/Users/vlebert/Python/tableclone/~/Python/tableclone/.env/lib/python3.11/site-packages/httpx/_models.py:749), in Response.raise_for_status(self)
    748 message = message.format(self, error_type=error_type)
--> 749 raise HTTPStatusError(message, request=request, response=self)

HTTPStatusError: Client error '401 Unauthorized' for url 'https://jcefjfikurjcjxsspstf.supabase.co/auth/v1/user'
For more information check: https://httpstatuses.com/401

During handling of the above exception, another exception occurred:

AuthApiError                              Traceback (most recent call last)
Cell In[96], line 1
----> 1 user = supa_client.auth.get_user()
...
    116         return xform(result)
    117 except Exception as e:
--> 118     raise handle_exception(e)

AuthApiError: invalid claim: missing sub claim

Then

url = "****"
anon_key="****"
token = "****"

client_options = ClientOptions()
client_options.headers.update(headers={
    "apikey":anon_key,
    "Authorization": f"Bearer {token}"
    })

supa_client = create_client(url,
                            anon_key)
supa_client.auth = supa_client._init_supabase_auth_client(url,client_options)

user = supa_client.auth.get_user()

AttributeError                            Traceback (most recent call last)
File [~/Python/tableclone/.env/lib/python3.11/site-packages/gotrue/_sync/gotrue_base_api.py:106](https://file+.vscode-resource.vscode-cdn.net/Users/vlebert/Python/tableclone/~/Python/tableclone/.env/lib/python3.11/site-packages/gotrue/_sync/gotrue_base_api.py:106), in SyncGoTrueBaseAPI._request(self, method, path, jwt, redirect_to, headers, query, body, no_resolve_json, xform)
    105 try:
--> 106     response = self._http_client.request(
    107         method,
    108         url,
    109         headers=headers,
    110         params=query,
    111         json=body.dict() if isinstance(body, BaseModel) else body,
    112     )
    113     response.raise_for_status()

File [~/Python/tableclone/.env/lib/python3.11/site-packages/httpx/_client.py:808](https://file+.vscode-resource.vscode-cdn.net/Users/vlebert/Python/tableclone/~/Python/tableclone/.env/lib/python3.11/site-packages/httpx/_client.py:808), in Client.request(self, method, url, content, data, files, json, params, headers, cookies, auth, follow_redirects, timeout, extensions)
    806     warnings.warn(message, DeprecationWarning)
--> 808 request = self.build_request(
    809     method=method,
    810     url=url,
    811     content=content,
    812     data=data,
    813     files=files,
    814     json=json,
    815     params=params,
    816     headers=headers,
    817     cookies=cookies,
...
    116         return xform(result)
    117 except Exception as e:
--> 118     raise handle_exception(e)

AuthRetryableError: 'dict' object has no attribute 'encode'

In both case, I can't use user's context for RLS when querying a table

vlebert avatar Apr 24 '23 16:04 vlebert

And finally

url = "*****"
anon_key="*****"
supa_client = create_client(url,
                            anon_key)
user = supa_client.auth.get_user(
data = supa_client.auth.sign_in_with_password({"email": "*****, "password": "*****"})

supa_client.auth.get_user() Works as expected.

But querying a table doesn't seem to use user's auth (0 response, must have been filtered by RLS)

vlebert avatar Apr 24 '23 16:04 vlebert

as far i can tell, this is a bug. The workaround is:

supabase: Client = create_client(url, key)
supabase.auth.sign_in_with_password({'email':user,'password':passw})
supabase.postgrest.auth(supabase.auth.get_session().access_token)

doing this RLS works for me.

I am really sad that this is not documented.

gabrielgranado avatar May 17 '23 14:05 gabrielgranado

I just ran into the same problem with storage. It looks like it all stems from the same issue that you pointed out: In client.py, the self._get_auth_headers() call overrides the headers dictionary passed in via the constructor:

  • https://github.com/supabase-community/supabase-py/blob/develop/supabase/client.py#L61
  • https://github.com/supabase-community/supabase-py/blob/develop/supabase/client.py#L96

My current workaround:

key = auth_header.split(" ")[-1]

    supabase = create_client(
        supabase_url,
        key,
    )

It's pretty fragile (since I'm assuming the shape of auth_header without validating it), but it makes _get_auth_headers() return the right Authorization header. So, it should work for all of the APIs without needing to know the user's creds.

_get_auth_headers() should probably just check for the Authorization header in client_options and return it if it's present.

mindblight avatar May 21 '23 18:05 mindblight

+1

ERogalla avatar Dec 17 '23 21:12 ERogalla

hi bro ,i also encountered it ,and find that the method set_session Work normally with access and refresh_token,refresh_token can not be omitted!

Atticuszz avatar Dec 18 '23 02:12 Atticuszz

This has been fixed in the latest 2.4.3 release of supabase-py. Please test and let me know in this issue if it's not working as expected. Thanks again for the PR @Garee

silentworks avatar Apr 17 '24 14:04 silentworks