# Authentication

> Browser OAuth (auth.x.ai), device-code flow, XAI_API_KEY for CI, grok login/logout, credential storage in ~/.grok/auth.json, and token refresh behavior.

- Repository: xai-org/grok-build
- GitHub: https://github.com/xai-org/grok-build
- Human docs: https://grok-wiki.com/public/docs/xai-org-grok-build-90205de50458
- Complete Markdown: https://grok-wiki.com/public/docs/xai-org-grok-build-90205de50458/llms-full.txt

## Source Files

- `crates/codegen/xai-grok-pager/docs/user-guide/02-authentication.md`
- `crates/codegen/xai-grok-shell/src/auth/manager.rs`
- `crates/codegen/xai-grok-shell/src/auth/storage.rs`
- `crates/codegen/xai-grok-shell/src/auth/device_code.rs`
- `crates/codegen/xai-grok-shell/src/auth/flow.rs`
- `crates/codegen/xai-grok-pager/src/app/cli.rs`
- `crates/codegen/xai-grok-auth/src/lib.rs`

---

---
title: "Authentication"
description: "Browser OAuth (auth.x.ai), device-code flow, XAI_API_KEY for CI, grok login/logout, credential storage in ~/.grok/auth.json, and token refresh behavior."
---

Grok authenticates through `AuthManager` and `~/.grok/auth.json`: interactive login (`grok login`, first-launch TUI), non-interactive `XAI_API_KEY` / per-model keys, and mid-session refresh on expiry or 401. Default interactive transport is loopback OAuth against `https://auth.x.ai`; device-code and external provider binaries are first-class alternatives for headless and enterprise setups.

## Auth surfaces

| Surface | When it applies | Entry points |
|---------|-----------------|--------------|
| **Browser OAuth (loopback)** | Default interactive login | First `grok` launch, `grok login`, `grok login --oauth` |
| **Device code (RFC 8628)** | Headless/SSH/remote when browser cannot open on the machine | `grok login --device-auth` (alias `--device-code`) |
| **API key** | CI, automation, BYOK, no browser | `XAI_API_KEY` (legacy: `GROK_CODE_XAI_API_KEY`), per-model `api_key` / `env_key` |
| **Enterprise OIDC** | Customer IdP (Okta, Azure AD, Auth0, …) | `[grok_com_config.oidc]` or `GROK_OIDC_*` |
| **External auth provider** | Sandboxed/CI/air-gapped custom mint | `[auth] auth_provider_command` / `GROK_AUTH_PROVIDER_COMMAND` |

```text
  CLI / TUI / ACP                 AuthManager                 Disk / IdP
  ───────────────                 ───────────                 ──────────
  grok login  ──────────────────► run_auth_flow ────────────► ~/.grok/auth.json
  first launch / reauth           refresh_chain                auth.json.lock
  HTTP 401 / near-expiry  ──────► OIDC / external refresh ──► token endpoint
  resolve_credentials  ─────────► session | XAI_API_KEY       (scope map)
```

## Browser OAuth (default)

Default interactive path uses OAuth 2.1 authorization code + PKCE against the xAI OAuth2 issuer `https://auth.x.ai` (local-dev override: `GROK_LOCAL_AUTH=1` → `http://localhost:22255`).

```bash
grok              # first launch prompts sign-in when no usable session exists
grok login        # re-run sign-in; replaces cached session
grok login --oauth
```

<Steps>
  <Step title="Start login">
    Run `grok` or `grok login`. The CLI opens the authorization URL in the default browser (loopback callback on `http://127.0.0.1/callback` with a random port).
  </Step>
  <Step title="Complete consent">
    Sign in at the IdP / accounts UI. Grok exchanges the code for access and refresh tokens.
  </Step>
  <Step title="Persist and verify">
    Credentials land in `~/.grok/auth.json` under an issuer+client scope key. Successful CLI login prints a signed-in confirmation and may apply managed/team config immediately.
  </Step>
</Steps>

Default OAuth2 scopes include `openid`, `profile`, `email`, `offline_access`, `grok-cli:access`, `api:access`, and conversation read/write scopes. Enterprise OIDC uses a slightly different default set (`offline_access` + `api:access` among others) configured under `[grok_com_config.oidc]`.

## Device-code flow

For environments without a local browser (SSH, containers, remote VMs):

```bash
grok login --device-auth
# alias:
grok login --device-code
```

Grok implements RFC 8628:

