Skip to content
This repository has been archived by the owner on Aug 26, 2024. It is now read-only.

add refresh_expires_in property to credentials #166

Closed
wants to merge 6 commits into from
Closed
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
## 2.0.3-wip

* Require `package:http` v1.0.0
* add property `refreshToKenExpiration` to `Credentials`

## 2.0.2

Expand Down
56 changes: 48 additions & 8 deletions lib/src/credentials.dart
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,12 @@ class Credentials {
/// expiration date.
final DateTime? expiration;

/// The date at which the refresh token will expire.
///
/// This is likely to be a few seconds earlier than the server's idea of the
/// expiration date.
final DateTime? refreshToKenExpiration;

/// The function used to parse parameters from a host's response.
final GetParameters _getParameters;

Expand All @@ -85,8 +91,15 @@ class Credentials {
return expiration != null && DateTime.now().isAfter(expiration);
}

bool get isRefreshTokenExpired {
var refreshToKenExpiration = this.refreshToKenExpiration;
return refreshToKenExpiration != null &&
DateTime.now().isAfter(refreshToKenExpiration);
}

/// Whether it's possible to refresh these credentials.
bool get canRefresh => refreshToken != null && tokenEndpoint != null;
bool get canRefresh =>
refreshToken != null && tokenEndpoint != null && !isRefreshTokenExpired;

/// Creates a new set of credentials.
///
Expand Down Expand Up @@ -114,6 +127,7 @@ class Credentials {
this.tokenEndpoint,
Iterable<String>? scopes,
this.expiration,
this.refreshToKenExpiration,
String? delimiter,
Map<String, dynamic> Function(MediaType? mediaType, String body)?
getParameters})
Expand Down Expand Up @@ -176,13 +190,34 @@ class Credentials {
expirationDateTime = DateTime.fromMillisecondsSinceEpoch(expiration);
}

var accessTokenExpiration = parsed['accessTokenExpiration'];
DateTime? accessTokenExpirationDateTime;
if (accessTokenExpiration != null) {
validate(accessTokenExpiration is int,
'field "expiration" was not an int, was "$accessTokenExpiration"');
accessTokenExpiration = accessTokenExpiration as int;
accessTokenExpirationDateTime =
DateTime.fromMillisecondsSinceEpoch(accessTokenExpiration);
}

var refreshTokenExpiration = parsed['refreshTokenExpiration'];
DateTime? refreshTokenExpirationDateTime;
if (refreshTokenExpiration != null) {
validate(refreshTokenExpiration is int,
'field "expiration" was not an int, was "$refreshTokenExpiration"');
refreshTokenExpiration = refreshTokenExpiration as int;
refreshTokenExpirationDateTime =
DateTime.fromMillisecondsSinceEpoch(refreshTokenExpiration);
}

return Credentials(
parsed['accessToken'] as String,
refreshToken: parsed['refreshToken'] as String?,
idToken: parsed['idToken'] as String?,
tokenEndpoint: tokenEndpointUri,
scopes: (scopes as List).map((scope) => scope as String),
expiration: expirationDateTime,
refreshToKenExpiration: refreshTokenExpirationDateTime,
);
}

Expand All @@ -196,7 +231,9 @@ class Credentials {
'idToken': idToken,
'tokenEndpoint': tokenEndpoint?.toString(),
'scopes': scopes,
'expiration': expiration?.millisecondsSinceEpoch
'expiration': expiration?.millisecondsSinceEpoch,
'refreshTokenExpiration':
refreshToKenExpiration?.millisecondsSinceEpoch,
});

/// Returns a new set of refreshed credentials.
Expand Down Expand Up @@ -257,11 +294,14 @@ class Credentials {
// The authorization server may issue a new refresh token. If it doesn't,
// we should re-use the one we already have.
if (credentials.refreshToken != null) return credentials;
return Credentials(credentials.accessToken,
refreshToken: refreshToken,
idToken: credentials.idToken,
tokenEndpoint: credentials.tokenEndpoint,
scopes: credentials.scopes,
expiration: credentials.expiration);
return Credentials(
credentials.accessToken,
refreshToken: refreshToken,
idToken: credentials.idToken,
tokenEndpoint: credentials.tokenEndpoint,
scopes: credentials.scopes,
expiration: credentials.expiration,
refreshToKenExpiration: credentials.refreshToKenExpiration,
);
}
}
27 changes: 27 additions & 0 deletions lib/src/handle_access_token_response.dart
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,23 @@ Credentials handleAccessTokenResponse(http.Response response, Uri tokenEndpoint,
}
}

var refreshExpiresIn = parameters['refresh_expires_in'];
if (refreshExpiresIn != null) {
if (refreshExpiresIn is String) {
try {
refreshExpiresIn = double.parse(refreshExpiresIn).toInt();
} on FormatException {
throw FormatException(
'parameter "refresh_expires_in" could not be parsed as in, was: '
'"$refreshExpiresIn"',
);
}
} else if (refreshExpiresIn is! int) {
throw FormatException(
'parameter "expires_in" was not an int, was: "$refreshExpiresIn"');
}
}

