Skip to content

Commit 0ae2b71

Browse files
committed
refactor: store session and pkce in the same storage in gotrue_client
1 parent ccfcbf5 commit 0ae2b71

File tree

8 files changed

+201
-124
lines changed

8 files changed

+201
-124
lines changed

packages/gotrue/lib/src/gotrue_client.dart

+99-37
Original file line numberDiff line numberDiff line change
@@ -87,8 +87,14 @@ class GoTrueClient {
8787
Stream<AuthState> get onAuthStateChangeSync =>
8888
_onAuthStateChangeControllerSync.stream;
8989

90+
final Completer<void> _initalizedStorage = Completer<void>();
91+
9092
final AuthFlowType _flowType;
9193

94+
final bool _persistSession;
95+
96+
final String _storageKey;
97+
9298
final _log = Logger('supabase.auth');
9399

94100
/// Proxy to the web BroadcastChannel API. Should be null on non-web platforms.
@@ -101,8 +107,10 @@ class GoTrueClient {
101107
String? url,
102108
Map<String, String>? headers,
103109
bool? autoRefreshToken,
110+
bool? persistSession,
104111
Client? httpClient,
105112
GotrueAsyncStorage? asyncStorage,
113+
String? storageKey,
106114
AuthFlowType flowType = AuthFlowType.pkce,
107115
}) : _url = url ?? Constants.defaultGotrueUrl,
108116
_headers = {
@@ -111,7 +119,9 @@ class GoTrueClient {
111119
},
112120
_httpClient = httpClient,
113121
_asyncStorage = asyncStorage,
114-
_flowType = flowType {
122+
_flowType = flowType,
123+
_persistSession = persistSession ?? false,
124+
_storageKey = storageKey ?? Constants.defaultStorageKey {
115125
_autoRefreshToken = autoRefreshToken ?? true;
116126

117127
final gotrueUrl = url ?? Constants.defaultGotrueUrl;
@@ -127,10 +137,19 @@ class GoTrueClient {
127137
client: this,
128138
fetch: _fetch,
129139
);
140+
141+
assert(asyncStorage != null || !_persistSession,
142+
'You need to provide asyncStorage to persist session.');
143+
if (asyncStorage != null) {
144+
_initalizedStorage.complete(
145+
asyncStorage.initialize().catchError((e) => notifyException(e)));
146+
}
147+
130148
if (_autoRefreshToken) {
131149
startAutoRefresh();
132150
}
133151

152+
_initialize();
134153
_mayStartBroadcastChannel();
135154
}
136155

@@ -148,6 +167,37 @@ class GoTrueClient {
148167
/// Returns the current session, if any;
149168
Session? get currentSession => _currentSession;
150169

170+
/// This method should not throw as it is called from the constructor.
171+
Future<void> _initialize() async {
172+
try {
173+
if (_persistSession && _asyncStorage != null) {
174+
await _initalizedStorage.future;
175+
final jsonStr = await _asyncStorage!.getItem(key: _storageKey);
176+
var shouldEmitInitialSession = true;
177+
if (jsonStr != null) {
178+
await setInitialSession(jsonStr);
179+
shouldEmitInitialSession = false;
180+
181+
// Only try to recover session if the session got set in [setInitialSession]
182+
// because if not the session is missing data and already notified an
183+
// exception.
184+
if (currentSession != null) {
185+
// [notifyException] gets already called here if needed, so we can
186+
// catch any error.
187+
recoverSession(jsonStr).then((_) {}, onError: (_) {});
188+
}
189+
}
190+
if (shouldEmitInitialSession) {
191+
// Emit a null session if the user did not have persisted session
192+
notifyAllSubscribers(AuthChangeEvent.initialSession);
193+
}
194+
}
195+
} catch (error, stackTrace) {
196+
_log.warning('Error while loading initial session', error, stackTrace);
197+
notifyException(error, stackTrace);
198+
}
199+
}
200+
151201
/// Creates a new anonymous user.
152202
///
153203
/// Returns An `AuthResponse` with a session where the `is_anonymous` claim
@@ -172,7 +222,7 @@ class GoTrueClient {
172222

173223
final session = authResponse.session;
174224
if (session != null) {
175-
_saveSession(session);
225+
await _saveSession(session);
176226
notifyAllSubscribers(AuthChangeEvent.signedIn);
177227
}
178228

@@ -217,9 +267,8 @@ class GoTrueClient {
217267
assert(_asyncStorage != null,
218268
'You need to provide asyncStorage to perform pkce flow.');
219269
final codeVerifier = generatePKCEVerifier();
220-
await _asyncStorage!.setItem(
221-
key: '${Constants.defaultStorageKey}-code-verifier',
222-
value: codeVerifier);
270+
await _asyncStorage!
271+
.setItem(key: '$_storageKey-code-verifier', value: codeVerifier);
223272
codeChallenge = generatePKCEChallenge(codeVerifier);
224273
}
225274

@@ -259,7 +308,7 @@ class GoTrueClient {
259308

260309
final session = authResponse.session;
261310
if (session != null) {
262-
_saveSession(session);
311+
await _saveSession(session);
263312
notifyAllSubscribers(AuthChangeEvent.signedIn);
264313
}
265314

@@ -312,7 +361,7 @@ class GoTrueClient {
312361
final authResponse = AuthResponse.fromJson(response);
313362

314363
if (authResponse.session?.accessToken != null) {
315-
_saveSession(authResponse.session!);
364+
await _saveSession(authResponse.session!);
316365
notifyAllSubscribers(AuthChangeEvent.signedIn);
317366
}
318367
return authResponse;
@@ -339,8 +388,8 @@ class GoTrueClient {
339388
assert(_asyncStorage != null,
340389
'You need to provide asyncStorage to perform pkce flow.');
341390

342-
final codeVerifierRawString = await _asyncStorage!
343-
.getItem(key: '${Constants.defaultStorageKey}-code-verifier');
391+
final codeVerifierRawString =
392+
await _asyncStorage!.getItem(key: '$_storageKey-code-verifier');
344393
if (codeVerifierRawString == null) {
345394
throw AuthException('Code verifier could not be found in local storage.');
346395
}
@@ -363,14 +412,13 @@ class GoTrueClient {
363412
),
364413
);
365414

366-
await _asyncStorage!
367-
.removeItem(key: '${Constants.defaultStorageKey}-code-verifier');
415+
await _asyncStorage!.removeItem(key: '$_storageKey-code-verifier');
368416

369417
final authSessionUrlResponse = AuthSessionUrlResponse(
370418
session: Session.fromJson(response)!, redirectType: redirectType?.name);
371419

372420
final session = authSessionUrlResponse.session;
373-
_saveSession(session);
421+
await _saveSession(session);
374422
if (redirectType == AuthChangeEvent.passwordRecovery) {
375423
notifyAllSubscribers(AuthChangeEvent.passwordRecovery);
376424
} else {
@@ -434,7 +482,7 @@ class GoTrueClient {
434482
);
435483
}
436484

437-
_saveSession(authResponse.session!);
485+
await _saveSession(authResponse.session!);
438486
notifyAllSubscribers(AuthChangeEvent.signedIn);
439487

440488
return authResponse;
@@ -472,9 +520,8 @@ class GoTrueClient {
472520
assert(_asyncStorage != null,
473521
'You need to provide asyncStorage to perform pkce flow.');
474522
final codeVerifier = generatePKCEVerifier();
475-
await _asyncStorage!.setItem(
476-
key: '${Constants.defaultStorageKey}-code-verifier',
477-
value: codeVerifier);
523+
await _asyncStorage!
524+
.setItem(key: '$_storageKey-code-verifier', value: codeVerifier);
478525
codeChallenge = generatePKCEChallenge(codeVerifier);
479526
}
480527
await _fetch.request(
@@ -559,7 +606,7 @@ class GoTrueClient {
559606
);
560607
}
561608

562-
_saveSession(authResponse.session!);
609+
await _saveSession(authResponse.session!);
563610
notifyAllSubscribers(type == OtpType.recovery
564611
? AuthChangeEvent.passwordRecovery
565612
: AuthChangeEvent.signedIn);
@@ -594,9 +641,8 @@ class GoTrueClient {
594641
assert(_asyncStorage != null,
595642
'You need to provide asyncStorage to perform pkce flow.');
596643
final codeVerifier = generatePKCEVerifier();
597-
await _asyncStorage!.setItem(
598-
key: '${Constants.defaultStorageKey}-code-verifier',
599-
value: codeVerifier);
644+
await _asyncStorage!
645+
.setItem(key: '$_storageKey-code-verifier', value: codeVerifier);
600646
codeChallenge = generatePKCEChallenge(codeVerifier);
601647
codeChallengeMethod = codeVerifier == codeChallenge ? 'plain' : 's256';
602648
}
@@ -832,7 +878,7 @@ class GoTrueClient {
832878
final redirectType = url.queryParameters['type'];
833879

834880
if (storeSession == true) {
835-
_saveSession(session);
881+
await _saveSession(session);
836882
if (redirectType == 'recovery') {
837883
notifyAllSubscribers(AuthChangeEvent.passwordRecovery);
838884
} else {
@@ -855,9 +901,8 @@ class GoTrueClient {
855901
final accessToken = currentSession?.accessToken;
856902

857903
if (scope != SignOutScope.others) {
858-
_removeSession();
859-
await _asyncStorage?.removeItem(
860-
key: '${Constants.defaultStorageKey}-code-verifier');
904+
await _removeSession();
905+
await _asyncStorage?.removeItem(key: '$_storageKey-code-verifier');
861906
notifyAllSubscribers(AuthChangeEvent.signedOut);
862907
}
863908

@@ -889,7 +934,7 @@ class GoTrueClient {
889934
'You need to provide asyncStorage to perform pkce flow.');
890935
final codeVerifier = generatePKCEVerifier();
891936
await _asyncStorage!.setItem(
892-
key: '${Constants.defaultStorageKey}-code-verifier',
937+
key: '$_storageKey-code-verifier',
893938
value: '$codeVerifier/${AuthChangeEvent.passwordRecovery.name}',
894939
);
895940
codeChallenge = generatePKCEChallenge(codeVerifier);
@@ -978,9 +1023,7 @@ class GoTrueClient {
9781023
if (session == null) {
9791024
_log.warning("Can't recover session from string, session is null");
9801025
await signOut();
981-
throw notifyException(
982-
AuthException('Current session is missing data.'),
983-
);
1026+
throw AuthException('Session to restore is missing data.');
9841027
}
9851028

9861029
if (session.isExpired) {
@@ -995,7 +1038,7 @@ class GoTrueClient {
9951038
} else {
9961039
final shouldEmitEvent = _currentSession == null ||
9971040
_currentSession?.user.id != session.user.id;
998-
_saveSession(session);
1041+
await _saveSession(session);
9991042

10001043
if (shouldEmitEvent) {
10011044
notifyAllSubscribers(AuthChangeEvent.tokenRefreshed);
@@ -1126,7 +1169,7 @@ class GoTrueClient {
11261169
'You need to provide asyncStorage to perform pkce flow.');
11271170
final codeVerifier = generatePKCEVerifier();
11281171
await _asyncStorage!.setItem(
1129-
key: '${Constants.defaultStorageKey}-code-verifier',
1172+
key: '$_storageKey-code-verifier',
11301173
value: codeVerifier,
11311174
);
11321175

@@ -1146,17 +1189,36 @@ class GoTrueClient {
11461189
}
11471190

11481191
/// set currentSession and currentUser
1149-
void _saveSession(Session session) {
1192+
Future<void> _saveSession(Session session) async {
11501193
_log.finest('Saving session: $session');
11511194
_log.fine('Saving session');
11521195
_currentSession = session;
11531196
_currentUser = session.user;
1197+
1198+
if (_persistSession && _asyncStorage != null) {
1199+
if (!_initalizedStorage.isCompleted) {
1200+
await _initalizedStorage.future;
1201+
}
1202+
_asyncStorage!.setItem(
1203+
key: _storageKey,
1204+
value: jsonEncode(session.toJson()),
1205+
);
1206+
}
11541207
}
11551208

1156-
void _removeSession() {
1209+
Future<void> _removeSession() async {
11571210
_log.fine('Removing session');
11581211
_currentSession = null;
11591212
_currentUser = null;
1213+
1214+
if (_persistSession && _asyncStorage != null) {
1215+
if (!_initalizedStorage.isCompleted) {
1216+
await _initalizedStorage.future;
1217+
}
1218+
_asyncStorage!.removeItem(
1219+
key: _storageKey,
1220+
);
1221+
}
11601222
}
11611223

11621224
void _mayStartBroadcastChannel() {
@@ -1170,7 +1232,7 @@ class GoTrueClient {
11701232
try {
11711233
_broadcastChannel = web.getBroadcastChannel(broadcastKey);
11721234
_broadcastChannelSubscription =
1173-
_broadcastChannel?.onMessage.listen((messageEvent) {
1235+
_broadcastChannel?.onMessage.listen((messageEvent) async {
11741236
final rawEvent = messageEvent['event'];
11751237
_log.finest('Received broadcast message: $messageEvent');
11761238
_log.info('Received broadcast event: $rawEvent');
@@ -1195,9 +1257,9 @@ class GoTrueClient {
11951257
session = Session.fromJson(messageEvent['session']);
11961258
}
11971259
if (session != null) {
1198-
_saveSession(session);
1260+
await _saveSession(session);
11991261
} else {
1200-
_removeSession();
1262+
await _removeSession();
12011263
}
12021264
notifyAllSubscribers(event, session: session, broadcast: false);
12031265
}
@@ -1247,14 +1309,14 @@ class GoTrueClient {
12471309
throw AuthSessionMissingException();
12481310
}
12491311

1250-
_saveSession(session);
1312+
await _saveSession(session);
12511313
notifyAllSubscribers(AuthChangeEvent.tokenRefreshed);
12521314

12531315
_refreshTokenCompleter?.complete(data);
12541316
return data;
12551317
} on AuthException catch (error, stack) {
12561318
if (error is! AuthRetryableFetchException) {
1257-
_removeSession();
1319+
await _removeSession();
12581320
notifyAllSubscribers(AuthChangeEvent.signedOut);
12591321
} else {
12601322
notifyException(error, stack);

packages/gotrue/lib/src/types/gotrue_async_storage.dart

+3
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22
abstract class GotrueAsyncStorage {
33
const GotrueAsyncStorage();
44

5+
/// May be implemented to allow for initialization of the storage before use.
6+
Future<void> initialize() async {}
7+
58
/// Retrieves an item asynchronously from the storage with the key.
69
Future<String?> getItem({required String key});
710

packages/supabase/lib/src/supabase_client.dart

+8-13
Original file line numberDiff line numberDiff line change
@@ -137,11 +137,7 @@ class SupabaseClient {
137137
},
138138
_httpClient = httpClient,
139139
_isolate = isolate ?? (YAJsonIsolate()..initialize()) {
140-
_authInstance = _initSupabaseAuthClient(
141-
autoRefreshToken: authOptions.autoRefreshToken,
142-
gotrueAsyncStorage: authOptions.pkceAsyncStorage,
143-
authFlowType: authOptions.authFlowType,
144-
);
140+
_authInstance = _initSupabaseAuthClient(authOptions: authOptions);
145141
_authHttpClient =
146142
AuthHttpClient(_supabaseKey, httpClient ?? Client(), _getAccessToken);
147143
rest = _initRestClient();
@@ -273,22 +269,21 @@ class SupabaseClient {
273269
_authInstance?.dispose();
274270
}
275271

276-
GoTrueClient _initSupabaseAuthClient({
277-
bool? autoRefreshToken,
278-
required GotrueAsyncStorage? gotrueAsyncStorage,
279-
required AuthFlowType authFlowType,
280-
}) {
272+
GoTrueClient _initSupabaseAuthClient(
273+
{required AuthClientOptions authOptions}) {
281274
final authHeaders = {...headers};
282275
authHeaders['apikey'] = _supabaseKey;
283276
authHeaders['Authorization'] = 'Bearer $_supabaseKey';
284277

285278
return GoTrueClient(
286279
url: _authUrl,
287280
headers: authHeaders,
288-
autoRefreshToken: autoRefreshToken,
281+
autoRefreshToken: authOptions.autoRefreshToken,
289282
httpClient: _httpClient,
290-
asyncStorage: gotrueAsyncStorage,
291-
flowType: authFlowType,
283+
asyncStorage: authOptions.asyncStorage,
284+
storageKey: authOptions.storageKey,
285+
persistSession: authOptions.persistSession,
286+
flowType: authOptions.authFlowType,
292287
);
293288
}
294289

0 commit comments

Comments
 (0)