> ## Documentation Index
> Fetch the complete documentation index at: https://docs.zespan.com/llms.txt
> Use this file to discover all available pages before exploring further.

# zespan auth

> Sign in to the CLI as yourself with an OAuth 2.0 device grant, so control-plane commands act as a person with a role — not as a project API key.

`zespan auth` gives the CLI a credential that belongs to **you** rather than to a
project. Control-plane commands — `zespan policy pull`, `test`, `plan`, `apply`,
`zespan link`, `zespan projects list` — check your role in the organization, so
they need a person's session. A project API key cannot supply one.

<Note>
  **Ingest and the data plane are unchanged.** `/v1/ingest`, guardrail checks,
  the SDKs, and [`zespan doctor`](/cli/doctor) still authenticate with
  `ZESPAN_API_KEY`. Signing in adds a second credential; it does not replace the
  first. See [the two credentials](/cli/overview#two-credentials-two-planes).
</Note>

## Quick start

<CodeGroup>
  ```bash npx theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
  npx @zespan/cli auth login
  ```

  ```bash Installed theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
  zespan auth login
  ```
</CodeGroup>

```text theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
── Sign in to Zespan ───────────────────────────────────────
ℹ Your code:  3KS23N6T
  Open:       https://app.zespan.com/device?user_code=3KS23N6T
  Approving a terminal asks for your second factor, even though you are already signed in.
  This machine has not signed in before — approving it registers it as a new device.
Approved.
✔ Signed in. Credential written to /home/you/.zespan/credentials.json (readable only by you).
  You are Ada Lovelace <ada@acme.dev>.
  Organizations: Acme Health.
  Next: run `zespan link` to choose a project for this directory.
```

<Note>
  The CLI prints the second-factor line to everyone, because it cannot know which
  account will approve. If your account has no second factor configured, the
  approval screen simply asks you to confirm instead — see [Approving a
  terminal](#approving-a-terminal).
</Note>

## How the login works

`zespan auth login` is an [OAuth 2.0 device authorization grant
(RFC 8628)](https://datatracker.ietf.org/doc/html/rfc8628) — the same shape as
signing a TV into a streaming service. The terminal never sees your password.

<Steps>
  <Step title="The CLI asks for a code">
    It requests a short user code from the API, sending this machine's public key
    and — when it can determine them — its operating system, OS version, and CLI
    version.
  </Step>

  <Step title="You approve in the browser">
    The CLI prints the code and opens the dashboard's `/device` screen with the
    code pre-filled. The code is valid for **10 minutes**.
  </Step>

  <Step title="You confirm the request">
    Check what the screen says about the terminal that asked, then authorize it.
    If your account has a second factor, you re-enter it here even though you are
    already signed in. See [Approving a terminal](#approving-a-terminal).
  </Step>

  <Step title="The CLI receives a token">
    It polls every 5 seconds until you approve, then writes the token to
    `~/.zespan/credentials.json` and caches who you turned out to be so later
    commands can name you without a network call.
  </Step>
</Steps>

## Approving a terminal

What the approval screen asks of you depends on whether your account has
two-factor authentication turned on.

| Your account             | What approval takes                                                                                                                 |
| ------------------------ | ----------------------------------------------------------------------------------------------------------------------------------- |
| **Has a second factor**  | You re-enter a TOTP code, an emailed one-time code, or a backup code, then authorize — even though the browser is already signed in |
| **Has no second factor** | You review the terminal that asked and authorize. There is no code to enter, and nothing is blocked                                 |

The requirement on an enrolled account is enforced by the API as well as by the
page, so a request that skips the screen entirely is refused too. The proof is
bound to the exact browser session that supplied it and is good for **five
minutes**, so verifying in one browser cannot authorize a terminal from another.

<Tip>
  **Turning two-factor authentication on is worth it here.** It adds the
  confirmation step to this screen and to your sign-ins, so a terminal cannot be
  authorized from a session someone walked up to on an unlocked machine — which
  is the attack this step exists for, and the one a re-entered code actually
  stops. Turn it on under **Settings → Profile → Two-factor authentication**. It
  is not required to use the CLI: the same account can already apply enforcement
  policy from the dashboard without one, and this path is not the place to
  invent a stricter rule.
</Tip>

A device grant is phishable by construction: someone can start
`zespan auth login` on their own machine and read the eight-character code to you
over the phone. Two things on the approval screen exist to make that attempt
fail.

### The terminal that asked

On both approval paths, before you authorize, the screen shows what the server
observed about the machine that made the request:

| Row               | Where it comes from                             |
| ----------------- | ----------------------------------------------- |
| Operating system  | Reported by the CLI when it asked for the code  |
| CLI version       | Reported by the CLI when it asked for the code  |
| Request came from | The IP address the API resolved for the request |

None of it comes from the link you followed, and none of it is editable. If it
does not describe the machine in front of you, cancel. An older CLI that reported
nothing about itself says exactly that, rather than showing a blank.

<Warning>
  **Self-hosters: the IP row is only as trustworthy as your proxy.** Zespan
  resolves the address from `X-Forwarded-For`, so an edge that appends to a
  client-supplied value — or an API reachable without going through the edge —
  lets a caller choose the address a human is shown here. On the row that exists
  precisely to be corroborated by eye, that is worth an hour of your time. See
  [Client IP behind a proxy](/platform/client-ip).
</Warning>

### The warning

The screen states, before you authorize, that the code should already be on
screen in a terminal in front of you — and that if someone read it to you, you
should cancel.

## Machines are remembered

The first login on a machine mints an Ed25519 keypair at `~/.zespan/device-key.pem`
(with the public half beside it) and sends the public key with the grant. The API
fingerprints it and records the machine, so signing in again from the same
machine keeps the **same** device identity rather than registering a new one
every time.

That identity is what makes the audit trail readable: a policy apply records the
user, the session, and the device it came from, not just a name. See
[Attribution](/cli/policy#every-apply-names-a-person).

`zespan auth logout` removes the credential but **keeps the device key**, so
signing back in reuses the same device.

## Abandoned logins are cleaned up

A login you start and never finish leaves a pending grant on the server, holding
the code, the machine's public key, and the address the request came from. Two
behaviours bound that, and both are worth knowing before you script anything
around `zespan auth login`.

**Starting a login is rate-limited.** Requesting a code is capped at **30 per 5
minutes per IP address**. That is generous enough for a whole team behind one
office address onboarding together, and it applies only to *starting* a login —
the CLI's polling for the result is deliberately not counted, since one login
polls roughly 120 times.

**Pending grants are swept.** A code is valid for 10 minutes; a grant that has
expired is deleted **15 minutes** later by a sweep that runs **every 15 minutes**,
so nothing survives beyond roughly 40 minutes from the moment it was requested.
The grace period is not an oversight — it is what lets a slow client be told its
code expired rather than getting a bare error.

<Warning>
  **Self-hosters: both depend on infrastructure that is easy to leave out.** The
  rate limit needs `REDIS_URL` configured on the API — without Redis it does not
  apply at all. The sweep runs in the **worker** process, and only when
  `WORKER_TYPE` is unset or `all`; if you split workers by type and never run a
  general one, nothing consumes the sweep queue and expired grants accumulate
  indefinitely. See [Environment
  variables](/platform/environment-variables#workers).
</Warning>

## Files on disk

| Path                         | Contents                                                                   | Commit it?                 |
| ---------------------------- | -------------------------------------------------------------------------- | -------------------------- |
| `~/.zespan/credentials.json` | Your session token, the host it belongs to, and your cached name and email | **Never** — it is a secret |
| `~/.zespan/device-key.pem`   | This machine's private device key                                          | **Never**                  |
| `~/.zespan/device-key.pub`   | The matching public key, replayed on every login                           | **Never**                  |
| `<repo>/.zespan/config.json` | The linked project and org — written by [`zespan link`](/cli/link)         | Yes — it holds no secret   |

The credential and key files are written with owner-only permissions (`0600`)
and re-tightened on every write, so an older CLI that left the file world-readable
is fixed the next time you sign in. Windows has no POSIX mode to set; there the
files rely on your user profile's permissions.

<Note>
  **A token is tied to the host it was minted against.** If you sign in to
  `https://api.zespan.com` and then run a command with `--api-url` pointing
  somewhere else, the CLI refuses rather than sending your session token to a host
  it was not issued for.
</Note>

## Commands

```text theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
Usage: zespan auth <command> [flags]

Commands:
  login    Sign in with your Zespan account and store a device credential
  status   Show who you are signed in as, and what this directory is linked to
  logout   Remove the stored credential from this machine

Flags:
  --no-browser       Print the URL instead of opening a browser (SSH, CI, headless)
  --api-url <url>    API base URL (or ZESPAN_API_URL)
  --app-url <url>    Dashboard base URL for the approval page (or ZESPAN_APP_URL)
  --help             Show this help
```

### `zespan auth login`

```bash theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
zespan auth login
```

On a machine with no browser — an SSH session, a container, a headless build
host — pass `--no-browser`. The CLI prints the URL and the code instead of trying
to open anything, and you approve them from any other machine:

```bash theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
zespan auth login --no-browser
```

Without `--no-browser`, a login started where there is no terminal to open a
browser from is refused immediately, before a code is minted — rather than
hanging for ten minutes on a code nobody can see.

### `zespan auth status`

```bash theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
zespan auth status
```

Prints the resolved identity: the account, the host, where the credential lives,
its expiry if the server reported one, the organizations you reach with your role
in each, and the project linked in this directory.

It still answers when the network is down — the account details fall back to the
copy cached at login, and the output says so rather than silently showing stale
data as if it were live.

### `zespan auth logout`

```bash theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
zespan auth logout
```

Removes `~/.zespan/credentials.json` from this machine. The device key stays, so
the next login is recognised as the same machine. Running it when nothing is
stored says so rather than pretending to have done something.

## Self-hosted deployments

The API tells the CLI where to send you by building the approval URL from
`NEXT_PUBLIC_APP_URL` — the same variable that already builds invitation and
two-factor links. Set it to your dashboard's public origin; see [Environment
variables](/platform/environment-variables). The approval screen is the web app's
`/device` route.

If you need to override it for a single login — a tunnel, a preview deployment —
pass `--app-url` (or set `ZESPAN_APP_URL`):

```bash theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
zespan auth login --api-url https://api.internal.example --app-url https://zespan.internal.example
```

One more thing to check before you hand this to a team: the approval screen shows
the IP the request came from, and that row means what it says only if your proxy
overwrites `X-Forwarded-For`. See [Client IP behind a
proxy](/platform/client-ip).

## When a login fails

<AccordionGroup>
  <Accordion title="The login was denied in the browser">
    Someone pressed **Cancel this request**. Nothing was granted. If it was not
    you, that is the control working — do not approve a code you did not start.
  </Accordion>

  <Accordion title="The login code expired before it was approved">
    The grant lives for 10 minutes. Run `zespan auth login` again for a fresh
    code.
  </Accordion>

  <Accordion title="This device was revoked, so it can no longer sign in">
    A revoked machine cannot re-register itself by signing in again. Remove the
    revocation, or delete `~/.zespan/device-key.pem` to sign in as a new device —
    the CLI prints the exact path.
  </Accordion>

  <Accordion title="Your saved sign-in is no longer valid">
    The stored token was rejected. Run `zespan auth login` to sign in again from
    this machine.
  </Accordion>

  <Accordion title="Signed in, but could not read your account details just now">
    The token was minted and written — the login succeeded. Only the follow-up
    read of your account details did not answer. Run `zespan auth status` once the
    network is back.
  </Accordion>
</AccordionGroup>

## Next steps

<CardGroup cols={2}>
  <Card title="zespan link" icon="link" href="/cli/link">
    Choose the project this directory belongs to, and list what you can reach.
  </Card>

  <Card title="zespan policy" icon="file-code" href="/cli/policy">
    The commands that need this sign-in, and what an apply records about you.
  </Card>

  <Card title="CLI overview" icon="terminal" href="/cli/overview">
    The two credentials, configuration precedence, and install options.
  </Card>

  <Card title="API keys" icon="key" href="/account/api-keys">
    The project key the SDKs and `zespan doctor` still use.
  </Card>

  <Card title="Client IP behind a proxy" icon="route" href="/platform/client-ip">
    What self-hosters must configure so the approval screen's IP row is real.
  </Card>
</CardGroup>
