refresh method

Future<DVOAuthTokenResponse> refresh({
  1. required String clientId,
  2. required String? clientSecret,
  3. required String refreshToken,
  4. List<String>? scopes,
})

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);
}