xai-org/grok-build · error · OidcError

OidcError::IssuerMismatch

Error message

OidcError::IssuerMismatch

What it means

OidcError::IssuerMismatch is thrown after jsonwebtoken::decode succeeds when the token's `iss` claim does not equal the expected_issuer passed to validate_and_extract_user_info. This is a defense-in-depth check on top of the library-level issuer validation, ensuring the ID token came from the identity provider the client configured.

Source

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

        .ok_or_else(|| anyhow::Error::new(OidcError::JwkNotFound { kid: kid.clone() }))?;
    let decoding_key = jsonwebtoken::DecodingKey::from_jwk(jwk)?;
    let alg = header.alg;
    ensure_alg_allowed(
        alg,
        discovery.id_token_signing_alg_values_supported.as_deref(),
    )?;
    let mut validation = jsonwebtoken::Validation::new(alg);
    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,

View on GitHub (pinned to bc7f02eddd)

Solutions

  1. Compare the token's `iss` claim (decode the JWT payload) with the configured expected_issuer and align them exactly.
  2. Fix the issuer URL in the OIDC config, matching byte-for-byte what the IdP puts in `iss` (watch trailing slashes and scheme).
  3. Re-authenticate against the correct environment/tenant so the token issuer matches configuration.

Example fix

// before: config issuer with trailing slash, token iss without
let expected_issuer = "https://auth.example.com/";
// after: match the IdP's iss claim exactly
let expected_issuer = "https://auth.example.com";
Defensive patterns

Strategy: validation

Validate before calling

// pre-check issuer claim before invoking the flow
fn token_issuer(token: &str) -> Option<String> {
    let payload = token.split('.').nth(1)?;
    let bytes = base64_url::decode(payload).ok()?;
    serde_json::from_slice::<serde_json::Value>(&bytes).ok()?.get("iss")?.as_str().map(String::from)
}
assert_eq!(token_issuer(token).as_deref(), Some(expected_issuer));

Type guard

fn issuer_matches(token: &str, expected: &str) -> bool { token_issuer(token).as_deref() == Some(expected) }

Try / catch

match result {
    Err(e) if e.to_string().contains("IssuerMismatch") => eprintln!("token issuer != configured issuer; check OIDC_ISSUER config"),
    other => other,
}

Prevention

When it happens

Trigger: Calling extract_user_info with expected_issuer X but a token whose `iss` claim is Y (protocol.rs:683-685). Happens when discovery/config issuer and the token's issuer diverge.

Common situations: Wrong issuer URL in client config (trailing-slash differences, http vs https, staging vs prod); multi-tenant IdP issuing tenant-specific issuers; environment variable pointing to a different realm.

Related errors


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