refresh method

Future<OAuthSession> refresh(
  1. OAuthSession session
)

Refreshes an OAuth 2.0 access token using DPoP-bound refresh token flow.

This method exchanges a refresh token for a new access token while maintaining the DPoP binding. It reuses the original DPoP key pair and handles nonce updates from the authorization server.

session The current OAuthSession containing the refresh token and DPoP credentials to be used for token refresh. The session must include valid DPoP keys and nonce.

Returns a Future<OAuthSession> containing the new access token, possibly a new refresh token, and updated session metadata.

The DPoP keys are preserved from the original session while the nonce may be updated.

Throws:

  • OAuthException in the following cases:
    • When no refresh token is available in the session
    • When the token refresh request fails
    • When the server returns an error response

Example:

final newSession = await oauth.refresh(currentSession);
print('New access token: ${newSession.accessToken}');

The method maintains DPoP proof-of-possession by:

  1. Reusing the DPoP key pair from the original session
  2. Creating a new DPoP proof header for the refresh request
  3. Updating the DPoP nonce if provided in the response

The returned OAuthSession preserves the DPoP binding by:

  • Keeping the same $publicKey and $privateKey
  • Updating the $dPoPNonce if provided by the server
  • Maintaining the DPoP-bound token type

Implementation

Future<OAuthSession> refresh(final OAuthSession session) async {
  if (session.refreshToken.isEmpty) {
    throw OAuthException('No refresh token available');
  }

  final endpoint = Uri.https(service, '/oauth/token');

  final dPoPHeader = getDPoPHeader(
    clientId: metadata.clientId,
    endpoint: endpoint.toString(),
    method: 'POST',
    dPoPNonce: session.$dPoPNonce,
    publicKey: session.$publicKey,
    privateKey: session.$privateKey,
  );

  final response = await http.post(
    endpoint,
    headers: {
      'DPoP': dPoPHeader,
    },
    body: {
      'client_id': metadata.clientId,
      'grant_type': 'refresh_token',
      'refresh_token': session.refreshToken,
    },
  );

  final body = jsonDecode(response.body);

  if (body['error'] == 'use_dpop_nonce' &&
      response.headers.containsKey('dpop-nonce')) {
    session.$dPoPNonce = response.headers['dpop-nonce']!;

    // Retry with next DPoP nonce
    return await refresh(session);
  }

  if (response.statusCode != 200) {
    throw OAuthException(response.body);
  }

  return OAuthSession(
    accessToken: body['access_token'],
    refreshToken: body['refresh_token'],
    tokenType: body['token_type'],
    scope: body['scope'],
    expiresAt:
        DateTime.now().toUtc().add(Duration(seconds: body['expires_in'])),
    sub: body['sub'],
    $dPoPNonce: response.headers['dpop-nonce']!,
    $publicKey: session.$publicKey,
    $privateKey: session.$privateKey,
  );
}