From 00b4b367d39e07d1f7b04ab718144e212dabcf24 Mon Sep 17 00:00:00 2001 From: AristideVB Date: Wed, 4 Dec 2024 17:18:29 +0100 Subject: [PATCH 1/3] feat: add shouldRefreshBeforeRequest to Fresh for proactive token validation --- packages/fresh_dio/lib/src/fresh.dart | 23 +++++++++++++- .../fresh_graphql/lib/src/fresh_link.dart | 31 ++++++++++++++++--- 2 files changed, 48 insertions(+), 6 deletions(-) diff --git a/packages/fresh_dio/lib/src/fresh.dart b/packages/fresh_dio/lib/src/fresh.dart index 832c62f..2688d9b 100644 --- a/packages/fresh_dio/lib/src/fresh.dart +++ b/packages/fresh_dio/lib/src/fresh.dart @@ -6,6 +6,9 @@ import 'package:fresh_dio/fresh_dio.dart'; /// Signature for `shouldRefresh` on [Fresh]. typedef ShouldRefresh = bool Function(Response? response); +/// Signature for proactive token validation before a request. +typedef ShouldRefreshBeforeRequest = bool Function(T? token); + /// Signature for `refreshToken` on [Fresh]. typedef RefreshToken = Future Function(T? token, Dio httpClient); @@ -30,10 +33,12 @@ class Fresh extends QueuedInterceptor with FreshMixin { required TokenStorage tokenStorage, required RefreshToken refreshToken, ShouldRefresh? shouldRefresh, + ShouldRefreshBeforeRequest? shouldRefreshBeforeRequest, Dio? httpClient, }) : _refreshToken = refreshToken, _tokenHeader = tokenHeader, _shouldRefresh = shouldRefresh ?? _defaultShouldRefresh, + _shouldRefreshBeforeRequest = shouldRefreshBeforeRequest, _httpClient = httpClient ?? Dio() { this.tokenStorage = tokenStorage; } @@ -53,6 +58,7 @@ class Fresh extends QueuedInterceptor with FreshMixin { required TokenStorage tokenStorage, required RefreshToken refreshToken, ShouldRefresh? shouldRefresh, + ShouldRefreshBeforeRequest? shouldRefreshBeforeRequest, Dio? httpClient, TokenHeaderBuilder? tokenHeader, }) { @@ -60,6 +66,7 @@ class Fresh extends QueuedInterceptor with FreshMixin { refreshToken: refreshToken, tokenStorage: tokenStorage, shouldRefresh: shouldRefresh, + shouldRefreshBeforeRequest: shouldRefreshBeforeRequest, httpClient: httpClient, tokenHeader: tokenHeader ?? (token) { @@ -73,6 +80,7 @@ class Fresh extends QueuedInterceptor with FreshMixin { final Dio _httpClient; final TokenHeaderBuilder _tokenHeader; final ShouldRefresh _shouldRefresh; + final ShouldRefreshBeforeRequest? _shouldRefreshBeforeRequest; final RefreshToken _refreshToken; @override @@ -103,7 +111,20 @@ Example: ''', ); - final currentToken = await token; + var currentToken = await token; + + if (_shouldRefreshBeforeRequest != null && + currentToken != null && + _shouldRefreshBeforeRequest!(currentToken)) { + try { + final refreshedToken = await _refreshToken(currentToken, _httpClient); + await setToken(refreshedToken); + currentToken = await token; + } on RevokeTokenException catch (_) { + unawaited(revokeToken()); + } + } + final headers = currentToken != null ? _tokenHeader(currentToken) : const {}; diff --git a/packages/fresh_graphql/lib/src/fresh_link.dart b/packages/fresh_graphql/lib/src/fresh_link.dart index bc8b604..6b6615a 100644 --- a/packages/fresh_graphql/lib/src/fresh_link.dart +++ b/packages/fresh_graphql/lib/src/fresh_link.dart @@ -8,14 +8,17 @@ import 'package:http/http.dart' as http; /// Signature for `shouldRefresh` on [FreshLink]. typedef ShouldRefresh = bool Function(Response); +/// Signature for proactive token validation before a request. +typedef ShouldRefreshBeforeRequest = bool Function(T? token); + /// Signature for `refreshToken` on [FreshLink]. typedef RefreshToken = Future Function(T, http.Client); /// {@template fresh_link} -/// A GraphQL Link which handles manages an authentication token automatically. +/// A GraphQL Link which manages an authentication token automatically. /// /// A constructor that returns a Fresh interceptor that uses the -/// [OAuth2Token] token, the standard token class and define the` +/// [OAuth2Token] token, the standard token class and defines the /// tokenHeader as 'authorization': '${token.tokenType} ${token.accessToken}' /// /// ```dart @@ -37,15 +40,17 @@ class FreshLink extends Link with FreshMixin { required TokenStorage tokenStorage, required RefreshToken refreshToken, required ShouldRefresh shouldRefresh, + ShouldRefreshBeforeRequest? shouldRefreshBeforeRequest, TokenHeaderBuilder? tokenHeader, }) : _refreshToken = refreshToken, _tokenHeader = (tokenHeader ?? (_) => {}), - _shouldRefresh = shouldRefresh { + _shouldRefresh = shouldRefresh, + _shouldRefreshBeforeRequest = shouldRefreshBeforeRequest { this.tokenStorage = tokenStorage; } ///{@template fresh_link} - ///A GraphQL Link which handles manages an authentication token automatically. + ///A GraphQL Link which manages an authentication token automatically. /// /// ```dart /// final freshLink = FreshLink.oAuth2( @@ -64,12 +69,14 @@ class FreshLink extends Link with FreshMixin { required TokenStorage tokenStorage, required RefreshToken refreshToken, required ShouldRefresh shouldRefresh, + ShouldRefreshBeforeRequest? shouldRefreshBeforeRequest, TokenHeaderBuilder? tokenHeader, }) { return FreshLink( refreshToken: refreshToken, tokenStorage: tokenStorage, shouldRefresh: shouldRefresh, + shouldRefreshBeforeRequest: shouldRefreshBeforeRequest, tokenHeader: tokenHeader ?? (token) { return { @@ -82,10 +89,24 @@ class FreshLink extends Link with FreshMixin { final RefreshToken _refreshToken; final TokenHeaderBuilder _tokenHeader; final ShouldRefresh _shouldRefresh; + final ShouldRefreshBeforeRequest? _shouldRefreshBeforeRequest; @override Stream request(Request request, [NextLink? forward]) async* { - final currentToken = await token; + var currentToken = await token; + + if (_shouldRefreshBeforeRequest != null && + currentToken != null && + _shouldRefreshBeforeRequest!(currentToken)) { + try { + final refreshedToken = await _refreshToken(currentToken, http.Client()); + await setToken(refreshedToken); + currentToken = await token; + } on RevokeTokenException catch (_) { + unawaited(revokeToken()); + } + } + final tokenHeaders = currentToken != null ? _tokenHeader(currentToken) : const {}; From 2496408e3c2b2c753f313f0310c979591862c26b Mon Sep 17 00:00:00 2001 From: AristideVB Date: Wed, 4 Dec 2024 17:24:46 +0100 Subject: [PATCH 2/3] feat: update ShouldRefreshBeforeRequest to return a Future for asynchronous token validation --- packages/fresh_dio/lib/src/fresh.dart | 4 ++-- packages/fresh_graphql/lib/src/fresh_link.dart | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/fresh_dio/lib/src/fresh.dart b/packages/fresh_dio/lib/src/fresh.dart index 2688d9b..1c31af3 100644 --- a/packages/fresh_dio/lib/src/fresh.dart +++ b/packages/fresh_dio/lib/src/fresh.dart @@ -7,7 +7,7 @@ import 'package:fresh_dio/fresh_dio.dart'; typedef ShouldRefresh = bool Function(Response? response); /// Signature for proactive token validation before a request. -typedef ShouldRefreshBeforeRequest = bool Function(T? token); +typedef ShouldRefreshBeforeRequest = Future Function(T? token); /// Signature for `refreshToken` on [Fresh]. typedef RefreshToken = Future Function(T? token, Dio httpClient); @@ -115,7 +115,7 @@ Example: if (_shouldRefreshBeforeRequest != null && currentToken != null && - _shouldRefreshBeforeRequest!(currentToken)) { + await _shouldRefreshBeforeRequest!(currentToken)) { try { final refreshedToken = await _refreshToken(currentToken, _httpClient); await setToken(refreshedToken); diff --git a/packages/fresh_graphql/lib/src/fresh_link.dart b/packages/fresh_graphql/lib/src/fresh_link.dart index 6b6615a..03e0e66 100644 --- a/packages/fresh_graphql/lib/src/fresh_link.dart +++ b/packages/fresh_graphql/lib/src/fresh_link.dart @@ -9,7 +9,7 @@ import 'package:http/http.dart' as http; typedef ShouldRefresh = bool Function(Response); /// Signature for proactive token validation before a request. -typedef ShouldRefreshBeforeRequest = bool Function(T? token); +typedef ShouldRefreshBeforeRequest = Future Function(T? token); /// Signature for `refreshToken` on [FreshLink]. typedef RefreshToken = Future Function(T, http.Client); @@ -97,7 +97,7 @@ class FreshLink extends Link with FreshMixin { if (_shouldRefreshBeforeRequest != null && currentToken != null && - _shouldRefreshBeforeRequest!(currentToken)) { + await _shouldRefreshBeforeRequest!(currentToken)) { try { final refreshedToken = await _refreshToken(currentToken, http.Client()); await setToken(refreshedToken); From 6eac5db13a83c1d1289ceed9ffc6652842776f6e Mon Sep 17 00:00:00 2001 From: AristideVB Date: Wed, 4 Dec 2024 18:13:48 +0100 Subject: [PATCH 3/3] docs: update examples for fresh_graphql and fresh_dio with proactive token validation --- .../lib/src/jsonplaceholder_client.dart | 31 +++++++++++++ packages/fresh_graphql/example/main.dart | 46 +++++++++++++++++-- 2 files changed, 74 insertions(+), 3 deletions(-) diff --git a/packages/fresh_dio/example/packages/jsonplaceholder_client/lib/src/jsonplaceholder_client.dart b/packages/fresh_dio/example/packages/jsonplaceholder_client/lib/src/jsonplaceholder_client.dart index e411352..be2f7ee 100644 --- a/packages/fresh_dio/example/packages/jsonplaceholder_client/lib/src/jsonplaceholder_client.dart +++ b/packages/fresh_dio/example/packages/jsonplaceholder_client/lib/src/jsonplaceholder_client.dart @@ -33,6 +33,15 @@ class JsonplaceholderClient { ); }, shouldRefresh: (_) => Random().nextInt(3) == 0, + shouldRefreshBeforeRequest: (token) async { + print('Checking token validity before request...'); + final now = currentUnixTime(); + final issuedAt = await fetchIssuedAt(); + if (token?.expiresIn != null && issuedAt != null) { + return (issuedAt + token!.expiresIn!) < now; + } + return false; + }, ); final Dio _httpClient; @@ -45,10 +54,13 @@ class JsonplaceholderClient { required String password, }) async { await Future.delayed(const Duration(seconds: 1)); + final issuedAt = currentUnixTime(); + await storeIssuedAt(issuedAt); await _fresh.setToken( const OAuth2Token( accessToken: 'initial_access_token', refreshToken: 'initial_refresh_token', + expiresIn: 60, ), ); } @@ -69,4 +81,23 @@ class JsonplaceholderClient { .map((dynamic item) => Photo.fromJson(item as Map)) .toList(); } + + /// Returns the current Unix time in seconds (since January 1, 1970, UTC). + static int currentUnixTime() { + return DateTime.now().millisecondsSinceEpoch ~/ 1000; + } + + /// Simulate storing issuedAt when a token is set or refreshed. + static int? _storedIssuedAt; + + static Future storeIssuedAt(int issuedTime) async { + print('Storing issuedAt: $issuedTime'); + _storedIssuedAt = issuedTime; + } + + /// Simulate fetching issuedAt from storage. + static Future fetchIssuedAt() async { + print('Fetching issuedAt...'); + return _storedIssuedAt; + } } diff --git a/packages/fresh_graphql/example/main.dart b/packages/fresh_graphql/example/main.dart index a9aa8a3..053c7aa 100644 --- a/packages/fresh_graphql/example/main.dart +++ b/packages/fresh_graphql/example/main.dart @@ -14,6 +14,24 @@ const getJobsQuery = ''' } '''; +// Mock storage for issuedAt +int? issuedAt; + +// Simulate storing issuedAt when the token is set +Future storeIssuedAt(int issuedTime) async { + print('Storing issuedAt: $issuedTime'); + issuedAt = issuedTime; +} + +// Simulate fetching issuedAt from storage +Future fetchIssuedAt() async { + print('Fetching issuedAt...'); + return issuedAt; +} + +/// Returns the current Unix time in seconds (since January 1, 1970, UTC). +int currentUnixTime() => DateTime.now().millisecondsSinceEpoch ~/ 1000; + void main() async { final freshLink = FreshLink.oAuth2( tokenStorage: InMemoryTokenStorage(), @@ -21,14 +39,36 @@ void main() async { // Perform refresh and return new token print('refreshing token!'); await Future.delayed(const Duration(seconds: 1)); - if (Random().nextInt(1) == 0) { + if (Random().nextInt(2) == 0) { throw RevokeTokenException(); } - return const OAuth2Token(accessToken: 't0ps3cret_r3fresh3d!'); + final newIssuedAt = currentUnixTime(); + await storeIssuedAt(newIssuedAt); + return const OAuth2Token(accessToken: 'refreshed_token!', expiresIn: 30); }, shouldRefresh: (_) => Random().nextInt(2) == 0, + shouldRefreshBeforeRequest: (token) async { + print('Checking token validity before request...'); + final now = currentUnixTime(); + final storedIssuedAt = await fetchIssuedAt(); + if (token?.expiresIn != null && storedIssuedAt != null) { + return (storedIssuedAt + token!.expiresIn!) < now; + } + return false; + }, )..authenticationStatus.listen(print); - await freshLink.setToken(const OAuth2Token(accessToken: 't0ps3cret!')); + + // Set the initial token and store issuedAt + final initialIssuedAt = currentUnixTime(); + await storeIssuedAt(initialIssuedAt); + + await freshLink.setToken( + const OAuth2Token( + accessToken: 't0ps3cret!', + expiresIn: 30, + ), + ); + final graphQLClient = GraphQLClient( cache: GraphQLCache(), link: Link.from([freshLink, HttpLink('https://api.graphql.jobs')]),