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

  1. Inspect the wrapped OAuth2Error error code to identify the exact server-side rejection and fix the corresponding configuration.
  2. Ensure the subject token is valid, unexpired, and issued by the same issuer the exchange endpoint trusts.
  3. Configure audience/target mapping on the IDP (e.g. Keycloak token-exchange permissions, Azure On-Behalf-Of scope) for the requested resource.
  4. 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

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


AI-assisted analysis of spring-projects/spring-security@96852e8860 (2026-09-10). Data as JSON: /api/errors/4730eda43b1a32c5. Report an issue: GitHub.