# Authentication

Every client (the CLI, an agent over MCP, or code calling the
[REST API](/docs/api)) authenticates the same way: an OAuth grant a human
approves once, which issues a short-lived access token plus a refresh token to
renew it with. No surface mints a long-lived key on demand, and nothing here
requires pasting a secret into a config file.

Two grants exist, one per kind of client:

- The **device grant** (RFC 8628), for the CLI or anything else running in a
  terminal. The client shows a code; you approve it in a browser.
- The **authorization-code grant with PKCE**, for agents and MCP clients.
  The client sends its human to a consent screen and receives the tokens on
  redirect.

## Scopes

| Scope | Unlocks |
| --- | --- |
| `pages:read` | Listing and reading your pages (`find`, `get`, the `list` tool) |
| `pages:write` | Publishing, updating, visibility, delete |
| `domains:bind` | Binding a custom domain (Pro) |

`emit login` requests `pages:read` and `pages:write`. `domains:bind` is a
**step-up**: `emit bind-domain` re-runs the device flow to request it for that
one operation, and the elevated scope is not persisted into your stored
credential. Scopes are fixed at approval; nothing widens a credential's
scopes after the fact.

## Token lifetimes

An approval issues a pair:

- An **access token**, valid for one hour, sent as the bearer on every
  request.
- A **refresh token**, valid for 90 days, used to mint the next pair.

Refresh tokens are **single-use and rotating**: each renewal spends the old
refresh token and returns a new one, so a leaked refresh token that has
already been spent is dead on arrival (`invalid_grant`). A client used at
least once every 90 days stays signed in indefinitely.

## The device grant

| Method | Path | Body | Returns |
| --- | --- | --- | --- |
| `POST` | `/oauth/device/code` | form: `client_id`, `scope` | `{ device_code, user_code, verification_uri, verification_uri_complete, expires_in, interval }` |
| `POST` | `/oauth/token` | form: `grant_type=urn:ietf:params:oauth:grant-type:device_code`, `device_code`, `client_id` | the token pair, or `400 { error }` while pending |

The `verification_uri` is `emits.app/device`: the human opens it in the
browser where they are signed in, enters the code, and approves the scopes.
The client polls `/oauth/token` at the returned `interval` until approval.

## The authorization-code grant

For clients with a browser available (or able to send their human to one):

1. Direct the browser to `GET /oauth/authorize` with `client_id`,
   `redirect_uri`, `scope`, `state`, and a PKCE `code_challenge`
   (`S256` is required; this is a public-client flow).
2. The consent screen shows the client's registered name and the requested
   scopes; the human approves.
3. The browser is redirected back with a one-time `code`.
4. The client exchanges it at `POST /oauth/token`
   (`grant_type=authorization_code`, with the PKCE `code_verifier`) for the
   token pair.

Renewal is `POST /oauth/token` with `grant_type=refresh_token`.

## Dynamic client registration

Agents do not pre-register by hand. `POST /oauth/register` (RFC 7591) accepts
a client's name and redirect URIs and returns a `client_id` on the spot.
This is how an MCP client bootstraps itself from
[the 401 handshake](/docs/mcp#the-remote-server) with no human setup. The
registered name is what the consent screen (and the console) shows, and
registration is rate-limited.

## Discovery

Everything above is discoverable, so a conforming client needs no
configuration beyond the API origin:

| Document | Path |
| --- | --- |
| Authorization server metadata (RFC 8414) | `/.well-known/oauth-authorization-server` |
| Protected-resource metadata (RFC 9728) | `/.well-known/oauth-protected-resource` (and `…/mcp` for the MCP endpoint) |
| API catalog (RFC 9727) | `/.well-known/api-catalog` |

`GET /v1/token` introspects the token you hold: `200 { scopes }` when live, a
uniform `401` when not (expiry, revocation, and never-existed are not
distinguished).

## Revocation

The console lists every approved client under **Settings → API access** — one
entry per client, named as it appeared on the consent screen, with its scopes
and when its access stops. Revoking an entry ends the whole authorization:
the access token stops working and the refresh token can no longer renew
(`invalid_grant`). Signing in again is the only way back. Published pages
stay up; revocation severs the client, not the content.