1. `POST {issuer}/oauth2/device/code` with `client_id`, scopes, and client surface headers
2. Prints verification URL + user code to stderr (CLI) or the TUI
3. Polls `POST {issuer}/oauth2/token` with grant type `urn:ietf:params:oauth:grant-type:device_code`
4. On success, persists credentials and continues

| Poll error | Behavior |
|------------|----------|
| `authorization_pending` | Keep polling |
| `slow_down` | Increase interval by 5s |
| `access_denied` | Fail: user rejected |
| `expired_token` | Fail: re-run `grok login --device-auth` |
| HTTP 404 on device code | Device flow not enabled for this deployment |

### Device vs loopback selection

Interactive transport precedence (xAI OAuth2 only; enterprise OIDC always uses loopback):

1. CLI: `--oauth` (loopback) wins over `--device-auth`
2. Env: `GROK_LOGIN_DEVICE_FLOW`
3. Config: `[auth] login_device_flow`
4. Remote feature flag `grok_build_login_device_flow` (2s fetch timeout → loopback)
5. Default: **loopback**

```toml
# ~/.grok/config.toml
[auth]
login_device_flow = true
```

```bash
export GROK_LOGIN_DEVICE_FLOW=true
```

<Note>
Enterprise OIDC (`[grok_com_config.oidc]`) has no device endpoint. `grok login --device-auth` falls back to browser sign-in with a short stderr notice.
</Note>

## API key (`XAI_API_KEY`)

For CI/CD and non-interactive runs:

```bash
export XAI_API_KEY="xai-..."
grok -p "summarize this repo"
```

| Variable | Role |
|----------|------|
| `XAI_API_KEY` | Primary API key env |
| `GROK_CODE_XAI_API_KEY` | Legacy fallback if `XAI_API_KEY` is unset |
| Per-model `api_key` / `env_key` in `config.toml` | Highest priority for that model (BYOK / OpenAI-compatible) |

`XAI_API_KEY` is used when no active session token applies. An interactive session stored in `auth.json` is preferred as the default auth method when both exist, so OIDC refresh stays alive. To force the env key only, run `grok logout` or delete the session entry.

Admin kill switch: `GROK_DISABLE_API_KEY_AUTH` / `[grok_com_config] disable_api_key_auth` (or pinning `force_login_team_uuid`) stops first-party API-key auth so deployments can force IdP login. BYOK to non–first-party base URLs is not swapped by that kill switch.

## `grok login` / `grok logout`

### Login

```bash
grok login                    # reauth; default loopback (or resolved device transport)
grok login --oauth            # force loopback
grok login --device-auth      # force device code
```

| Flag | Effect |
|------|--------|
| `--oauth` (alias `--oidc`) | Force loopback-callback OAuth |
| `--device-auth` (`--device-code`) | Force RFC 8628 device flow |
| `--legacy` | Hidden no-op (OAuth-only era) |

Loopback reauth clears the current scope (and legacy `accounts.x.ai` scope) up front so abandoned logins do not leave mixed credentials. Device-branch login uses force-interactive mode so abandoning the prompt does not wipe an existing session.

After successful login, Grok may sync managed/team configuration and print whether team or deployment config was applied.

### Logout

```bash
grok logout
```

Takes no flags. Clears the active session from memory and `auth.json`, resets external telemetry identity, and clears orphan managed-config files when no principal remains.

| Outcome | stderr |
|---------|--------|
| Was signed in | `Logged out` or `Logged out (was signed in as {email})` |
| No session | `No cached session to log out of.` |
| Env key still set | Notes that `XAI_API_KEY` remains and will still authenticate |

TUI `/logout` and ACP logout use the same `perform_logout` core as the CLI.

## Credential storage (`~/.grok/auth.json`)

Path: `{GROK_HOME}/auth.json` (default home is `~/.grok`).

The file is a JSON object (scope key → credential). Scope keys include:

| Scope pattern | Meaning |
|---------------|---------|
| `{issuer}::{client_id}` | Active OIDC / OAuth2 session |
| `xai::api_key` | Stored plain API key scope |
| `https://accounts.x.ai/sign-in` | Legacy WebLogin (ignored for use; migration requires re-login) |

### Credential fields (`GrokAuth`)

| Field | Purpose |
|-------|---------|
| `key` | Access token / API key bearer |
| `auth_mode` | `oidc`, `external`, `api_key` (`web_login` deserialize-only, skipped) |
| `create_time` | Local mint time |
| `expires_at` | Server `expires_in` when present |
| `refresh_token` | Silent OIDC / reference for external providers |
| `oidc_issuer` / `oidc_client_id` | Refresh discovery and first-party classification |
| User / team / org profile fields | Enriched from `/user` after login |

