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
- Restart the login flow so a fresh nonce is generated and correctly carried through authorize -> callback -> token exchange.
- Verify the nonce generated at authorize time is the same one passed as expected_nonce to extract_user_info (check state persistence).
- 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
- Store nonce per-session alongside CSRF state, never globally.
- Never reuse ID tokens across login attempts.
- Persist nonce through redirects and clear it after successful validation.
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
- no OIDC auth entry found in {}. Run `grok login` first.
- auth entry has no refresh_token — cannot refresh expired tok
- auth entry has no oidc_issuer — cannot refresh expired token
- auth entry has no oidc_client_id — cannot refresh expired to
- OidcError::JwkNotFound
AI-assisted analysis of xai-org/grok-build@bc7f02eddd (2026-08-31).
Data as JSON: /api/errors/5c5eda5481e31461.
Report an issue: GitHub.