# crates/provider-auth reference

> Reference for the code under `crates/provider-auth`: what it exports, how it is invoked, its options and defaults, and its error cases.

- Repository: egoist/lorca
- GitHub: https://github.com/egoist/lorca
- Human docs: https://grok-wiki.com/public/docs/egoist-lorca-2cf67495e5e6
- Complete Markdown: https://grok-wiki.com/public/docs/egoist-lorca-2cf67495e5e6/llms-full.txt

## Source Files

- `crates/provider-auth/src/lib.rs`
- `crates/cli/src/provider_auth.rs`

---

---
title: "crates/provider-auth reference"
description: "Reference for the code under `crates/provider-auth`: what it exports, how it is invoked, its options and defaults, and its error cases."
---

`lorca-provider-auth` (`crates/provider-auth`) owns ChatGPT and Grok OAuth token types plus PKCE loopback sign-in. Import it as `lorca_provider_auth::{chatgpt, grok}`. Devices run `login`; Runners call `refresh` before model calls. API-key providers live in `crates/cli/src/provider_auth.rs`, outside this crate.

:::files
crates/provider-auth/
  Cargo.toml          # package name: lorca-provider-auth
  src/
    lib.rs            # pub mod chatgpt; pub mod grok;
    chatgpt.rs        # ChatGPT PKCE on fixed port 1455
    grok.rs           # Grok PKCE on an ephemeral loopback port
:::

## What this crate exports

| Module | Primary types | Primary functions |
| --- | --- | --- |
| `chatgpt` | `ChatGptTokens`, `PkceFlow` | `login`, `refresh`, `exchange_code`, `wait_for_callback`, `jwt_claims` |
| `grok` | `GrokTokens`, `PkceFlow`, `Endpoints`, `Callback` | `login`, `refresh`, `revoke`, `exchange_code`, `jwt_claims` |

Both modules are public from `lib.rs`. There is no crate-level re-export of the token structs; callers use `lorca_provider_auth::chatgpt::ChatGptTokens` or `::grok::GrokTokens`.

### Token shapes

<ParamField body="ChatGptTokens.access_token" type="string" required>
Bearer access token from `auth.openai.com`.
</ParamField>
<ParamField body="ChatGptTokens.refresh_token" type="string" required>
Refresh token for `grant_type=refresh_token`.
</ParamField>
<ParamField body="ChatGptTokens.id_token" type="string | null">
Optional OIDC id token; used with the access token to recover `account_id` / `email`.
</ParamField>
<ParamField body="ChatGptTokens.account_id" type="string" required>
ChatGPT account id from JWT claim `https://api.openai.com/auth.chatgpt_account_id` (or `chatgpt_account_id`).
</ParamField>
<ParamField body="ChatGptTokens.email" type="string | null">
Email from JWT claims when present.
</ParamField>
<ParamField body="ChatGptTokens.expires_at" type="u64" required>
Unix seconds. `is_expired()` is true when `now + 60 >= expires_at`.
</ParamField>

<ParamField body="GrokTokens.access_token" type="string" required>
Bearer access token from `auth.x.ai`.
</ParamField>
<ParamField body="GrokTokens.refresh_token" type="string" required>
Refresh token; xAI may rotate it on refresh.
</ParamField>
<ParamField body="GrokTokens.id_token" type="string | null">
Optional id token.
</ParamField>
<ParamField body="GrokTokens.account_id" type="string | null">
`sub` from JWT or `/oauth2/userinfo` when available.
</ParamField>
<ParamField body="GrokTokens.email" type="string | null">
Email / preferred username / name from claims or userinfo.
</ParamField>
<ParamField body="GrokTokens.expires_at" type="u64" required>
Unix seconds. `is_expired()` is true when `now + 5 * 60 >= expires_at` (five-minute skew; tokens last about six hours).
</ParamField>

## How it is invoked

```mermaid
sequenceDiagram
  participant UI as Device UI / open_url
  participant CLI as cli provider_auth
  participant Auth as lorca-provider-auth
  participant IdP as auth.openai.com / auth.x.ai
  participant Agent as agent ChatGpt/Grok provider

  UI->>CLI: providers.connect_chatgpt / connect_grok
  CLI->>Auth: login(http, open_url, 5m)
  Auth->>Auth: bind loopback callback
  Auth->>UI: open authorize URL
  UI->>IdP: user consents
  IdP->>Auth: redirect with code
  Auth->>IdP: exchange_code
  Auth-->>CLI: ChatGptTokens / GrokTokens
  CLI->>CLI: update_credentials + sync blob
  Agent->>Auth: refresh when is_expired()
  Auth->>IdP: grant_type=refresh_token
  Agent->>Agent: store refreshed tokens
```

