exchangeCodeForToken method

Future<OAuth2PkceTokenResponse> exchangeCodeForToken({
  1. required String code,
  2. String? codeVerifier,
  3. required String redirectUri,
  4. bool includeClientSecret = true,
  5. 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)
  • includeClientSecret: Whether to include the client secret in the token request. Defaults to true, which sends credentials according to OAuth2PkceServerConfig.credentialsLocation. Set to false when the OAuth2 provider requires that the secret is omitted for certain client types (e.g., Microsoft rejects secrets for non-web clients). When false, only the client ID is sent in the request body per RFC 6749 §2.3.1, regardless of OAuth2PkceServerConfig.credentialsLocation.
  • 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,
  final bool includeClientSecret = true,
  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);

    if (includeClientSecret) {
      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;
      }
    } else {
      // Without a secret, client_id is always sent in the body per
      // RFC 6749 §2.3.1 (Authorization header requires both id and secret).
      bodyParams[config.clientIdKey] = config.clientId;
    }

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