Writes are owner-only (`0o600`), prefer atomic temp+rename, fall back to in-place rewrite on disk full, and use advisory locking via sibling `auth.json.lock` so concurrent processes do not double-spend refresh tokens. Corrupt JSON is renamed to `auth.json.corrupt.<millis>` before recovery writes.

`AuthManager` re-reads disk on refresh/recovery paths, so external updates to `auth.json` are picked up without restarting when a request path reloads from disk.

## Token refresh behavior

```mermaid
stateDiagram-v2
  [*] --> Valid: login / load auth.json
  Valid --> NearExpiry: within early-invalidation buffer
  NearExpiry --> Refreshing: proactive refresh task
  Valid --> Refreshing: HTTP 401/403 (ServerRejected)
  Refreshing --> Valid: Success (persist + hot_swap)
  Refreshing --> StickyFail: permanent IdP rejection
  Refreshing --> Valid: transient failure (retry later)
  StickyFail --> [*]: re-login required (or credential change)
  Valid --> [*]: logout / clear scope
```

### When refresh runs

| Trigger | Mechanism |
|---------|-----------|
| **Pre-request / proactive** | Background task wakes before `expires_at` (or `create_time + 30d` when no server expiry) minus early-invalidation buffer |
| **Server rejected** | 401/403 → `AuthManager` recovery with `RefreshReason::ServerRejected` |
| **External provider** | Re-runs the configured binary with `GROK_AUTH_EXPIRED=1` (not OAuth refresh grant) |
| **OIDC / OAuth2** | Uses stored `refresh_token` against the issuer token endpoint |

### Timing knobs

| Knob | Default | Meaning |
|------|---------|---------|
| Server `expires_in` → `expires_at` | From IdP | Hard expiry when present |
| Fallback lifetime | **30 days** from `create_time` | When `expires_at` is absent |
| Early invalidation | **300s** | Treat token expired this many seconds early |
| `GROK_AUTH_EARLY_INVALIDATION_SECS` | overrides buffer | Set `0` to disable proactive early buffer |
| `GROK_AUTH_TOKEN_TTL` / `auth_token_ttl` | optional | Synthesize expiry for bare-string external tokens |

Refresh holds a cross-process file lock (refresh path timeout ~45s) so sibling processes adopt a peer-refreshed token instead of reusing the same refresh token. Permanent refresh failures are sticky per credential for ~5 minutes of real time (wall clock + monotonic).

## Auth precedence

### Per-request credentials (`resolve_credentials`)

Highest to lowest:

1. **Per-model `api_key` or `env_key`** under `[model.<name>]`
2. **Active session token** from `AuthManager` / `auth.json`
3. **`XAI_API_KEY`** (then legacy `GROK_CODE_XAI_API_KEY`)

### Interactive login chain (`run_auth_flow`)

When obtaining or refreshing a session:

1. Compatible cached credential (unless reauth / force interactive)
2. Disk re-read + silent refresh if expired
3. **External auth provider** (`auth_provider_command`)
4. Devbox auto-mint when applicable (skipped if `preferred_method = api_key`)
5. **Enterprise OIDC** loopback when configured
6. **xAI OAuth2** loopback or device flow

### Preferred method pin

```toml
[auth]
preferred_method = "api_key"   # or "oidc"
```

When set, automatic selection is fail-closed: only that method family is used; no silent fallthrough. Config-toml only (not env).

## Enterprise OIDC

```toml
# ~/.grok/config.toml
[grok_com_config.oidc]
issuer = "https://acme.okta.com"
client_id = "0oa1b2c3d4e5f6g7h8i9"
# scopes default: openid, profile, email, offline_access, api:access
# audience = "..."   # if required by the IdP
```

```bash
export GROK_OIDC_ISSUER="https://acme.okta.com"
export GROK_OIDC_CLIENT_ID="0oa1b2c3d4e5f6g7h8i9"
# optional:
export GROK_OIDC_SCOPES="openid,profile,email,offline_access,api:access"
export GROK_OIDC_AUDIENCE="..."
export GROK_CLI_CHAT_PROXY_BASE_URL="https://grok-proxy.acme.com/v1"
```

Register a public client with Authorization Code + PKCE and loopback redirect `http://127.0.0.1/callback` (port-agnostic per RFC 8252). Discovery uses `{issuer}/.well-known/openid-configuration`.

