spring-projects/spring-security · error · ClientAuthorizationException
OAuth2Error from upstream authorization exception (dynamic)
Error message
OAuth2Error from upstream authorization exception (dynamic)
What it means
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.
Source
Thrown at oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/RefreshTokenOAuth2AuthorizedClientProvider.java:120
authorizedClient.getClientRegistration(), context.getPrincipal().getName(),
tokenResponse.getAccessToken(), tokenResponse.getRefreshToken());
if (this.applicationEventPublisher != null) {
OAuth2AuthorizedClientRefreshedEvent authorizedClientRefreshedEvent = new OAuth2AuthorizedClientRefreshedEvent(
tokenResponse, refreshedAuthorizedClient);
this.applicationEventPublisher.publishEvent(authorizedClientRefreshedEvent);
}
return refreshedAuthorizedClient;
}
private OAuth2AccessTokenResponse getTokenResponse(OAuth2AuthorizedClient authorizedClient,
OAuth2RefreshTokenGrantRequest refreshTokenGrantRequest) {
try {
return this.accessTokenResponseClient.getTokenResponse(refreshTokenGrantRequest);
}
catch (OAuth2AuthorizationException ex) {
throw new ClientAuthorizationException(ex.getError(),
authorizedClient.getClientRegistration().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 refresh_token} grant.
* @param accessTokenResponseClient the client used when requesting an access token
* credential at the Token Endpoint for the {@code refresh_token} grant
*/
public void setAccessTokenResponseClient(
OAuth2AccessTokenResponseClient<OAuth2RefreshTokenGrantRequest> accessTokenResponseClient) {
Assert.notNull(accessTokenResponseClient, "accessTokenResponseClient cannot be null");View on GitHub (pinned to 96852e8860)
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).
Example fix
// before
OAuth2AuthorizedClient client = provider.authorize(request); // throws
// after
try {
client = provider.authorize(request);
} catch (ClientAuthorizationException ex) {
// refresh token invalid: clear stored client and restart authorization code flow
authorizedClientService.removeAuthorizedClient(
request.getClientRegistration().getRegistrationId(), request.getPrincipal().getName());
} Defensive patterns
Strategy: try-catch
Validate before calling
// before refreshing, check the token is still worth refreshing
boolean refreshable = authorizedClient.getRefreshToken() != null
&& (authorizedClient.getAccessToken().getExpiresAt() == null
|| clock.instant().isAfter(authorizedClient.getAccessToken().getExpiresAt().minus(clockSkew))); Try / catch
try {
return refreshTokenProvider.authorize(refreshTokenGrantRequest);
} catch (ClientAuthorizationException ex) {
if ("invalid_grant".equals(ex.getError().getErrorCode())) {
// refresh token expired/revoked: remove stored client, trigger authorization_code flow
}
throw ex;
} Prevention
- 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.
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- OAuth 2.0 Parameter: ${parameterName}
- Unable to create an {OAuth2AuthorizedClientManager} bean. Ex
- invalid_dpop_proof
- invalid_scope
- oidc_provider_not_configured
AI-assisted analysis of spring-projects/spring-security@96852e8860 (2026-09-10).
Data as JSON: /api/errors/5b8439cf461000f4.
Report an issue: GitHub.