{"record":{"id":"4730eda43b1a32c5","repo":"spring-projects/spring-security","slug":"oauth2error-from-upstream-authorization-exception-4730ed","errorCode":null,"errorMessage":"OAuth2Error from upstream authorization exception (dynamic)","messagePattern":"OAuth2Error from upstream authorization exception \\(dynamic\\)","errorType":"exception","errorClass":"ClientAuthorizationException","httpStatus":null,"severity":"error","filePath":"oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/TokenExchangeOAuth2AuthorizedClientProvider.java","lineNumber":109,"sourceCode":"\n\t\treturn new OAuth2AuthorizedClient(clientRegistration, context.getPrincipal().getName(),\n\t\t\t\ttokenResponse.getAccessToken(), tokenResponse.getRefreshToken());\n\t}\n\n\tprivate @Nullable OAuth2Token resolveSubjectToken(OAuth2AuthorizationContext context) {\n\t\tif (context.getPrincipal().getPrincipal() instanceof OAuth2Token accessToken) {\n\t\t\treturn accessToken;\n\t\t}\n\t\treturn null;\n\t}\n\n\tprivate OAuth2AccessTokenResponse getTokenResponse(ClientRegistration clientRegistration,\n\t\t\tTokenExchangeGrantRequest tokenExchangeGrantRequest) {\n\t\ttry {\n\t\t\treturn this.accessTokenResponseClient.getTokenResponse(tokenExchangeGrantRequest);\n\t\t}\n\t\tcatch (OAuth2AuthorizationException ex) {\n\t\t\tthrow new ClientAuthorizationException(ex.getError(), clientRegistration.getRegistrationId(), ex);\n\t\t}\n\t}\n\n\tprivate boolean hasTokenExpired(OAuth2Token token) {\n\t\tInstant expiresAt = token.getExpiresAt();\n\t\treturn expiresAt != null && this.clock.instant().isAfter(expiresAt.minus(this.clockSkew));\n\t}\n\n\t/**\n\t * Sets the client used when requesting an access token credential at the Token\n\t * Endpoint for the {@code token-exchange} grant.\n\t * @param accessTokenResponseClient the client used when requesting an access token\n\t * credential at the Token Endpoint for the {@code token-exchange} grant\n\t */\n\tpublic void setAccessTokenResponseClient(\n\t\t\tOAuth2AccessTokenResponseClient<TokenExchangeGrantRequest> accessTokenResponseClient) {\n\t\tAssert.notNull(accessTokenResponseClient, \"accessTokenResponseClient cannot be null\");\n\t\tthis.accessTokenResponseClient = accessTokenResponseClient;","sourceCodeStart":91,"sourceCodeEnd":127,"githubUrl":"https://github.com/spring-projects/spring-security/blob/96852e8860138a482cb13d1479573f24ff6443c6/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/TokenExchangeOAuth2AuthorizedClientProvider.java#L91-L127","documentation":"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).","triggerScenarios":"tokenResponse(...) delegates to accessTokenResponseClient.getTokenResponse(tokenExchangeGrantRequest); the authorization server rejects the subject_token/token-exchange request, and the resulting OAuth2AuthorizationException is rethrown as ClientAuthorizationException.","commonSituations":"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.","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."],"exampleFix":"// before\nOAuth2AuthorizedClient c = provider.authorize(request); // throws on exchange failure\n// after\ntry {\n    c = provider.authorize(request);\n} catch (ClientAuthorizationException ex) {\n    logger.warn(\"token exchange failed: {}\", ex.getError().getErrorCode());\n    // fall back to service-account client-credentials client\n}","handlingStrategy":"try-catch","validationCode":"// pre-flight: verify the subject token is still valid before exchanging\nboolean subjectTokenUsable = subjectToken.getTokenValue() != null\n    && (subjectToken.getExpiresAt() == null\n        || clock.instant().isBefore(subjectToken.getExpiresAt()));","typeGuard":null,"tryCatchPattern":"try {\n    return tokenExchangeProvider.authorize(tokenExchangeGrantRequest);\n} catch (ClientAuthorizationException ex) {\n    log.warn(\"token exchange rejected: {}\", ex.getError().getErrorCode());\n    // fallback: client-credentials grant or re-authentication\n}","preventionTips":["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."],"tags":["oauth2","token-exchange","spring-security","rfc8693"],"backgroundTag":"oauth-token-exchange-failed","analyzedSha":"96852e8860138a482cb13d1479573f24ff6443c6","analyzedAt":"2026-09-10T23:25:23.477Z","contentChangedAt":"2026-09-10T23:25:23.477Z","schemaVersion":2},"datasetVersion":"2026-09-14T16:17:12.679Z"}