From 0ae2b7149286d4719bbaafdf1ecb3d76c8b4eb7a Mon Sep 17 00:00:00 2001 From: Vinzent Date: Thu, 21 Nov 2024 21:38:31 +0100 Subject: [PATCH 1/5] refactor: store session and pkce in the same storage in gotrue_client --- packages/gotrue/lib/src/gotrue_client.dart | 136 +++++++++++++----- .../lib/src/types/gotrue_async_storage.dart | 3 + .../supabase/lib/src/supabase_client.dart | 21 ++- .../lib/src/supabase_client_options.dart | 9 ++ .../src/flutter_go_true_client_options.dart | 13 ++ .../lib/src/local_storage.dart | 49 +++++++ .../supabase_flutter/lib/src/supabase.dart | 31 ++-- .../lib/src/supabase_auth.dart | 63 -------- 8 files changed, 201 insertions(+), 124 deletions(-) diff --git a/packages/gotrue/lib/src/gotrue_client.dart b/packages/gotrue/lib/src/gotrue_client.dart index 63f5c85f..b93db808 100644 --- a/packages/gotrue/lib/src/gotrue_client.dart +++ b/packages/gotrue/lib/src/gotrue_client.dart @@ -87,8 +87,14 @@ class GoTrueClient { Stream get onAuthStateChangeSync => _onAuthStateChangeControllerSync.stream; + final Completer _initalizedStorage = Completer(); + final AuthFlowType _flowType; + final bool _persistSession; + + final String _storageKey; + final _log = Logger('supabase.auth'); /// Proxy to the web BroadcastChannel API. Should be null on non-web platforms. @@ -101,8 +107,10 @@ class GoTrueClient { String? url, Map? headers, bool? autoRefreshToken, + bool? persistSession, Client? httpClient, GotrueAsyncStorage? asyncStorage, + String? storageKey, AuthFlowType flowType = AuthFlowType.pkce, }) : _url = url ?? Constants.defaultGotrueUrl, _headers = { @@ -111,7 +119,9 @@ class GoTrueClient { }, _httpClient = httpClient, _asyncStorage = asyncStorage, - _flowType = flowType { + _flowType = flowType, + _persistSession = persistSession ?? false, + _storageKey = storageKey ?? Constants.defaultStorageKey { _autoRefreshToken = autoRefreshToken ?? true; final gotrueUrl = url ?? Constants.defaultGotrueUrl; @@ -127,10 +137,19 @@ class GoTrueClient { client: this, fetch: _fetch, ); + + assert(asyncStorage != null || !_persistSession, + 'You need to provide asyncStorage to persist session.'); + if (asyncStorage != null) { + _initalizedStorage.complete( + asyncStorage.initialize().catchError((e) => notifyException(e))); + } + if (_autoRefreshToken) { startAutoRefresh(); } + _initialize(); _mayStartBroadcastChannel(); } @@ -148,6 +167,37 @@ class GoTrueClient { /// Returns the current session, if any; Session? get currentSession => _currentSession; + /// This method should not throw as it is called from the constructor. + Future _initialize() async { + try { + if (_persistSession && _asyncStorage != null) { + await _initalizedStorage.future; + final jsonStr = await _asyncStorage!.getItem(key: _storageKey); + var shouldEmitInitialSession = true; + if (jsonStr != null) { + await setInitialSession(jsonStr); + shouldEmitInitialSession = false; + + // Only try to recover session if the session got set in [setInitialSession] + // because if not the session is missing data and already notified an + // exception. + if (currentSession != null) { + // [notifyException] gets already called here if needed, so we can + // catch any error. + recoverSession(jsonStr).then((_) {}, onError: (_) {}); + } + } + if (shouldEmitInitialSession) { + // Emit a null session if the user did not have persisted session + notifyAllSubscribers(AuthChangeEvent.initialSession); + } + } + } catch (error, stackTrace) { + _log.warning('Error while loading initial session', error, stackTrace); + notifyException(error, stackTrace); + } + } + /// Creates a new anonymous user. /// /// Returns An `AuthResponse` with a session where the `is_anonymous` claim @@ -172,7 +222,7 @@ class GoTrueClient { final session = authResponse.session; if (session != null) { - _saveSession(session); + await _saveSession(session); notifyAllSubscribers(AuthChangeEvent.signedIn); } @@ -217,9 +267,8 @@ class GoTrueClient { assert(_asyncStorage != null, 'You need to provide asyncStorage to perform pkce flow.'); final codeVerifier = generatePKCEVerifier(); - await _asyncStorage!.setItem( - key: '${Constants.defaultStorageKey}-code-verifier', - value: codeVerifier); + await _asyncStorage! + .setItem(key: '$_storageKey-code-verifier', value: codeVerifier); codeChallenge = generatePKCEChallenge(codeVerifier); } @@ -259,7 +308,7 @@ class GoTrueClient { final session = authResponse.session; if (session != null) { - _saveSession(session); + await _saveSession(session); notifyAllSubscribers(AuthChangeEvent.signedIn); } @@ -312,7 +361,7 @@ class GoTrueClient { final authResponse = AuthResponse.fromJson(response); if (authResponse.session?.accessToken != null) { - _saveSession(authResponse.session!); + await _saveSession(authResponse.session!); notifyAllSubscribers(AuthChangeEvent.signedIn); } return authResponse; @@ -339,8 +388,8 @@ class GoTrueClient { assert(_asyncStorage != null, 'You need to provide asyncStorage to perform pkce flow.'); - final codeVerifierRawString = await _asyncStorage! - .getItem(key: '${Constants.defaultStorageKey}-code-verifier'); + final codeVerifierRawString = + await _asyncStorage!.getItem(key: '$_storageKey-code-verifier'); if (codeVerifierRawString == null) { throw AuthException('Code verifier could not be found in local storage.'); } @@ -363,14 +412,13 @@ class GoTrueClient { ), ); - await _asyncStorage! - .removeItem(key: '${Constants.defaultStorageKey}-code-verifier'); + await _asyncStorage!.removeItem(key: '$_storageKey-code-verifier'); final authSessionUrlResponse = AuthSessionUrlResponse( session: Session.fromJson(response)!, redirectType: redirectType?.name); final session = authSessionUrlResponse.session; - _saveSession(session); + await _saveSession(session); if (redirectType == AuthChangeEvent.passwordRecovery) { notifyAllSubscribers(AuthChangeEvent.passwordRecovery); } else { @@ -434,7 +482,7 @@ class GoTrueClient { ); } - _saveSession(authResponse.session!); + await _saveSession(authResponse.session!); notifyAllSubscribers(AuthChangeEvent.signedIn); return authResponse; @@ -472,9 +520,8 @@ class GoTrueClient { assert(_asyncStorage != null, 'You need to provide asyncStorage to perform pkce flow.'); final codeVerifier = generatePKCEVerifier(); - await _asyncStorage!.setItem( - key: '${Constants.defaultStorageKey}-code-verifier', - value: codeVerifier); + await _asyncStorage! + .setItem(key: '$_storageKey-code-verifier', value: codeVerifier); codeChallenge = generatePKCEChallenge(codeVerifier); } await _fetch.request( @@ -559,7 +606,7 @@ class GoTrueClient { ); } - _saveSession(authResponse.session!); + await _saveSession(authResponse.session!); notifyAllSubscribers(type == OtpType.recovery ? AuthChangeEvent.passwordRecovery : AuthChangeEvent.signedIn); @@ -594,9 +641,8 @@ class GoTrueClient { assert(_asyncStorage != null, 'You need to provide asyncStorage to perform pkce flow.'); final codeVerifier = generatePKCEVerifier(); - await _asyncStorage!.setItem( - key: '${Constants.defaultStorageKey}-code-verifier', - value: codeVerifier); + await _asyncStorage! + .setItem(key: '$_storageKey-code-verifier', value: codeVerifier); codeChallenge = generatePKCEChallenge(codeVerifier); codeChallengeMethod = codeVerifier == codeChallenge ? 'plain' : 's256'; } @@ -832,7 +878,7 @@ class GoTrueClient { final redirectType = url.queryParameters['type']; if (storeSession == true) { - _saveSession(session); + await _saveSession(session); if (redirectType == 'recovery') { notifyAllSubscribers(AuthChangeEvent.passwordRecovery); } else { @@ -855,9 +901,8 @@ class GoTrueClient { final accessToken = currentSession?.accessToken; if (scope != SignOutScope.others) { - _removeSession(); - await _asyncStorage?.removeItem( - key: '${Constants.defaultStorageKey}-code-verifier'); + await _removeSession(); + await _asyncStorage?.removeItem(key: '$_storageKey-code-verifier'); notifyAllSubscribers(AuthChangeEvent.signedOut); } @@ -889,7 +934,7 @@ class GoTrueClient { 'You need to provide asyncStorage to perform pkce flow.'); final codeVerifier = generatePKCEVerifier(); await _asyncStorage!.setItem( - key: '${Constants.defaultStorageKey}-code-verifier', + key: '$_storageKey-code-verifier', value: '$codeVerifier/${AuthChangeEvent.passwordRecovery.name}', ); codeChallenge = generatePKCEChallenge(codeVerifier); @@ -978,9 +1023,7 @@ class GoTrueClient { if (session == null) { _log.warning("Can't recover session from string, session is null"); await signOut(); - throw notifyException( - AuthException('Current session is missing data.'), - ); + throw AuthException('Session to restore is missing data.'); } if (session.isExpired) { @@ -995,7 +1038,7 @@ class GoTrueClient { } else { final shouldEmitEvent = _currentSession == null || _currentSession?.user.id != session.user.id; - _saveSession(session); + await _saveSession(session); if (shouldEmitEvent) { notifyAllSubscribers(AuthChangeEvent.tokenRefreshed); @@ -1126,7 +1169,7 @@ class GoTrueClient { 'You need to provide asyncStorage to perform pkce flow.'); final codeVerifier = generatePKCEVerifier(); await _asyncStorage!.setItem( - key: '${Constants.defaultStorageKey}-code-verifier', + key: '$_storageKey-code-verifier', value: codeVerifier, ); @@ -1146,17 +1189,36 @@ class GoTrueClient { } /// set currentSession and currentUser - void _saveSession(Session session) { + Future _saveSession(Session session) async { _log.finest('Saving session: $session'); _log.fine('Saving session'); _currentSession = session; _currentUser = session.user; + + if (_persistSession && _asyncStorage != null) { + if (!_initalizedStorage.isCompleted) { + await _initalizedStorage.future; + } + _asyncStorage!.setItem( + key: _storageKey, + value: jsonEncode(session.toJson()), + ); + } } - void _removeSession() { + Future _removeSession() async { _log.fine('Removing session'); _currentSession = null; _currentUser = null; + + if (_persistSession && _asyncStorage != null) { + if (!_initalizedStorage.isCompleted) { + await _initalizedStorage.future; + } + _asyncStorage!.removeItem( + key: _storageKey, + ); + } } void _mayStartBroadcastChannel() { @@ -1170,7 +1232,7 @@ class GoTrueClient { try { _broadcastChannel = web.getBroadcastChannel(broadcastKey); _broadcastChannelSubscription = - _broadcastChannel?.onMessage.listen((messageEvent) { + _broadcastChannel?.onMessage.listen((messageEvent) async { final rawEvent = messageEvent['event']; _log.finest('Received broadcast message: $messageEvent'); _log.info('Received broadcast event: $rawEvent'); @@ -1195,9 +1257,9 @@ class GoTrueClient { session = Session.fromJson(messageEvent['session']); } if (session != null) { - _saveSession(session); + await _saveSession(session); } else { - _removeSession(); + await _removeSession(); } notifyAllSubscribers(event, session: session, broadcast: false); } @@ -1247,14 +1309,14 @@ class GoTrueClient { throw AuthSessionMissingException(); } - _saveSession(session); + await _saveSession(session); notifyAllSubscribers(AuthChangeEvent.tokenRefreshed); _refreshTokenCompleter?.complete(data); return data; } on AuthException catch (error, stack) { if (error is! AuthRetryableFetchException) { - _removeSession(); + await _removeSession(); notifyAllSubscribers(AuthChangeEvent.signedOut); } else { notifyException(error, stack); diff --git a/packages/gotrue/lib/src/types/gotrue_async_storage.dart b/packages/gotrue/lib/src/types/gotrue_async_storage.dart index 29ce2b15..b4a1304e 100644 --- a/packages/gotrue/lib/src/types/gotrue_async_storage.dart +++ b/packages/gotrue/lib/src/types/gotrue_async_storage.dart @@ -2,6 +2,9 @@ abstract class GotrueAsyncStorage { const GotrueAsyncStorage(); + /// May be implemented to allow for initialization of the storage before use. + Future initialize() async {} + /// Retrieves an item asynchronously from the storage with the key. Future getItem({required String key}); diff --git a/packages/supabase/lib/src/supabase_client.dart b/packages/supabase/lib/src/supabase_client.dart index 4f500b8b..5382ec9e 100644 --- a/packages/supabase/lib/src/supabase_client.dart +++ b/packages/supabase/lib/src/supabase_client.dart @@ -137,11 +137,7 @@ class SupabaseClient { }, _httpClient = httpClient, _isolate = isolate ?? (YAJsonIsolate()..initialize()) { - _authInstance = _initSupabaseAuthClient( - autoRefreshToken: authOptions.autoRefreshToken, - gotrueAsyncStorage: authOptions.pkceAsyncStorage, - authFlowType: authOptions.authFlowType, - ); + _authInstance = _initSupabaseAuthClient(authOptions: authOptions); _authHttpClient = AuthHttpClient(_supabaseKey, httpClient ?? Client(), _getAccessToken); rest = _initRestClient(); @@ -273,11 +269,8 @@ class SupabaseClient { _authInstance?.dispose(); } - GoTrueClient _initSupabaseAuthClient({ - bool? autoRefreshToken, - required GotrueAsyncStorage? gotrueAsyncStorage, - required AuthFlowType authFlowType, - }) { + GoTrueClient _initSupabaseAuthClient( + {required AuthClientOptions authOptions}) { final authHeaders = {...headers}; authHeaders['apikey'] = _supabaseKey; authHeaders['Authorization'] = 'Bearer $_supabaseKey'; @@ -285,10 +278,12 @@ class SupabaseClient { return GoTrueClient( url: _authUrl, headers: authHeaders, - autoRefreshToken: autoRefreshToken, + autoRefreshToken: authOptions.autoRefreshToken, httpClient: _httpClient, - asyncStorage: gotrueAsyncStorage, - flowType: authFlowType, + asyncStorage: authOptions.asyncStorage, + storageKey: authOptions.storageKey, + persistSession: authOptions.persistSession, + flowType: authOptions.authFlowType, ); } diff --git a/packages/supabase/lib/src/supabase_client_options.dart b/packages/supabase/lib/src/supabase_client_options.dart index 3587e618..fe326a4b 100644 --- a/packages/supabase/lib/src/supabase_client_options.dart +++ b/packages/supabase/lib/src/supabase_client_options.dart @@ -8,13 +8,22 @@ class PostgrestClientOptions { class AuthClientOptions { final bool autoRefreshToken; + + @Deprecated( + "The storage for the session is now handled by the auth client itself and is combined with the storage for pkce, so please use [asyncStorage] insetad") final GotrueAsyncStorage? pkceAsyncStorage; + final GotrueAsyncStorage? asyncStorage; final AuthFlowType authFlowType; + final String? storageKey; + final bool? persistSession; const AuthClientOptions({ this.autoRefreshToken = true, this.pkceAsyncStorage, + this.asyncStorage, this.authFlowType = AuthFlowType.pkce, + this.storageKey, + this.persistSession, }); } diff --git a/packages/supabase_flutter/lib/src/flutter_go_true_client_options.dart b/packages/supabase_flutter/lib/src/flutter_go_true_client_options.dart index 2c82a0f7..956dc4b8 100644 --- a/packages/supabase_flutter/lib/src/flutter_go_true_client_options.dart +++ b/packages/supabase_flutter/lib/src/flutter_go_true_client_options.dart @@ -1,6 +1,8 @@ import 'package:supabase_flutter/supabase_flutter.dart'; class FlutterAuthClientOptions extends AuthClientOptions { + @Deprecated( + "The storage for the session is now handled by the auth client itself and is combined with the storage for pkce, so please use [asyncStorage] insetad") final LocalStorage? localStorage; /// If true, the client will start the deep link observer and obtain sessions @@ -11,6 +13,9 @@ class FlutterAuthClientOptions extends AuthClientOptions { super.authFlowType, super.autoRefreshToken, super.pkceAsyncStorage, + super.asyncStorage, + super.storageKey, + super.persistSession, this.localStorage, this.detectSessionInUri = true, }); @@ -20,13 +25,21 @@ class FlutterAuthClientOptions extends AuthClientOptions { bool? autoRefreshToken, LocalStorage? localStorage, GotrueAsyncStorage? pkceAsyncStorage, + GotrueAsyncStorage? asyncStorage, + String? storageKey, + bool? persistSession, bool? detectSessionInUri, }) { return FlutterAuthClientOptions( authFlowType: authFlowType ?? this.authFlowType, autoRefreshToken: autoRefreshToken ?? this.autoRefreshToken, + // ignore: deprecated_member_use_from_same_package localStorage: localStorage ?? this.localStorage, + // ignore: deprecated_member_use pkceAsyncStorage: pkceAsyncStorage ?? this.pkceAsyncStorage, + asyncStorage: asyncStorage, + storageKey: storageKey, + persistSession: persistSession, detectSessionInUri: detectSessionInUri ?? this.detectSessionInUri, ); } diff --git a/packages/supabase_flutter/lib/src/local_storage.dart b/packages/supabase_flutter/lib/src/local_storage.dart index 7b201584..11bdef21 100644 --- a/packages/supabase_flutter/lib/src/local_storage.dart +++ b/packages/supabase_flutter/lib/src/local_storage.dart @@ -3,6 +3,7 @@ import 'dart:async'; import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; import 'package:shared_preferences/shared_preferences.dart'; +import 'package:supabase_flutter/src/supabase_auth.dart'; import 'package:supabase_flutter/supabase_flutter.dart'; import './local_storage_stub.dart' @@ -148,3 +149,51 @@ class SharedPreferencesGotrueAsyncStorage extends GotrueAsyncStorage { await _prefs.setString(key, value); } } + +/// Combines the storage for pkce and session into one. +/// +/// Previously the session got stored by [SupabaseAuth] and the pkce flow by +/// [GoTrueClient] in a separate storage and with different interface. +/// This combiens both into one. +/// +/// This introduces another level of abstraction for the actual +/// session storage, but is necessary to prevent breaking changes. +class PkceAndSessionLocalStorage extends GotrueAsyncStorage { + final LocalStorage sessionLocalStorage; + final GotrueAsyncStorage pkceAsyncStorage; + + PkceAndSessionLocalStorage(this.sessionLocalStorage, this.pkceAsyncStorage); + @override + Future initialize() async { + await sessionLocalStorage.initialize(); + await pkceAsyncStorage.initialize(); + super.initialize(); + } + + @override + Future getItem({required String key}) { + if (key.endsWith("-code-verifier")) { + return pkceAsyncStorage.getItem(key: key); + } else { + return sessionLocalStorage.accessToken(); + } + } + + @override + Future removeItem({required String key}) async { + if (key.endsWith("-code-verifier")) { + await pkceAsyncStorage.removeItem(key: key); + } else { + await sessionLocalStorage.removePersistedSession(); + } + } + + @override + Future setItem({required String key, required String value}) async { + if (key.endsWith("-code-verifier")) { + await pkceAsyncStorage.setItem(key: key, value: value); + } else { + await sessionLocalStorage.persistSession(value); + } + } +} diff --git a/packages/supabase_flutter/lib/src/supabase.dart b/packages/supabase_flutter/lib/src/supabase.dart index 952460b0..77e5d389 100644 --- a/packages/supabase_flutter/lib/src/supabase.dart +++ b/packages/supabase_flutter/lib/src/supabase.dart @@ -101,19 +101,39 @@ class Supabase with WidgetsBindingObserver { _log.config("Initialize Supabase v$version"); + // ignore: deprecated_member_use if (authOptions.pkceAsyncStorage == null) { authOptions = authOptions.copyWith( pkceAsyncStorage: SharedPreferencesGotrueAsyncStorage(), ); } + // ignore: deprecated_member_use_from_same_package if (authOptions.localStorage == null) { authOptions = authOptions.copyWith( localStorage: SharedPreferencesLocalStorage( persistSessionKey: "sb-${Uri.parse(url).host.split(".").first}-auth-token", + // For now we don't set the above key that is used by supabase-js too + // as [AuthClientOptions.storageKey], because this would change + // the key for exsting pkce items. For v3 we should change this. ), ); } + if (authOptions.persistSession == null) { + authOptions = authOptions.copyWith( + persistSession: true, + ); + } + + if (authOptions.asyncStorage == null) { + authOptions = authOptions.copyWith( + asyncStorage: PkceAndSessionLocalStorage( + // ignore: deprecated_member_use_from_same_package + authOptions.localStorage!, + // ignore: deprecated_member_use + authOptions.pkceAsyncStorage!, + )); + } _instance._init( url, anonKey, @@ -130,13 +150,6 @@ class Supabase with WidgetsBindingObserver { final supabaseAuth = SupabaseAuth(); _instance._supabaseAuth = supabaseAuth; await supabaseAuth.initialize(options: authOptions); - - // Wrap `recoverSession()` in a `CancelableOperation` so that it can be canceled in dispose - // if still in progress - _instance._restoreSessionCancellableOperation = - CancelableOperation.fromFuture( - supabaseAuth.recoverSession(), - ); } _log.info('***** Supabase init completed *****'); @@ -160,16 +173,12 @@ class Supabase with WidgetsBindingObserver { bool _debugEnable = false; - /// Wraps the `recoverSession()` call so that it can be terminated when `dispose()` is called - late CancelableOperation _restoreSessionCancellableOperation; - CancelableOperation? _realtimeReconnectOperation; StreamSubscription? _logSubscription; /// Dispose the instance to free up resources. Future dispose() async { - await _restoreSessionCancellableOperation.cancel(); _logSubscription?.cancel(); client.dispose(); _instance._supabaseAuth?.dispose(); diff --git a/packages/supabase_flutter/lib/src/supabase_auth.dart b/packages/supabase_flutter/lib/src/supabase_auth.dart index ada94a24..13215b5d 100644 --- a/packages/supabase_flutter/lib/src/supabase_auth.dart +++ b/packages/supabase_flutter/lib/src/supabase_auth.dart @@ -15,7 +15,6 @@ import 'package:url_launcher/url_launcher.dart'; class SupabaseAuth with WidgetsBindingObserver { static WidgetsBinding? get _widgetsBindingInstance => WidgetsBinding.instance; - late LocalStorage _localStorage; late AuthFlowType _authFlowType; /// Whether to automatically refresh the token @@ -26,8 +25,6 @@ class SupabaseAuth with WidgetsBindingObserver { /// throughout your app's life. static bool _initialDeeplinkIsHandled = false; - StreamSubscription? _authSubscription; - StreamSubscription? _deeplinkSubscription; final _appLinks = AppLinks(); @@ -40,65 +37,14 @@ class SupabaseAuth with WidgetsBindingObserver { Future initialize({ required FlutterAuthClientOptions options, }) async { - _localStorage = options.localStorage!; _authFlowType = options.authFlowType; _autoRefreshToken = options.autoRefreshToken; - _authSubscription = Supabase.instance.client.auth.onAuthStateChange.listen( - (data) { - _onAuthStateChange(data.event, data.session); - }, - onError: (error, stackTrace) {}, - ); - - await _localStorage.initialize(); - - final hasPersistedSession = await _localStorage.hasAccessToken(); - var shouldEmitInitialSession = true; - if (hasPersistedSession) { - final persistedSession = await _localStorage.accessToken(); - if (persistedSession != null) { - try { - await Supabase.instance.client.auth - .setInitialSession(persistedSession); - shouldEmitInitialSession = false; - } catch (error, stackTrace) { - _log.warning( - 'Error while setting initial session', error, stackTrace); - } - } - } - if (shouldEmitInitialSession) { - Supabase.instance.client.auth - // ignore: invalid_use_of_internal_member - .notifyAllSubscribers(AuthChangeEvent.initialSession); - } _widgetsBindingInstance?.addObserver(this); if (options.detectSessionInUri) { await _startDeeplinkObserver(); } - - // Emit a null session if the user did not have persisted session - } - - /// Recovers the session from local storage. - /// - /// Called lazily after `.initialize()` by `Supabase` instance - Future recoverSession() async { - try { - final hasPersistedSession = await _localStorage.hasAccessToken(); - if (hasPersistedSession) { - final persistedSession = await _localStorage.accessToken(); - if (persistedSession != null) { - await Supabase.instance.client.auth.recoverSession(persistedSession); - } - } - } on AuthException catch (error, stackTrace) { - _log.warning(error.message, error, stackTrace); - } catch (error, stackTrace) { - _log.warning("Error while recovering session", error, stackTrace); - } } /// Dispose the instance to free up resources @@ -106,7 +52,6 @@ class SupabaseAuth with WidgetsBindingObserver { if (!kIsWeb && Platform.environment.containsKey('FLUTTER_TEST')) { _initialDeeplinkIsHandled = false; } - _authSubscription?.cancel(); _stopDeeplinkObserver(); _widgetsBindingInstance?.removeObserver(this); } @@ -127,14 +72,6 @@ class SupabaseAuth with WidgetsBindingObserver { } } - void _onAuthStateChange(AuthChangeEvent event, Session? session) { - if (session != null) { - _localStorage.persistSession(jsonEncode(session.toJson())); - } else if (event == AuthChangeEvent.signedOut) { - _localStorage.removePersistedSession(); - } - } - /// If _authCallbackUrlHost not init, we treat all deep links as auth callback bool _isAuthCallbackDeeplink(Uri uri) { return (uri.fragment.contains('access_token') && From dd698d65ca414682add8d5fed6fa3c83dc12cd4b Mon Sep 17 00:00:00 2001 From: Vinzent Date: Thu, 21 Nov 2024 22:09:45 +0100 Subject: [PATCH 2/5] fix: properly copyWith --- packages/gotrue/lib/src/gotrue_client.dart | 2 +- .../lib/src/flutter_go_true_client_options.dart | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/gotrue/lib/src/gotrue_client.dart b/packages/gotrue/lib/src/gotrue_client.dart index b93db808..b86e7546 100644 --- a/packages/gotrue/lib/src/gotrue_client.dart +++ b/packages/gotrue/lib/src/gotrue_client.dart @@ -126,7 +126,7 @@ class GoTrueClient { final gotrueUrl = url ?? Constants.defaultGotrueUrl; _log.config( - 'Initialize GoTrueClient v$version with url: $_url, autoRefreshToken: $_autoRefreshToken, flowType: $_flowType, tickDuration: ${Constants.autoRefreshTickDuration}, tickThreshold: ${Constants.autoRefreshTickThreshold}'); + 'Initialize GoTrueClient v$version with url: $_url, persistSession: $_persistSession, _storageKey: $storageKey, autoRefreshToken: $_autoRefreshToken, flowType: $_flowType, tickDuration: ${Constants.autoRefreshTickDuration}, tickThreshold: ${Constants.autoRefreshTickThreshold}'); _log.finest('Initialize with headers: $_headers'); admin = GoTrueAdminApi( gotrueUrl, diff --git a/packages/supabase_flutter/lib/src/flutter_go_true_client_options.dart b/packages/supabase_flutter/lib/src/flutter_go_true_client_options.dart index 956dc4b8..e82db799 100644 --- a/packages/supabase_flutter/lib/src/flutter_go_true_client_options.dart +++ b/packages/supabase_flutter/lib/src/flutter_go_true_client_options.dart @@ -37,9 +37,9 @@ class FlutterAuthClientOptions extends AuthClientOptions { localStorage: localStorage ?? this.localStorage, // ignore: deprecated_member_use pkceAsyncStorage: pkceAsyncStorage ?? this.pkceAsyncStorage, - asyncStorage: asyncStorage, - storageKey: storageKey, - persistSession: persistSession, + asyncStorage: asyncStorage ?? this.asyncStorage, + storageKey: storageKey ?? this.storageKey, + persistSession: persistSession ?? this.persistSession, detectSessionInUri: detectSessionInUri ?? this.detectSessionInUri, ); } From 4e150de14b2cc74498b68439d44c25da122bfe10 Mon Sep 17 00:00:00 2001 From: Vinzent Date: Fri, 22 Nov 2024 00:44:16 +0100 Subject: [PATCH 3/5] fix: only broadcast session when persisting session --- packages/gotrue/lib/src/gotrue_client.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/gotrue/lib/src/gotrue_client.dart b/packages/gotrue/lib/src/gotrue_client.dart index b86e7546..3baa3097 100644 --- a/packages/gotrue/lib/src/gotrue_client.dart +++ b/packages/gotrue/lib/src/gotrue_client.dart @@ -1222,7 +1222,7 @@ class GoTrueClient { } void _mayStartBroadcastChannel() { - if (const bool.fromEnvironment('dart.library.html')) { + if (_persistSession && const bool.fromEnvironment('dart.library.html')) { // Used by the js library as well final broadcastKey = "sb-${Uri.parse(_url).host.split(".").first}-auth-token"; From 0094ec010be3815d7638f95c6fcfb49dffcae31c Mon Sep 17 00:00:00 2001 From: Vinzent Date: Sat, 23 Nov 2024 15:37:24 +0100 Subject: [PATCH 4/5] refactor: update docs and default values --- packages/gotrue/lib/src/gotrue_client.dart | 10 +++++- packages/gotrue/lib/src/types/types.dart | 2 ++ .../supabase/lib/src/supabase_client.dart | 13 +++----- .../lib/src/supabase_client_options.dart | 32 +++++++++++++++++-- .../src/flutter_go_true_client_options.dart | 29 +++++++++++++++-- .../supabase_flutter/lib/src/supabase.dart | 31 +++++++----------- 6 files changed, 84 insertions(+), 33 deletions(-) diff --git a/packages/gotrue/lib/src/gotrue_client.dart b/packages/gotrue/lib/src/gotrue_client.dart index 3baa3097..03df2bd3 100644 --- a/packages/gotrue/lib/src/gotrue_client.dart +++ b/packages/gotrue/lib/src/gotrue_client.dart @@ -29,9 +29,17 @@ part 'gotrue_mfa_api.dart'; /// /// [autoRefreshToken] whether to refresh the token automatically or not. Defaults to true. /// +/// [persistSession] whether to persist the session via [asyncStorage] or not. +/// Defaults to false. Session is only broadcasted via [BroadcastChannel] if +/// set to true. +/// /// [httpClient] custom http client. /// -/// [asyncStorage] local storage to store pkce code verifiers. Required when using the pkce flow. +/// [asyncStorage] local storage to store sessions and pkce code verifiers. +/// Required when using the pkce flow and persisting sessions. +/// +/// [storageKey] key to store the session with in [asyncStorage]. +/// The pkce code verifiers are suffixed with `-code-verifier` /// /// Set [flowType] to [AuthFlowType.implicit] to perform old implicit auth flow. /// {@endtemplate} diff --git a/packages/gotrue/lib/src/types/types.dart b/packages/gotrue/lib/src/types/types.dart index c1001433..abf300cc 100644 --- a/packages/gotrue/lib/src/types/types.dart +++ b/packages/gotrue/lib/src/types/types.dart @@ -1,3 +1,5 @@ +/// An interface to use [html.BroadcastChannel] on web to broadcast sessions to +/// other tabs. typedef BroadcastChannel = ({ Stream> onMessage, void Function(Map) postMessage, diff --git a/packages/supabase/lib/src/supabase_client.dart b/packages/supabase/lib/src/supabase_client.dart index 5382ec9e..6f2c7416 100644 --- a/packages/supabase/lib/src/supabase_client.dart +++ b/packages/supabase/lib/src/supabase_client.dart @@ -11,20 +11,19 @@ import 'auth_http_client.dart'; import 'counter.dart'; /// {@template supabase_client} +/// /// Creates a Supabase client to interact with your Supabase instance. /// /// [supabaseUrl] and [supabaseKey] can be found on your Supabase dashboard. /// -/// You can access none public schema by passing different [schema]. -/// /// Default headers can be overridden by specifying [headers]. /// /// Custom http client can be used by passing [httpClient] parameter. /// -/// [storageRetryAttempts] specifies how many retry attempts there should be to -/// upload a file to Supabase storage when failed due to network interruption. -/// -/// [realtimeClientOptions] specifies different options you can pass to `RealtimeClient`. +/// [realtimeClientOptions], [authOptions], [storageOptions], +/// [postgrestOptions] specify different options you can pass to +/// [RealtimeClient], [GoTrueClient], [SupabaseStorageClient], +/// [PostgrestClient]. /// /// [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 @@ -36,8 +35,6 @@ import 'counter.dart'; /// Pass an instance of `YAJsonIsolate` to [isolate] to use your own persisted /// isolate instance. A new instance will be created if [isolate] is omitted. /// -/// Pass an instance of [gotrueAsyncStorage] and set the [authFlowType] to -/// `AuthFlowType.pkce`in order to perform auth actions with pkce flow. /// {@endtemplate} class SupabaseClient { final String _supabaseKey; diff --git a/packages/supabase/lib/src/supabase_client_options.dart b/packages/supabase/lib/src/supabase_client_options.dart index fe326a4b..52b23db4 100644 --- a/packages/supabase/lib/src/supabase_client_options.dart +++ b/packages/supabase/lib/src/supabase_client_options.dart @@ -6,6 +6,28 @@ class PostgrestClientOptions { const PostgrestClientOptions({this.schema = 'public'}); } +/// {@template supabase_auth_client_options} +/// +/// Configuration for the auth client with appropriate default values when using +/// the `supabase` package. For usage via `supabase_flutter` use +/// [FlutterAuthClientOptions] instead +/// +/// [autoRefreshToken] whether to refresh the token automatically or not. Defaults to true. +/// +/// [asyncStorage] a storage interface to store sessions +/// (if [persistSession] is `true`) and pkce code verifiers +/// (if [authFlowType] is [AuthFlowType.pkce]), which is the default. +/// +/// [storageKey] key to store the session with in [asyncStorage]. +/// The pkce code verifiers are suffixed with `-code-verifier` +/// +/// [persistSession] whether to persist the session via [asyncStorage] or not. +/// Session is only broadcasted via [BroadcastChannel] if set to true. +/// +/// Set [authFlowType] to [AuthFlowType.implicit] to use the old implicit flow for authentication +/// involving deep links. +/// +/// {@endtemplate} class AuthClientOptions { final bool autoRefreshToken; @@ -13,22 +35,28 @@ class AuthClientOptions { "The storage for the session is now handled by the auth client itself and is combined with the storage for pkce, so please use [asyncStorage] insetad") final GotrueAsyncStorage? pkceAsyncStorage; final GotrueAsyncStorage? asyncStorage; + final AuthFlowType authFlowType; + final String? storageKey; - final bool? persistSession; + final bool persistSession; + /// {@macro supabase_auth_client_options} const AuthClientOptions({ this.autoRefreshToken = true, this.pkceAsyncStorage, this.asyncStorage, this.authFlowType = AuthFlowType.pkce, this.storageKey, - this.persistSession, + this.persistSession = false, }); } class StorageClientOptions { final int retryAttempts; + /// [retryAttempts] specifies how many retry attempts there should be + /// to upload a file to Supabase storage when failed due to network + /// interruption. const StorageClientOptions({this.retryAttempts = 0}); } diff --git a/packages/supabase_flutter/lib/src/flutter_go_true_client_options.dart b/packages/supabase_flutter/lib/src/flutter_go_true_client_options.dart index e82db799..72e29c3b 100644 --- a/packages/supabase_flutter/lib/src/flutter_go_true_client_options.dart +++ b/packages/supabase_flutter/lib/src/flutter_go_true_client_options.dart @@ -1,21 +1,44 @@ import 'package:supabase_flutter/supabase_flutter.dart'; +/// {@template supabase_flutter_auth_client_options} +/// +/// [autoRefreshToken] whether to refresh the token automatically or not. Defaults to true. +/// +/// [asyncStorage] a storage interface to store sessions +/// (if [persistSession] is `true`) and pkce code verifiers +/// (if [authFlowType] is [AuthFlowType.pkce]) +/// +/// [storageKey] key to store the session with in [asyncStorage]. +/// The pkce code verifiers are suffixed with `-code-verifier` +/// +/// [persistSession] whether to persist the session via [asyncStorage] or not. +/// Session is only broadcasted via [BroadcastChannel] if set to true. +/// +/// Set [authFlowType] to [AuthFlowType.implicit] to use the old implicit flow for authentication +/// involving deep links. +/// +/// [detectSessionInUri] If true, the client will start the deep link observer and obtain sessions +/// when a valid URI is detected. +/// +/// PKCE flow uses shared preferences for storing the code verifier by default. +/// Pass a custom storage to [asyncStorage] to override the behavior. +/// +/// {@endtemplate} class FlutterAuthClientOptions extends AuthClientOptions { @Deprecated( "The storage for the session is now handled by the auth client itself and is combined with the storage for pkce, so please use [asyncStorage] insetad") final LocalStorage? localStorage; - /// If true, the client will start the deep link observer and obtain sessions - /// when a valid URI is detected. final bool detectSessionInUri; + /// {@macro supabase_flutter_auth_client_options} const FlutterAuthClientOptions({ super.authFlowType, super.autoRefreshToken, super.pkceAsyncStorage, super.asyncStorage, super.storageKey, - super.persistSession, + super.persistSession = true, this.localStorage, this.detectSessionInUri = true, }); diff --git a/packages/supabase_flutter/lib/src/supabase.dart b/packages/supabase_flutter/lib/src/supabase.dart index 77e5d389..c7b58b96 100644 --- a/packages/supabase_flutter/lib/src/supabase.dart +++ b/packages/supabase_flutter/lib/src/supabase.dart @@ -48,28 +48,26 @@ class Supabase with WidgetsBindingObserver { /// Initialize the current supabase instance /// /// This must be called only once. If called more than once, an - /// [AssertionError] is thrown + /// [AssertionError] is thrown. + /// (after calling [dispose], [initialize] can be called again) /// /// [url] and [anonKey] can be found on your Supabase dashboard. /// - /// You can access none public schema by passing different [schema]. - /// /// Default headers can be overridden by specifying [headers]. /// - /// Pass [localStorage] to override the default local storage option used to - /// persist auth. - /// /// Custom http client can be used by passing [httpClient] parameter. /// - /// [storageRetryAttempts] specifies how many retry attempts there should be - /// to upload a file to Supabase storage when failed due to network - /// interruption. - /// - /// Set [authFlowType] to [AuthFlowType.implicit] to use the old implicit flow for authentication - /// involving deep links. + /// [realtimeClientOptions], [authOptions], [storageOptions], + /// [postgrestOptions] specify different options you can pass to + /// [RealtimeClient], [GoTrueClient], [SupabaseStorageClient], + /// [PostgrestClient]. /// - /// PKCE flow uses shared preferences for storing the code verifier by default. - /// Pass a custom storage to [pkceAsyncStorage] to override the behavior. + /// [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. /// /// If [debug] is set to `true`, debug logs will be printed in debug console. Default is `kDebugMode`. static Future initialize({ @@ -119,11 +117,6 @@ class Supabase with WidgetsBindingObserver { ), ); } - if (authOptions.persistSession == null) { - authOptions = authOptions.copyWith( - persistSession: true, - ); - } if (authOptions.asyncStorage == null) { authOptions = authOptions.copyWith( From d62da8feb248729b64d2a883fd4690c31ab31256 Mon Sep 17 00:00:00 2001 From: Vinzent Date: Sat, 23 Nov 2024 16:54:21 +0100 Subject: [PATCH 5/5] refactor: fix typo and add documentation --- packages/gotrue/lib/src/gotrue_client.dart | 30 +++++++++++++--------- 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/packages/gotrue/lib/src/gotrue_client.dart b/packages/gotrue/lib/src/gotrue_client.dart index 03df2bd3..67cb8814 100644 --- a/packages/gotrue/lib/src/gotrue_client.dart +++ b/packages/gotrue/lib/src/gotrue_client.dart @@ -39,7 +39,7 @@ part 'gotrue_mfa_api.dart'; /// Required when using the pkce flow and persisting sessions. /// /// [storageKey] key to store the session with in [asyncStorage]. -/// The pkce code verifiers are suffixed with `-code-verifier` +/// The pkce code verifiers are suffixed with `-code-verifier`. /// /// Set [flowType] to [AuthFlowType.implicit] to perform old implicit auth flow. /// {@endtemplate} @@ -73,7 +73,9 @@ class GoTrueClient { final _onAuthStateChangeControllerSync = BehaviorSubject(sync: true); - /// Local storage to store pkce code verifiers. + /// Local storage to store session and pkce code verifiers. + /// + /// check [_initializedStorage] before usage. final GotrueAsyncStorage? _asyncStorage; /// Receive a notification every time an auth event happens. @@ -95,12 +97,18 @@ class GoTrueClient { Stream get onAuthStateChangeSync => _onAuthStateChangeControllerSync.stream; - final Completer _initalizedStorage = Completer(); + /// Completes when the [_asyncStorage] is initialized. + /// + /// Initialization is started in the constructor and should be awaited before + /// accessing the storage. + final Completer _initializedStorage = Completer(); final AuthFlowType _flowType; final bool _persistSession; + /// Key to store the session with in [_asyncStorage]. + /// The pkce code verifiers are suffixed with `-code-verifier`. final String _storageKey; final _log = Logger('supabase.auth'); @@ -149,7 +157,7 @@ class GoTrueClient { assert(asyncStorage != null || !_persistSession, 'You need to provide asyncStorage to persist session.'); if (asyncStorage != null) { - _initalizedStorage.complete( + _initializedStorage.complete( asyncStorage.initialize().catchError((e) => notifyException(e))); } @@ -179,7 +187,7 @@ class GoTrueClient { Future _initialize() async { try { if (_persistSession && _asyncStorage != null) { - await _initalizedStorage.future; + await _initializedStorage.future; final jsonStr = await _asyncStorage!.getItem(key: _storageKey); var shouldEmitInitialSession = true; if (jsonStr != null) { @@ -1204,8 +1212,8 @@ class GoTrueClient { _currentUser = session.user; if (_persistSession && _asyncStorage != null) { - if (!_initalizedStorage.isCompleted) { - await _initalizedStorage.future; + if (!_initializedStorage.isCompleted) { + await _initializedStorage.future; } _asyncStorage!.setItem( key: _storageKey, @@ -1220,12 +1228,10 @@ class GoTrueClient { _currentUser = null; if (_persistSession && _asyncStorage != null) { - if (!_initalizedStorage.isCompleted) { - await _initalizedStorage.future; + if (!_initializedStorage.isCompleted) { + await _initializedStorage.future; } - _asyncStorage!.removeItem( - key: _storageKey, - ); + _asyncStorage!.removeItem(key: _storageKey); } }