From 92c13fa46af1c3a51803c8064b1e2b90462a2399 Mon Sep 17 00:00:00 2001 From: Andrew Smith Date: Tue, 20 Aug 2024 23:17:45 +0000 Subject: [PATCH 1/2] feat: add third-party auth support --- supabase/_async/client.py | 36 ++++++++++++++++++++-------------- supabase/_sync/client.py | 36 ++++++++++++++++++++-------------- supabase/lib/client_options.py | 4 ++++ supabase/utils.py | 16 +++++++++++++++ 4 files changed, 62 insertions(+), 30 deletions(-) create mode 100644 supabase/utils.py diff --git a/supabase/_async/client.py b/supabase/_async/client.py index ad21c7cd..e2fec5b5 100644 --- a/supabase/_async/client.py +++ b/supabase/_async/client.py @@ -16,17 +16,11 @@ from storage3.constants import DEFAULT_TIMEOUT as DEFAULT_STORAGE_CLIENT_TIMEOUT from supafunc import AsyncFunctionsClient -from ..lib.client_options import AsyncClientOptions as ClientOptions +from ..lib.client_options import ClientOptions +from ..utils import AuthProxy, SupabaseException from .auth_client import AsyncSupabaseAuthClient -# Create an exception class when user does not provide a valid url or key. -class SupabaseException(Exception): - def __init__(self, message: str): - self.message = message - super().__init__(self.message) - - class AsyncClient: """Supabase client class.""" @@ -78,10 +72,15 @@ def __init__( self.functions_url = f"{supabase_url}/functions/v1" # Instantiate clients. - self.auth = self._init_supabase_auth_client( - auth_url=self.auth_url, - client_options=options, - ) + if not options.access_token: + self.auth = self._init_supabase_auth_client( + auth_url=self.auth_url, + client_options=options, + ) + else: + self.access_token = options.access_token + self.auth = AuthProxy() + self.realtime = self._init_realtime_client( realtime_url=self.realtime_url, supabase_key=self.supabase_key, @@ -90,7 +89,9 @@ def __init__( self._postgrest = None self._storage = None self._functions = None - self.auth.on_auth_state_change(self._listen_to_auth_events) + + if not options.access_token: + self.auth.on_auth_state_change(self._listen_to_auth_events) @classmethod async def create( @@ -104,8 +105,13 @@ async def create( if auth_header is None: try: - session = await client.auth.get_session() - session_access_token = client._create_auth_header(session.access_token) + if not options.access_token: + session = await client.auth.get_session() + session_access_token = client._create_auth_header( + session.access_token + ) + else: + session_access_token = options.access_token except Exception as err: session_access_token = None diff --git a/supabase/_sync/client.py b/supabase/_sync/client.py index 680a7aea..c85e0fbb 100644 --- a/supabase/_sync/client.py +++ b/supabase/_sync/client.py @@ -15,17 +15,11 @@ from storage3.constants import DEFAULT_TIMEOUT as DEFAULT_STORAGE_CLIENT_TIMEOUT from supafunc import SyncFunctionsClient -from ..lib.client_options import SyncClientOptions as ClientOptions +from ..lib.client_options import ClientOptions +from ..utils import AuthProxy, SupabaseException from .auth_client import SyncSupabaseAuthClient -# Create an exception class when user does not provide a valid url or key. -class SupabaseException(Exception): - def __init__(self, message: str): - self.message = message - super().__init__(self.message) - - class SyncClient: """Supabase client class.""" @@ -77,10 +71,15 @@ def __init__( self.functions_url = f"{supabase_url}/functions/v1" # Instantiate clients. - self.auth = self._init_supabase_auth_client( - auth_url=self.auth_url, - client_options=options, - ) + if not options.access_token: + self.auth = self._init_supabase_auth_client( + auth_url=self.auth_url, + client_options=options, + ) + else: + self.access_token = options.access_token + self.auth = AuthProxy() + self.realtime = self._init_realtime_client( realtime_url=self.realtime_url, supabase_key=self.supabase_key, @@ -89,7 +88,9 @@ def __init__( self._postgrest = None self._storage = None self._functions = None - self.auth.on_auth_state_change(self._listen_to_auth_events) + + if not options.access_token: + self.auth.on_auth_state_change(self._listen_to_auth_events) @classmethod def create( @@ -103,8 +104,13 @@ def create( if auth_header is None: try: - session = client.auth.get_session() - session_access_token = client._create_auth_header(session.access_token) + if not options.access_token: + session = client.auth.get_session() + session_access_token = client._create_auth_header( + session.access_token + ) + else: + session_access_token = options.access_token except Exception as err: session_access_token = None diff --git a/supabase/lib/client_options.py b/supabase/lib/client_options.py index 47498c13..4d0429d9 100644 --- a/supabase/lib/client_options.py +++ b/supabase/lib/client_options.py @@ -59,6 +59,8 @@ class ClientOptions: flow_type: AuthFlowType = "pkce" """flow type to use for authentication""" + access_token: Union[str, None] = None + def replace( self, schema: Optional[str] = None, @@ -74,6 +76,7 @@ def replace( int, float, Timeout ] = DEFAULT_STORAGE_CLIENT_TIMEOUT, flow_type: Optional[AuthFlowType] = None, + access_token: Union[str, None] = None, ) -> "ClientOptions": """Create a new SupabaseClientOptions with changes""" client_options = ClientOptions() @@ -92,6 +95,7 @@ def replace( storage_client_timeout or self.storage_client_timeout ) client_options.flow_type = flow_type or self.flow_type + client_options.access_token = access_token or self.access_token return client_options diff --git a/supabase/utils.py b/supabase/utils.py new file mode 100644 index 00000000..7d8d4dd2 --- /dev/null +++ b/supabase/utils.py @@ -0,0 +1,16 @@ +from gotrue.errors import AuthError + + +# Create an exception class when user does not provide a valid url or key. +class SupabaseException(Exception): + def __init__(self, message: str): + self.message = message + super().__init__(self.message) + + +class AuthProxy: + def __getattr__(self, attr): + raise AuthError( + f"Supabase Client is configured with the access_token option, accessing supabase.auth.{attr} is not possible.", + None, + ) From fdec57ef3071122c411ad80d27491a1aa110c483 Mon Sep 17 00:00:00 2001 From: Guilherme Souza Date: Fri, 28 Feb 2025 13:01:15 -0300 Subject: [PATCH 2/2] update access_token to be a Callable and add tests --- supabase/lib/client_options.py | 19 ++++++++++++++++--- supabase/utils.py | 6 +----- tests/test_client.py | 18 ++++++++++++++++++ 3 files changed, 35 insertions(+), 8 deletions(-) diff --git a/supabase/lib/client_options.py b/supabase/lib/client_options.py index 4d0429d9..31ea7c36 100644 --- a/supabase/lib/client_options.py +++ b/supabase/lib/client_options.py @@ -1,5 +1,5 @@ from dataclasses import dataclass, field -from typing import Dict, Optional, Union +from typing import Awaitable, Callable, Dict, Optional, Union from gotrue import ( AsyncMemoryStorage, @@ -59,7 +59,14 @@ class ClientOptions: flow_type: AuthFlowType = "pkce" """flow type to use for authentication""" - access_token: Union[str, None] = None + access_token: Union[Callable[[], str], None] = None + """Optional function for using a third-party authentication system with Supabase. + The function should return an access token or ID token (JWT) by obtaining it from the third-party auth client library. + Note that this funciton may be called concurrently and many times. + Use memoization and locking techniques if this is not supported by the clinet libraries. + + When set, the `auth` namespace of the Supabase client cannot be used. + Create another client if you wish to use Supabase Auth and third-party authentications concurrently in the same application.""" def replace( self, @@ -76,7 +83,7 @@ def replace( int, float, Timeout ] = DEFAULT_STORAGE_CLIENT_TIMEOUT, flow_type: Optional[AuthFlowType] = None, - access_token: Union[str, None] = None, + access_token: Union[Callable[[], str], None] = None, ) -> "ClientOptions": """Create a new SupabaseClientOptions with changes""" client_options = ClientOptions() @@ -104,6 +111,8 @@ class AsyncClientOptions(ClientOptions): storage: AsyncSupportedStorage = field(default_factory=AsyncMemoryStorage) """A storage provider. Used to store the logged in session.""" + access_token: Union[Callable[[], Awaitable[str]], None] = None + def replace( self, schema: Optional[str] = None, @@ -119,6 +128,7 @@ def replace( int, float, Timeout ] = DEFAULT_STORAGE_CLIENT_TIMEOUT, flow_type: Optional[AuthFlowType] = None, + access_token: Union[Callable[[], Awaitable[str]], None] = None, ) -> "AsyncClientOptions": """Create a new SupabaseClientOptions with changes""" client_options = AsyncClientOptions() @@ -137,6 +147,7 @@ def replace( storage_client_timeout or self.storage_client_timeout ) client_options.flow_type = flow_type or self.flow_type + client_options.access_token = access_token or self.access_token return client_options @@ -157,6 +168,7 @@ def replace( int, float, Timeout ] = DEFAULT_STORAGE_CLIENT_TIMEOUT, flow_type: Optional[AuthFlowType] = None, + access_token: Union[Callable[[], Awaitable[str]], None] = None, ) -> "SyncClientOptions": """Create a new SupabaseClientOptions with changes""" client_options = SyncClientOptions() @@ -175,4 +187,5 @@ def replace( storage_client_timeout or self.storage_client_timeout ) client_options.flow_type = flow_type or self.flow_type + client_options.access_token = access_token or self.access_token return client_options diff --git a/supabase/utils.py b/supabase/utils.py index 7d8d4dd2..70410848 100644 --- a/supabase/utils.py +++ b/supabase/utils.py @@ -1,6 +1,3 @@ -from gotrue.errors import AuthError - - # Create an exception class when user does not provide a valid url or key. class SupabaseException(Exception): def __init__(self, message: str): @@ -10,7 +7,6 @@ def __init__(self, message: str): class AuthProxy: def __getattr__(self, attr): - raise AuthError( + raise SupabaseException( f"Supabase Client is configured with the access_token option, accessing supabase.auth.{attr} is not possible.", - None, ) diff --git a/tests/test_client.py b/tests/test_client.py index d8f0be0c..93ca51f0 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -119,3 +119,21 @@ def test_updates_the_authorization_header_on_auth_events() -> None: assert client.storage.session.headers.get("apiKey") == key assert client.storage.session.headers.get("Authorization") == updated_authorization + + +def test_init_client_with_access_token() -> None: + url = os.environ.get("SUPABASE_TEST_URL") + key = os.environ.get("SUPABASE_TEST_KEY") + + client = create_client( + url, key, options=ClientOptions(access_token=lambda: "secretuserjwt") + ) + + assert client.access_token is not None + + with pytest.raises(SupabaseException) as e: + client.auth.get_user() + assert ( + str(e.value.message) + == "Supabase Client is configured with the access_token option, accessing supabase.auth.get_user is not possible." + )