# Authentication

Machine callers authenticate with a Bearer token:

```http
Authorization: Bearer rt_ro_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
```

There is no OAuth flow on this endpoint. Tokens are minted deliberately, one per client, and revoked individually.

## Token format

```
rt_<scopeTag>_<43 characters base64url>
```

- `rt` identifies it as an nLight.fit API token. A credential without this prefix is never looked up in the database, so presenting an unrelated key costs nothing to reject.
- `<scopeTag>` is `ro` or `rw`. It is **cosmetic** — it exists so you can recognise a token you find in a config file. Authorization always reads scopes from the database, never from the token string, which the holder controls.
- The secret is 32 bytes of CSPRNG entropy, base64url-encoded to 43 characters with no padding.

## Scopes

| Scope | Grants |
| --- | --- |
| `read` | Every non-mutating tool. Required by every MCP method today, including `initialize` and `ping`. |
| `write` | Anything that changes stored data. Defined and enforced, but nothing over MCP requires it yet. |

Scopes are **explicit and non-cumulative**. A `write` token does not automatically get `read`. That is deliberate: granting one capability should never silently grant another. If you want a token that can do both, ask for both.

A token that is valid but lacks the scope a request needs gets `403`, not `401` — you are who you say you are, you simply may not do this. Telling the client to re-authenticate would send it round a loop it cannot win.

## What is stored

Only `sha256(token)`. The plaintext is shown exactly once, at creation, and never again. A leaked database dump therefore yields no usable credentials, and a lost token cannot be recovered — revoke it and mint another.

Each token row also holds a name you chose, a 12-character display prefix so you can tell two tokens apart in a list, the granted scopes, creation time, last-used time, and revocation state.

### Why SHA-256 and not bcrypt

bcrypt exists to slow down guessing of low-entropy human passwords. These tokens carry 256 bits of entropy, so brute force is not in the threat model, and a deliberately slow hash would add roughly 250ms to every single MCP call. Account passwords still use bcrypt, which is the right choice for what those protect.

## Expiry and revocation

Tokens do not expire by default. That is only safe because revocation is immediate: every call looks the token up in the database, so a revoked token stops working on the next request rather than whenever a signature would have lapsed.

Revocation is idempotent. Revoking an already-revoked token reports success without changing the original revocation timestamp, so a panicked second attempt is not an error.

## Why not session tokens

Session tokens are stateless and signed, which means they cannot be revoked — a stateless token stays valid until it expires, with no server-side blocklist.

That trade-off is acceptable for a 24-hour browser session. It is not acceptable for a credential that lives indefinitely inside a third party's cloud and unlocks a complete medical history. If one leaks it has to die immediately, which means the server must look it up. So API tokens are opaque random strings backed by a database row, not signed self-describing tokens.

## Session tokens are accepted, read-only

A browser session token presented to `/api/mcp` will authenticate, but it is treated as carrying `read` and nothing else. This exists as a convenience for local testing and for any in-app "verify my connection" affordance.

Anything that mutates data over MCP must present a purpose-built token, so a stolen browser session can never be replayed into a write.

## What is not accepted

The compatibility mode that lets some older in-app requests identify a user by a plain `userId` in the request body is never consulted here. That fallback exists only to keep already-open browser tabs working through a rollout. This surface is new, has no legacy clients, and is reachable from the public internet — honouring a bare `userId` would hand every account to anyone who can type a 24-character id.

## Related

- [Token management](/docs/token-scripts) — minting, listing and revoking tokens
- [Client setup](/docs/clients) — wiring a token into Claude or Cursor
- [Errors and limits](/docs/errors) — the full status code table
