vxcontrol/pentagi · error

nonce mismatch in Google ID Token claims

Error message

nonce mismatch in Google ID Token claims

What it means

Thrown when the nonce claim inside the decoded Google ID Token does not equal the nonce value that PentAGI generated and embedded in the OAuth state at authorization start. This is an anti-replay check: it proves the ID token returned in the code exchange corresponds to this exact login attempt, not a replayed or cross-request token. Note that idToken.Nonce is already checked at line 36 — this second check validates the nonce found in the raw claims payload.

Source

Thrown at backend/pkg/server/oauth/google.go:50

		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")
		}

		return claims.Email, claims.EmailVerified, nil
	}
}

func NewGoogleOAuthClient(clientID, clientSecret, redirectURL string) OAuthClient {
	return NewOAuthClient("google", &oauth2.Config{
		ClientID:     clientID,
		ClientSecret: clientSecret,
		RedirectURL:  redirectURL,
		Scopes: []string{
			"https://www.googleapis.com/auth/userinfo.email",
			"openid",

View on GitHub (pinned to ea665308ba)

Solutions

  1. Have the user retry the login from a single tab and clear any stale OAuth state cookies.
  2. Verify the state cookie handling: the nonce used at /auth/authorize must be the same one carried through state to the callback (check for cookie name collisions across tabs).
  3. Do not cache or reuse authorization codes/states; each login must generate a fresh nonce.
  4. If you see this consistently, log both expected and received nonce to identify which side (state cookie vs token) is stale.
  5. Ensure any reverse proxy is not caching the callback URL response.
Defensive patterns

Strategy: validation

Validate before calling

// before initiating login, ensure one state/nonce per attempt and no concurrent sessions sharing the state cookie
state, nonce, err := newOAuthState(userID) // fresh random nonce per attempt
if err != nil { return err }
// never reuse a state value; bind it to a single-use cookie with Max-Age ~10m

Type guard

func nonceMatches(expected, claimsNonce string) bool {
    return expected != "" && claimsNonce != "" && subtle.ConstantTimeCompare([]byte(expected), []byte(claimsNonce)) == 1
}

Try / catch

email, verified, err := resolver.Resolve(ctx, nonce, token)
if err != nil {
    if strings.Contains(err.Error(), "nonce mismatch") {
        return fmt.Errorf("login attempt expired or replayed — please start a new sign-in")
    }
    return err
}

Prevention

When it happens

Trigger: AuthAuthorize callback flow: the state-derived nonce differs from claims.Nonce — e.g. the user completed the OAuth flow in a second browser tab/attempt so the callback state belongs to an older authorization request, or the same code/state pair was replayed.

Common situations: User opens the login link twice (two in-flight states, callback uses stale state cookie); browser back-button resubmitting an old callback; multiple concurrent login sessions overwriting the state cookie; clock/retry issues causing the token exchange to run twice.

Related errors


AI-assisted analysis of vxcontrol/pentagi@ea665308ba (2026-09-01). Data as JSON: /api/errors/38ca2cf5937509cf. Report an issue: GitHub.