exchangeCodeForToken method
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 applicationcodeVerifier: 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 newhttp.Clientis 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:
- OAuth2InvalidResponseException if the response cannot be parsed
- OAuth2MissingAccessTokenException if the access token is missing
- OAuth2NetworkErrorException if a network error occurs
- OAuth2UnknownException for other unexpected 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();
}
}
}