diff --git a/packages/supabase/lib/src/auth_http_client.dart b/packages/supabase/lib/src/auth_http_client.dart index 6b7a554f..d0fec71b 100644 --- a/packages/supabase/lib/src/auth_http_client.dart +++ b/packages/supabase/lib/src/auth_http_client.dart @@ -1,32 +1,16 @@ import 'package:http/http.dart'; -import 'package:supabase/supabase.dart'; class AuthHttpClient extends BaseClient { final Client _inner; - final GoTrueClient _auth; - final String _supabaseKey; - AuthHttpClient(this._supabaseKey, this._inner, this._auth); + final String _supabaseKey; + final Future Function() _getAccessToken; + AuthHttpClient(this._supabaseKey, this._inner, this._getAccessToken); @override Future send(BaseRequest request) async { - if (_auth.currentSession?.isExpired ?? false) { - try { - await _auth.refreshSession(); - } catch (error) { - final expiresAt = _auth.currentSession?.expiresAt; - if (expiresAt != null) { - // Failed to refresh the token. - final isExpiredWithoutMargin = DateTime.now() - .isAfter(DateTime.fromMillisecondsSinceEpoch(expiresAt * 1000)); - if (isExpiredWithoutMargin) { - // Throw the error instead of making an API request with an expired token. - rethrow; - } - } - } - } - final authBearer = _auth.currentSession?.accessToken ?? _supabaseKey; + final accessToken = await _getAccessToken(); + final authBearer = accessToken ?? _supabaseKey; request.headers.putIfAbsent("Authorization", () => 'Bearer $authBearer'); request.headers.putIfAbsent("apikey", () => _supabaseKey); diff --git a/packages/supabase/lib/src/supabase_client.dart b/packages/supabase/lib/src/supabase_client.dart index 3383f267..3c5a9c63 100644 --- a/packages/supabase/lib/src/supabase_client.dart +++ b/packages/supabase/lib/src/supabase_client.dart @@ -24,6 +24,13 @@ import 'counter.dart'; /// /// [realtimeClientOptions] specifies different options you can pass to `RealtimeClient`. /// +/// [accessToken] 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 function may be +/// called concurrently and many times. Use memoization and locking techniques +/// if this is not supported by the client libraries. When set, the `auth` +/// namespace of the Supabase client cannot be used. +/// /// Pass an instance of `YAJsonIsolate` to [isolate] to use your own persisted /// isolate instance. A new instance will be created if [isolate] is omitted. /// @@ -43,7 +50,7 @@ class SupabaseClient { final Client? _httpClient; late final Client _authHttpClient; - late final GoTrueClient auth; + late final GoTrueClient _authInstance; /// Supabase Functions allows you to deploy and invoke edge functions. late final FunctionsClient functions; @@ -52,8 +59,9 @@ class SupabaseClient { late final SupabaseStorageClient storage; late final RealtimeClient realtime; late final PostgrestClient rest; - late StreamSubscription _authStateSubscription; + StreamSubscription? _authStateSubscription; late final YAJsonIsolate _isolate; + final Future Function()? accessToken; /// Increment ID of the stream to create different realtime topic for each stream final _incrementId = Counter(); @@ -83,13 +91,15 @@ class SupabaseClient { ..clear() ..addAll(_headers); - auth.headers - ..clear() - ..addAll({ - ...Constants.defaultHeaders, - ..._getAuthHeaders(), - ...headers, - }); + if (accessToken == null) { + auth.headers + ..clear() + ..addAll({ + ...Constants.defaultHeaders, + ..._getAuthHeaders(), + ...headers, + }); + } // To apply the new headers in the realtime client, // manually unsubscribe and resubscribe to all channels. @@ -106,6 +116,7 @@ class SupabaseClient { AuthClientOptions authOptions = const AuthClientOptions(), StorageClientOptions storageOptions = const StorageClientOptions(), RealtimeClientOptions realtimeClientOptions = const RealtimeClientOptions(), + this.accessToken, Map? headers, Client? httpClient, YAJsonIsolate? isolate, @@ -122,18 +133,30 @@ class SupabaseClient { }, _httpClient = httpClient, _isolate = isolate ?? (YAJsonIsolate()..initialize()) { - auth = _initSupabaseAuthClient( + _authInstance = _initSupabaseAuthClient( autoRefreshToken: authOptions.autoRefreshToken, gotrueAsyncStorage: authOptions.pkceAsyncStorage, authFlowType: authOptions.authFlowType, ); _authHttpClient = - AuthHttpClient(_supabaseKey, httpClient ?? Client(), auth); + AuthHttpClient(_supabaseKey, httpClient ?? Client(), _getAccessToken); rest = _initRestClient(); functions = _initFunctionsClient(); storage = _initStorageClient(storageOptions.retryAttempts); realtime = _initRealtimeClient(options: realtimeClientOptions); - _listenForAuthEvents(); + if (accessToken == null) { + _listenForAuthEvents(); + } + } + + GoTrueClient get auth { + if (accessToken == null) { + return _authInstance; + } else { + throw AuthException( + 'Supabase Client is configured with the accessToken option, accessing supabase.auth is not possible.', + ); + } } /// Perform a table operation. @@ -200,8 +223,32 @@ class SupabaseClient { return realtime.removeAllChannels(); } + Future _getAccessToken() async { + if (accessToken != null) { + return await accessToken!(); + } + + if (_authInstance.currentSession?.isExpired ?? false) { + try { + await _authInstance.refreshSession(); + } catch (error) { + final expiresAt = _authInstance.currentSession?.expiresAt; + if (expiresAt != null) { + // Failed to refresh the token. + final isExpiredWithoutMargin = DateTime.now() + .isAfter(DateTime.fromMillisecondsSinceEpoch(expiresAt * 1000)); + if (isExpiredWithoutMargin) { + // Throw the error instead of making an API request with an expired token. + rethrow; + } + } + } + } + return _authInstance.currentSession?.accessToken; + } + Future dispose() async { - await _authStateSubscription.cancel(); + await _authStateSubscription?.cancel(); await _isolate.dispose(); } diff --git a/packages/supabase/test/client_test.dart b/packages/supabase/test/client_test.dart index 3de1c971..36f0c03d 100644 --- a/packages/supabase/test/client_test.dart +++ b/packages/supabase/test/client_test.dart @@ -186,6 +186,16 @@ void main() { mockServer.close(); }); + + test('create a client with third-party auth accessToken', () async { + final supabase = SupabaseClient('URL', 'KEY', accessToken: () async { + return 'jwt'; + }); + expect( + () => supabase.auth.currentUser, + throwsA(AuthException( + 'Supabase Client is configured with the accessToken option, accessing supabase.auth is not possible.'))); + }); }); group('Custom Header', () { diff --git a/packages/supabase_flutter/lib/src/supabase.dart b/packages/supabase_flutter/lib/src/supabase.dart index af2c2540..7cb2930e 100644 --- a/packages/supabase_flutter/lib/src/supabase.dart +++ b/packages/supabase_flutter/lib/src/supabase.dart @@ -75,6 +75,7 @@ class Supabase { PostgrestClientOptions postgrestOptions = const PostgrestClientOptions(), StorageClientOptions storageOptions = const StorageClientOptions(), FlutterAuthClientOptions authOptions = const FlutterAuthClientOptions(), + Future Function()? accessToken, bool? debug, }) async { assert( @@ -103,6 +104,7 @@ class Supabase { authOptions: authOptions, postgrestOptions: postgrestOptions, storageOptions: storageOptions, + accessToken: accessToken, ); _instance._debugEnable = debug ?? kDebugMode; _instance.log('***** Supabase init completed $_instance'); @@ -154,6 +156,7 @@ class Supabase { required PostgrestClientOptions postgrestOptions, required StorageClientOptions storageOptions, required AuthClientOptions authOptions, + required Future Function()? accessToken, }) { final headers = { ...Constants.defaultHeaders, @@ -168,6 +171,7 @@ class Supabase { postgrestOptions: postgrestOptions, storageOptions: storageOptions, authOptions: authOptions, + accessToken: accessToken, ); _initialized = true; } diff --git a/packages/supabase_flutter/test/supabase_flutter_test.dart b/packages/supabase_flutter/test/supabase_flutter_test.dart index 325e6099..17189517 100644 --- a/packages/supabase_flutter/test/supabase_flutter_test.dart +++ b/packages/supabase_flutter/test/supabase_flutter_test.dart @@ -99,7 +99,7 @@ void main() { /// Check if the current version of AppLinks uses an explicit call to get /// the initial link. This is only the case before version 6.0.0, where we /// can find the getInitialAppLink function. - /// + /// /// CI pipeline is set so that it tests both app_links newer and older than v6.0.0 bool appLinksExposesInitialLinkInStream() { try {