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
Show file tree
Hide file tree
Changes from all commits
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
158 changes: 117 additions & 41 deletions packages/gotrue/lib/src/gotrue_client.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand Down Expand Up @@ -65,7 +73,9 @@ class GoTrueClient {
final _onAuthStateChangeControllerSync =
BehaviorSubject<AuthState>(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.
Expand All @@ -87,8 +97,20 @@ class GoTrueClient {
Stream<AuthState> get onAuthStateChangeSync =>
_onAuthStateChangeControllerSync.stream;

/// Completes when the [_asyncStorage] is initialized.
///
/// Initialization is started in the constructor and should be awaited before
/// accessing the storage.
final Completer<void> _initializedStorage = Completer<void>();

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');

/// Proxy to the web BroadcastChannel API. Should be null on non-web platforms.
Expand All @@ -101,8 +123,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,12 +135,14 @@ 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;
_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,
Expand All @@ -127,10 +153,19 @@ class GoTrueClient {
client: this,
fetch: _fetch,
);

assert(asyncStorage != null || !_persistSession,
'You need to provide asyncStorage to persist session.');
if (asyncStorage != null) {
_initializedStorage.complete(
asyncStorage.initialize().catchError((e) => notifyException(e)));
}

if (_autoRefreshToken) {
startAutoRefresh();
}

_initialize();
_mayStartBroadcastChannel();
}

Expand All @@ -148,6 +183,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 _initializedStorage.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 +238,7 @@ class GoTrueClient {

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

Expand Down Expand Up @@ -217,9 +283,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 +324,7 @@ class GoTrueClient {

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

Expand Down Expand Up @@ -312,7 +377,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 +404,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 +428,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 +498,7 @@ class GoTrueClient {
);
}

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

return authResponse;
Expand Down Expand Up @@ -472,9 +536,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 +622,7 @@ class GoTrueClient {
);
}

_saveSession(authResponse.session!);
await _saveSession(authResponse.session!);
notifyAllSubscribers(type == OtpType.recovery
? AuthChangeEvent.passwordRecovery
: AuthChangeEvent.signedIn);
Expand Down Expand Up @@ -594,9 +657,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 +894,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 +917,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 +950,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 +1039,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 +1054,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 +1185,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,21 +1205,38 @@ 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 (!_initializedStorage.isCompleted) {
await _initializedStorage.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 (!_initializedStorage.isCompleted) {
await _initializedStorage.future;
}
_asyncStorage!.removeItem(key: _storageKey);
}
}

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";
Expand All @@ -1170,7 +1246,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 +1271,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 +1323,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
Loading