Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/proactive token validation before requests #111

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -45,10 +54,13 @@ class JsonplaceholderClient {
required String password,
}) async {
await Future<void>.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,
),
);
}
Expand All @@ -69,4 +81,23 @@ class JsonplaceholderClient {
.map((dynamic item) => Photo.fromJson(item as Map<String, dynamic>))
.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<void> storeIssuedAt(int issuedTime) async {
print('Storing issuedAt: $issuedTime');
_storedIssuedAt = issuedTime;
}

/// Simulate fetching issuedAt from storage.
static Future<int?> fetchIssuedAt() async {
print('Fetching issuedAt...');
return _storedIssuedAt;
}
}
23 changes: 22 additions & 1 deletion packages/fresh_dio/lib/src/fresh.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ import 'package:fresh_dio/fresh_dio.dart';
/// Signature for `shouldRefresh` on [Fresh].
typedef ShouldRefresh = bool Function(Response<dynamic>? response);

/// Signature for proactive token validation before a request.
typedef ShouldRefreshBeforeRequest<T> = Future<bool> Function(T? token);

/// Signature for `refreshToken` on [Fresh].
typedef RefreshToken<T> = Future<T> Function(T? token, Dio httpClient);

Expand All @@ -30,10 +33,12 @@ class Fresh<T> extends QueuedInterceptor with FreshMixin<T> {
required TokenStorage<T> tokenStorage,
required RefreshToken<T> refreshToken,
ShouldRefresh? shouldRefresh,
ShouldRefreshBeforeRequest<T>? shouldRefreshBeforeRequest,
Dio? httpClient,
}) : _refreshToken = refreshToken,
_tokenHeader = tokenHeader,
_shouldRefresh = shouldRefresh ?? _defaultShouldRefresh,
_shouldRefreshBeforeRequest = shouldRefreshBeforeRequest,
_httpClient = httpClient ?? Dio() {
this.tokenStorage = tokenStorage;
}
Expand All @@ -53,13 +58,15 @@ class Fresh<T> extends QueuedInterceptor with FreshMixin<T> {
required TokenStorage<OAuth2Token> tokenStorage,
required RefreshToken<OAuth2Token> refreshToken,
ShouldRefresh? shouldRefresh,
ShouldRefreshBeforeRequest<OAuth2Token>? shouldRefreshBeforeRequest,
Dio? httpClient,
TokenHeaderBuilder<OAuth2Token>? tokenHeader,
}) {
return Fresh<OAuth2Token>(
refreshToken: refreshToken,
tokenStorage: tokenStorage,
shouldRefresh: shouldRefresh,
shouldRefreshBeforeRequest: shouldRefreshBeforeRequest,
httpClient: httpClient,
tokenHeader: tokenHeader ??
(token) {
Expand All @@ -73,6 +80,7 @@ class Fresh<T> extends QueuedInterceptor with FreshMixin<T> {
final Dio _httpClient;
final TokenHeaderBuilder<T> _tokenHeader;
final ShouldRefresh _shouldRefresh;
final ShouldRefreshBeforeRequest<T>? _shouldRefreshBeforeRequest;
final RefreshToken<T> _refreshToken;

@override
Expand Down Expand Up @@ -103,7 +111,20 @@ Example:
''',
);

final currentToken = await token;
var currentToken = await token;

if (_shouldRefreshBeforeRequest != null &&
currentToken != null &&
await _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 <String, String>{};
Expand Down
46 changes: 43 additions & 3 deletions packages/fresh_graphql/example/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -14,21 +14,61 @@ const getJobsQuery = '''
}
''';

// Mock storage for issuedAt
int? issuedAt;

// Simulate storing issuedAt when the token is set
Future<void> storeIssuedAt(int issuedTime) async {
print('Storing issuedAt: $issuedTime');
issuedAt = issuedTime;
}

// Simulate fetching issuedAt from storage
Future<int?> 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(),
refreshToken: (token, client) async {
// Perform refresh and return new token
print('refreshing token!');
await Future<void>.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')]),
Expand Down
31 changes: 26 additions & 5 deletions packages/fresh_graphql/lib/src/fresh_link.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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<T> = Future<bool> Function(T? token);

/// Signature for `refreshToken` on [FreshLink].
typedef RefreshToken<T> = Future<T> 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
Expand All @@ -37,15 +40,17 @@ class FreshLink<T> extends Link with FreshMixin<T> {
required TokenStorage<T> tokenStorage,
required RefreshToken<T?> refreshToken,
required ShouldRefresh shouldRefresh,
ShouldRefreshBeforeRequest<T>? shouldRefreshBeforeRequest,
TokenHeaderBuilder<T?>? tokenHeader,
}) : _refreshToken = refreshToken,
_tokenHeader = (tokenHeader ?? (_) => <String, String>{}),
_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(
Expand All @@ -64,12 +69,14 @@ class FreshLink<T> extends Link with FreshMixin<T> {
required TokenStorage<OAuth2Token> tokenStorage,
required RefreshToken<OAuth2Token?> refreshToken,
required ShouldRefresh shouldRefresh,
ShouldRefreshBeforeRequest<OAuth2Token>? shouldRefreshBeforeRequest,
TokenHeaderBuilder<OAuth2Token?>? tokenHeader,
}) {
return FreshLink<OAuth2Token>(
refreshToken: refreshToken,
tokenStorage: tokenStorage,
shouldRefresh: shouldRefresh,
shouldRefreshBeforeRequest: shouldRefreshBeforeRequest,
tokenHeader: tokenHeader ??
(token) {
return {
Expand All @@ -82,10 +89,24 @@ class FreshLink<T> extends Link with FreshMixin<T> {
final RefreshToken<T?> _refreshToken;
final TokenHeaderBuilder<T?> _tokenHeader;
final ShouldRefresh _shouldRefresh;
final ShouldRefreshBeforeRequest<T>? _shouldRefreshBeforeRequest;

@override
Stream<Response> request(Request request, [NextLink? forward]) async* {
final currentToken = await token;
var currentToken = await token;

if (_shouldRefreshBeforeRequest != null &&
currentToken != null &&
await _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 <String, String>{};
Expand Down