spring-projects/spring-security · error · OAuth2AuthenticationException

invalid_id_token

invalid_id_token

Error message

${ex.getMessage()} (JwtException during ID Token decode)

What it means

During an OIDC refresh-token flow, the OidcAuthorizedClientRefreshedEventListener decodes the new id_token returned by the token endpoint using the configured JwtDecoder. If decoding fails with any JwtException (expired signature, malformed token, bad signature, etc.), the listener wraps the exception's message in an OAuth2AuthenticationException with error code 'invalid_id_token'. This means the refreshed ID Token could not be validated cryptographically or structurally before the per-claim checks run.

Source

Thrown at oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/authentication/OidcAuthorizedClientRefreshedEventListener.java:229

	}

	private OidcIdToken createOidcToken(ClientRegistration clientRegistration,
			OAuth2AccessTokenResponse accessTokenResponse) {
		JwtDecoder jwtDecoder = this.jwtDecoderFactory.createDecoder(clientRegistration);
		Jwt jwt = getJwt(accessTokenResponse, jwtDecoder);
		return new OidcIdToken(jwt.getTokenValue(), jwt.getIssuedAt(), jwt.getExpiresAt(), jwt.getClaims());
	}

	private Jwt getJwt(OAuth2AccessTokenResponse accessTokenResponse, JwtDecoder jwtDecoder) {
		try {
			Map<String, Object> parameters = accessTokenResponse.getAdditionalParameters();
			String idToken = (String) parameters.get(OidcParameterNames.ID_TOKEN);
			Assert.hasText(idToken, "id_token parameter cannot be null or empty");
			return jwtDecoder.decode(idToken);
		}
		catch (JwtException ex) {
			OAuth2Error invalidIdTokenError = new OAuth2Error(INVALID_ID_TOKEN_ERROR_CODE, ex.getMessage(), null);
			throw new OAuth2AuthenticationException(invalidIdTokenError, invalidIdTokenError.toString(), ex);
		}
	}

	private void validateIdToken(OidcUser existingOidcUser, OidcIdToken idToken) {
		// OpenID Connect Core 1.0 - Section 12.2 Successful Refresh Response
		// If an ID Token is returned as a result of a token refresh request, the
		// following requirements apply:
		// its iss Claim Value MUST be the same as in the ID Token issued when the
		// original authentication occurred,
		validateIssuer(existingOidcUser, idToken);
		// its sub Claim Value MUST be the same as in the ID Token issued when the
		// original authentication occurred,
		validateSubject(existingOidcUser, idToken);
		// its iat Claim MUST represent the time that the new ID Token is issued,
		validateIssuedAt(existingOidcUser, idToken);
		// its aud Claim Value MUST be the same as in the ID Token issued when the
		// original authentication occurred,
		validateAudience(existingOidcUser, idToken);

View on GitHub (pinned to 96852e8860)

Solutions

  1. Check the nested JwtException cause in the logs — it names the exact decode failure (signature, expiry, malformed).
  2. Ensure the JwtDecoder's JWKS cache can fetch the provider's current signing keys (verify jwkSetUri is reachable and keys were not rotated).
  3. Increase the clock skew on the decoder's validators, e.g. JwtTimestampValidator(Duration.ofSeconds(60)), if the failure is exp/iat tolerance.
  4. Decode the returned id_token manually (jwt.io or Nimbus) to inspect claims and confirm what the provider actually sent.
  5. If the provider does not return an id_token on refresh, confirm your ClientRegistration is OIDC-scoped and handle the null id_token path instead of forcing decode.

Example fix

// before
JwtDecoder decoder = NimbusJwtDecoder.withJwkSetUri(jwkSetUri).build();
// after
JwtDecoder decoder = NimbusJwtDecoder.withJwkSetUri(jwkSetUri)
    .decoders-Alg(SignatureAlgorithm.RS256)
    .build();
// and relax clock skew when needed:
OAuth2TokenValidator<Jwt> withSkew = new DelegatingOAuth2TokenValidator<>(
    new JwtTimestampValidator(Duration.ofSeconds(60)),
    new OidcIdTokenValidator(clientRegistration));
((NimbusJwtDecoder) decoder).setJwtValidator(withSkew);
Defensive patterns

Strategy: try-catch

Validate before calling

// pre-check: decode before refresh commit
try { jwtDecoder.decode(idToken); } catch (JwtException e) { /* abort refresh */ }

Try / catch

try {
    listener.onApplicationEvent(event);
} catch (OAuth2AuthenticationException ex) {
    if ("invalid_id_token".equals(ex.getError().getErrorCode())) {
        authorizedClientService.removeAuthorizedClient(registrationId, principalName);
        // force re-authentication
    }
    logger.warn("ID token decode failed: {}", ex.getCause(), ex);
}

Prevention

When it happens

Trigger: A refresh token response returns an id_token that fails JwtDecoder.decode: expired iat/exp outside clock skew, signature verification failure (wrong keys/JWKS unreachable), malformed JWT, or missing claims required by the validator chain.

Common situations: Provider rotates signing keys and the cached JWKS is stale; the refresh response omits id_token but a non-OIDC flow is misconfigured; clock skew between client and IdP exceeds the validator's tolerance; custom JwtValidator rejects the token; the wrong decoder is registered for the registration.

Related errors


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