{"record":{"id":"70a39d42b9737c61","repo":"vxcontrol/pentagi","slug":"nonce-mismatch-in-google-id-token","errorCode":null,"errorMessage":"nonce mismatch in Google ID Token","messagePattern":"nonce mismatch in Google ID Token","errorType":"http","errorClass":null,"httpStatus":null,"severity":"error","filePath":"backend/pkg/server/oauth/google.go","lineNumber":37,"sourceCode":"\treturn func(ctx context.Context, nonce string, token *oauth2.Token) (string, bool, error) {\n\t\tprovider, err := oidc.NewProvider(ctx, \"https://accounts.google.com\")\n\t\tif err != nil {\n\t\t\treturn \"\", false, fmt.Errorf(\"could not create Google OpenID client: %w\", err)\n\t\t}\n\n\t\toidToken, ok := token.Extra(\"id_token\").(string)\n\t\tif !ok {\n\t\t\treturn \"\", false, fmt.Errorf(\"id_token is not present in the token\")\n\t\t}\n\n\t\tverifier := provider.Verifier(&oidc.Config{ClientID: clientID})\n\t\tidToken, err := verifier.Verify(ctx, oidToken)\n\t\tif err != nil {\n\t\t\treturn \"\", false, fmt.Errorf(\"could not verify Google ID Token: %w\", err)\n\t\t}\n\n\t\tif idToken.Nonce != nonce {\n\t\t\treturn \"\", false, fmt.Errorf(\"nonce mismatch in Google ID Token\")\n\t\t}\n\n\t\tif err = idToken.VerifyAccessToken(token.AccessToken); err != nil {\n\t\t\treturn \"\", false, fmt.Errorf(\"failed to verify Google Access Token: %w\", err)\n\t\t}\n\n\t\tclaims := googleTokenClaims{}\n\t\tif err := idToken.Claims(&claims); err != nil {\n\t\t\treturn \"\", false, fmt.Errorf(\"failed to parse Google ID Token claims: %w\", err)\n\t\t}\n\n\t\tif claims.Nonce != nonce {\n\t\t\treturn \"\", false, fmt.Errorf(\"nonce mismatch in Google ID Token claims\")\n\t\t}\n\n\t\tif claims.Email == \"\" {\n\t\t\treturn \"\", false, fmt.Errorf(\"email is empty in Google ID Token claims\")\n\t\t}","sourceCodeStart":19,"sourceCodeEnd":55,"githubUrl":"https://github.com/vxcontrol/pentagi/blob/ea665308baaff015b226f308438a68d929d0f29b/backend/pkg/server/oauth/google.go#L19-L55","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":null,"handlingStrategy":"validation","validationCode":"// always send a fresh nonce in the authorize request and keep it per-state\nnonce := rand.Text()\nstate := rand.Text()\nstore.Set(\"oauth_state:\"+state, nonce, 10*time.Minute)\nurl := conf.AuthCodeURL(state, oidc.Nonce(nonce))","typeGuard":null,"tryCatchPattern":"email, verified, err := googleEmailResolver(ctx, nonce, token)\nif err != nil {\n    if strings.Contains(err.Error(), \"nonce mismatch\") {\n        // restart the flow with a fresh nonce; do not retry with the same token\n        return \"\", false, fmt.Errorf(\"session mismatch: restart the Google sign-in from the beginning\")\n    }\n    return \"\", false, err\n}","preventionTips":["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."],"tags":["oauth","oidc","google","security","nonce"],"backgroundTag":"oidc-nonce-mismatch","analyzedSha":"ea665308baaff015b226f308438a68d929d0f29b","analyzedAt":"2026-09-01T14:16:31.421Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T10:18:20.063Z"}