### From the CLI feature boundary

`crates/cli` enables this crate with Cargo feature `provider-auth` (`runner` includes it). `crates/mobile` links `lorca` with `features = ["provider-auth"]` and `default-features = false`.

`crates/cli/src/provider_auth.rs` wraps the crate:

| Function | Behavior |
| --- | --- |
| `connect_chatgpt(app, open_url)` | `chatgpt::login(&app.http, open_url, 5 minutes)` then stores under credentials kind `chatgpt` |
| `connect_grok(app, open_url)` | Builds `Endpoints` from `LORCA_GROK_ISSUER` or `Endpoints::xai()`, then `grok::login(..., 5 minutes)`, stores kind `grok` |
| `disconnect(app, "grok")` | Clears credentials and best-effort `grok::revoke` on a background task |

JSON API methods (feature-gated):

| Method | Params | Success result |
| --- | --- | --- |
| `providers.connect_chatgpt` | `{}` | `{ email, providers }` |
| `providers.connect_grok` | `{}` | `{ email, providers }` |
| `providers.auth.cancel` | `{}` | `null` (cancels in-flight OAuth) |
| `providers.disconnect` | `{ kind }` | `{ providers }` |

Runner builds open the system browser via `open::that`. Non-runner Devices emit `Event::ProviderAuth { kind, url }` so the phone/desktop UI can open an in-app browser while the core keeps the localhost listener alive.

Terminal entry:

```bash
lorca provider set chatgpt
lorca provider set grok
lorca provider remove grok
```

ChatGPT/Grok take no API key; the CLI prints `Finish the sign-in in the browser…` and dispatches the connect method (to a running `lorca serve` when present).

### From the agent crate

`crates/agent` always depends on `lorca-provider-auth`. Provider modules re-export the OAuth surface:

- `lorca_agent::providers::chatgpt` → `oauth` + `ChatGptTokens`
- `lorca_agent::providers::grok` → `oauth` + `GrokTokens`

Before each model call, `fresh_tokens()` loads stored tokens, skips refresh when not expired, otherwise calls `oauth::refresh`, handles a concurrent refresh that already rotated the refresh token, then `store`s the new tokens.

## Options and defaults

### ChatGPT (`chatgpt` module)

| Constant / option | Default |
| --- | --- |
| `CLIENT_ID` | `app_EMoamEEZ73f0CkXaXp7hrann` |
| `ISSUER` | `https://auth.openai.com` |
| `CALLBACK_PORT` | `1455` |
| Redirect URI | `http://localhost:1455/auth/callback` |
| `SCOPES` | `openid profile email offline_access` |
| PKCE | S256; 64-byte verifier, 32-byte state |
| Login timeout (CLI) | `5 * 60` seconds |
| Token `expires_in` fallback | `3600` seconds |
| Expiry skew | 60 seconds |

Authorize URL also sets `id_token_add_organizations=true` and `codex_cli_simplified_flow=true` (same shape as the Codex CLI flow).

### Grok (`grok` module)

| Constant / option | Default |
| --- | --- |
| `CLIENT_ID` | `b1a00492-073a-47ea-816f-4c329264a828` (public desktop client; no secret) |
| `ISSUER` | `https://auth.x.ai` |
| `SCOPES` | `openid profile email offline_access grok-cli:access api:access` |
| `REFERRER` | `lorca` |
| `ACCOUNTS_ORIGIN` | `https://accounts.x.ai` (CORS for the accounts page fetch) |
| Callback | `http://127.0.0.1:{ephemeral}/callback` (`TcpListener::bind(..., 0)`) |
| Login timeout (CLI) | `5 * 60` seconds |
| Token `expires_in` fallback | `6 * 3600` seconds |
| Expiry skew | 5 minutes |

<ParamField body="LORCA_GROK_ISSUER" type="string">
Optional env override. When set (non-empty), CLI connect/disconnect and agent providers use `Endpoints::at(issuer)` instead of `Endpoints::xai()`. Trailing slashes are stripped. Endpoints derived: `/oauth2/authorize`, `/oauth2/token`, `/oauth2/userinfo`, `/oauth2/revoke`.
</ParamField>

### `login` signatures

```rust
chatgpt::login(
    client: &reqwest::Client,
    open_url: impl FnOnce(&str) -> Result<(), String>,
    timeout: Duration,
) -> Result<ChatGptTokens, String>

grok::login(
    client: &reqwest::Client,
    endpoints: &Endpoints,
    open_url: impl FnOnce(&str) -> Result<(), String>,
    timeout: Duration,
) -> Result<GrokTokens, String>
```

