Start writing the web app
All checks were successful
ci/woodpecker/push/build Pipeline was successful
All checks were successful
ci/woodpecker/push/build Pipeline was successful
Signed-off-by: Nikolai Rodionov <allanger@posteo.com>
This commit was merged in pull request #5.
This commit is contained in:
167
lib/core/tokens/application/tokens_application.dart
Normal file
167
lib/core/tokens/application/tokens_application.dart
Normal file
@@ -0,0 +1,167 @@
|
||||
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, TokenState>(TokensController.new);
|
||||
|
||||
class TokensController extends AsyncNotifier<TokenState> {
|
||||
@override
|
||||
Future<TokenState> 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<void> 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<void> 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<void> 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<void> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
42
lib/core/tokens/data/token_storage_repository.dart
Normal file
42
lib/core/tokens/data/token_storage_repository.dart
Normal file
@@ -0,0 +1,42 @@
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
|
||||
const accessTokenKey = "access_token";
|
||||
const refreshTokenKey = "refresh_token";
|
||||
|
||||
final tokenStorageRepositoryProvider = Provider<TokenStorageRepository>((ref) {
|
||||
return TokenStorageRepository();
|
||||
});
|
||||
|
||||
// Secure storage backend for storing tokens in the browser
|
||||
class TokenStorageRepository {
|
||||
static const _storage = FlutterSecureStorage();
|
||||
TokenStorageRepository();
|
||||
|
||||
// Store the refresh token in the storage
|
||||
Future<void> storeRefreshToken(String token) async {
|
||||
await _storage.write(key: refreshTokenKey, value: token);
|
||||
}
|
||||
|
||||
// Store the access token in the storage
|
||||
Future<void> storeAccessToken(String token) async {
|
||||
await _storage.write(key: accessTokenKey, value: token);
|
||||
}
|
||||
|
||||
// Get the acccess token from the storage
|
||||
Future<String?> getAccessToken() async {
|
||||
final token = await _storage.read(key: accessTokenKey);
|
||||
return token;
|
||||
}
|
||||
|
||||
// Get the refresh token from the storage
|
||||
Future<String?> getRefreshToken() async {
|
||||
final token = await _storage.read(key: refreshTokenKey);
|
||||
return token;
|
||||
}
|
||||
|
||||
Future<void> clearStorage() async {
|
||||
await _storage.delete(key: refreshTokenKey);
|
||||
await _storage.delete(key: accessTokenKey);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user