spring-projects/spring-security · error · OAuth2AuthenticationException

invalid_id_token

invalid_id_token

Error message

Invalid issuer

What it means

When an OIDC session's tokens are refreshed, RefreshOidcUserReactiveOAuth2AuthorizationSuccessHandler validates the newly received ID token against the previously stored OidcUser before updating the security context. If either the new or the existing ID token's issuer is missing, or the two issuer values differ, it throws an OAuth2AuthenticationException with the invalid_id_token error, since issuer mismatch indicates the token may not come from the trusted provider.

Source

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

		validateAudience(existingOidcUser, idToken);
		// if the ID Token contains an auth_time Claim, its value MUST represent the time
		// of the original authentication - not the time that the new ID token is issued,
		validateAuthenticatedAt(existingOidcUser, idToken);
		// it SHOULD NOT have a nonce Claim, even when the ID Token issued at the time of
		// the original authentication contained nonce; however, if it is present, its
		// value MUST be the same as in the ID Token issued at the time of the original
		// authentication,
		validateNonce(existingOidcUser, idToken);
	}

	private void validateIssuer(OidcUser existingOidcUser, OidcIdToken idToken) {
		URL idTokenIssuer = idToken.getIssuer();
		URL existingIdTokenIssuer = existingOidcUser.getIdToken().getIssuer();
		if (idTokenIssuer == null || existingIdTokenIssuer == null
				|| !idTokenIssuer.toString().equals(existingIdTokenIssuer.toString())) {
			OAuth2Error oauth2Error = new OAuth2Error(INVALID_ID_TOKEN_ERROR_CODE, "Invalid issuer",
					REFRESH_TOKEN_RESPONSE_ERROR_URI);
			throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
		}
	}

	private void validateSubject(OidcUser existingOidcUser, OidcIdToken idToken) {
		if (!Objects.equals(idToken.getSubject(), existingOidcUser.getIdToken().getSubject())) {
			OAuth2Error oauth2Error = new OAuth2Error(INVALID_ID_TOKEN_ERROR_CODE, "Invalid subject",
					REFRESH_TOKEN_RESPONSE_ERROR_URI);
			throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
		}
	}

	private void validateIssuedAt(OidcUser existingOidcUser, OidcIdToken idToken) {
		Instant idTokenIssuedAt = idToken.getIssuedAt();
		Instant existingIdTokenIssuedAt = existingOidcUser.getIdToken().getIssuedAt();
		if (idTokenIssuedAt == null || existingIdTokenIssuedAt == null
				|| !idTokenIssuedAt.isAfter(existingIdTokenIssuedAt.minus(this.clockSkew))) {
			OAuth2Error oauth2Error = new OAuth2Error(INVALID_ID_TOKEN_ERROR_CODE, "Invalid issued at time",
					REFRESH_TOKEN_RESPONSE_ERROR_URI);

View on GitHub (pinned to 96852e8860)

Solutions

  1. Ensure the IDP always issues the same iss value that matches your configured issuer/registration; fix provider or realm configuration.
  2. If the issuer legitimately changed (migration), force users to re-authenticate (invalidate sessions) rather than refreshing old tokens.
  3. Verify issuer URI formatting (trailing slash, http vs https, host aliases) is identical between config and the token's iss claim.
  4. Subclass the handler to customize or relax issuer comparison only if your deployment genuinely supports multiple issuers.

Example fix

// before: realm renamed, old sessions refresh against new issuer
// after: pin issuer and rotate sessions on change
// application.yml
spring.security.oauth2.client.provider.myidp.issuer-uri=https://idp.example.com/realms/stable
// plus a migration plan that invalidates existing sessions
Defensive patterns

Strategy: try-catch

Validate before calling

boolean sameIssuer = newIdToken.getIssuer() != null
    && existingOidcUser.getIdToken().getIssuer() != null
    && newIdToken.getIssuer().toString().equals(existingOidcUser.getIdToken().getIssuer().toString());

Type guard

boolean issuerMatches(OidcIdToken fresh, OidcIdToken stored) {
    return fresh.getIssuer() != null && stored.getIssuer() != null
        && fresh.getIssuer().toString().equals(stored.getIssuer().toString());
}

Try / catch

try {
    handler.onAuthenticationSuccess(exchange, authentication);
} catch (OAuth2AuthenticationException ex) {
    if ("invalid_id_token".equals(ex.getError().getErrorCode())) {
        // invalidate session and restart authorization code flow
    }
}

Prevention

When it happens

Trigger: A refresh-token flow yields a new OidcIdToken whose iss claim differs from (or is absent compared to) the issuer recorded on the existing session's OidcUser, during validateIdToken inside the reactive authorization success handler.

Common situations: Identity provider reconfigured with a new issuer URL (different scheme/host/path) mid-session; sessions surviving a provider migration or regional endpoint change; misconfigured issuer in provider metadata vs application config; tokens minted by a different realm/environment (e.g. dev vs prod issuer) sharing the same client.

Related errors


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