{"record":{"id":"98d26e4bb1675c18","repo":"spring-projects/spring-security","slug":"invalid-nonce","errorCode":"invalid_nonce","errorMessage":"Invalid nonce","messagePattern":"Invalid nonce","errorType":"error_code","errorClass":"OAuth2AuthenticationException","httpStatus":null,"severity":"error","filePath":"oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/RefreshOidcUserReactiveOAuth2AuthorizationSuccessHandler.java","lineNumber":306,"sourceCode":"\t\t// the most recent time when the end-user reauthenticated when \"prompt=login\" is\n\t\t// passed in the authentication request\n\t\tif (!idToken.getAuthenticatedAt().equals(existingOidcUser.getIdToken().getAuthenticatedAt())\n\t\t\t\t&& (existingOidcUser.getIdToken().getAuthenticatedAt() == null\n\t\t\t\t\t\t|| !idToken.getAuthenticatedAt().isAfter(existingOidcUser.getIdToken().getAuthenticatedAt()))) {\n\t\t\tOAuth2Error oauth2Error = new OAuth2Error(INVALID_ID_TOKEN_ERROR_CODE, \"Invalid authenticated at time\",\n\t\t\t\t\tREFRESH_TOKEN_RESPONSE_ERROR_URI);\n\t\t\tthrow new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());\n\t\t}\n\t}\n\n\tprivate void validateNonce(OidcUser existingOidcUser, OidcIdToken idToken) {\n\t\tif (!StringUtils.hasText(idToken.getNonce())) {\n\t\t\treturn;\n\t\t}\n\t\tif (!idToken.getNonce().equals(existingOidcUser.getIdToken().getNonce())) {\n\t\t\tOAuth2Error oauth2Error = new OAuth2Error(INVALID_NONCE_ERROR_CODE, \"Invalid nonce\",\n\t\t\t\t\tREFRESH_TOKEN_RESPONSE_ERROR_URI);\n\t\t\tthrow new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());\n\t\t}\n\t}\n\n\tprivate Mono<Void> refreshSecurityContext(ServerWebExchange exchange, ClientRegistration clientRegistration,\n\t\t\tOAuth2AuthenticationToken authenticationToken, OidcUser oidcUser) {\n\t\tCollection<? extends GrantedAuthority> mappedAuthorities = this.authoritiesMapper\n\t\t\t.mapAuthorities(oidcUser.getAuthorities());\n\t\tOAuth2AuthenticationToken authenticationResult = new OAuth2AuthenticationToken(oidcUser, mappedAuthorities,\n\t\t\t\tclientRegistration.getRegistrationId());\n\t\tauthenticationResult.setDetails(authenticationToken.getDetails());\n\t\tSecurityContext securityContext = new SecurityContextImpl(authenticationResult);\n\t\treturn this.serverSecurityContextRepository.save(exchange, securityContext);\n\t}\n\n\tprivate static final class NonRotatingWebSessionServerSecurityContextRepository\n\t\t\timplements ServerSecurityContextRepository {\n\n\t\tprivate static final Log logger = LogFactory.getLog(NonRotatingWebSessionServerSecurityContextRepository.class);","sourceCodeStart":288,"sourceCodeEnd":324,"githubUrl":"https://github.com/spring-projects/spring-security/blob/96852e8860138a482cb13d1479573f24ff6443c6/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/RefreshOidcUserReactiveOAuth2AuthorizationSuccessHandler.java#L288-L324","documentation":"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.","triggerScenarios":"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().","commonSituations":"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.","solutions":["Verify the IDP returns the same nonce claim that was sent in the initial authorization request.","Check that the nonce stored in the session matches the one embedded in the authorization request URL.","If the provider should not send a nonce on refresh at all, fix provider behavior or configure the nonce only for the initial request.","Force full re-authentication for the affected session and investigate potential token replay."],"exampleFix":"// before: nonce generated anew per request but compared against stale stored value\n// after: store the nonce used in the authorization request and keep it stable for the session's lifetime\nsession.setAttribute(\"nonce\", nonce); // reuse for subsequent refresh validations","handlingStrategy":"validation","validationCode":"boolean nonceOk = !StringUtils.hasText(newIdToken.getNonce())\n    || newIdToken.getNonce().equals(existingOidcUser.getIdToken().getNonce());\nif (!nonceOk) { /* reject before refresh */ }","typeGuard":"boolean nonceMatches(OidcIdToken fresh, OidcIdToken stored) {\n    return !StringUtils.hasText(fresh.getNonce())\n        || fresh.getNonce().equals(stored.getNonce());\n}","tryCatchPattern":"try {\n    handler.onAuthenticationSuccess(exchange, authentication);\n} catch (OAuth2AuthenticationException ex) {\n    if (\"invalid_nonce\".equals(ex.getError().getErrorCode())) {\n        // treat as replay: invalidate session, restart authorization code flow\n    }\n}","preventionTips":["Generate one nonce per session and reuse it in the stored authorization request.","Compare the token's nonce with the exact value sent in the authorization request URL.","Treat nonce mismatch as possible token replay and force re-login."],"tags":["oauth2","oidc","nonce","spring-security","id-token"],"backgroundTag":"jwt-nonce-mismatch","analyzedSha":"96852e8860138a482cb13d1479573f24ff6443c6","analyzedAt":"2026-09-10T23:25:23.477Z","contentChangedAt":"2026-09-10T23:25:23.477Z","schemaVersion":2},"datasetVersion":"2026-09-14T16:17:12.679Z"}