vxcontrol/pentagi · error
nonce mismatch in Google ID Token
Error message
nonce mismatch in Google ID Token
What it means
After cryptographic verification succeeds, the resolver requires that the id_token's nonce claim exactly equals the nonce sent in the authorization request (backend/pkg/server/oauth/google.go:37). A mismatch means the token cannot be bound to this specific login session, so it is rejected to prevent token injection/replay.
Source
Thrown at backend/pkg/server/oauth/google.go:37
return func(ctx context.Context, nonce string, token *oauth2.Token) (string, bool, error) {
provider, err := oidc.NewProvider(ctx, "https://accounts.google.com")
if err != nil {
return "", false, fmt.Errorf("could not create Google OpenID client: %w", err)
}
oidToken, ok := token.Extra("id_token").(string)
if !ok {
return "", false, fmt.Errorf("id_token is not present in the token")
}
verifier := provider.Verifier(&oidc.Config{ClientID: clientID})
idToken, err := verifier.Verify(ctx, oidToken)
if err != nil {
return "", false, fmt.Errorf("could not verify Google ID Token: %w", err)
}
if idToken.Nonce != nonce {
return "", false, fmt.Errorf("nonce mismatch in Google ID Token")
}
if err = idToken.VerifyAccessToken(token.AccessToken); err != nil {
return "", false, fmt.Errorf("failed to verify Google Access Token: %w", err)
}
claims := googleTokenClaims{}
if err := idToken.Claims(&claims); err != nil {
return "", false, fmt.Errorf("failed to parse Google ID Token claims: %w", err)
}
if claims.Nonce != nonce {
return "", false, fmt.Errorf("nonce mismatch in Google ID Token claims")
}
if claims.Email == "" {
return "", false, fmt.Errorf("email is empty in Google ID Token claims")
}View on GitHub (pinned to ea665308ba)
Solutions
- Restart the OAuth flow: generate a fresh nonce, store it server-side (or in a short-lived cookie), and include it in the authorize request.
- Ensure exactly one in-flight Google login per browser session — cancel the other tab's flow.
- Store the nonce per OAuth state value so parallel logins don't overwrite each other.
- Treat repeated mismatches as potential token replay and investigate, do not bypass the check.
Defensive patterns
Strategy: validation
Validate before calling
// always send a fresh nonce in the authorize request and keep it per-state
nonce := rand.Text()
state := rand.Text()
store.Set("oauth_state:"+state, nonce, 10*time.Minute)
url := conf.AuthCodeURL(state, oidc.Nonce(nonce)) Try / catch
email, verified, err := googleEmailResolver(ctx, nonce, token)
if err != nil {
if strings.Contains(err.Error(), "nonce mismatch") {
// restart the flow with a fresh nonce; do not retry with the same token
return "", false, fmt.Errorf("session mismatch: restart the Google sign-in from the beginning")
}
return "", false, err
} Prevention
- Store the nonce keyed by the OAuth state parameter so parallel logins cannot clobber it.
- Never reuse a nonce across login attempts; generate a fresh one per authorization.
- Use short expiry on stored nonces and clear them after a single use.
- Treat repeated nonce mismatches as possible token replay and alert on them.
When it happens
Trigger: idToken.Nonce != nonce: the callback is processed with a nonce value that differs from the one embedded in the ID token — e.g. the stored nonce was overwritten by a second concurrent login, the token is from an earlier session, or no nonce was actually included in the authorize request.
Common situations: Two browser tabs starting OAuth simultaneously and clobbering the stored nonce (cookie/session); replaying an old callback URL; the application regenerating the nonce between the authorize redirect and the callback; multi-instance deployments without shared nonce storage.
Related errors
- could not verify Google ID Token: %w
- failed to verify Google Access Token: %w
- nonce mismatch in Google ID Token claims
- could not create Google OpenID client: %w
- id_token is not present in the token
AI-assisted analysis of vxcontrol/pentagi@ea665308ba (2026-09-01).
Data as JSON: /api/errors/70a39d42b9737c61.
Report an issue: GitHub.