Skip to content

Commit

Permalink
refactor: store session and pkce in the same storage in gotrue_client
Browse files Browse the repository at this point in the history
  • Loading branch information
Vinzent03 committed Nov 21, 2024
1 parent ccfcbf5 commit 0ae2b71
Show file tree
Hide file tree
Showing 8 changed files with 201 additions and 124 deletions.
136 changes: 99 additions & 37 deletions packages/gotrue/lib/src/gotrue_client.dart
Original file line number Diff line number Diff line change
Expand Up @@ -87,8 +87,14 @@ class GoTrueClient {
Stream<AuthState> get onAuthStateChangeSync =>
_onAuthStateChangeControllerSync.stream;

final Completer<void> _initalizedStorage = Completer<void>();

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.
Expand All @@ -101,8 +107,10 @@ class GoTrueClient {
String? url,
Map<String, String>? headers,
bool? autoRefreshToken,
bool? persistSession,
Client? httpClient,
GotrueAsyncStorage? asyncStorage,
String? storageKey,
AuthFlowType flowType = AuthFlowType.pkce,
}) : _url = url ?? Constants.defaultGotrueUrl,
_headers = {
Expand All @@ -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;
Expand All @@ -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();
}

Expand All @@ -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<void> _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
Expand All @@ -172,7 +222,7 @@ class GoTrueClient {

final session = authResponse.session;
if (session != null) {
_saveSession(session);
await _saveSession(session);
notifyAllSubscribers(AuthChangeEvent.signedIn);
}

Expand Down Expand Up @@ -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);
}

Expand Down Expand Up @@ -259,7 +308,7 @@ class GoTrueClient {

final session = authResponse.session;
if (session != null) {
_saveSession(session);
await _saveSession(session);
notifyAllSubscribers(AuthChangeEvent.signedIn);
}

Expand Down Expand Up @@ -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;
Expand All @@ -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.');
}
Expand All @@ -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 {
Expand Down Expand Up @@ -434,7 +482,7 @@ class GoTrueClient {
);
}

_saveSession(authResponse.session!);
await _saveSession(authResponse.session!);
notifyAllSubscribers(AuthChangeEvent.signedIn);

return authResponse;
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -559,7 +606,7 @@ class GoTrueClient {
);
}

_saveSession(authResponse.session!);
await _saveSession(authResponse.session!);
notifyAllSubscribers(type == OtpType.recovery
? AuthChangeEvent.passwordRecovery
: AuthChangeEvent.signedIn);
Expand Down Expand Up @@ -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';
}
Expand Down Expand Up @@ -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 {
Expand All @@ -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);
}

Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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) {
Expand All @@ -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);
Expand Down Expand Up @@ -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,
);

Expand All @@ -1146,17 +1189,36 @@ class GoTrueClient {
}

/// set currentSession and currentUser
void _saveSession(Session session) {
Future<void> _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<void> _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() {
Expand All @@ -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');
Expand All @@ -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);
}
Expand Down Expand Up @@ -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);
Expand Down
3 changes: 3 additions & 0 deletions packages/gotrue/lib/src/types/gotrue_async_storage.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
abstract class GotrueAsyncStorage {
const GotrueAsyncStorage();

/// May be implemented to allow for initialization of the storage before use.
Future<void> initialize() async {}

/// Retrieves an item asynchronously from the storage with the key.
Future<String?> getItem({required String key});

Expand Down
21 changes: 8 additions & 13 deletions packages/supabase/lib/src/supabase_client.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -273,22 +269,21 @@ 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';

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,
);
}

Expand Down
Loading

0 comments on commit 0ae2b71

Please sign in to comment.