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 5 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 .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
build/
packages
.packages
.idea/
Cantasura marked this conversation as resolved.
Show resolved Hide resolved

# Or the files created by dart2js.
*.dart.js
Expand Down
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