{"record":{"id":"5b8439cf461000f4","repo":"spring-projects/spring-security","slug":"oauth2error-from-upstream-authorization-exception","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/RefreshTokenOAuth2AuthorizedClientProvider.java","lineNumber":120,"sourceCode":"\t\t\t\tauthorizedClient.getClientRegistration(), context.getPrincipal().getName(),\n\t\t\t\ttokenResponse.getAccessToken(), tokenResponse.getRefreshToken());\n\n\t\tif (this.applicationEventPublisher != null) {\n\t\t\tOAuth2AuthorizedClientRefreshedEvent authorizedClientRefreshedEvent = new OAuth2AuthorizedClientRefreshedEvent(\n\t\t\t\t\ttokenResponse, refreshedAuthorizedClient);\n\t\t\tthis.applicationEventPublisher.publishEvent(authorizedClientRefreshedEvent);\n\t\t}\n\n\t\treturn refreshedAuthorizedClient;\n\t}\n\n\tprivate OAuth2AccessTokenResponse getTokenResponse(OAuth2AuthorizedClient authorizedClient,\n\t\t\tOAuth2RefreshTokenGrantRequest refreshTokenGrantRequest) {\n\t\ttry {\n\t\t\treturn this.accessTokenResponseClient.getTokenResponse(refreshTokenGrantRequest);\n\t\t}\n\t\tcatch (OAuth2AuthorizationException ex) {\n\t\t\tthrow new ClientAuthorizationException(ex.getError(),\n\t\t\t\t\tauthorizedClient.getClientRegistration().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 refresh_token} grant.\n\t * @param accessTokenResponseClient the client used when requesting an access token\n\t * credential at the Token Endpoint for the {@code refresh_token} grant\n\t */\n\tpublic void setAccessTokenResponseClient(\n\t\t\tOAuth2AccessTokenResponseClient<OAuth2RefreshTokenGrantRequest> accessTokenResponseClient) {\n\t\tAssert.notNull(accessTokenResponseClient, \"accessTokenResponseClient cannot be null\");","sourceCodeStart":102,"sourceCodeEnd":138,"githubUrl":"https://github.com/spring-projects/spring-security/blob/96852e8860138a482cb13d1479573f24ff6443c6/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/RefreshTokenOAuth2AuthorizedClientProvider.java#L102-L138","documentation":"RefreshTokenOAuth2AuthorizedClientProvider wraps the access-token response client's refresh grant call. If the underlying client throws an OAuth2AuthorizationException (any OAuth2Error returned by the authorization server, e.g. invalid_grant, invalid_scope), the provider rethrows it as a ClientAuthorizationException carrying the client's registration id, preserving the upstream OAuth2Error. The exact message is dynamic and comes from the authorization server's error response.","triggerScenarios":"authorizedClientProvider.authorize(...) decides a refresh is needed and calls accessTokenResponseClient.getTokenResponse(refreshTokenGrantRequest); the authorization server rejects the refresh grant with an OAuth2AuthorizationException, which is rethrown wrapped in ClientAuthorizationException.","commonSituations":"Refresh token expired or revoked (invalid_grant) after password change, user logout, or IDP session revocation; refresh token rotated and the old one used concurrently; scopes narrowed; IDP rejects refresh due to policy (max lifetime, device binding); misconfigured client credentials.","solutions":["Catch ClientAuthorizationException and trigger a fresh authorization code flow (re-login) — the refresh token is no longer usable.","Verify you always use the latest rotated refresh token (IDPs like Keycloak rotate refresh tokens; old ones become invalid).","Check client-id/client-secret and token endpoint configuration against the provider's current settings.","Inspect the wrapped OAuth2Error (getErrorCode) to see the specific authorization-server error and address it (e.g. invalid_scope → adjust requested scopes)."],"exampleFix":"// before\nOAuth2AuthorizedClient client = provider.authorize(request); // throws\n// after\ntry {\n    client = provider.authorize(request);\n} catch (ClientAuthorizationException ex) {\n    // refresh token invalid: clear stored client and restart authorization code flow\n    authorizedClientService.removeAuthorizedClient(\n        request.getClientRegistration().getRegistrationId(), request.getPrincipal().getName());\n}","handlingStrategy":"try-catch","validationCode":"// before refreshing, check the token is still worth refreshing\nboolean refreshable = authorizedClient.getRefreshToken() != null\n    && (authorizedClient.getAccessToken().getExpiresAt() == null\n        || clock.instant().isAfter(authorizedClient.getAccessToken().getExpiresAt().minus(clockSkew)));","typeGuard":null,"tryCatchPattern":"try {\n    return refreshTokenProvider.authorize(refreshTokenGrantRequest);\n} catch (ClientAuthorizationException ex) {\n    if (\"invalid_grant\".equals(ex.getError().getErrorCode())) {\n        // refresh token expired/revoked: remove stored client, trigger authorization_code flow\n    }\n    throw ex;\n}","preventionTips":["Always persist the latest refresh token when the IDP rotates them.","Catch ClientAuthorizationException and fall back to a full authorization-code login.","Monitor refresh failures to detect IDP session revocations and credential drift early."],"tags":["oauth2","refresh-token","spring-security","authorization-server"],"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-14T11:17:12.474Z"}