diff --git a/lib/access_token_response.dart b/lib/access_token_response.dart index 5ef8905..9ea763e 100644 --- a/lib/access_token_response.dart +++ b/lib/access_token_response.dart @@ -1,6 +1,7 @@ import 'dart:convert'; import 'package:http/http.dart' as http; +import 'package:jwt_decoder/jwt_decoder.dart'; import 'package:oauth2_client/oauth2_response.dart'; /// Represents the response to an Access Token Request. @@ -79,6 +80,22 @@ class AccessTokenResponse extends OAuth2Response { return expired; } + bool isRefreshTokenExpired() { + if (!hasRefreshToken()) return true; + + final decodedRefreshToken = JwtDecoder.tryDecode(refreshToken ?? ''); + + if (decodedRefreshToken != null) { + final int? expirationTimestamp = decodedRefreshToken.containsKey('exp') ? decodedRefreshToken['exp'] : null; + if (expirationTimestamp != null) { + final refreshTokenExpirationDate = DateTime.fromMillisecondsSinceEpoch(Duration(seconds: expirationTimestamp).inMilliseconds); + return refreshTokenExpirationDate.difference(DateTime.now()).inSeconds < 0; + } + } + + return false; + } + ///Checks if the access token must be refreeshed bool refreshNeeded({secondsToExpiration = 30}) { var needsRefresh = false; @@ -106,6 +123,14 @@ class AccessTokenResponse extends OAuth2Response { return isValid() ? respMap['access_token'] : null; } + Map? get decodedAccessToken { + final rawAccessToken = accessToken; + if (rawAccessToken != null) { + return JwtDecoder.tryDecode(rawAccessToken); + } + return null; + } + String? get tokenType { //Some providers (e.g. Slack) don't return the token_type parameter, even if it's required... //In those cases, fallback to "bearer" diff --git a/lib/oauth2_client.dart b/lib/oauth2_client.dart index 1562b51..0715332 100644 --- a/lib/oauth2_client.dart +++ b/lib/oauth2_client.dart @@ -260,7 +260,7 @@ class OAuth2Client { } /// Refreshes an Access Token issuing a refresh_token grant to the OAuth2 server. - Future refreshToken(String refreshToken, + Future refreshToken(String refreshToken, {httpClient, required String clientId, String? clientSecret}) async { final Map params = getRefreshUrlParams(refreshToken: refreshToken); @@ -271,7 +271,12 @@ class OAuth2Client { params: params, httpClient: httpClient); - return http2TokenResponse(response); + if (response.statusCode >= 200 && response.statusCode < 300) { + return http2TokenResponse(response); + } + + return null; + } /// Revokes both the Access and the Refresh tokens in the provided [tknResp] diff --git a/lib/oauth2_helper.dart b/lib/oauth2_helper.dart index 666e738..ce62441 100644 --- a/lib/oauth2_helper.dart +++ b/lib/oauth2_helper.dart @@ -1,3 +1,4 @@ +import 'package:jwt_decoder/jwt_decoder.dart'; import 'package:oauth2_client/access_token_response.dart'; import 'package:oauth2_client/oauth2_exception.dart'; import 'package:oauth2_client/oauth2_client.dart'; @@ -88,7 +89,7 @@ class OAuth2Helper { if (tknResp != null) { if (tknResp.refreshNeeded()) { //The access token is expired - if (tknResp.refreshToken != null) { + if (tknResp.refreshToken != null && !tknResp.isRefreshTokenExpired()) { tknResp = await refreshToken(tknResp.refreshToken!); } else { //No refresh token, fetch a new token diff --git a/pubspec.yaml b/pubspec.yaml index 510dc77..d9e2066 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -16,6 +16,7 @@ dependencies: http: ^0.13.4 meta: ^1.7.0 random_string: ^2.3.1 + jwt_decoder: ^2.0.1 dev_dependencies: flutter_test: diff --git a/test/oauth2_client_test.dart b/test/oauth2_client_test.dart index 8661a1f..d33c2fc 100644 --- a/test/oauth2_client_test.dart +++ b/test/oauth2_client_test.dart @@ -354,8 +354,8 @@ void main() { .captured[1], {'Authorization': 'Basic bXljbGllbnRpZDp0ZXN0X3NlY3JldA=='}); - expect(resp.isValid(), true); - expect(resp.accessToken, accessToken); + expect(resp?.isValid() ?? false, true); + expect(resp?.accessToken ?? '', accessToken); }); test('Error in refreshing token', () async { @@ -381,7 +381,7 @@ void main() { .captured[1], {'Authorization': 'Basic bXljbGllbnRpZDp0ZXN0X3NlY3JldA=='}); - expect(resp.isValid(), false); + expect(resp?.isValid() ?? false, false); }); test('Authorization url params (1/5)', () {