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..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,6 +59,15 @@ class ClientOptions: flow_type: AuthFlowType = "pkce" """flow type to use for authentication""" + 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, schema: Optional[str] = None, @@ -74,6 +83,7 @@ def replace( int, float, Timeout ] = DEFAULT_STORAGE_CLIENT_TIMEOUT, flow_type: Optional[AuthFlowType] = None, + access_token: Union[Callable[[], str], None] = None, ) -> "ClientOptions": """Create a new SupabaseClientOptions with changes""" client_options = ClientOptions() @@ -92,6 +102,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 @@ -100,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, @@ -115,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() @@ -133,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 @@ -153,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() @@ -171,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 new file mode 100644 index 00000000..70410848 --- /dev/null +++ b/supabase/utils.py @@ -0,0 +1,12 @@ +# 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 SupabaseException( + f"Supabase Client is configured with the access_token option, accessing supabase.auth.{attr} is not possible.", + ) 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." + )