xai-org/grok-build · error · OidcError

NonceMismatch

NonceMismatch

Error message

OidcError::NonceMismatch

What it means

OidcError::NonceMismatch is thrown when the ID token's `nonce` claim differs from the expected_nonce supplied to validate_and_extract_user_info (protocol.rs:691-693). The nonce binds the ID token to a specific authorization request, preventing token replay/injection attacks; any mismatch means the token may not belong to this login attempt.

Source

Thrown at crates/codegen/xai-grok-shell/src/auth/oidc/protocol.rs:692

    validation.set_issuer(&[expected_issuer]);
    validation.set_audience(&[expected_client_id]);
    validation.validate_exp = true;
    validation.validate_aud = true;
    validation.required_spec_claims = ["sub", "iss", "aud", "exp"]
        .into_iter()
        .map(ToOwned::to_owned)
        .collect();
    let token_data = jsonwebtoken::decode::<IdTokenClaims>(token, &decoding_key, &validation)?;
    if token_data.claims.iss.as_deref() != Some(expected_issuer) {
        return Err(anyhow::Error::new(OidcError::IssuerMismatch));
    }
    if let Some(ref aud) = token_data.claims.aud
        && !aud_matches(aud, expected_client_id)
    {
        return Err(anyhow::Error::new(OidcError::AudienceMismatch));
    }
    if token_data.claims.nonce.as_deref() != Some(expected_nonce) {
        return Err(anyhow::Error::new(OidcError::NonceMismatch));
    }
    Ok(OidcUserInfo {
        user_id: token_data
            .claims
            .sub
            .unwrap_or_else(|| "unknown".to_string()),
        email: token_data.claims.email,
        first_name: token_data.claims.first_name,
        last_name: token_data.claims.last_name,
        profile_image_asset_id: token_data.claims.picture,
        principal_type: None,
        principal_id: None,
        team_id: None,
        team_name: None,
        team_role: None,
        organization_id: None,
        organization_name: None,
        organization_role: None,

View on GitHub (pinned to bc7f02eddd)

Solutions

  1. Restart the login flow so a fresh nonce is generated and correctly carried through authorize -> callback -> token exchange.
  2. Verify the nonce generated at authorize time is the same one passed as expected_nonce to extract_user_info (check state persistence).
  3. Ensure concurrent logins store nonce state per-session, not in a single shared variable.

Example fix

// before: shared nonce across concurrent sessions
static NONCE: OnceLock<String> = OnceLock::new();
// after: per-session nonce stored alongside the CSRF state
session_state.insert(sid, SessionState { nonce, pkce_verifier, .. });
Defensive patterns

Strategy: try-catch

Validate before calling

// ensure nonce stored at authorize time is passed unchanged
let stored = session_state.get(&sid).map(|s| s.nonce.clone());
assert_eq!(stored.as_deref(), Some(expected_nonce));

Try / catch

match result {
    Err(e) if e.to_string().contains("NonceMismatch") => { /* restart login flow to mint a new nonce */ start_login_flow(); }
    other => other,
}

Prevention

When it happens

Trigger: extract_user_info receives a token whose `nonce` claim is missing or different from the nonce generated for the current authorization request.

Common situations: Replaying an old ID token from a previous login; the callback response mixed up between concurrent login sessions; nonce state overwritten or not persisted between the authorize redirect and the token exchange; a token endpoint response cached from an earlier attempt.

Related errors


AI-assisted analysis of xai-org/grok-build@bc7f02eddd (2026-08-31). Data as JSON: /api/errors/5c5eda5481e31461. Report an issue: GitHub.