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

  1. Catch ClientAuthorizationException and trigger a fresh authorization code flow (re-login) — the refresh token is no longer usable.
  2. Verify you always use the latest rotated refresh token (IDPs like Keycloak rotate refresh tokens; old ones become invalid).
  3. Check client-id/client-secret and token endpoint configuration against the provider's current settings.
  4. 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

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


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