spring-projects/spring-security · error · OAuth2AuthenticationException

invalid_nonce

invalid_nonce

Error message

Invalid nonce

What it means

validateNonce compares the nonce claim of the refreshed ID token with the nonce recorded on the previously authenticated OidcUser. If the new token contains a nonce that does not match the stored one, the handler throws an OAuth2AuthenticationException with the invalid_nonce error, guarding against nonce replay/mixing between the original authorization request and refreshed tokens.

Source

Thrown at oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/RefreshOidcUserReactiveOAuth2AuthorizationSuccessHandler.java:306

		// the most recent time when the end-user reauthenticated when "prompt=login" is
		// passed in the authentication request
		if (!idToken.getAuthenticatedAt().equals(existingOidcUser.getIdToken().getAuthenticatedAt())
				&& (existingOidcUser.getIdToken().getAuthenticatedAt() == null
						|| !idToken.getAuthenticatedAt().isAfter(existingOidcUser.getIdToken().getAuthenticatedAt()))) {
			OAuth2Error oauth2Error = new OAuth2Error(INVALID_ID_TOKEN_ERROR_CODE, "Invalid authenticated at time",
					REFRESH_TOKEN_RESPONSE_ERROR_URI);
			throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
		}
	}

	private void validateNonce(OidcUser existingOidcUser, OidcIdToken idToken) {
		if (!StringUtils.hasText(idToken.getNonce())) {
			return;
		}
		if (!idToken.getNonce().equals(existingOidcUser.getIdToken().getNonce())) {
			OAuth2Error oauth2Error = new OAuth2Error(INVALID_NONCE_ERROR_CODE, "Invalid nonce",
					REFRESH_TOKEN_RESPONSE_ERROR_URI);
			throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
		}
	}

	private Mono<Void> refreshSecurityContext(ServerWebExchange exchange, ClientRegistration clientRegistration,
			OAuth2AuthenticationToken authenticationToken, OidcUser oidcUser) {
		Collection<? extends GrantedAuthority> mappedAuthorities = this.authoritiesMapper
			.mapAuthorities(oidcUser.getAuthorities());
		OAuth2AuthenticationToken authenticationResult = new OAuth2AuthenticationToken(oidcUser, mappedAuthorities,
				clientRegistration.getRegistrationId());
		authenticationResult.setDetails(authenticationToken.getDetails());
		SecurityContext securityContext = new SecurityContextImpl(authenticationResult);
		return this.serverSecurityContextRepository.save(exchange, securityContext);
	}

	private static final class NonRotatingWebSessionServerSecurityContextRepository
			implements ServerSecurityContextRepository {

		private static final Log logger = LogFactory.getLog(NonRotatingWebSessionServerSecurityContextRepository.class);

View on GitHub (pinned to 96852e8860)

Solutions

  1. Verify the IDP returns the same nonce claim that was sent in the initial authorization request.
  2. Check that the nonce stored in the session matches the one embedded in the authorization request URL.
  3. If the provider should not send a nonce on refresh at all, fix provider behavior or configure the nonce only for the initial request.
  4. Force full re-authentication for the affected session and investigate potential token replay.

Example fix

// before: nonce generated anew per request but compared against stale stored value
// after: store the nonce used in the authorization request and keep it stable for the session's lifetime
session.setAttribute("nonce", nonce); // reuse for subsequent refresh validations
Defensive patterns

Strategy: validation

Validate before calling

boolean nonceOk = !StringUtils.hasText(newIdToken.getNonce())
    || newIdToken.getNonce().equals(existingOidcUser.getIdToken().getNonce());
if (!nonceOk) { /* reject before refresh */ }

Type guard

boolean nonceMatches(OidcIdToken fresh, OidcIdToken stored) {
    return !StringUtils.hasText(fresh.getNonce())
        || fresh.getNonce().equals(stored.getNonce());
}

Try / catch

try {
    handler.onAuthenticationSuccess(exchange, authentication);
} catch (OAuth2AuthenticationException ex) {
    if ("invalid_nonce".equals(ex.getError().getErrorCode())) {
        // treat as replay: invalidate session, restart authorization code flow
    }
}

Prevention

When it happens

Trigger: validateIdToken calls validateNonce during a refresh-token flow; the new OidcIdToken has a non-empty nonce (StringUtils.hasText true) whose value differs from existingOidcUser.getIdToken().getNonce().

Common situations: IDP echoes a different nonce on refreshed ID tokens than the one sent in the original authorization request; a replayed or cross-session token; nonce generated per-request but stored value not updated; token response hijacked from a different session.

Related errors


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