refresh method
The refresh_token grant. The presented token is spent; a spent one
presented again revokes the grant.
Implementation
Future<DVOAuthTokenResponse> refresh({
required String clientId,
required String? clientSecret,
required String refreshToken,
List<String>? scopes,
}) async {
final DVOAuthClient client = await _authenticateClient(
clientId,
clientSecret,
);
final DVRecord? record = await _tokens.read(DVSecretHash.of(refreshToken));
if (record == null || record.values['kind'] != 'refresh') {
throw const DVOAuthError('invalid_grant', 'unknown refresh token');
}
final String grantId = '${record.values['grant_id']}';
final DVRecord? grant = await _grants.read(grantId);
// Checked before spending: another client presenting this token must not
// be able to burn it, or to trigger the reuse revocation below.
if (grant == null || grant.values['client_id'] != client.id) {
throw const DVOAuthError('invalid_grant', 'unknown refresh token');
}
if (record.values['used_at'] != null) {
await _revokeGrant(grantId, 'rotated refresh token presented again');
throw const DVOAuthError('invalid_grant', 'refresh token already used');
}
final List<String> granted = _list(grant.values['scopes']);
final List<String> requested = scopes == null
? _list(record.values['scopes'])
: _unique(scopes);
if (requested.isEmpty || !requested.every(granted.contains)) {
throw const DVOAuthError(
'invalid_scope',
'a refresh cannot widen the granted scopes',
);
}
if (record.values['revoked_at'] != null ||
!_now().isBefore(_date(record.values['expires_at'])!)) {
throw const DVOAuthError('invalid_grant', 'refresh token expired');
}
await _liveGrant(grantId);
if (!await _spend(_tokens, record)) {
await _revokeGrant(grantId, 'refresh token presented twice at once');
throw const DVOAuthError('invalid_grant', 'refresh token already used');
}
return _issue(await _liveGrant(grantId), requested, refresh: true);
}