Skip to content

Commit

Permalink
re-authentication attempts are made if API returns 401
Browse files Browse the repository at this point in the history
  • Loading branch information
fremartini committed Feb 13, 2022
1 parent 911aa9f commit e01b29d
Show file tree
Hide file tree
Showing 5 changed files with 100 additions and 31 deletions.
5 changes: 4 additions & 1 deletion lib/cubits/authentication/authentication_cubit.dart
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,10 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
}

Future<void> authenticated(
String email, String passcode, String token) async {
String email,
String passcode,
String token,
) async {
await _storage.saveAuthenticatedUser(
email,
passcode,
Expand Down
10 changes: 9 additions & 1 deletion lib/data/storage/secure_storage.dart
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,10 @@ class SecureStorage {
Future<bool> get hasToken async => await readToken() != null;

Future<void> 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);
Expand All @@ -38,6 +41,11 @@ class SecureStorage {
_logger.d('Email, passcode and token removed from Secure Storage');
}

Future<void> updateToken(String token) async {
await _storage.write(key: _tokenKey, value: token);
_logger.d('Token updated in Secure Storage');
}

Future<String?> readEmail() async {
return _storage.read(key: _emailKey);
}
Expand Down
8 changes: 1 addition & 7 deletions lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -18,11 +17,6 @@ void main() {
class App extends StatelessWidget {
final _navigatorKey = GlobalKey<NavigatorState>();

AuthenticationCubit _createAuthenticationCubit(BuildContext _) {
final storage = sl.get<SecureStorage>();
return AuthenticationCubit(storage)..appStarted();
}

EnvironmentCubit _createEnvironmentCubit(BuildContext _) {
final repo = sl.get<AppConfigRepository>();
return EnvironmentCubit(repo)..getConfig();
Expand All @@ -32,7 +26,7 @@ class App extends StatelessWidget {
Widget build(BuildContext context) {
return MultiBlocProvider(
providers: [
BlocProvider(create: _createAuthenticationCubit),
BlocProvider.value(value: sl<AuthenticationCubit>()..appStarted()),
BlocProvider(create: _createEnvironmentCubit),
],
child: SplashRouter(
Expand Down
14 changes: 12 additions & 2 deletions lib/service_locator.dart
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -38,15 +39,19 @@ void configureServices() {
CoffeecardApi.create(),
CoffeecardApiV2.create(),
],
authenticator: ReactivationAuthenticator(sl.get<SecureStorage>()),
authenticator: ReactivationAuthenticator(
sl.get<SecureStorage>(),
),
);

final _shiftplanningChopper = ChopperClient(
baseUrl: CoffeeCardApiConstants.shiftyUrl,
// TODO load the url from config files
converter: $JsonSerializableConverter(),
services: [ShiftplanningApi.create()],
authenticator: ReactivationAuthenticator(sl.get<SecureStorage>()),
authenticator: ReactivationAuthenticator(
sl.get<SecureStorage>(),
),
);

sl.registerSingleton<CoffeecardApi>(
Expand Down Expand Up @@ -94,4 +99,9 @@ void configureServices() {
sl.registerFactory<OpeningHoursRepository>(
() => OpeningHoursRepository(sl<ShiftplanningApi>(), sl<Logger>()),
);

// Cubits
sl.registerSingleton<AuthenticationCubit>(
AuthenticationCubit(sl.get<SecureStorage>()),
);
}
94 changes: 74 additions & 20 deletions lib/utils/reactivation_authenticator.dart
Original file line number Diff line number Diff line change
@@ -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<void> _evict() async {
final authenticationCubit = sl.get<AuthenticationCubit>();
await authenticationCubit.unauthenticated();
}

@override
FutureOr<Request?> authenticate(Request request, Response response) async {
FutureOr<Request?> 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<String, String> updatedHeaders =
Map<String, String>.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<Request?> refreshToken(
Request request,
Response response,
) async {
_retryCount++;

final accountRepository = sl.get<AccountRepository>();
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<String, String> updatedHeaders =
Map<String, String>.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;
Expand Down

0 comments on commit e01b29d

Please sign in to comment.