## External auth provider

When browser login is not possible, point Grok at a command that mints tokens:

```toml
[auth]
auth_provider_command = "/usr/local/bin/my-auth-provider"
auth_provider_label = "Acme Corp"   # optional TUI label
auth_token_ttl = 3600               # optional for bare-string tokens
```

| Stream / exit | Contract |
|---------------|----------|
| **stdout** | Token only: bare string **or** JSON |
| **stderr** | Human UX (URLs, progress); TUI surfaces first `https://` link |
| Exit `0` | Success |
| Non-zero / empty stdout / timeout | Fall through to interactive login (interactive path); refresh path fails |

JSON stdout shape:

```json
{
  "access_token": "eyJ...",
  "refresh_token": "optional",
  "expires_in": 3600,
  "issuer": "https://idp.example.com"
}
```

On refresh, Grok re-runs the binary with `GROK_AUTH_EXPIRED=1`. Interactive external runs time out at 300s async; sync refresh uses a shorter timeout (~5s).

| Variable | Description |
|----------|-------------|
| `GROK_AUTH_PROVIDER_COMMAND` | Path/command (`sh -c`) |
| `GROK_AUTH_PROVIDER_LABEL` | Login button label |
| `GROK_AUTH_TOKEN_TTL` | Bare-token lifetime seconds |
| `GROK_AUTH_EXPIRED` | Set to `1` by Grok on refresh invocations |
| `GROK_AUTH_EARLY_INVALIDATION_SECS` | Proactive refresh buffer |

## Environment and config reference

| Key / env | Role |
|-----------|------|
| `XAI_API_KEY` / `GROK_CODE_XAI_API_KEY` | Global API key |
| `GROK_LOGIN_DEVICE_FLOW` | Prefer device login |
| `GROK_OIDC_ISSUER` / `GROK_OIDC_CLIENT_ID` | Enterprise OIDC |
| `GROK_OAUTH2_ISSUER` / `GROK_OAUTH2_CLIENT_ID` | Override OAuth2 provider |
| `GROK_AUTH_PROVIDER_*` | External binary auth |
| `GROK_DISABLE_API_KEY_AUTH` | Force IdP (sticky over user config) |
| `GROK_LOCAL_AUTH` | Local accounts-app issuer |
| `[auth] preferred_method` | Fail-closed method pin |
| `[auth] login_device_flow` | Device transport preference |

## Failure modes and recovery

| Symptom | Likely cause | Fix |
|---------|--------------|-----|
| Auth fails after long idle | Refresh rejected or sticky permanent failure | `grok logout` then `grok login` |
| Device code unavailable | Deployment without device endpoint / enterprise OIDC | Use loopback or `XAI_API_KEY` |
| API key ignored | Active session preferred, or kill switch | `grok logout` or disable kill switch if intentional |
| Corrupt auth file | Partial write / crash | Look for `auth.json.corrupt.*` backup; re-login |
| External provider silent | Wrote progress to stdout | Token-only stdout; URLs on stderr |
| OIDC redirect fails | IdP disallows loopback | Allow `http://127.0.0.1/callback` |
| CI needs auth without browser | No TTY / no session | Export `XAI_API_KEY` or external provider + non-interactive mode |

Debug logging:

```bash
# TUI file log
GROK_LOG_FILE=/tmp/grok.log RUST_LOG=debug grok

# Headless stderr
RUST_LOG=debug grok -p "hello" 2> /tmp/grok.log
```

Useful log lines: `auth: running external auth provider`, `auth: external auth provider returned fresh token`, `auth: using cached credentials`, `login: resolved interactive transport`.

## Next

<CardGroup>
  <Card title="Quickstart" href="/quickstart">
    First interactive session after install and sign-in.
  </Card>
  <Card title="Headless mode" href="/headless-mode">
    Non-interactive `-p` runs, CI patterns, and credential expectations.
  </Card>
  <Card title="Custom models" href="/custom-models">
    Per-model `api_key` / `env_key` and provider-neutral endpoints (BYOK).
  </Card>
  <Card title="Configure Grok" href="/configure-grok">
    `config.toml` precedence, feature flags, and `grok inspect`.
  </Card>
  <Card title="CLI reference" href="/cli-reference">
    Top-level `login` / `logout` and shared runtime flags.
  </Card>
  <Card title="Troubleshooting" href="/troubleshooting">
    Auth recovery, terminal issues, and diagnostics.
  </Card>
</CardGroup>
