Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: Combine the storage interface for storage and pkce in gotrue_client #1087

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
refactor: store session and pkce in the same storage in gotrue_client
  • Loading branch information
Vinzent03 committed Nov 21, 2024
commit 0ae2b7149286d4719bbaafdf1ecb3d76c8b4eb7a
136 changes: 99 additions & 37 deletions packages/gotrue/lib/src/gotrue_client.dart
Original file line number Diff line number Diff line change
@@ -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.
@@ -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 = {
@@ -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<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
@@ -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<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() {
@@ -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);
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
@@ -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});

21 changes: 8 additions & 13 deletions packages/supabase/lib/src/supabase_client.dart
Original file line number Diff line number Diff line change
@@ -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,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,
);
}

Loading
Loading