Both bind the callback **before** calling `open_url`, so the browser redirect cannot race the listener. Dropping the login future aborts the waiter (`AbortOnDrop`) and releases the port.

Grok’s callback answers CORS preflight from `https://accounts.x.ai` (including `Access-Control-Allow-Private-Network`) so the accounts page can fetch the loopback URL cross-origin instead of falling back to a manual code paste.

## Error cases

All public fallible APIs return `Result<_, String>` with human-readable messages.

### Shared / login lifecycle

| Cause | Typical message |
| --- | --- |
| `open_url` fails | whatever the callback returns (e.g. `Cannot open the browser: …`, or CLI cancel path `Sign-in cancelled`) |
| Browser never returns | `Timed out waiting for the browser` |
| OAuth `error` / `error_description` query | `Sign-in was denied: {error}` |
| State mismatch | `Sign-in state mismatch` |
| Token HTTP failure | `{status}: {error_description\|error\|Token request rejected}` |
| Network to token endpoint | `Token request failed: …` / `Token refresh failed: …` |
| Unparseable token body | `Token response unreadable: …` |
| Missing `access_token` / `refresh_token` | `Token response has no access_token` / `… no refresh_token` |

### ChatGPT-specific

| Cause | Message |
| --- | --- |
| Port 1455 unavailable | `Cannot listen on port 1455: …` |
| JWT lacks ChatGPT account id | `Sign-in token carries no ChatGPT account id` |

### Grok-specific

| Cause | Message |
| --- | --- |
| Cannot bind ephemeral port | `Cannot listen for the sign-in callback: …` |
| Callback closed with no result | `The sign-in callback closed` |
| Callback missing `code` | `The callback carried no authorization code` |
| Forbidden / subscription text in token error | `{status}: {message}. Grok sign-in needs a SuperGrok or X Premium+ subscription.` |
| Revoke non-success | `Revoke answered {status}` (logged at debug on disconnect; local disconnect still proceeds) |

### CLI wrapper errors that sit above the crate

These come from `crates/cli/src/provider_auth.rs` / API dispatch, not from `lorca-provider-auth` itself:

- Unknown disconnect kind → `Unknown provider {kind}`
- Passing an API key to ChatGPT/Grok via CLI → `{kind} connects with a browser sign-in and takes no API key`

<Warning>
API-key connect paths (`connect_deepseek`, `connect_anthropic`, `connect_opencode`, `connect_opencode_go`) validate keys and write `ApiKeyCredential` values in the CLI crate. They do not call `lorca-provider-auth`.
</Warning>

## Storage and refresh contract

Tokens serialize into the account `Credentials` blob (`chatgpt` / `grok` fields) under the account DEK, synced as the encrypted `credentials` slot. Every Device shares one set; a Runner builds live providers from them.

Refresh behavior to keep in mind:

1. ChatGPT refresh posts `grant_type=refresh_token` + `client_id` to `{ISSUER}/oauth/token`.
2. Grok refresh posts the same grant to `{issuer}/oauth2/token`. If the response omits `refresh_token`, the previous refresh token is kept.
3. Agent `fresh_tokens` retries against the shared store when refresh fails because another holder already rotated the refresh token.
4. Grok disconnect best-effort revokes the refresh token at `/oauth2/revoke`; failure does not undo the local clear.

## Verify locally

About one command if the workspace builds:

```bash
cargo test -p lorca-provider-auth
```

Covered signals: authorize URL carries PKCE + scopes; ChatGPT cancel releases port 1455; Grok callback binds a free port, answers accounts.x.ai CORS, survives idle preconnects, and refresh keeps an old refresh token when none returns.

Next: open `crates/provider-auth/src/grok.rs` and skim `Callback::wait` if you are wiring a custom Device UI for Grok sign-in.

## Related pages

<CardGroup>
  <Card title="crates/cli reference" href="/ref-crates-cli">
    Feature flags, `provider_auth` wrappers, and `lorca provider` commands that call this crate.
  </Card>
  <Card title="crates/agent reference" href="/ref-crates-agent">
    ChatGPT/Grok providers that refresh tokens through `lorca-provider-auth` before model calls.
  </Card>
  <Card title="HTTP API reference" href="/http-api-reference">
    `providers.connect_*`, `providers.auth.cancel`, and `providers.disconnect` method shapes.
  </Card>
  <Card title="Configuration reference" href="/configuration-reference">
    Env overrides such as `LORCA_GROK_ISSUER` and credential storage paths.
  </Card>
</CardGroup>
