From 6cbcbdd3e1a4b4c6c3450403cb1d3a0061d5ec13 Mon Sep 17 00:00:00 2001 From: dshukertjr Date: Sun, 10 Mar 2024 12:11:04 +0900 Subject: [PATCH 01/25] align _callRefreshSession --- packages/gotrue/lib/src/gotrue_client.dart | 152 ++++++++++-------- packages/gotrue/lib/src/helper.dart | 5 + .../gotrue/lib/src/types/auth_exception.dart | 8 + packages/gotrue/pubspec.yaml | 1 + 4 files changed, 99 insertions(+), 67 deletions(-) diff --git a/packages/gotrue/lib/src/gotrue_client.dart b/packages/gotrue/lib/src/gotrue_client.dart index fcacd8ed..5db4bfb0 100644 --- a/packages/gotrue/lib/src/gotrue_client.dart +++ b/packages/gotrue/lib/src/gotrue_client.dart @@ -1,6 +1,5 @@ import 'dart:async'; import 'dart:convert'; -import 'dart:math'; import 'package:collection/collection.dart'; import 'package:gotrue/gotrue.dart'; @@ -12,6 +11,7 @@ import 'package:gotrue/src/types/fetch_options.dart'; import 'package:http/http.dart'; import 'package:jwt_decode/jwt_decode.dart'; import 'package:meta/meta.dart'; +import 'package:retry/retry.dart'; import 'package:rxdart/subjects.dart'; part 'gotrue_mfa_api.dart'; @@ -50,6 +50,7 @@ class GoTrueClient { late bool _autoRefreshToken; + /// Timer to refresh the token including retries. Timer? _refreshTokenTimer; int _refreshTokenRetryCount = 0; @@ -711,7 +712,7 @@ class GoTrueClient { if (refreshToken.isEmpty) { throw AuthException('No current session.'); } - return await _callRefreshToken(refreshToken: refreshToken); + return await _callRefreshToken(refreshToken); } /// Gets the session data from a magic link or oauth2 callback URL @@ -924,11 +925,9 @@ class GoTrueClient { } if (session.isExpired) { - if (_autoRefreshToken && session.refreshToken != null) { - return await _callRefreshToken( - refreshToken: session.refreshToken, - accessToken: session.accessToken, - ); + final refreshToken = session.refreshToken; + if (_autoRefreshToken && refreshToken != null) { + return await _callRefreshToken(refreshToken); } else { await signOut(); throw notifyException(AuthException('Session expired.')); @@ -944,6 +943,53 @@ class GoTrueClient { } } + /// Starts an auto-refresh process in the background. Close to the time of expiration a process is started to + /// refresh the session. If refreshing fails it will be retried for as long as necessary. + /// + /// If you set the `autoRefreshToken` to `true`, you don't need to call this function, it will be called for you. + void startAutoRefresh() async { + stopAutoRefresh(); + + // _saveSession restarts the timer with the current time. + final session = _currentSession; + if (session != null) { + _saveSession(session); + } + } + + /// Stops an active auto refresh process running in the background (if any). + /// + /// If you call this method any managed visibility change callback will be + /// removed and you must manage visibility changes on your own. + void stopAutoRefresh() { + _refreshTokenTimer?.cancel(); + _refreshTokenTimer = null; + } + + /// Generates a new JWT. + /// @param refreshToken A valid refresh token that was returned on login. + Future _refreshAccessToken(String refreshToken) async { + return await retry( + // Make a GET request + () async { + final options = GotrueRequestOptions( + headers: _headers, + body: {'refresh_token': refreshToken}, + query: {'grant_type': 'refresh_token'}); + final response = await _fetch + .request('$_url/token', RequestMethodType.post, options: options); + final authResponse = AuthResponse.fromJson(response); + return authResponse; + }, + // Retry on SocketException or TimeoutException + retryIf: (e) { + return e is ClientException; + }, + maxDelay: Duration(seconds: 10), + maxAttempts: 99999, + ); + } + /// Returns the OAuth sign in URL constructed from the [url] parameter. Future _getUrlForProvider( OAuthProvider provider, { @@ -1023,11 +1069,7 @@ class GoTrueClient { if (_refreshTokenRetryCount < Constants.maxRetryCount) { _refreshTokenTimer = Timer(timerDuration, () async { try { - await _callRefreshToken( - refreshToken: refreshToken, - accessToken: accessToken, - ignorePendingRequest: true, - ); + await _callRefreshToken(refreshToken); } catch (_) { // Catch any error, because in this case they should be handled by listening to [onAuthStateChange] } @@ -1053,73 +1095,49 @@ class GoTrueClient { /// To call [_callRefreshToken] during a running request [ignorePendingRequest] is used to bypass that check. /// /// When a [ClientException] occurs [_setTokenRefreshTimer] is used to schedule a retry in the background, which emits the result via [onAuthStateChange]. - Future _callRefreshToken({ - String? refreshToken, - String? accessToken, - bool ignorePendingRequest = false, - }) async { - if (_refreshTokenCompleter?.isCompleted ?? true) { - _refreshTokenCompleter = Completer(); - // Catch any error in case nobody awaits the future - _refreshTokenCompleter!.future.then( - (value) => null, - onError: (error, stack) => null, - ); - } else if (!ignorePendingRequest) { + Future _callRefreshToken(String refreshToken) async { + // Refreshing is already in progress + if (_refreshTokenCompleter != null) { return _refreshTokenCompleter!.future; } - final token = refreshToken ?? currentSession?.refreshToken; - if (token == null) { - throw AuthException('No current session.'); - } - - final jwt = accessToken ?? currentSession?.accessToken; try { - final body = {'refresh_token': token}; - if (jwt != null) { - _headers['Authorization'] = 'Bearer $jwt'; - } - final options = GotrueRequestOptions( - headers: _headers, - body: body, - query: {'grant_type': 'refresh_token'}); - final response = await _fetch - .request('$_url/token', RequestMethodType.post, options: options); - final authResponse = AuthResponse.fromJson(response); - - if (authResponse.session == null) { - throw AuthException('Invalid session data.'); - } + _refreshTokenCompleter = Completer(); - _saveSession(authResponse.session!); - if (!_refreshTokenCompleter!.isCompleted) { - _refreshTokenCompleter!.complete(authResponse); + final data = await _refreshAccessToken(refreshToken); + + final session = data.session; + + if (session == null) { + throw AuthSessionMissingError(); } + _saveSession(session); notifyAllSubscribers(AuthChangeEvent.tokenRefreshed); - return authResponse; - } on ClientException catch (e, stack) { - _setTokenRefreshTimer( - Constants.retryInterval * pow(2, _refreshTokenRetryCount), - refreshToken: token, - accessToken: accessToken, - ); - if (!_refreshTokenCompleter!.isCompleted) { - _refreshTokenCompleter!.completeError(e, stack); - } - rethrow; - } catch (error, stack) { + + _refreshTokenCompleter?.complete(data); + return data; + } catch (error) { + // this._debug(debugName, 'error', error) + if (error is AuthException) { - if (error.message.startsWith('Invalid Refresh Token:')) { - await signOut(); + // const result = { session: null, error } + + if (!isAuthRetryableFetchError(error)) { + _removeSession(); + + notifyAllSubscribers(AuthChangeEvent.signedOut); } + + _refreshTokenCompleter?.completeError(error); + + rethrow; } - if (!_refreshTokenCompleter!.isCompleted) { - _refreshTokenCompleter!.completeError(error, stack); - } - _onAuthStateChangeController.addError(error, stack); + + _refreshTokenCompleter?.completeError(error); rethrow; + } finally { + _refreshTokenCompleter = null; } } diff --git a/packages/gotrue/lib/src/helper.dart b/packages/gotrue/lib/src/helper.dart index c4d850d3..c5b79d4c 100644 --- a/packages/gotrue/lib/src/helper.dart +++ b/packages/gotrue/lib/src/helper.dart @@ -2,6 +2,7 @@ import 'dart:convert'; import 'dart:math'; import 'package:crypto/crypto.dart'; +import 'package:gotrue/gotrue.dart'; /// Converts base 10 int into String representation of base 16 int and takes the last two digets. String dec2hex(int dec) { @@ -21,3 +22,7 @@ String generatePKCEChallenge(String verifier) { return base64UrlEncode(sha256.convert(ascii.encode(verifier)).bytes) .split('=')[0]; } + +bool isAuthRetryableFetchError(Object error) { + return error is AuthRetryableFetchError; +} diff --git a/packages/gotrue/lib/src/types/auth_exception.dart b/packages/gotrue/lib/src/types/auth_exception.dart index df3fe7ae..3125b9bd 100644 --- a/packages/gotrue/lib/src/types/auth_exception.dart +++ b/packages/gotrue/lib/src/types/auth_exception.dart @@ -24,3 +24,11 @@ class AuthException implements Exception { class AuthPKCEGrantCodeExchangeError extends AuthException { AuthPKCEGrantCodeExchangeError(String message) : super(message); } + +class AuthSessionMissingError extends AuthException { + AuthSessionMissingError() : super('Auth session missing!', statusCode: '400'); +} + +class AuthRetryableFetchError extends AuthException { + AuthRetryableFetchError() : super('AuthRetryableFetchError'); +} diff --git a/packages/gotrue/pubspec.yaml b/packages/gotrue/pubspec.yaml index 798ca3eb..857b2870 100644 --- a/packages/gotrue/pubspec.yaml +++ b/packages/gotrue/pubspec.yaml @@ -13,6 +13,7 @@ dependencies: crypto: ^3.0.2 http: '>=0.13.0 <2.0.0' jwt_decode: ^0.3.1 + retry: ^3.1.0 rxdart: ^0.27.7 meta: ^1.7.0 From a9d522818df2b57b27373523496c64f862ec17e5 Mon Sep 17 00:00:00 2001 From: dshukertjr Date: Mon, 11 Mar 2024 16:54:21 +0900 Subject: [PATCH 02/25] complete fetch rework --- packages/gotrue/lib/src/fetch.dart | 140 +++++++++++------- .../gotrue/lib/src/types/auth_exception.dart | 22 +++ 2 files changed, 111 insertions(+), 51 deletions(-) diff --git a/packages/gotrue/lib/src/fetch.dart b/packages/gotrue/lib/src/fetch.dart index ac10a3aa..9d2ab905 100644 --- a/packages/gotrue/lib/src/fetch.dart +++ b/packages/gotrue/lib/src/fetch.dart @@ -1,8 +1,8 @@ import 'dart:convert'; +import 'package:collection/collection.dart'; import 'package:gotrue/src/types/auth_exception.dart'; import 'package:gotrue/src/types/fetch_options.dart'; -import 'package:http/http.dart' as http; import 'package:http/http.dart'; enum RequestMethodType { get, post, put, delete } @@ -16,23 +16,42 @@ class GotrueFetch { return code >= 200 && code <= 299; } - AuthException _handleError(http.Response error) { - late AuthException errorRes; + AuthException _handleError(dynamic error) { + if (error is! Response) { + throw AuthRetryableFetchError(); + } + + // If the status is 500 or above, it's likely a server error, + // and can be retried. + if (error.statusCode >= 500) { + throw AuthRetryableFetchError(); + } + final dynamic data; try { - final parsedJson = json.decode(error.body) as Map; - final String message = (parsedJson['msg'] ?? - parsedJson['message'] ?? - parsedJson['error_description'] ?? - parsedJson['error'] ?? - error.body) - .toString(); - errorRes = AuthException(message, statusCode: '${error.statusCode}'); - } catch (_) { - errorRes = AuthException(error.body, statusCode: '${error.statusCode}'); + data = jsonDecode(error.body); + } catch (error) { + // TODO: properly provide the error message + throw AuthUnknownError(message: error.toString(), originalError: error); + } + + // Check if weak password reasons only contain strings + if (data is Map && + data['weak_password'] is Map && + data['weak_password']['reasons'] is List && + (data['weak_password']['reasons'] as List).isNotEmpty && + (data['weak_password']['reasons'] as List) + .whereNot((element) => element is String) + .isEmpty) { + // TODO: properly provide the error message + throw AuthWeakPasswordError( + message: '', + statusCode: error.statusCode.toString(), + reasons: data['weak_password']['reasons'], + ); } - return errorRes; + throw AuthApiError('An', statusCode: error.statusCode.toString()); } Future request( @@ -51,52 +70,71 @@ class GotrueFetch { } Uri uri = Uri.parse(url); uri = uri.replace(queryParameters: {...uri.queryParameters, ...qs}); - late final http.Response response; + final response = await _handleRequest( + method: method, uri: uri, options: options, headers: headers); + } + + Future _handleRequest({ + required RequestMethodType method, + required Uri uri, + required GotrueRequestOptions? options, + required Map headers, + }) async { final bodyStr = json.encode(options?.body ?? {}); if (method != RequestMethodType.get) { headers['Content-Type'] = 'application/json'; } - switch (method) { - case RequestMethodType.get: - response = await (httpClient?.get ?? http.get)( - uri, - headers: headers, - ); - - break; - case RequestMethodType.post: - response = await (httpClient?.post ?? http.post)( - uri, - headers: headers, - body: bodyStr, - ); - break; - case RequestMethodType.put: - response = await (httpClient?.put ?? http.put)( - uri, - headers: headers, - body: bodyStr, - ); - break; - case RequestMethodType.delete: - response = await (httpClient?.delete ?? http.delete)( - uri, - headers: headers, - body: bodyStr, - ); - break; - } + Response response; + try { + switch (method) { + case RequestMethodType.get: + response = await (httpClient?.get ?? get)( + uri, + headers: headers, + ); - if (isSuccessStatusCode(response.statusCode)) { - if (options?.noResolveJson == true) { - return response.body; - } else { - return json.decode(utf8.decode(response.bodyBytes)); + break; + case RequestMethodType.post: + response = await (httpClient?.post ?? post)( + uri, + headers: headers, + body: bodyStr, + ); + break; + case RequestMethodType.put: + response = await (httpClient?.put ?? put)( + uri, + headers: headers, + body: bodyStr, + ); + break; + case RequestMethodType.delete: + response = await (httpClient?.delete ?? delete)( + uri, + headers: headers, + body: bodyStr, + ); + break; } - } else { + } catch (e) { + // fetch failed, likely due to a network or CORS error + throw AuthRetryableFetchError(); + } + + if (!isSuccessStatusCode(response.statusCode)) { throw _handleError(response); } + + if (options?.noResolveJson == true) { + return response.body; + } + + try { + return json.decode(utf8.decode(response.bodyBytes)); + } catch (error) { + throw _handleError(error); + } } } diff --git a/packages/gotrue/lib/src/types/auth_exception.dart b/packages/gotrue/lib/src/types/auth_exception.dart index 3125b9bd..431b43a6 100644 --- a/packages/gotrue/lib/src/types/auth_exception.dart +++ b/packages/gotrue/lib/src/types/auth_exception.dart @@ -32,3 +32,25 @@ class AuthSessionMissingError extends AuthException { class AuthRetryableFetchError extends AuthException { AuthRetryableFetchError() : super('AuthRetryableFetchError'); } + +class AuthApiError extends AuthException { + AuthApiError(String message, {String? statusCode}) + : super(message, statusCode: statusCode); +} + +class AuthUnknownError extends AuthException { + final Object originalError; + + AuthUnknownError({required String message, required this.originalError}) + : super(message); +} + +class AuthWeakPasswordError extends AuthException { + final List reasons; + + AuthWeakPasswordError({ + required String message, + required String statusCode, + required this.reasons, + }) : super(message, statusCode: statusCode); +} From 2fa41397867f359e8b75662574f86f52a02c3904 Mon Sep 17 00:00:00 2001 From: dshukertjr Date: Fri, 22 Mar 2024 12:45:13 +0700 Subject: [PATCH 03/25] Update the methods on gotrue-dart --- packages/gotrue/lib/src/constants.dart | 3 + packages/gotrue/lib/src/gotrue_client.dart | 138 +++++++++++---------- 2 files changed, 76 insertions(+), 65 deletions(-) diff --git a/packages/gotrue/lib/src/constants.dart b/packages/gotrue/lib/src/constants.dart index 6e5f0bb5..1b50b4b6 100644 --- a/packages/gotrue/lib/src/constants.dart +++ b/packages/gotrue/lib/src/constants.dart @@ -14,6 +14,9 @@ class Constants { static const expiryMargin = Duration(seconds: 10); static const int maxRetryCount = 10; static const retryInterval = Duration(milliseconds: 200); + static const autoRefreshTickDuration = Duration(seconds: 30); + static const autoRefreshTockThreshold = 3; + // AUTO_REFRESH_TICK_THRESHOLD } enum AuthChangeEvent { diff --git a/packages/gotrue/lib/src/gotrue_client.dart b/packages/gotrue/lib/src/gotrue_client.dart index 5db4bfb0..b51c880d 100644 --- a/packages/gotrue/lib/src/gotrue_client.dart +++ b/packages/gotrue/lib/src/gotrue_client.dart @@ -50,10 +50,7 @@ class GoTrueClient { late bool _autoRefreshToken; - /// Timer to refresh the token including retries. - Timer? _refreshTokenTimer; - - int _refreshTokenRetryCount = 0; + Timer? _autoRefreshTicker; /// Completer to combine multiple simultaneous token refresh requests. Completer? _refreshTokenCompleter; @@ -580,14 +577,22 @@ class GoTrueClient { return res['url'] as String; } - /// Force refreshes the session including the user data in case it was updated - /// in a different session. - Future refreshSession() async { + /// Returns a new session, regardless of expiry status. + /// Takes in an optional current session. If not passed in, then refreshSession() will attempt to retrieve it from getSession(). + /// If the current session's refresh token is invalid, an error will be thrown. + Future refreshSession([String? refreshToken]) async { if (currentSession?.accessToken == null) { throw AuthException('Not logged in.'); } - return await _callRefreshToken(); + final currentSessionRefreshToken = + _currentSession?.refreshToken ?? refreshToken; + + if (currentSessionRefreshToken == null) { + throw AuthSessionMissingError(); + } + + return await _callRefreshToken(currentSessionRefreshToken); } /// Sends a reauthentication OTP to the user's email or phone number. @@ -950,11 +955,14 @@ class GoTrueClient { void startAutoRefresh() async { stopAutoRefresh(); - // _saveSession restarts the timer with the current time. - final session = _currentSession; - if (session != null) { - _saveSession(session); - } + // final ticker = setInterval(() => this._autoRefreshTokenTick(), AUTO_REFRESH_TICK_DURATION) + _autoRefreshTicker = Timer.periodic( + Constants.autoRefreshTickDuration, + (Timer t) => _autoRefreshTokenTick(), + ); + + await Future.delayed(Duration.zero); + await _autoRefreshTokenTick(); } /// Stops an active auto refresh process running in the background (if any). @@ -962,13 +970,54 @@ class GoTrueClient { /// If you call this method any managed visibility change callback will be /// removed and you must manage visibility changes on your own. void stopAutoRefresh() { - _refreshTokenTimer?.cancel(); - _refreshTokenTimer = null; + // TODO: we might need to move this into supabase_flutter to remove the visibility change callback + final ticker = _autoRefreshTicker; + _autoRefreshTicker = null; + } + + Future _autoRefreshTokenTick() async { + // TODO: should we implement browser lock in Flutter as well? + try { + final now = DateTime.now(); + final session = _currentSession; + if (session == null) { + return; + } + final refreshToken = session.refreshToken; + if (refreshToken == null) { + // TODO: properly handle case where refresh token is null + // check the js implementation + return; + } + + final expiresAt = session.expiresAt; + + if (expiresAt == null) { + // TODO: properly handle expiresAt being null + // check the js implementation + return; + } + + final expiresInTicks = + ((DateTime.fromMillisecondsSinceEpoch(expiresAt * 1000) + .difference(now)) + .inMilliseconds / + Constants.autoRefreshTickDuration.inMilliseconds) + .floor(); + + // Only tick if the next tick comes after the retry threshold + if (expiresInTicks <= Constants.autoRefreshTockThreshold) { + await _callRefreshToken(refreshToken); + } + } catch (error) { + // TODO: in js, it's just printing here + } } /// Generates a new JWT. - /// @param refreshToken A valid refresh token that was returned on login. + /// [refreshToken] A valid refresh token that was returned on login. Future _refreshAccessToken(String refreshToken) async { + final startedAt = DateTime.now(); return await retry( // Make a GET request () async { @@ -981,8 +1030,10 @@ class GoTrueClient { final authResponse = AuthResponse.fromJson(response); return authResponse; }, - // Retry on SocketException or TimeoutException retryIf: (e) { + // Do not retry if the next retry comes after the next tick. +// Date.now() + (attempt + 1) * 200 - startedAt < AUTO_REFRESH_TICK_DURATION +// TODO: Compare the remaining time until tik and the time until the next retry return e is ClientException; }, maxDelay: Duration(seconds: 10), @@ -1033,58 +1084,20 @@ class GoTrueClient { return OAuthResponse(provider: provider, url: oauthUrl); } - void _saveSession(Session session) async { + /// set currentSession and currentUser + /// process to _startAutoRefreshToken if possible + void _saveSession(Session session) { _currentSession = session; _currentUser = session.user; - final expiresAt = session.expiresAt; - if (_autoRefreshToken && expiresAt != null) { - _refreshTokenTimer?.cancel(); - - final timeNow = (DateTime.now().millisecondsSinceEpoch / 1000).round(); - final expiresIn = expiresAt - timeNow; - final refreshDurationBeforeExpires = expiresIn > 60 ? 60 : 1; - final nextDuration = expiresIn - refreshDurationBeforeExpires; - try { - if (nextDuration > 0) { - _refreshTokenRetryCount = 0; - final timerDuration = Duration(seconds: nextDuration); - _setTokenRefreshTimer(timerDuration); - } else { - await _callRefreshToken(); - } - } catch (e) { - // Catch any error, because in this case they should be handled by listening to [onAuthStateChange] - } - } - } - - void _setTokenRefreshTimer( - Duration timerDuration, { - String? refreshToken, - String? accessToken, - }) { - _refreshTokenTimer?.cancel(); - _refreshTokenRetryCount++; - if (_refreshTokenRetryCount < Constants.maxRetryCount) { - _refreshTokenTimer = Timer(timerDuration, () async { - try { - await _callRefreshToken(refreshToken); - } catch (_) { - // Catch any error, because in this case they should be handled by listening to [onAuthStateChange] - } - }); - } else { - throw AuthException('Access token refresh retry limit exceeded.'); - } + // TODO: in js, here it sets the session on local storage } void _removeSession() { _currentSession = null; _currentUser = null; - _refreshTokenRetryCount = 0; - _refreshTokenTimer?.cancel(); + // TODO: in js, here it removes the session on local storage } /// Generates a new JWT. @@ -1092,8 +1105,6 @@ class GoTrueClient { /// To prevent multiple simultaneous requests it catches an already ongoing request by using the global [_refreshTokenCompleter]. /// If that's not null and not completed it returns the future of the ongoing request. /// - /// To call [_callRefreshToken] during a running request [ignorePendingRequest] is used to bypass that check. - /// /// When a [ClientException] occurs [_setTokenRefreshTimer] is used to schedule a retry in the background, which emits the result via [onAuthStateChange]. Future _callRefreshToken(String refreshToken) async { // Refreshing is already in progress @@ -1121,11 +1132,8 @@ class GoTrueClient { // this._debug(debugName, 'error', error) if (error is AuthException) { - // const result = { session: null, error } - if (!isAuthRetryableFetchError(error)) { _removeSession(); - notifyAllSubscribers(AuthChangeEvent.signedOut); } From 64c75596236fc5dfef35c67792bf15c7897c15ff Mon Sep 17 00:00:00 2001 From: dshukertjr Date: Fri, 22 Mar 2024 15:08:07 +0700 Subject: [PATCH 04/25] update the app lifecycle change listener --- packages/gotrue/lib/src/gotrue_client.dart | 3 +- .../lib/src/supabase_auth.dart | 30 ++++--------------- 2 files changed, 7 insertions(+), 26 deletions(-) diff --git a/packages/gotrue/lib/src/gotrue_client.dart b/packages/gotrue/lib/src/gotrue_client.dart index b51c880d..cdfef45f 100644 --- a/packages/gotrue/lib/src/gotrue_client.dart +++ b/packages/gotrue/lib/src/gotrue_client.dart @@ -970,8 +970,7 @@ class GoTrueClient { /// If you call this method any managed visibility change callback will be /// removed and you must manage visibility changes on your own. void stopAutoRefresh() { - // TODO: we might need to move this into supabase_flutter to remove the visibility change callback - final ticker = _autoRefreshTicker; + _autoRefreshTicker?.cancel(); _autoRefreshTicker = null; } diff --git a/packages/supabase_flutter/lib/src/supabase_auth.dart b/packages/supabase_flutter/lib/src/supabase_auth.dart index 258daa1b..27d44f7f 100644 --- a/packages/supabase_flutter/lib/src/supabase_auth.dart +++ b/packages/supabase_flutter/lib/src/supabase_auth.dart @@ -107,30 +107,12 @@ class SupabaseAuth with WidgetsBindingObserver { void didChangeAppLifecycleState(AppLifecycleState state) { switch (state) { case AppLifecycleState.resumed: - _recoverSupabaseSession(); - default: - } - } - - /// Recover/refresh session if it's available - /// e.g. called on a splash screen when the app starts. - Future _recoverSupabaseSession() async { - final bool exist = await _localStorage.hasAccessToken(); - if (!exist) { - return; - } - - final String? jsonStr = await _localStorage.accessToken(); - if (jsonStr == null) { - return; - } - - try { - await Supabase.instance.client.auth.recoverSession(jsonStr); - } catch (error) { - // When there is an exception thrown while recovering the session, - // the appropriate action (retry, revoking session) will be taken by - // the gotrue library, so need to do anything here. + Supabase.instance.client.auth.startAutoRefresh(); + case AppLifecycleState.detached: + case AppLifecycleState.hidden: + case AppLifecycleState.inactive: + case AppLifecycleState.paused: + Supabase.instance.client.auth.stopAutoRefresh(); } } From 1cdf63e14001139a1bb9a4610a83d6bafeac8293 Mon Sep 17 00:00:00 2001 From: dshukertjr Date: Tue, 2 Apr 2024 17:39:59 +0900 Subject: [PATCH 05/25] Complete ticking behavior except tests --- packages/gotrue/lib/src/constants.dart | 14 +++-- packages/gotrue/lib/src/fetch.dart | 23 +++++-- packages/gotrue/lib/src/gotrue_client.dart | 60 ++++++++++--------- packages/gotrue/lib/src/helper.dart | 5 -- .../supabase/lib/src/auth_http_client.dart | 11 +++- .../lib/src/supabase_auth.dart | 1 + 6 files changed, 69 insertions(+), 45 deletions(-) diff --git a/packages/gotrue/lib/src/constants.dart b/packages/gotrue/lib/src/constants.dart index 1b50b4b6..aacfcbcf 100644 --- a/packages/gotrue/lib/src/constants.dart +++ b/packages/gotrue/lib/src/constants.dart @@ -11,11 +11,15 @@ class Constants { /// storage key prefix to store code verifiers static const String defaultStorageKey = 'supabase.auth.token'; - static const expiryMargin = Duration(seconds: 10); - static const int maxRetryCount = 10; - static const retryInterval = Duration(milliseconds: 200); - static const autoRefreshTickDuration = Duration(seconds: 30); - static const autoRefreshTockThreshold = 3; + + /// The margin to use when checking if a token is expired. + static const expiryMargin = Duration(seconds: 30); + + /// Current session will be checked for refresh at this interval. + static const autoRefreshTickDuration = Duration(seconds: 10); + + /// A token refresh will be attempted this many ticks before the current session expires. + static const autoRefreshTickThreshold = 3; // AUTO_REFRESH_TICK_THRESHOLD } diff --git a/packages/gotrue/lib/src/fetch.dart b/packages/gotrue/lib/src/fetch.dart index 9d2ab905..77ac0e0e 100644 --- a/packages/gotrue/lib/src/fetch.dart +++ b/packages/gotrue/lib/src/fetch.dart @@ -16,6 +16,18 @@ class GotrueFetch { return code >= 200 && code <= 299; } + String _getErrorMessage(dynamic error) { + if (error is Map) { + return error['msg'] ?? + error['message'] ?? + error['error_description'] ?? + error['error']?.toString() ?? + error.toString(); + } + + return error.toString(); + } + AuthException _handleError(dynamic error) { if (error is! Response) { throw AuthRetryableFetchError(); @@ -31,7 +43,6 @@ class GotrueFetch { try { data = jsonDecode(error.body); } catch (error) { - // TODO: properly provide the error message throw AuthUnknownError(message: error.toString(), originalError: error); } @@ -43,15 +54,17 @@ class GotrueFetch { (data['weak_password']['reasons'] as List) .whereNot((element) => element is String) .isEmpty) { - // TODO: properly provide the error message throw AuthWeakPasswordError( - message: '', + message: _getErrorMessage(data), statusCode: error.statusCode.toString(), reasons: data['weak_password']['reasons'], ); } - throw AuthApiError('An', statusCode: error.statusCode.toString()); + throw AuthApiError( + _getErrorMessage(data), + statusCode: error.statusCode.toString(), + ); } Future request( @@ -71,7 +84,7 @@ class GotrueFetch { Uri uri = Uri.parse(url); uri = uri.replace(queryParameters: {...uri.queryParameters, ...qs}); - final response = await _handleRequest( + return await _handleRequest( method: method, uri: uri, options: options, headers: headers); } diff --git a/packages/gotrue/lib/src/gotrue_client.dart b/packages/gotrue/lib/src/gotrue_client.dart index cdfef45f..a574c0c9 100644 --- a/packages/gotrue/lib/src/gotrue_client.dart +++ b/packages/gotrue/lib/src/gotrue_client.dart @@ -1,5 +1,6 @@ import 'dart:async'; import 'dart:convert'; +import 'dart:math'; import 'package:collection/collection.dart'; import 'package:gotrue/gotrue.dart'; @@ -975,7 +976,7 @@ class GoTrueClient { } Future _autoRefreshTokenTick() async { - // TODO: should we implement browser lock in Flutter as well? + print('auto refresh token tick: ${DateTime.now().toIso8601String()}'); try { final now = DateTime.now(); final session = _currentSession; @@ -984,16 +985,12 @@ class GoTrueClient { } final refreshToken = session.refreshToken; if (refreshToken == null) { - // TODO: properly handle case where refresh token is null - // check the js implementation return; } final expiresAt = session.expiresAt; if (expiresAt == null) { - // TODO: properly handle expiresAt being null - // check the js implementation return; } @@ -1005,11 +1002,11 @@ class GoTrueClient { .floor(); // Only tick if the next tick comes after the retry threshold - if (expiresInTicks <= Constants.autoRefreshTockThreshold) { + if (expiresInTicks <= Constants.autoRefreshTickThreshold) { await _callRefreshToken(refreshToken); } } catch (error) { - // TODO: in js, it's just printing here + // Do nothing. JS client prints here } } @@ -1017,9 +1014,11 @@ class GoTrueClient { /// [refreshToken] A valid refresh token that was returned on login. Future _refreshAccessToken(String refreshToken) async { final startedAt = DateTime.now(); + var attempt = 0; return await retry( // Make a GET request () async { + attempt++; final options = GotrueRequestOptions( headers: _headers, body: {'refresh_token': refreshToken}, @@ -1031,12 +1030,21 @@ class GoTrueClient { }, retryIf: (e) { // Do not retry if the next retry comes after the next tick. -// Date.now() + (attempt + 1) * 200 - startedAt < AUTO_REFRESH_TICK_DURATION -// TODO: Compare the remaining time until tik and the time until the next retry - return e is ClientException; + final nextBackOff = + Duration(milliseconds: (200 * pow(2, attempt - 1).floor())); + + return e is AuthRetryableFetchError && + (DateTime.now().millisecondsSinceEpoch + + nextBackOff.inMilliseconds - + startedAt.millisecondsSinceEpoch) < + Constants.autoRefreshTickDuration.inMilliseconds; }, maxDelay: Duration(seconds: 10), - maxAttempts: 99999, + randomizationFactor: 0, + + // Max interval between retries is 10 sec, so just set the maxAttempts + // to something that will yield a more than 10 sec interval. + maxAttempts: 999, ); } @@ -1088,15 +1096,11 @@ class GoTrueClient { void _saveSession(Session session) { _currentSession = session; _currentUser = session.user; - - // TODO: in js, here it sets the session on local storage } void _removeSession() { _currentSession = null; _currentUser = null; - - // TODO: in js, here it removes the session on local storage } /// Generates a new JWT. @@ -1114,6 +1118,12 @@ class GoTrueClient { try { _refreshTokenCompleter = Completer(); + // Catch any error in case nobody awaits the future + _refreshTokenCompleter!.future.then( + (_) => null, + onError: (_, __) => null, + ); + final data = await _refreshAccessToken(refreshToken); final session = data.session; @@ -1127,20 +1137,16 @@ class GoTrueClient { _refreshTokenCompleter?.complete(data); return data; - } catch (error) { - // this._debug(debugName, 'error', error) - - if (error is AuthException) { - if (!isAuthRetryableFetchError(error)) { - _removeSession(); - notifyAllSubscribers(AuthChangeEvent.signedOut); - } - - _refreshTokenCompleter?.completeError(error); - - rethrow; + } on AuthException catch (error) { + if (error is! AuthRetryableFetchError) { + _removeSession(); + notifyAllSubscribers(AuthChangeEvent.signedOut); } + _refreshTokenCompleter?.completeError(error); + + rethrow; + } catch (error) { _refreshTokenCompleter?.completeError(error); rethrow; } finally { diff --git a/packages/gotrue/lib/src/helper.dart b/packages/gotrue/lib/src/helper.dart index c5b79d4c..c4d850d3 100644 --- a/packages/gotrue/lib/src/helper.dart +++ b/packages/gotrue/lib/src/helper.dart @@ -2,7 +2,6 @@ import 'dart:convert'; import 'dart:math'; import 'package:crypto/crypto.dart'; -import 'package:gotrue/gotrue.dart'; /// Converts base 10 int into String representation of base 16 int and takes the last two digets. String dec2hex(int dec) { @@ -22,7 +21,3 @@ String generatePKCEChallenge(String verifier) { return base64UrlEncode(sha256.convert(ascii.encode(verifier)).bytes) .split('=')[0]; } - -bool isAuthRetryableFetchError(Object error) { - return error is AuthRetryableFetchError; -} diff --git a/packages/supabase/lib/src/auth_http_client.dart b/packages/supabase/lib/src/auth_http_client.dart index eab8c7bc..91e554d5 100644 --- a/packages/supabase/lib/src/auth_http_client.dart +++ b/packages/supabase/lib/src/auth_http_client.dart @@ -10,12 +10,17 @@ class AuthHttpClient extends BaseClient { @override Future send(BaseRequest request) async { + String? accessToken = _auth.currentSession?.accessToken; if (_auth.currentSession?.isExpired ?? false) { try { - await _auth.refreshSession(); - } catch (_) {} + final res = await _auth.refreshSession(); + accessToken = res.session?.accessToken; + } catch (error) { + // Make a request with the Supabase key instead of an expired JWT + accessToken = _supabaseKey; + } } - final authBearer = _auth.currentSession?.accessToken ?? _supabaseKey; + final authBearer = accessToken ?? _supabaseKey; request.headers.putIfAbsent("Authorization", () => 'Bearer $authBearer'); request.headers.putIfAbsent("apikey", () => _supabaseKey); diff --git a/packages/supabase_flutter/lib/src/supabase_auth.dart b/packages/supabase_flutter/lib/src/supabase_auth.dart index 27d44f7f..0dddfe68 100644 --- a/packages/supabase_flutter/lib/src/supabase_auth.dart +++ b/packages/supabase_flutter/lib/src/supabase_auth.dart @@ -72,6 +72,7 @@ class SupabaseAuth with WidgetsBindingObserver { // ignore: invalid_use_of_internal_member .notifyAllSubscribers(AuthChangeEvent.initialSession); } + Supabase.instance.client.auth.startAutoRefresh(); } /// Recovers the session from local storage. From 151e04653511280677ffef423a939413864bcb5c Mon Sep 17 00:00:00 2001 From: dshukertjr Date: Tue, 2 Apr 2024 17:41:39 +0900 Subject: [PATCH 06/25] minor comment update --- packages/supabase/lib/src/auth_http_client.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/supabase/lib/src/auth_http_client.dart b/packages/supabase/lib/src/auth_http_client.dart index 91e554d5..fb265e59 100644 --- a/packages/supabase/lib/src/auth_http_client.dart +++ b/packages/supabase/lib/src/auth_http_client.dart @@ -16,7 +16,8 @@ class AuthHttpClient extends BaseClient { final res = await _auth.refreshSession(); accessToken = res.session?.accessToken; } catch (error) { - // Make a request with the Supabase key instead of an expired JWT + // Make a request with the Supabase key instead of an expired JWT to + // align the behavior with the JS client. accessToken = _supabaseKey; } } From 4d7cf9c0e1ff866eec27dff8ae47d4fd72b60194 Mon Sep 17 00:00:00 2001 From: dshukertjr Date: Tue, 2 Apr 2024 17:42:48 +0900 Subject: [PATCH 07/25] update the behavior when the SDK failed to refresh the token when making request --- packages/supabase/lib/src/auth_http_client.dart | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/packages/supabase/lib/src/auth_http_client.dart b/packages/supabase/lib/src/auth_http_client.dart index fb265e59..bc594d8d 100644 --- a/packages/supabase/lib/src/auth_http_client.dart +++ b/packages/supabase/lib/src/auth_http_client.dart @@ -10,18 +10,21 @@ class AuthHttpClient extends BaseClient { @override Future send(BaseRequest request) async { - String? accessToken = _auth.currentSession?.accessToken; if (_auth.currentSession?.isExpired ?? false) { try { - final res = await _auth.refreshSession(); - accessToken = res.session?.accessToken; + await _auth.refreshSession(); } catch (error) { - // Make a request with the Supabase key instead of an expired JWT to - // align the behavior with the JS client. - accessToken = _supabaseKey; + // Failed to refresh the token. + final isExpiredWithoutMargin = DateTime.now().isAfter( + DateTime.fromMillisecondsSinceEpoch( + _auth.currentSession!.expiresAt! * 1000)); + if (isExpiredWithoutMargin) { + // Throw the error instead of making an API request with an expired token. + rethrow; + } } } - final authBearer = accessToken ?? _supabaseKey; + final authBearer = _auth.currentSession?.accessToken ?? _supabaseKey; request.headers.putIfAbsent("Authorization", () => 'Bearer $authBearer'); request.headers.putIfAbsent("apikey", () => _supabaseKey); From 0d3c00edd3e320f9da14839b521fa572da7a57a8 Mon Sep 17 00:00:00 2001 From: dshukertjr Date: Tue, 2 Apr 2024 22:16:05 +0900 Subject: [PATCH 08/25] adjust gotrue tests --- packages/gotrue/lib/src/gotrue_client.dart | 47 ++++++++++++-------- packages/gotrue/test/client_test.dart | 27 +++++------ packages/gotrue/test/custom_http_client.dart | 6 ++- packages/gotrue/test/utils.dart | 7 +-- 4 files changed, 47 insertions(+), 40 deletions(-) diff --git a/packages/gotrue/lib/src/gotrue_client.dart b/packages/gotrue/lib/src/gotrue_client.dart index a574c0c9..2849e3f5 100644 --- a/packages/gotrue/lib/src/gotrue_client.dart +++ b/packages/gotrue/lib/src/gotrue_client.dart @@ -924,28 +924,37 @@ class GoTrueClient { /// Recover session from stringified [Session]. Future recoverSession(String jsonStr) async { - final session = Session.fromJson(json.decode(jsonStr)); - if (session == null) { - await signOut(); - throw notifyException(AuthException('Current session is missing data.')); - } - - if (session.isExpired) { - final refreshToken = session.refreshToken; - if (_autoRefreshToken && refreshToken != null) { - return await _callRefreshToken(refreshToken); - } else { + try { + final session = Session.fromJson(json.decode(jsonStr)); + if (session == null) { await signOut(); - throw notifyException(AuthException('Session expired.')); + throw notifyException( + AuthException('Current session is missing data.'), + ); } - } else { - final shouldEmitEvent = _currentSession == null || - _currentSession?.user.id != session.user.id; - _saveSession(session); - if (shouldEmitEvent) notifyAllSubscribers(AuthChangeEvent.tokenRefreshed); + if (session.isExpired) { + final refreshToken = session.refreshToken; + if (_autoRefreshToken && refreshToken != null) { + return await _callRefreshToken(refreshToken); + } else { + await signOut(); + throw notifyException(AuthException('Session expired.')); + } + } else { + final shouldEmitEvent = _currentSession == null || + _currentSession?.user.id != session.user.id; + _saveSession(session); - return AuthResponse(session: session); + if (shouldEmitEvent) { + notifyAllSubscribers(AuthChangeEvent.tokenRefreshed); + } + + return AuthResponse(session: session); + } + } catch (error) { + notifyException(error); + rethrow; } } @@ -1164,7 +1173,7 @@ class GoTrueClient { /// For internal use only. @internal - Exception notifyException(Exception exception, [StackTrace? stackTrace]) { + Object notifyException(Object exception, [StackTrace? stackTrace]) { _onAuthStateChangeController.addError( exception, stackTrace ?? StackTrace.current, diff --git a/packages/gotrue/test/client_test.dart b/packages/gotrue/test/client_test.dart index 0b098540..d4f5c181 100644 --- a/packages/gotrue/test/client_test.dart +++ b/packages/gotrue/test/client_test.dart @@ -373,26 +373,19 @@ void main() { final session = '{"access_token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2ODAzNDE3MDUsInN1YiI6IjRkMjU4M2RhLThkZTQtNDlkMy05Y2QxLTM3YTlhNzRmNTViZCIsImVtYWlsIjoiZmFrZTE2ODAzMzgxMDVAZW1haWwuY29tIiwicGhvbmUiOiIiLCJhcHBfbWV0YWRhdGEiOnsicHJvdmlkZXIiOiJlbWFpbCIsInByb3ZpZGVycyI6WyJlbWFpbCJdfSwidXNlcl9tZXRhZGF0YSI6eyJIZWxsbyI6IldvcmxkIn0sInJvbGUiOiIiLCJhYWwiOiJhYWwxIiwiYW1yIjpbeyJtZXRob2QiOiJwYXNzd29yZCIsInRpbWVzdGFtcCI6MTY4MDMzODEwNX1dLCJzZXNzaW9uX2lkIjoiYzhiOTg2Y2UtZWJkZC00ZGUxLWI4MjAtZjIyOWYyNjg1OGIwIn0.0x1rFlPKbIU1rZPY1SH_FNSZaXerfkFA1Y-EOlhuzUs","expires_in":3600,"refresh_token":"-yeS4omysFs9tpUYBws9Rg","token_type":"bearer","provider_token":null,"provider_refresh_token":null,"user":{"id":"4d2583da-8de4-49d3-9cd1-37a9a74f55bd","app_metadata":{"provider":"email","providers":["email"]},"user_metadata":{"Hello":"World"},"aud":"","email":"fake1680338105@email.com","phone":"","created_at":"2023-04-01T08:35:05.208586Z","confirmed_at":null,"email_confirmed_at":"2023-04-01T08:35:05.220096086Z","phone_confirmed_at":null,"last_sign_in_at":"2023-04-01T08:35:05.222755878Z","role":"","updated_at":"2023-04-01T08:35:05.226938Z"},"expiresAt":1680341705}'; - ///These 3 are bundled and in sum 4 refresh token requests are made, because the first 3 fail in [RetryTestHttpClient] - final future1 = Future.wait([ - client.recoverSession(session), + ///These 3 are bundled and in sum 1 refresh token requests is made, because the first 3 fail in [RetryTestHttpClient] + final responses = await Future.wait([ client.recoverSession(session), client.recoverSession(session), ]); - await expectLater(future1, throwsA(isA())); - expect(httpClient.retryCount, 1); - - /// Again these 3 are bundled and only one refresh token request is made - final future2 = Future.wait([ - client.recoverSession(session), - client.recoverSession(session), - client.recoverSession(session), - ]); + expect(responses[0].session?.accessToken, isNotNull); + expect( + responses[0].session?.accessToken, + responses[1].session?.accessToken, + ); - await expectLater(future2, throwsA(isA())); - expect(client.onAuthStateChange, emits(isA())); - expect(httpClient.retryCount, 2); + expect(httpClient.retryCount, 4); }); test('Sign out on wrong refresh token', () async { @@ -410,10 +403,10 @@ void main() { ]), ); - final session = + final expiredSession = getSessionData(DateTime.now().subtract(Duration(hours: 1))); - await expectLater(client.recoverSession(session.sessionString), + await expectLater(client.recoverSession(expiredSession.sessionString), throwsA(isA())); expect(stream, emitsError(isA())); diff --git a/packages/gotrue/test/custom_http_client.dart b/packages/gotrue/test/custom_http_client.dart index fe2ed433..fe1f04d1 100644 --- a/packages/gotrue/test/custom_http_client.dart +++ b/packages/gotrue/test/custom_http_client.dart @@ -76,7 +76,11 @@ class RetryTestHttpClient extends BaseClient { throw ClientException('Retry #$retryCount'); } final jwt = JWT( - {'exp': (DateTime.now().millisecondsSinceEpoch / 1000).round() + 60}, + { + 'exp': (DateTime.now().millisecondsSinceEpoch / 1000).round() + 60, + 'retry_count': + retryCount, // Add retryCount so that tokens issued on different retries are different. + }, subject: userId1, ); diff --git a/packages/gotrue/test/utils.dart b/packages/gotrue/test/utils.dart index 2bec283e..10b4f5b9 100644 --- a/packages/gotrue/test/utils.dart +++ b/packages/gotrue/test/utils.dart @@ -51,13 +51,14 @@ String getServiceRoleToken(DotEnv env) { } /// Construct session data for a given expiration date -({String accessToken, String sessionString}) getSessionData(DateTime dateTime) { - final expiresAt = dateTime.millisecondsSinceEpoch ~/ 1000; +({String accessToken, String sessionString}) getSessionData( + DateTime expireDateTime) { + final expiresAt = expireDateTime.millisecondsSinceEpoch ~/ 1000; final accessTokenMid = base64.encode(utf8.encode(json.encode( {'exp': expiresAt, 'sub': '1234567890', 'role': 'authenticated'}))); final accessToken = 'any.$accessTokenMid.any'; final sessionString = - '{"access_token":"$accessToken","expires_in":${dateTime.difference(DateTime.now()).inSeconds},"refresh_token":"-yeS4omysFs9tpUYBws9Rg","token_type":"bearer","provider_token":null,"provider_refresh_token":null,"user":{"id":"4d2583da-8de4-49d3-9cd1-37a9a74f55bd","app_metadata":{"provider":"email","providers":["email"]},"user_metadata":{"Hello":"World"},"aud":"","email":"fake1680338105@email.com","phone":"","created_at":"2023-04-01T08:35:05.208586Z","confirmed_at":null,"email_confirmed_at":"2023-04-01T08:35:05.220096086Z","phone_confirmed_at":null,"last_sign_in_at":"2023-04-01T08:35:05.222755878Z","role":"","updated_at":"2023-04-01T08:35:05.226938Z"}}'; + '{"access_token":"$accessToken","expires_in":${expireDateTime.difference(DateTime.now()).inSeconds},"refresh_token":"-yeS4omysFs9tpUYBws9Rg","token_type":"bearer","provider_token":null,"provider_refresh_token":null,"user":{"id":"4d2583da-8de4-49d3-9cd1-37a9a74f55bd","app_metadata":{"provider":"email","providers":["email"]},"user_metadata":{"Hello":"World"},"aud":"","email":"fake1680338105@email.com","phone":"","created_at":"2023-04-01T08:35:05.208586Z","confirmed_at":null,"email_confirmed_at":"2023-04-01T08:35:05.220096086Z","phone_confirmed_at":null,"last_sign_in_at":"2023-04-01T08:35:05.222755878Z","role":"","updated_at":"2023-04-01T08:35:05.226938Z"}}'; return (accessToken: accessToken, sessionString: sessionString); } From 21f60cb8bd7b5299b2cc46f4c438c726e33331a2 Mon Sep 17 00:00:00 2001 From: dshukertjr Date: Tue, 2 Apr 2024 22:52:27 +0900 Subject: [PATCH 09/25] fix supabase test --- packages/supabase/test/client_test.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/supabase/test/client_test.dart b/packages/supabase/test/client_test.dart index 7fb18638..3de1c971 100644 --- a/packages/supabase/test/client_test.dart +++ b/packages/supabase/test/client_test.dart @@ -135,7 +135,7 @@ void main() { }); test('call recoverSession', () async { - final expiresAt = DateTime.now().add(Duration(seconds: 11)); + final expiresAt = DateTime.now().add(Duration(seconds: 31)); final mockServer = await HttpServer.bind('localhost', 0); final supabase = SupabaseClient( From b7b919926dbf8781d92224273706865f168f926c Mon Sep 17 00:00:00 2001 From: dshukertjr Date: Tue, 2 Apr 2024 23:06:24 +0900 Subject: [PATCH 10/25] fix supabase_flutter tests --- .github/workflows/supabase_flutter.yml | 5 ++++- packages/supabase_flutter/test/widget_test.dart | 1 + 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/supabase_flutter.yml b/.github/workflows/supabase_flutter.yml index 23799960..41aeae46 100644 --- a/.github/workflows/supabase_flutter.yml +++ b/.github/workflows/supabase_flutter.yml @@ -48,11 +48,13 @@ jobs: with: java-version: '12.x' - - uses: subosito/flutter-action@v1 + - uses: subosito/flutter-action@v2 with: flutter-version: ${{ matrix.flutter-version }} channel: 'stable' + - run: flutter --version + - name: Bootstrap workspace run: | cd ../../ @@ -64,6 +66,7 @@ jobs: run: dart format lib test -l 80 --set-exit-if-changed - name: analyzer + if: ${{ matrix.sdk == '3.x'}} run: flutter analyze --fatal-warnings --fatal-infos . - name: Run tests diff --git a/packages/supabase_flutter/test/widget_test.dart b/packages/supabase_flutter/test/widget_test.dart index 37d770b3..d1db3ff3 100644 --- a/packages/supabase_flutter/test/widget_test.dart +++ b/packages/supabase_flutter/test/widget_test.dart @@ -27,5 +27,6 @@ void main() { await tester.tap(find.text('Sign out')); await tester.pump(); expect(find.text('You have signed out'), findsOneWidget); + Supabase.instance.client.auth.stopAutoRefresh(); }); } From 23aa6128a71b21fb23386ac9ef0bb8da7f3f1aeb Mon Sep 17 00:00:00 2001 From: dshukertjr Date: Tue, 2 Apr 2024 23:17:54 +0900 Subject: [PATCH 11/25] Fix supabase-flutter test --- packages/supabase_flutter/lib/src/supabase_auth.dart | 2 +- packages/supabase_flutter/test/widget_test.dart | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/supabase_flutter/lib/src/supabase_auth.dart b/packages/supabase_flutter/lib/src/supabase_auth.dart index 0dddfe68..ab13c221 100644 --- a/packages/supabase_flutter/lib/src/supabase_auth.dart +++ b/packages/supabase_flutter/lib/src/supabase_auth.dart @@ -110,10 +110,10 @@ class SupabaseAuth with WidgetsBindingObserver { case AppLifecycleState.resumed: Supabase.instance.client.auth.startAutoRefresh(); case AppLifecycleState.detached: - case AppLifecycleState.hidden: case AppLifecycleState.inactive: case AppLifecycleState.paused: Supabase.instance.client.auth.stopAutoRefresh(); + default: } } diff --git a/packages/supabase_flutter/test/widget_test.dart b/packages/supabase_flutter/test/widget_test.dart index d1db3ff3..9d8664d5 100644 --- a/packages/supabase_flutter/test/widget_test.dart +++ b/packages/supabase_flutter/test/widget_test.dart @@ -25,8 +25,7 @@ void main() { ); await tester.pumpWidget(const MaterialApp(home: MockWidget())); await tester.tap(find.text('Sign out')); - await tester.pump(); + await tester.pumpAndSettle(); expect(find.text('You have signed out'), findsOneWidget); - Supabase.instance.client.auth.stopAutoRefresh(); }); } From fe5c2ee041ab0c6c203248b710f3555f3d5f4df6 Mon Sep 17 00:00:00 2001 From: dshukertjr Date: Tue, 2 Apr 2024 23:24:39 +0900 Subject: [PATCH 12/25] stop autorefresh timer within widget test --- packages/supabase_flutter/test/widget_test.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/supabase_flutter/test/widget_test.dart b/packages/supabase_flutter/test/widget_test.dart index 9d8664d5..098efbde 100644 --- a/packages/supabase_flutter/test/widget_test.dart +++ b/packages/supabase_flutter/test/widget_test.dart @@ -23,6 +23,7 @@ void main() { pkceAsyncStorage: MockAsyncStorage(), ), ); + Supabase.instance.client.auth.stopAutoRefresh(); await tester.pumpWidget(const MaterialApp(home: MockWidget())); await tester.tap(find.text('Sign out')); await tester.pumpAndSettle(); From 01e46d96dbb17d4991ab899e4b9fdcb27227b69b Mon Sep 17 00:00:00 2001 From: Tyler Date: Wed, 3 Apr 2024 09:52:48 +0900 Subject: [PATCH 13/25] Update packages/gotrue/lib/src/constants.dart Co-authored-by: Vinzent --- packages/gotrue/lib/src/constants.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/gotrue/lib/src/constants.dart b/packages/gotrue/lib/src/constants.dart index aacfcbcf..49851917 100644 --- a/packages/gotrue/lib/src/constants.dart +++ b/packages/gotrue/lib/src/constants.dart @@ -20,7 +20,6 @@ class Constants { /// A token refresh will be attempted this many ticks before the current session expires. static const autoRefreshTickThreshold = 3; - // AUTO_REFRESH_TICK_THRESHOLD } enum AuthChangeEvent { From dfd64587a29a3f05822cc122e6bd473252a79c6c Mon Sep 17 00:00:00 2001 From: Tyler Date: Wed, 3 Apr 2024 10:09:39 +0900 Subject: [PATCH 14/25] Update packages/gotrue/lib/src/gotrue_client.dart Co-authored-by: Vinzent --- packages/gotrue/lib/src/gotrue_client.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/gotrue/lib/src/gotrue_client.dart b/packages/gotrue/lib/src/gotrue_client.dart index 2849e3f5..56b304a7 100644 --- a/packages/gotrue/lib/src/gotrue_client.dart +++ b/packages/gotrue/lib/src/gotrue_client.dart @@ -965,7 +965,6 @@ class GoTrueClient { void startAutoRefresh() async { stopAutoRefresh(); - // final ticker = setInterval(() => this._autoRefreshTokenTick(), AUTO_REFRESH_TICK_DURATION) _autoRefreshTicker = Timer.periodic( Constants.autoRefreshTickDuration, (Timer t) => _autoRefreshTokenTick(), From b89eef4262ac12db228361fa345dced4c2e975a4 Mon Sep 17 00:00:00 2001 From: Tyler Date: Wed, 3 Apr 2024 10:09:52 +0900 Subject: [PATCH 15/25] Update packages/gotrue/lib/src/gotrue_client.dart Co-authored-by: Vinzent --- packages/gotrue/lib/src/gotrue_client.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/gotrue/lib/src/gotrue_client.dart b/packages/gotrue/lib/src/gotrue_client.dart index 56b304a7..d3076168 100644 --- a/packages/gotrue/lib/src/gotrue_client.dart +++ b/packages/gotrue/lib/src/gotrue_client.dart @@ -984,7 +984,6 @@ class GoTrueClient { } Future _autoRefreshTokenTick() async { - print('auto refresh token tick: ${DateTime.now().toIso8601String()}'); try { final now = DateTime.now(); final session = _currentSession; From be4e903fb129f19457b5e9015919d41bafd9a93c Mon Sep 17 00:00:00 2001 From: Tyler Date: Wed, 3 Apr 2024 10:25:42 +0900 Subject: [PATCH 16/25] Update packages/gotrue/lib/src/gotrue_client.dart Co-authored-by: Vinzent --- 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 d3076168..72b129df 100644 --- a/packages/gotrue/lib/src/gotrue_client.dart +++ b/packages/gotrue/lib/src/gotrue_client.dart @@ -587,7 +587,7 @@ class GoTrueClient { } final currentSessionRefreshToken = - _currentSession?.refreshToken ?? refreshToken; + refreshToken ?? _currentSession?.refreshToken; if (currentSessionRefreshToken == null) { throw AuthSessionMissingError(); From ea404b16a111ea7cc2d97a26966369ffcccf75ce Mon Sep 17 00:00:00 2001 From: Tyler Date: Wed, 3 Apr 2024 10:27:16 +0900 Subject: [PATCH 17/25] Update packages/gotrue/lib/src/gotrue_client.dart --- packages/gotrue/lib/src/gotrue_client.dart | 3 --- 1 file changed, 3 deletions(-) diff --git a/packages/gotrue/lib/src/gotrue_client.dart b/packages/gotrue/lib/src/gotrue_client.dart index 72b129df..937fe5a1 100644 --- a/packages/gotrue/lib/src/gotrue_client.dart +++ b/packages/gotrue/lib/src/gotrue_client.dart @@ -975,9 +975,6 @@ class GoTrueClient { } /// Stops an active auto refresh process running in the background (if any). - /// - /// If you call this method any managed visibility change callback will be - /// removed and you must manage visibility changes on your own. void stopAutoRefresh() { _autoRefreshTicker?.cancel(); _autoRefreshTicker = null; From c49e232f9c2ef6f0d472200adc102ae26bb575dc Mon Sep 17 00:00:00 2001 From: Tyler Date: Wed, 3 Apr 2024 10:28:04 +0900 Subject: [PATCH 18/25] Update packages/gotrue/lib/src/gotrue_client.dart --- packages/gotrue/lib/src/gotrue_client.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/gotrue/lib/src/gotrue_client.dart b/packages/gotrue/lib/src/gotrue_client.dart index 937fe5a1..a5b4de1e 100644 --- a/packages/gotrue/lib/src/gotrue_client.dart +++ b/packages/gotrue/lib/src/gotrue_client.dart @@ -1096,7 +1096,6 @@ class GoTrueClient { } /// set currentSession and currentUser - /// process to _startAutoRefreshToken if possible void _saveSession(Session session) { _currentSession = session; _currentUser = session.user; From 1930ac35b692a54c8736b2748b0b56aeb96d088f Mon Sep 17 00:00:00 2001 From: dshukertjr Date: Wed, 3 Apr 2024 10:35:27 +0900 Subject: [PATCH 19/25] minor refactor within _autoRefreshTokenTick --- packages/gotrue/lib/src/gotrue_client.dart | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/packages/gotrue/lib/src/gotrue_client.dart b/packages/gotrue/lib/src/gotrue_client.dart index a5b4de1e..78a0cb31 100644 --- a/packages/gotrue/lib/src/gotrue_client.dart +++ b/packages/gotrue/lib/src/gotrue_client.dart @@ -983,17 +983,12 @@ class GoTrueClient { Future _autoRefreshTokenTick() async { try { final now = DateTime.now(); - final session = _currentSession; - if (session == null) { - return; - } - final refreshToken = session.refreshToken; + final refreshToken = _currentSession?.refreshToken; if (refreshToken == null) { return; } - final expiresAt = session.expiresAt; - + final expiresAt = _currentSession?.expiresAt; if (expiresAt == null) { return; } From 668b4ed23442c68cd19386fdc3744d2379f4fb3a Mon Sep 17 00:00:00 2001 From: dshukertjr Date: Wed, 3 Apr 2024 10:47:08 +0900 Subject: [PATCH 20/25] rename errors to exceptions --- packages/gotrue/lib/src/fetch.dart | 13 ++++++------ packages/gotrue/lib/src/gotrue_client.dart | 8 +++---- .../gotrue/lib/src/types/auth_exception.dart | 21 ++++++++++--------- 3 files changed, 22 insertions(+), 20 deletions(-) diff --git a/packages/gotrue/lib/src/fetch.dart b/packages/gotrue/lib/src/fetch.dart index 77ac0e0e..a98e5f7d 100644 --- a/packages/gotrue/lib/src/fetch.dart +++ b/packages/gotrue/lib/src/fetch.dart @@ -30,20 +30,21 @@ class GotrueFetch { AuthException _handleError(dynamic error) { if (error is! Response) { - throw AuthRetryableFetchError(); + throw AuthRetryableFetchException(); } // If the status is 500 or above, it's likely a server error, // and can be retried. if (error.statusCode >= 500) { - throw AuthRetryableFetchError(); + throw AuthRetryableFetchException(); } final dynamic data; try { data = jsonDecode(error.body); } catch (error) { - throw AuthUnknownError(message: error.toString(), originalError: error); + throw AuthUnknownException( + message: error.toString(), originalError: error); } // Check if weak password reasons only contain strings @@ -54,14 +55,14 @@ class GotrueFetch { (data['weak_password']['reasons'] as List) .whereNot((element) => element is String) .isEmpty) { - throw AuthWeakPasswordError( + throw AuthWeakPasswordException( message: _getErrorMessage(data), statusCode: error.statusCode.toString(), reasons: data['weak_password']['reasons'], ); } - throw AuthApiError( + throw AuthApiException( _getErrorMessage(data), statusCode: error.statusCode.toString(), ); @@ -133,7 +134,7 @@ class GotrueFetch { } } catch (e) { // fetch failed, likely due to a network or CORS error - throw AuthRetryableFetchError(); + throw AuthRetryableFetchException(); } if (!isSuccessStatusCode(response.statusCode)) { diff --git a/packages/gotrue/lib/src/gotrue_client.dart b/packages/gotrue/lib/src/gotrue_client.dart index 78a0cb31..6feb1581 100644 --- a/packages/gotrue/lib/src/gotrue_client.dart +++ b/packages/gotrue/lib/src/gotrue_client.dart @@ -590,7 +590,7 @@ class GoTrueClient { refreshToken ?? _currentSession?.refreshToken; if (currentSessionRefreshToken == null) { - throw AuthSessionMissingError(); + throw AuthSessionMissingException(); } return await _callRefreshToken(currentSessionRefreshToken); @@ -1032,7 +1032,7 @@ class GoTrueClient { final nextBackOff = Duration(milliseconds: (200 * pow(2, attempt - 1).floor())); - return e is AuthRetryableFetchError && + return e is AuthRetryableFetchException && (DateTime.now().millisecondsSinceEpoch + nextBackOff.inMilliseconds - startedAt.millisecondsSinceEpoch) < @@ -1127,7 +1127,7 @@ class GoTrueClient { final session = data.session; if (session == null) { - throw AuthSessionMissingError(); + throw AuthSessionMissingException(); } _saveSession(session); @@ -1136,7 +1136,7 @@ class GoTrueClient { _refreshTokenCompleter?.complete(data); return data; } on AuthException catch (error) { - if (error is! AuthRetryableFetchError) { + if (error is! AuthRetryableFetchException) { _removeSession(); notifyAllSubscribers(AuthChangeEvent.signedOut); } diff --git a/packages/gotrue/lib/src/types/auth_exception.dart b/packages/gotrue/lib/src/types/auth_exception.dart index 431b43a6..0584a69c 100644 --- a/packages/gotrue/lib/src/types/auth_exception.dart +++ b/packages/gotrue/lib/src/types/auth_exception.dart @@ -25,30 +25,31 @@ class AuthPKCEGrantCodeExchangeError extends AuthException { AuthPKCEGrantCodeExchangeError(String message) : super(message); } -class AuthSessionMissingError extends AuthException { - AuthSessionMissingError() : super('Auth session missing!', statusCode: '400'); +class AuthSessionMissingException extends AuthException { + AuthSessionMissingException() + : super('Auth session missing!', statusCode: '400'); } -class AuthRetryableFetchError extends AuthException { - AuthRetryableFetchError() : super('AuthRetryableFetchError'); +class AuthRetryableFetchException extends AuthException { + AuthRetryableFetchException() : super('AuthRetryableFetchError'); } -class AuthApiError extends AuthException { - AuthApiError(String message, {String? statusCode}) +class AuthApiException extends AuthException { + AuthApiException(String message, {String? statusCode}) : super(message, statusCode: statusCode); } -class AuthUnknownError extends AuthException { +class AuthUnknownException extends AuthException { final Object originalError; - AuthUnknownError({required String message, required this.originalError}) + AuthUnknownException({required String message, required this.originalError}) : super(message); } -class AuthWeakPasswordError extends AuthException { +class AuthWeakPasswordException extends AuthException { final List reasons; - AuthWeakPasswordError({ + AuthWeakPasswordException({ required String message, required String statusCode, required this.reasons, From 0058223c381d1aa182573b80eb5e4217c58447f1 Mon Sep 17 00:00:00 2001 From: Vinzent Date: Fri, 5 Apr 2024 16:25:04 +0200 Subject: [PATCH 21/25] style: remove redundant bracket --- packages/gotrue/lib/src/gotrue_client.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/gotrue/lib/src/gotrue_client.dart b/packages/gotrue/lib/src/gotrue_client.dart index 6feb1581..c978bffc 100644 --- a/packages/gotrue/lib/src/gotrue_client.dart +++ b/packages/gotrue/lib/src/gotrue_client.dart @@ -994,8 +994,8 @@ class GoTrueClient { } final expiresInTicks = - ((DateTime.fromMillisecondsSinceEpoch(expiresAt * 1000) - .difference(now)) + (DateTime.fromMillisecondsSinceEpoch(expiresAt * 1000) + .difference(now) .inMilliseconds / Constants.autoRefreshTickDuration.inMilliseconds) .floor(); From 711dfd70009605f9e8a7cb6fa4375d63f352f17f Mon Sep 17 00:00:00 2001 From: dshukertjr Date: Sun, 7 Apr 2024 15:44:04 +0900 Subject: [PATCH 22/25] Have supabase_flutter respect the autoRefreshToken option for refreshing session --- packages/gotrue/lib/src/gotrue_client.dart | 2 -- packages/supabase_flutter/lib/src/supabase_auth.dart | 12 ++++++++++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/packages/gotrue/lib/src/gotrue_client.dart b/packages/gotrue/lib/src/gotrue_client.dart index c978bffc..038937e5 100644 --- a/packages/gotrue/lib/src/gotrue_client.dart +++ b/packages/gotrue/lib/src/gotrue_client.dart @@ -960,8 +960,6 @@ class GoTrueClient { /// Starts an auto-refresh process in the background. Close to the time of expiration a process is started to /// refresh the session. If refreshing fails it will be retried for as long as necessary. - /// - /// If you set the `autoRefreshToken` to `true`, you don't need to call this function, it will be called for you. void startAutoRefresh() async { stopAutoRefresh(); diff --git a/packages/supabase_flutter/lib/src/supabase_auth.dart b/packages/supabase_flutter/lib/src/supabase_auth.dart index ab13c221..0ecd6b56 100644 --- a/packages/supabase_flutter/lib/src/supabase_auth.dart +++ b/packages/supabase_flutter/lib/src/supabase_auth.dart @@ -17,6 +17,9 @@ class SupabaseAuth with WidgetsBindingObserver { late LocalStorage _localStorage; late AuthFlowType _authFlowType; + /// Whether to automatically refresh the token + late bool _autoRefreshToken; + /// **ATTENTION**: `getInitialLink`/`getInitialUri` should be handled /// ONLY ONCE in your app's lifetime, since it is not meant to change /// throughout your app's life. @@ -36,6 +39,7 @@ class SupabaseAuth with WidgetsBindingObserver { }) async { _localStorage = options.localStorage!; _authFlowType = options.authFlowType; + _autoRefreshToken = options.autoRefreshToken; _authSubscription = Supabase.instance.client.auth.onAuthStateChange.listen( (data) { @@ -72,7 +76,9 @@ class SupabaseAuth with WidgetsBindingObserver { // ignore: invalid_use_of_internal_member .notifyAllSubscribers(AuthChangeEvent.initialSession); } - Supabase.instance.client.auth.startAutoRefresh(); + if (_autoRefreshToken) { + Supabase.instance.client.auth.startAutoRefresh(); + } } /// Recovers the session from local storage. @@ -108,7 +114,9 @@ class SupabaseAuth with WidgetsBindingObserver { void didChangeAppLifecycleState(AppLifecycleState state) { switch (state) { case AppLifecycleState.resumed: - Supabase.instance.client.auth.startAutoRefresh(); + if (_autoRefreshToken) { + Supabase.instance.client.auth.startAutoRefresh(); + } case AppLifecycleState.detached: case AppLifecycleState.inactive: case AppLifecycleState.paused: From eece78a66b5a54fa6fb88aa17626c89786a62c3e Mon Sep 17 00:00:00 2001 From: dshukertjr Date: Sun, 7 Apr 2024 15:46:21 +0900 Subject: [PATCH 23/25] remove outdated comment on _callRefreshToken --- packages/gotrue/lib/src/gotrue_client.dart | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/gotrue/lib/src/gotrue_client.dart b/packages/gotrue/lib/src/gotrue_client.dart index 038937e5..550ee0d7 100644 --- a/packages/gotrue/lib/src/gotrue_client.dart +++ b/packages/gotrue/lib/src/gotrue_client.dart @@ -1103,8 +1103,6 @@ class GoTrueClient { /// /// To prevent multiple simultaneous requests it catches an already ongoing request by using the global [_refreshTokenCompleter]. /// If that's not null and not completed it returns the future of the ongoing request. - /// - /// When a [ClientException] occurs [_setTokenRefreshTimer] is used to schedule a retry in the background, which emits the result via [onAuthStateChange]. Future _callRefreshToken(String refreshToken) async { // Refreshing is already in progress if (_refreshTokenCompleter != null) { From f7cbb0ac0d2b6a076987cccdb8306637e545631f Mon Sep 17 00:00:00 2001 From: dshukertjr Date: Sun, 7 Apr 2024 15:50:07 +0900 Subject: [PATCH 24/25] Add error to onAuthStateChange stream for call refresh token --- packages/gotrue/lib/src/gotrue_client.dart | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/gotrue/lib/src/gotrue_client.dart b/packages/gotrue/lib/src/gotrue_client.dart index 550ee0d7..d491782f 100644 --- a/packages/gotrue/lib/src/gotrue_client.dart +++ b/packages/gotrue/lib/src/gotrue_client.dart @@ -1131,17 +1131,20 @@ class GoTrueClient { _refreshTokenCompleter?.complete(data); return data; - } on AuthException catch (error) { + } on AuthException catch (error, stack) { if (error is! AuthRetryableFetchException) { _removeSession(); notifyAllSubscribers(AuthChangeEvent.signedOut); + } else { + _onAuthStateChangeController.addError(error, stack); } _refreshTokenCompleter?.completeError(error); rethrow; - } catch (error) { + } catch (error, stack) { _refreshTokenCompleter?.completeError(error); + _onAuthStateChangeController.addError(error, stack); rethrow; } finally { _refreshTokenCompleter = null; From b03e8a57f370bfe467a4ca10082adba59569dc45 Mon Sep 17 00:00:00 2001 From: dshukertjr Date: Tue, 9 Apr 2024 09:29:53 +0900 Subject: [PATCH 25/25] startAutoRefresh within gotrue --- packages/gotrue/lib/src/gotrue_client.dart | 3 +++ packages/supabase_flutter/lib/src/supabase_auth.dart | 3 --- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/gotrue/lib/src/gotrue_client.dart b/packages/gotrue/lib/src/gotrue_client.dart index d491782f..0729391f 100644 --- a/packages/gotrue/lib/src/gotrue_client.dart +++ b/packages/gotrue/lib/src/gotrue_client.dart @@ -113,6 +113,9 @@ class GoTrueClient { client: this, fetch: _fetch, ); + if (_autoRefreshToken) { + startAutoRefresh(); + } } /// Getter for the headers diff --git a/packages/supabase_flutter/lib/src/supabase_auth.dart b/packages/supabase_flutter/lib/src/supabase_auth.dart index 0ecd6b56..a72fb4ca 100644 --- a/packages/supabase_flutter/lib/src/supabase_auth.dart +++ b/packages/supabase_flutter/lib/src/supabase_auth.dart @@ -76,9 +76,6 @@ class SupabaseAuth with WidgetsBindingObserver { // ignore: invalid_use_of_internal_member .notifyAllSubscribers(AuthChangeEvent.initialSession); } - if (_autoRefreshToken) { - Supabase.instance.client.auth.startAutoRefresh(); - } } /// Recovers the session from local storage.