for (var name in ['refresh_token', 'id_token', 'scope']) {
var value = parameters[name];
if (value != null && value is! String) {
Expand All @@ -99,13 +116,23 @@ Credentials handleAccessTokenResponse(http.Response response, Uri tokenEndpoint,
? null
: startTime.add(Duration(seconds: expiresIn as int) - _expirationGrace);

var accessExpiration = expiresIn == null
? null
: startTime.add(Duration(seconds: expiresIn as int) - _expirationGrace);

var refreshExpiration = refreshExpiresIn == null
? null
: startTime
.add(Duration(seconds: refreshExpiresIn as int) - _expirationGrace);

return Credentials(
parameters['access_token'] as String,
refreshToken: parameters['refresh_token'] as String?,
idToken: parameters['id_token'] as String?,
tokenEndpoint: tokenEndpoint,
scopes: scopes,
expiration: expiration,
refreshToKenExpiration: refreshExpiration,
);
} on FormatException catch (e) {
throw FormatException('Invalid OAuth response for "$tokenEndpoint": '
Expand Down
42 changes: 35 additions & 7 deletions test/credentials_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -23,20 +23,40 @@ void main() {
expect(credentials.isExpired, isFalse);
});

test('is not expired if the expiration is in the future', () {
test('is not expired if no expiration exists', () {
var credentials =
oauth2.Credentials('access token', refreshToken: 'refresh token');
expect(credentials.isRefreshTokenExpired, isFalse);
});

test('is not expired if the accessExpiration is in the future', () {
var expiration = DateTime.now().add(const Duration(hours: 1));
var credentials =
oauth2.Credentials('access token', expiration: expiration);
expect(credentials.isExpired, isFalse);
});

test('is not expired if the refreshExpiration is in the future', () {
var expiration = DateTime.now().add(const Duration(hours: 1));
var credentials =
oauth2.Credentials('access token', refreshToKenExpiration: expiration);
expect(credentials.isRefreshTokenExpired, isFalse);
});

test('is expired if the expiration is in the past', () {
var expiration = DateTime.now().subtract(const Duration(hours: 1));
var credentials =
oauth2.Credentials('access token', expiration: expiration);
expect(credentials.isExpired, isTrue);
});

test('is expired if the refreshExpiration is in the past', () {
var expiration = DateTime.now().subtract(const Duration(hours: 1));
var credentials =
oauth2.Credentials('access token', refreshToKenExpiration: expiration);
expect(credentials.isRefreshTokenExpired, isTrue);
});

test("can't refresh without a refresh token", () {
var credentials =
oauth2.Credentials('access token', tokenEndpoint: tokenEndpoint);
Expand Down Expand Up @@ -268,13 +288,19 @@ void main() {
var expiration = DateTime.now().subtract(const Duration(hours: 1));
expiration = DateTime.fromMillisecondsSinceEpoch(
expiration.millisecondsSinceEpoch);
var refreshExpiration = DateTime.now();
refreshExpiration = DateTime.fromMillisecondsSinceEpoch(
refreshExpiration.millisecondsSinceEpoch);

var credentials = oauth2.Credentials('access token',
refreshToken: 'refresh token',
idToken: 'id token',
tokenEndpoint: tokenEndpoint,
scopes: ['scope1', 'scope2'],
expiration: expiration);
var credentials = oauth2.Credentials(
'access token',
refreshToken: 'refresh token',
idToken: 'id token',
tokenEndpoint: tokenEndpoint,
scopes: ['scope1', 'scope2'],
expiration: expiration,
refreshToKenExpiration: refreshExpiration,
);
var reloaded = oauth2.Credentials.fromJson(credentials.toJson());

expect(reloaded.accessToken, equals(credentials.accessToken));
Expand All @@ -284,6 +310,8 @@ void main() {
equals(credentials.tokenEndpoint.toString()));
expect(reloaded.scopes, equals(credentials.scopes));
expect(reloaded.expiration, equals(credentials.expiration));
expect(reloaded.refreshToKenExpiration,
equals(credentials.refreshToKenExpiration));
});

test('should throw a FormatException for invalid JSON', () {
Expand Down
18 changes: 18 additions & 0 deletions test/handle_access_token_response_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -118,13 +118,15 @@ void main() {
Object? accessToken = 'access token',
Object? tokenType = 'bearer',
Object? expiresIn,
Object? refreshExpiresIn,
Object? refreshToken,
Object? scope}) {
return handle(http.Response(
jsonEncode({
'access_token': accessToken,
'token_type': tokenType,
'expires_in': expiresIn,
'refresh_expires_in': refreshExpiresIn,
'refresh_token': refreshToken,
'scope': scope
}),
Expand Down Expand Up @@ -228,12 +230,26 @@ void main() {
startTime.millisecondsSinceEpoch + 90 * 1000);
});

test(
'with refresh-expires-in sets the expiration to ten seconds earlier than'
' the server says', () {
var credentials = handleSuccess(refreshExpiresIn: 100);
expect(credentials.refreshToKenExpiration?.millisecondsSinceEpoch,
startTime.millisecondsSinceEpoch + 90 * 1000);
});

test('with expires-in encoded as string', () {
var credentials = handleSuccess(expiresIn: '110');
expect(credentials.expiration?.millisecondsSinceEpoch,
startTime.millisecondsSinceEpoch + 100 * 1000);
});

test('with expires-in encoded as string', () {
var credentials = handleSuccess(refreshExpiresIn: '110');
expect(credentials.refreshToKenExpiration?.millisecondsSinceEpoch,
startTime.millisecondsSinceEpoch + 100 * 1000);
});

test('with a non-string refresh token throws a FormatException', () {
expect(() => handleSuccess(refreshToken: 12), throwsFormatException);
});
Expand Down Expand Up @@ -275,13 +291,15 @@ void main() {
Object? accessToken = 'access token',
Object? tokenType = 'bearer',
Object? expiresIn,
Object? refreshExpiresIn,
Object? idToken = 'decode me',
Object? scope}) {
return handle(http.Response(
jsonEncode({
'access_token': accessToken,
'token_type': tokenType,
'expires_in': expiresIn,
'refresh_expires_in': refreshExpiresIn,
'id_token': idToken,
'scope': scope
}),
Expand Down
Loading