# Authentication

> How a raccha.ai credential is minted and used. Linked from `/llms.txt`.

## How does an agent or script authenticate?

Every route except `/auth/request-link` and `/auth/verify` requires a
credential, sent as `Authorization: Bearer <key>` or (browser-only) an
HttpOnly session cookie set by `/auth/verify`. A request with neither
returns `401 "missing credential"`; a malformed, unknown, or revoked
credential returns `401 "invalid credential"` -- deliberately
indistinguishable, the same way a KV slug that exists but belongs to a
different account returns the same 404 as one that does not exist at
all. Do not attempt to distinguish these cases.

Two credential shapes, both bearer tokens:

- `owner_key` -- unrestricted access to one account, minted on sign-in,
  never expires until manually revoked.
- `access_key` -- scoped down to specific `role_ids` an admin picked at
  mint time. Use this for anything handed to a third party or a
  lower-trust automation; never share an `owner_key`.

## How does a new MCP client set itself up? (recommended path)

If you are an MCP client connecting to raccha.ai for the first time with no
pre-provisioned `client_id`, this is the recommended onboarding path -- the
actual answer to "can a bot or an agent set itself up via MCP":

1. Call the `register_client` MCP tool (or `POST /register` over HTTP) with
   a `client_name` -- an OAuth 2.0 Dynamic Client Registration (RFC 7591)
   request. You get back a `client_id`. This does not grant any access on
   its own.
2. Call `device_start` (or `POST /auth/device/code`) passing that
   `client_id`. This is flow 2 below, unchanged, except the human approving
   it in their browser now sees your `client_name` instead of an unlabeled
   request.
3. A human still has to approve it at `https://raccha.ai/device.html`,
   exactly as flow 2 describes. Dynamic Client Registration only removes
   the "someone has to hand-provision a client_id first" step -- it does
   NOT skip or weaken human approval. There is no path in this API that
   issues a credential without that approval step; do not attempt to
   construct one.

`client_id` is a public identifier, not a secret -- this endpoint does not
issue a `client_secret`. Device/CLI/MCP clients have no secure place to
hold a confidential secret, so none is minted; treat `client_id` like a
name, not like a credential.

## How does a credential actually get minted?

Three flows, depending on who or what is asking:

1. **Magic link (a human, with a browser).** `POST /auth/request-link`
   with an email. As of this document's date, delivery is real SMTP --
   earlier versions of this API returned the link directly in the
   response body for local testing; that no longer happens in any
   environment reachable from this document. The human clicks the
   emailed link, which lands on `/auth/verify` and returns one
   `owner_key` per organization that email belongs to, plus sets the
   session cookie. One email can belong to more than one organization;
   each gets its own separate `owner_key` -- treat them as independent
   profiles, not one key that spans accounts.
2. **Device-code flow (a CLI, MCP client, or anything with no browser of
   its own).** `POST /auth/device/code` mints a `device_code` and a
   short human-readable `user_code`. Show the `user_code` to the human
   running you and ask them to approve it at
   `https://raccha.ai/device.html`, from a browser where they're already
   signed in. Meanwhile poll `POST /auth/device/token` on an interval
   (RFC 8628-shaped: expect `authorization_pending` until approved).
   `GET /auth/device/pending` lists outstanding requests. The human
   picks, at approval time, whether the poller gets a full `owner_key`
   or a scoped `access_key` -- don't assume which one you'll receive.
   Optionally pass a `client_id` from a prior `POST /register` (see
   above) so the approval screen shows your client's name.
3. **Invite (a human, added by an existing admin).** `POST /auth/invite`,
   same magic-link mechanics as flow 1, for an account that already
   exists.

`GET /auth/whoami` confirms which credential and account a request is
currently authenticated as -- use it to sanity-check a stored credential
before relying on it in a longer session.

## OAuth 2.1 + PKCE (Claude Code `claude mcp login raccha`)

This is the flow Claude Code uses for remote MCP login. It is per-org:
each successful authorization returns one token for exactly one organization.

1. **Discover metadata** — `GET https://raccha.ai/.well-known/oauth-authorization-server`
   returns `authorization_endpoint`, `token_endpoint`, `registration_endpoint`,
   supported grant types, and `code_challenge_methods_supported: ["S256"]`.
2. **Register the client** — `POST /register` with `client_name` and
   `redirect_uris`. The OAuth callback must be one of the registered URIs.
3. **Authorize** — redirect the user to `/authorize?response_type=code&client_id=...&redirect_uri=...&code_challenge=...&code_challenge_method=S256&state=...`.
   If the user is not signed in, raccha.ai sends them to the dashboard to sign in first.
   If they are signed in, they pick one org and approve or deny the request.
4. **Approval response** — on approval, the browser is redirected to
   `redirect_uri?code=<authorization_code>&state=...`. On denial, the redirect
   carries `error=access_denied&state=...`.
5. **Token exchange** — `POST /token` with `grant_type=authorization_code`,
   the `code`, `code_verifier`, `client_id`, and `redirect_uri`. The server
   validates the PKCE challenge and mints an `owner_key` for the chosen org.
   Response: `access_token` (the owner_key), `token_type=bearer`, `expires_in`,
   `refresh_token`, and `scope` (the account slug).
6. **Refresh** — `POST /token` with `grant_type=refresh_token` and the
   `refresh_token` to mint a fresh owner_key for the same org. The old refresh
   token is revoked.

Error responses follow the OAuth 2.1 JSON error format:
`{"error":"invalid_request|invalid_client|invalid_grant|unsupported_grant_type|..."}`.

## Multi-org helpers

Three MCP tools make multi-org clients practical:

- **`list_profiles(owner_key)`** — returns the same `profiles[]` shape as
  `/auth/verify` for the authenticated member's email. Use it to discover every
  org the member can act as.
- **`switch_org(owner_key, account_id)`** — given any valid owner_key for a user,
  mints and returns a fresh owner_key for `account_id` (must belong to the same
  email). Returns the new profile. Clients can save this locally as an additional
  org credential without starting a fresh browser login.
- **`create_org(owner_key, name)`** — creates a brand-new, deliberately-named org
  under the same email as `owner_key` (not a fresh signup). `name` is slugified
  into the org's namespace slug (e.g. "C Engineering" -> `c-engineering`); a
  colliding slug gets a short random suffix rather than an error. Subject to the
  same per-email account-creation quota signup enforces. Returns the new profile
  in the same shape as `switch_org`. Use this instead of signing up with a new
  email when you want a second org you control, with a name you choose instead
  of an auto-generated `ns-xxxxxxxx` slug.

There is intentionally no single token that spans all orgs: each org authorizes
separately and is billed separately.
