xai-org/grok-build · error · OidcError

OidcError::ForceLoginNoPrincipalsAllowed

Error message

OidcError::ForceLoginNoPrincipalsAllowed

What it means

OidcError::ForceLoginNoPrincipalsAllowed is raised by enforce_login_principal when the force_login_team_uuid policy is set to an empty AnyOf list. Since no team principal is permitted, the code fails closed and rejects the login instead of allowing everyone. It indicates a deployment misconfiguration, not a user mistake.

Source

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

/// Reject a token whose principal isn't allowed, BEFORE persisting (no partial
/// state). A restriction also rejects a token with no principal (else picking
/// "personal" on the consent page defeats it); an empty `AnyOf` fails closed.
///
/// The `actual` principal comes from the access-token claim
/// (`peek_access_token_principal`, an unverified `insecure_decode`). This
/// client-side check is fail-fast UX / defense-in-depth — NOT the security
/// boundary: the server re-validates the signed token on every API call and
/// is authoritative, so a locally tampered token still cannot reach the API.
pub(crate) fn enforce_login_principal(
    policy: Option<&ForceLoginTeam>,
    actual: Option<&str>,
) -> anyhow::Result<()> {
    let allowed: &[String] = match policy {
        None => return Ok(()),
        Some(ForceLoginTeam::Single(id)) => std::slice::from_ref(id),
        Some(ForceLoginTeam::AnyOf(ids)) if ids.is_empty() => {
            tracing::warn!("OIDC: force_login_team_uuid is an empty list; failing closed");
            return Err(anyhow::Error::new(OidcError::ForceLoginNoPrincipalsAllowed));
        }
        Some(ForceLoginTeam::AnyOf(ids)) => ids,
    };
    if let Some(actual) = actual
        && allowed.iter().any(|a| a == actual)
    {
        return Ok(());
    }
    let expected = if allowed.len() == 1 {
        format!("team {}", allowed[0])
    } else {
        format!("one of teams: {}", allowed.join(", "))
    };
    tracing::warn!(
        expected = %expected,
        actual = ?actual,
        "OIDC: login principal does not satisfy required policy; rejecting"
    );

View on GitHub (pinned to bc7f02eddd)

Solutions

  1. Remove force_login_team_uuid entirely from config to allow any principal
  2. Populate the list with at least one team UUID
  3. If the intent was 'block logins', disable OIDC login instead of using an empty allow-list

Example fix

// before
force_login_team_uuid: []
// after
force_login_team_uuid: ["<team-uuid>"]  // or delete the key to unpin
Defensive patterns

Strategy: validation

Validate before calling

// validate config before starting the login flow
fn validate_force_login(policy: Option<&ForceLoginTeam>) -> Result<(), String> {
    match policy {
        None | Some(ForceLoginTeam::Single(_)) => Ok(()),
        Some(ForceLoginTeam::AnyOf(ids)) if ids.is_empty() => {
            Err("force_login_team_uuid is empty; remove it or list >= 1 team".into())
        }
        Some(ForceLoginTeam::AnyOf(_)) => Ok(()),
    }
}

Type guard

fn has_allowed_principals(policy: Option<&ForceLoginTeam>) -> bool {
    match policy {
        None => true,
        Some(ForceLoginTeam::Single(_)) => true,
        Some(ForceLoginTeam::AnyOf(ids)) => !ids.is_empty(),
    }
}

Prevention

When it happens

Trigger: Calling the OIDC login flow (run_login_flow_with_config -> enforce_login_principal) when config.oidc.force_login_team_uuid is ForceLoginTeam::AnyOf with a zero-length ids vector.

Common situations: An administrator writes `force_login_team_uuid: []` in the deployment config (or an env/secret expansion yields an empty list) intending to disable pinning, but the empty list means 'no team allowed'.

Related errors


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