exchangeCodeForToken method

Future<OAuth2PkceTokenResponse> exchangeCodeForToken({
  1. required String code,
  2. String? codeVerifier,
  3. required String redirectUri,
  4. Client? httpClient,
})

Exchanges an authorization code for an access token.

Implements the OAuth2 token exchange flow per RFC 6749 with optional PKCE extension from RFC 7636.

Parameters:

  • code: Authorization code received after user authorizes the application
  • codeVerifier: Optional PKCE code verifier used to generate the code challenge. Required only if the OAuth2 provider uses PKCE. When provided, proves the client making this request initiated the authorization flow.
  • redirectUri: Must exactly match the redirect URI from the authorization request (required by OAuth2 specification for security)
  • httpClient: Optional HTTP client for testing with mocks. If not provided, a new http.Client is created

Returns the OAuth2PkceTokenResponse on success, including access token, optional refresh token, expiration time, and provider-specific data.

Throws one of the following OAuth2Exception subclasses in case of errors:

Implementation

Future<OAuth2PkceTokenResponse> exchangeCodeForToken({
  required final String code,
  final String? codeVerifier,
  required final String redirectUri,
  http.Client? httpClient,
}) async {
  final clientProvidedByUser = httpClient != null;
  httpClient ??= http.Client();

  try {
    final Map<String, dynamic> bodyParams = {
      'grant_type': 'authorization_code',
      'code': code,
      'redirect_uri': redirectUri,
      ...config.tokenRequestParams,
    };

    if (codeVerifier != null) {
      bodyParams['code_verifier'] = codeVerifier;
    }

    final headers = Map<String, String>.from(config.tokenRequestHeaders);

    switch (config.credentialsLocation) {
      case OAuth2CredentialsLocation.header:
        final credentials = base64Encode(
          utf8.encode('${config.clientId}:${config.clientSecret}'),
        );
        headers['Authorization'] = 'Basic $credentials';
        break;

      case OAuth2CredentialsLocation.body:
        bodyParams[config.clientIdKey] = config.clientId;
        bodyParams[config.clientSecretKey] = config.clientSecret;
        break;
    }

    final body = bodyParams.entries
        .map(
          (final e) =>
              '${Uri.encodeQueryComponent(e.key)}=${Uri.encodeQueryComponent(e.value.toString())}',
        )
        .join('&');

    final response = await httpClient.post(
      config.tokenEndpointUrl,
      headers: headers,
      body: body,
    );

    if (response.statusCode != 200) {
      throw OAuth2InvalidResponseException(
        'Failed to exchange authorization code for access token: '
        'HTTP ${response.statusCode}',
      );
    }

    Map<String, dynamic> responseBody;
    try {
      responseBody = jsonDecode(response.body) as Map<String, dynamic>;
    } catch (e) {
      throw OAuth2InvalidResponseException(
        'Failed to parse token response as JSON: ${e.toString()}',
      );
    }

    try {
      return config.parseTokenResponse(responseBody);
    } on OAuth2Exception {
      rethrow;
    } catch (e) {
      throw OAuth2MissingAccessTokenException(
        'Token response parsing failed: ${e.toString()}',
      );
    }
  } on OAuth2Exception {
    rethrow;
  } catch (e) {
    throw OAuth2NetworkErrorException(
      'Network error during token exchange: ${e.toString()}',
    );
  } finally {
    // Close the client only if we created it internally
    if (!clientProvidedByUser) {
      httpClient.close();
    }
  }
}