spring-projects/spring-security · error · ClientAuthorizationException
OAuth2Error from upstream authorization exception (dynamic)
Error message
OAuth2Error from upstream authorization exception (dynamic)
What it means
TokenExchangeOAuth2AuthorizedClientProvider performs an RFC 8693 token-exchange grant and wraps any OAuth2AuthorizationException from the token response client into a ClientAuthorizationException carrying the registration id and the upstream OAuth2Error. The message is dynamic — it reflects whatever error the authorization server returned for the exchange (e.g. invalid_grant, invalid_request, invalid_target).
Source
Thrown at oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/TokenExchangeOAuth2AuthorizedClientProvider.java:109
return new OAuth2AuthorizedClient(clientRegistration, context.getPrincipal().getName(),
tokenResponse.getAccessToken(), tokenResponse.getRefreshToken());
}
private @Nullable OAuth2Token resolveSubjectToken(OAuth2AuthorizationContext context) {
if (context.getPrincipal().getPrincipal() instanceof OAuth2Token accessToken) {
return accessToken;
}
return null;
}
private OAuth2AccessTokenResponse getTokenResponse(ClientRegistration clientRegistration,
TokenExchangeGrantRequest tokenExchangeGrantRequest) {
try {
return this.accessTokenResponseClient.getTokenResponse(tokenExchangeGrantRequest);
}
catch (OAuth2AuthorizationException ex) {
throw new ClientAuthorizationException(ex.getError(), clientRegistration.getRegistrationId(), ex);
}
}
private boolean hasTokenExpired(OAuth2Token token) {
Instant expiresAt = token.getExpiresAt();
return expiresAt != null && this.clock.instant().isAfter(expiresAt.minus(this.clockSkew));
}
/**
* Sets the client used when requesting an access token credential at the Token
* Endpoint for the {@code token-exchange} grant.
* @param accessTokenResponseClient the client used when requesting an access token
* credential at the Token Endpoint for the {@code token-exchange} grant
*/
public void setAccessTokenResponseClient(
OAuth2AccessTokenResponseClient<TokenExchangeGrantRequest> accessTokenResponseClient) {
Assert.notNull(accessTokenResponseClient, "accessTokenResponseClient cannot be null");
this.accessTokenResponseClient = accessTokenResponseClient;View on GitHub (pinned to 96852e8860)
Solutions
- Inspect the wrapped OAuth2Error error code to identify the exact server-side rejection and fix the corresponding configuration.
- Ensure the subject token is valid, unexpired, and issued by the same issuer the exchange endpoint trusts.
- Configure audience/target mapping on the IDP (e.g. Keycloak token-exchange permissions, Azure On-Behalf-Of scope) for the requested resource.
- Catch ClientAuthorizationException and fall back to a direct client-credentials or re-authentication flow.
Example fix
// before
OAuth2AuthorizedClient c = provider.authorize(request); // throws on exchange failure
// after
try {
c = provider.authorize(request);
} catch (ClientAuthorizationException ex) {
logger.warn("token exchange failed: {}", ex.getError().getErrorCode());
// fall back to service-account client-credentials client
} Defensive patterns
Strategy: try-catch
Validate before calling
// pre-flight: verify the subject token is still valid before exchanging
boolean subjectTokenUsable = subjectToken.getTokenValue() != null
&& (subjectToken.getExpiresAt() == null
|| clock.instant().isBefore(subjectToken.getExpiresAt())); Try / catch
try {
return tokenExchangeProvider.authorize(tokenExchangeGrantRequest);
} catch (ClientAuthorizationException ex) {
log.warn("token exchange rejected: {}", ex.getError().getErrorCode());
// fallback: client-credentials grant or re-authentication
} Prevention
- Configure audience/target permissions on the IDP before enabling token exchange.
- Validate that the subject token issuer matches the exchange endpoint's realm.
- Log ex.getError().getErrorCode() to distinguish invalid_grant vs invalid_target and fix config accordingly.
When it happens
Trigger: tokenResponse(...) delegates to accessTokenResponseClient.getTokenResponse(tokenExchangeGrantRequest); the authorization server rejects the subject_token/token-exchange request, and the resulting OAuth2AuthorizationException is rethrown as ClientAuthorizationException.
Common situations: Subject token expired or from a different issuer/realm than the exchange endpoint expects; missing invalid_target audience mapping between the two services on the IDP; service account lacking permission to perform impersonation/exchange; wrong token-exchange grant type enabled on the client policy.
Related errors
- OAuth 2.0 Token Exchange parameter: ${parameterName} - The p
- OAuth2Error from upstream authorization exception (dynamic)
- Invalidated authorization code used by registered client '%s
- Unable to create an {OAuth2AuthorizedClientManager} bean. Ex
- invalid_dpop_proof
AI-assisted analysis of spring-projects/spring-security@96852e8860 (2026-09-10).
Data as JSON: /api/errors/4730eda43b1a32c5.
Report an issue: GitHub.