import 'dart:developer'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:jwt_decoder/jwt_decoder.dart'; import 'package:softplayer_dart_proto/accounts/v1/accounts_v1.pb.dart'; import 'package:softplayer_web/core/api/v1/accounts.dart'; import 'package:softplayer_web/core/api/v1/refresh_session.dart'; import 'package:softplayer_web/core/grpc/grpc_client.dart'; import 'package:softplayer_web/core/tokens/data/token_storage_repository.dart'; import 'package:softplayer_web/features/authorization/application/authorization_application.dart'; class TokenState { final String? accessToken; final String? refreshToken; const TokenState({this.accessToken, this.refreshToken}); TokenState copyWith({String? accessToken, String? refreshToken}) { return TokenState( refreshToken: refreshToken ?? this.refreshToken, accessToken: accessToken ?? this.accessToken, ); } // Get an access token from the state String getAccessToken() { return accessToken ?? ""; } // Get a refresh token from the state String getRefreshToken() { return refreshToken ?? ""; } } const accessTokenHeader = "x-access-token"; const refreshTokenHeader = "x-refresh-token"; class Tokens { final String accessToken; final String refreshToken; const Tokens({required this.accessToken, required this.refreshToken}); factory Tokens.fromProto(TokenPair pair) { return Tokens( accessToken: pair.accessToken, refreshToken: pair.refreshToken, ); } } final tokensControllerProvider = AsyncNotifierProvider(TokensController.new); class TokensController extends AsyncNotifier { @override Future build() async { final tokenRepo = ref.read(tokenStorageRepositoryProvider); final accessToken = await tokenRepo.getAccessToken(); final refreshToken = await tokenRepo.getRefreshToken(); // If refresh token is not valid, we just drop the session, // even if there is an active access token, and return // an empty state if (!verifyJWT(refreshToken)) { log("Refresh token is not valid, logging out"); await tokenRepo.clearStorage(); return TokenState(); } // If access token is not valid, refresh it using the refresh token if (verifyJWT(accessToken)) { log("Access token is valid"); return TokenState(refreshToken: refreshToken, accessToken: accessToken); } log("Only refresh token is valid"); return TokenState(refreshToken: refreshToken); } Future resetTokens() async { final tokenRepo = ref.read(tokenStorageRepositoryProvider); await tokenRepo.clearStorage(); state = await AsyncValue.guard(() async { return TokenState(); }); } // Store an access token to the storage and save it to the state Future writeAccessToken(String token) async { final tokenRepo = ref.read(tokenStorageRepositoryProvider); await tokenRepo.storeAccessToken(token); state = await AsyncValue.guard(() async { return state.value!.copyWith(accessToken: token); }); } // Store a pair of tokens to the storage and refresh the state Future writeTokenPair(Tokens pair) async { final tokenRepo = ref.read(tokenStorageRepositoryProvider); log("Writing tokens"); await tokenRepo.storeRefreshToken(pair.refreshToken); await tokenRepo.storeAccessToken(pair.accessToken); state = await AsyncValue.guard(() async { return state.value!.copyWith( accessToken: pair.accessToken, refreshToken: pair.refreshToken, ); }); } Future checkTokens() async { final currentState = state.value; if (currentState == null || currentState.accessToken == null || currentState.refreshToken == null) { log("Trying to get tokens from the storage"); state = await AsyncValue.guard(() async { final tokenRepo = ref.read(tokenStorageRepositoryProvider); return TokenState( accessToken: await tokenRepo.getAccessToken(), refreshToken: await tokenRepo.getRefreshToken(), ); }); } log("checking tokens"); final inMemAccessToken = state.value!.getAccessToken(); // If we have a valid token in memory, nothing must be done // TODO: Store the expiration in memory, to make it cheaper to check if (verifyJWT(inMemAccessToken)) { return; } else { log("Access it not valid"); final inMemRefreshToken = state.value!.getRefreshToken(); if (verifyJWT(inMemRefreshToken)) { log("Trying to refresh"); try { final refreshSessionGrpc = ref.read(refreshSessionGrpcProvider); log("Trying to refresh"); final response = await refreshSessionGrpc.refreshSession( inMemRefreshToken, ); await writeTokenPair(Tokens.fromProto(response.tokenPair)); return; } catch (e) { log(e.toString()); } } } log("Refrsh it not valid"); await resetTokens(); ref.read(authorizationControllerProvider.notifier).logout(); } bool verifyJWT(String? token) { if (token == null || token.isEmpty) { return false; } try { bool isExpired = JwtDecoder.isExpired(token); return !isExpired; } catch (_) { rethrow; } } }