vxcontrol/pentagi · error

could not verify Google ID Token: %w

Error message

could not verify Google ID Token: %w

What it means

The resolver verifies the ID token's signature, issuer, audience, and expiry via provider.Verifier(...).Verify (backend/pkg/server/oauth/google.go:33). Any failure — bad signature, wrong client_id audience, expired token, malformed JWT — is wrapped as this error and the login is rejected.

Source

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

	EmailVerified bool   `json:"email_verified"`
}

func newGoogleEmailResolver(clientID string) OAuthEmailResolver {
	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")
		}

View on GitHub (pinned to ea665308ba)

Solutions

  1. Confirm the clientID passed to the verifier matches the OAuth client that issued the token (same Google Cloud project).
  2. Check server clock synchronization (NTP) to avoid spurious expiry/issuance-time failures.
  3. Have the user retry the login to obtain a fresh token if it simply expired mid-flow.
  4. Log the verification error with errors.Unwrap to distinguish audience vs signature vs expiry causes.
Defensive patterns

Strategy: try-catch

Validate before calling

// verify the token's audience claim matches the configured clientID before deeper checks
idToken, err := verifier.Verify(ctx, oidToken)
if err != nil {
    return fmt.Errorf("ID token rejected (check clientID, clock skew, expiry): %w", err)
}

Try / catch

email, verified, err := googleEmailResolver(ctx, nonce, token)
if err != nil {
    if strings.Contains(err.Error(), "could not verify Google ID Token") {
        return "", false, fmt.Errorf("sign-in rejected: ID token invalid or issued for a different OAuth client")
    }
    return "", false, err
}

Prevention

When it happens

Trigger: verifier.Verify returns an error: the id_token's aud does not equal the configured clientID, the token is expired or issued in the future (clock skew), the signature does not match Google's discovered keys, or the JWT is malformed.

Common situations: clientID/secret from one Google Cloud project used against tokens issued for another; server clock drift making valid tokens appear expired; swapping OAuth client credentials between environments (staging vs prod); corrupted or hand-modified tokens.

Related errors


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