spring-projects/spring-security · error · OAuth2AuthorizationException

OAuth2Error from authorization response error (dynamic)

Error message

OAuth2Error from authorization response error (dynamic)

What it means

OAuth2AuthorizationCodeAuthenticationProvider handles the OAuth2 authorization-code callback. If the authorization response reports an error (statusError), it throws an OAuth2AuthorizationException carrying that error verbatim; if the state parameter does not match the original authorization request, it throws an OAuth2AuthorizationException with invalid_state_parameter. The message is dynamic because it comes from the authorization server's error redirect.

Source

Thrown at oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/OAuth2AuthorizationCodeAuthenticationProvider.java:81

	 * provided parameters.
	 * @param accessTokenResponseClient the client used for requesting the access token
	 * credential from the Token Endpoint
	 */
	public OAuth2AuthorizationCodeAuthenticationProvider(
			OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> accessTokenResponseClient) {
		Assert.notNull(accessTokenResponseClient, "accessTokenResponseClient cannot be null");
		this.accessTokenResponseClient = accessTokenResponseClient;
	}

	@Override
	public Authentication authenticate(Authentication authentication) throws AuthenticationException {
		OAuth2AuthorizationCodeAuthenticationToken authorizationCodeAuthentication = (OAuth2AuthorizationCodeAuthenticationToken) authentication;
		OAuth2AuthorizationResponse authorizationResponse = authorizationCodeAuthentication.getAuthorizationExchange()
			.getAuthorizationResponse();
		if (authorizationResponse.statusError()) {
			OAuth2Error error = authorizationResponse.getError();
			Assert.notNull(error, "error cannot be null when status is error");
			throw new OAuth2AuthorizationException(error);
		}
		OAuth2AuthorizationRequest authorizationRequest = authorizationCodeAuthentication.getAuthorizationExchange()
			.getAuthorizationRequest();
		if (!Objects.equals(authorizationResponse.getState(), authorizationRequest.getState())) {
			OAuth2Error oauth2Error = new OAuth2Error(INVALID_STATE_PARAMETER_ERROR_CODE);
			throw new OAuth2AuthorizationException(oauth2Error);
		}
		OAuth2AccessTokenResponse accessTokenResponse = this.accessTokenResponseClient.getTokenResponse(
				new OAuth2AuthorizationCodeGrantRequest(authorizationCodeAuthentication.getClientRegistration(),
						authorizationCodeAuthentication.getAuthorizationExchange()));
		OAuth2AuthorizationCodeAuthenticationToken authenticationResult = new OAuth2AuthorizationCodeAuthenticationToken(
				authorizationCodeAuthentication.getClientRegistration(),
				authorizationCodeAuthentication.getAuthorizationExchange(), accessTokenResponse.getAccessToken(),
				accessTokenResponse.getRefreshToken(), accessTokenResponse.getAdditionalParameters());
		authenticationResult.setDetails(authorizationCodeAuthentication.getDetails());
		return authenticationResult;
	}

View on GitHub (pinned to 96852e8860)

Solutions

  1. Check the OAuth2Error error code in the exception — access_denied etc. is a user/provider decision, not a bug; handle it as a normal cancel.
  2. For invalid_state_parameter, ensure the OAuth2AuthorizationRequestRepository (session/cookie) persists across the redirect: enable sticky sessions or a shared session store in multi-instance deployments.
  3. Verify cookies are not blocked/stripped (SameSite/secure settings, proxy configuration) so state survives the round trip.
  4. Confirm redirect-uri and provider metadata are correct so the callback hits the same app instance/session that started the flow.

Example fix

// before: in-memory session lost behind LB with multiple instances
// after: sticky sessions or distributed session
// Spring Boot
spring.session.store-type=redis
// or at the LB: enable session affinity (sticky cookie) for the app instances
Defensive patterns

Strategy: try-catch

Validate before calling

// validate the callback before invoking the provider
OAuth2AuthorizationResponse resp = authorizationExchange.getAuthorizationResponse();
boolean callbackOk = !resp.statusError()
    && Objects.equals(resp.getState(), authorizationExchange.getAuthorizationRequest().getState());

Type guard

boolean isRecoverableAuthorizationError(OAuth2AuthorizationException ex) {
    return "access_denied".equals(ex.getError().getErrorCode())
        || "invalid_state_parameter".equals(ex.getError().getErrorCode());
}

Try / catch

try {
    return authenticationManager.authenticate(authorizationCodeAuthentication);
} catch (OAuth2AuthorizationException ex) {
    if ("access_denied".equals(ex.getError().getErrorCode())) {
        // user declined consent — show friendly cancel page
    } else if ("invalid_state_parameter".equals(ex.getError().getErrorCode())) {
        // restart the login flow; check session stickiness/cookies
    }
}

Prevention

When it happens

Trigger: authenticate(...) receives an OAuth2AuthorizationCodeAuthenticationToken whose authorization response has statusError() (e.g. error=access_denied) or whose state differs from the state stored in the authorization request; the provider throws OAuth2AuthorizationException with the upstream error or invalid_state_parameter.

Common situations: User denies consent at the provider (access_denied redirect); app restarted/scaled to another instance between request and callback so the AuthorizationRequestRepository (usually HttpSession) lost the stored state; load balancer without sticky sessions; cookies blocked, dropping the state attribute; registering the wrong redirect URI so the provider receives a malformed callback.

Related errors


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