From e01b29ddd2a0154340c016572e749de009385fd4 Mon Sep 17 00:00:00 2001 From: fremartini Date: Sun, 13 Feb 2022 14:55:43 +0100 Subject: [PATCH] re-authentication attempts are made if API returns 401 --- .../authentication/authentication_cubit.dart | 5 +- lib/data/storage/secure_storage.dart | 10 +- lib/main.dart | 8 +- lib/service_locator.dart | 14 ++- lib/utils/reactivation_authenticator.dart | 94 +++++++++++++++---- 5 files changed, 100 insertions(+), 31 deletions(-) diff --git a/lib/cubits/authentication/authentication_cubit.dart b/lib/cubits/authentication/authentication_cubit.dart index acb0dbfec..5dd94af1f 100644 --- a/lib/cubits/authentication/authentication_cubit.dart +++ b/lib/cubits/authentication/authentication_cubit.dart @@ -23,7 +23,10 @@ class AuthenticationCubit extends Cubit { } Future authenticated( - String email, String passcode, String token) async { + String email, + String passcode, + String token, + ) async { await _storage.saveAuthenticatedUser( email, passcode, diff --git a/lib/data/storage/secure_storage.dart b/lib/data/storage/secure_storage.dart index e8ba4fa19..eed2779c1 100644 --- a/lib/data/storage/secure_storage.dart +++ b/lib/data/storage/secure_storage.dart @@ -15,7 +15,10 @@ class SecureStorage { Future get hasToken async => await readToken() != null; Future saveAuthenticatedUser( - String email, String passcode, String token) async { + String email, + String passcode, + String token, + ) async { await _storage.write(key: _emailKey, value: email); await _storage.write(key: _passcodeKey, value: passcode); await _storage.write(key: _tokenKey, value: token); @@ -38,6 +41,11 @@ class SecureStorage { _logger.d('Email, passcode and token removed from Secure Storage'); } + Future updateToken(String token) async { + await _storage.write(key: _tokenKey, value: token); + _logger.d('Token updated in Secure Storage'); + } + Future readEmail() async { return _storage.read(key: _emailKey); } diff --git a/lib/main.dart b/lib/main.dart index 4d0e0d8c9..99a420684 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -3,7 +3,6 @@ import 'package:coffeecard/base/style/theme.dart'; import 'package:coffeecard/cubits/authentication/authentication_cubit.dart'; import 'package:coffeecard/cubits/environment/environment_cubit.dart'; import 'package:coffeecard/data/repositories/v1/app_config_repository.dart'; -import 'package:coffeecard/data/storage/secure_storage.dart'; import 'package:coffeecard/service_locator.dart'; import 'package:coffeecard/widgets/pages/splash_page.dart'; import 'package:coffeecard/widgets/routers/splash_router.dart'; @@ -18,11 +17,6 @@ void main() { class App extends StatelessWidget { final _navigatorKey = GlobalKey(); - AuthenticationCubit _createAuthenticationCubit(BuildContext _) { - final storage = sl.get(); - return AuthenticationCubit(storage)..appStarted(); - } - EnvironmentCubit _createEnvironmentCubit(BuildContext _) { final repo = sl.get(); return EnvironmentCubit(repo)..getConfig(); @@ -32,7 +26,7 @@ class App extends StatelessWidget { Widget build(BuildContext context) { return MultiBlocProvider( providers: [ - BlocProvider(create: _createAuthenticationCubit), + BlocProvider.value(value: sl()..appStarted()), BlocProvider(create: _createEnvironmentCubit), ], child: SplashRouter( diff --git a/lib/service_locator.dart b/lib/service_locator.dart index 4115a5c5a..7da8d834d 100644 --- a/lib/service_locator.dart +++ b/lib/service_locator.dart @@ -1,4 +1,5 @@ import 'package:chopper/chopper.dart'; +import 'package:coffeecard/cubits/authentication/authentication_cubit.dart'; import 'package:coffeecard/data/api/coffee_card_api_constants.dart'; import 'package:coffeecard/data/api/interceptors/authentication_interceptor.dart'; import 'package:coffeecard/data/repositories/shiftplanning/opening_hours_repository.dart'; @@ -38,7 +39,9 @@ void configureServices() { CoffeecardApi.create(), CoffeecardApiV2.create(), ], - authenticator: ReactivationAuthenticator(sl.get()), + authenticator: ReactivationAuthenticator( + sl.get(), + ), ); final _shiftplanningChopper = ChopperClient( @@ -46,7 +49,9 @@ void configureServices() { // TODO load the url from config files converter: $JsonSerializableConverter(), services: [ShiftplanningApi.create()], - authenticator: ReactivationAuthenticator(sl.get()), + authenticator: ReactivationAuthenticator( + sl.get(), + ), ); sl.registerSingleton( @@ -94,4 +99,9 @@ void configureServices() { sl.registerFactory( () => OpeningHoursRepository(sl(), sl()), ); + + // Cubits + sl.registerSingleton( + AuthenticationCubit(sl.get()), + ); } diff --git a/lib/utils/reactivation_authenticator.dart b/lib/utils/reactivation_authenticator.dart index b1120fdfb..fd4857807 100644 --- a/lib/utils/reactivation_authenticator.dart +++ b/lib/utils/reactivation_authenticator.dart @@ -1,36 +1,90 @@ import 'dart:async'; import 'package:chopper/chopper.dart'; +import 'package:coffeecard/cubits/authentication/authentication_cubit.dart'; import 'package:coffeecard/data/repositories/v1/account_repository.dart'; import 'package:coffeecard/data/storage/secure_storage.dart'; +import 'package:coffeecard/service_locator.dart'; class ReactivationAuthenticator extends Authenticator { final SecureStorage secureStorage; - //final AccountRepository _accountRepository; + DateTime? tokenRefreshedAt; + final Duration debounce = const Duration(seconds: 10); + int _retryCount = 0; + final int _retryLimit = 1; ReactivationAuthenticator(this.secureStorage); + bool _canRefreshToken() { + if (tokenRefreshedAt == null) { + return true; + } else { + return tokenRefreshedAt!.difference(DateTime.now()) < debounce; + } + } + + Future _evict() async { + final authenticationCubit = sl.get(); + await authenticationCubit.unauthenticated(); + } + @override - FutureOr authenticate(Request request, Response response) async { + FutureOr authenticate( + Request request, + Response response, [ + Request? originalRequest, + ]) async { if (response.statusCode == 401) { - final email = await secureStorage.readEmail(); - final passcode = await secureStorage.readPasscode(); - - if (email != null && passcode != null) { - /*final either = await _accountRepository.login(email, passcode); - - if (either.isRight) { - final Map updatedHeaders = - Map.of(request.headers); - - final newToken = 'Bearer ${either.right.token}'; - updatedHeaders.update( - 'Authorization', - (String _) => newToken, - ifAbsent: () => newToken, - ); - return request.copyWith(headers: updatedHeaders); - } */ + // sign the user out + if (_retryCount > _retryLimit) { + _evict(); + return null; + } + + // avoid refreshing the token multiple times if requests happen at the same time + if (!_canRefreshToken()) { + return null; + } + + return await refreshToken(request, response); + } + return null; + } + + Future refreshToken( + Request request, + Response response, + ) async { + _retryCount++; + + final accountRepository = sl.get(); + final email = await secureStorage.readEmail(); + final passcode = await secureStorage.readPasscode(); + + if (email != null && passcode != null) { + // this call may return 401 which triggers a recursive call + final either = await accountRepository.login( + email, + passcode, + ); + + if (either.isRight) { + tokenRefreshedAt = DateTime.now(); + _retryCount = 0; + + final Map updatedHeaders = + Map.of(request.headers); + + final token = either.right.token; + final bearerToken = 'Bearer ${either.right.token}'; + await secureStorage.updateToken(token); + + updatedHeaders.update( + 'Authorization', + (String _) => bearerToken, + ifAbsent: () => bearerToken, + ); + return request.copyWith(headers: updatedHeaders); } } return null;