# nLight.fit authentication

Machine callers authenticate with a Bearer token:

```http
Authorization: Bearer rt_ro_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
```

Tokens are minted one per client and revoked individually. You can mint one yourself, or let a client obtain one by signing you through the [OAuth flow](/docs/mcp#connecting-over-oauth) — either way the credential that ends up on the wire is the same kind of token, described below.

## 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 `tools/call`, `prompts/list` and `prompts/get`. |
| `write` | Anything that changes stored data. Defined and enforced, but nothing over MCP requires it yet. |

## What needs no token at all

`initialize`, `server/discover`, `ping`, `notifications/*` and `tools/list` are answered without a credential, so a client can complete the protocol handshake and enumerate the tool surface before it has one. None of them reads a member record.

An anonymous `tools/list` returns the generic tool descriptors. The same call with a token returns the same tools described against that member's own column names and date coverage, which is why the personalised catalog is behind the credential and the generic one is not. The result of an anonymous call says so in its `_meta`, rather than leaving you to infer it from descriptions that happen to name no columns.

Everything else answers `401` with a `WWW-Authenticate` challenge naming the authorization server — that challenge is what lets an MCP client start the OAuth flow on its own.

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.

## Tokens issued to a connector

A client that connects over OAuth receives an ordinary API token. There is no second credential format and no parallel validation path — the authorization server mints through the same code any other token comes from, which is why `/api/mcp` needed no changes to accept one.

Three things differ, and all three are recorded on the row rather than encoded in the token:

- It carries the name of the application that asked, so you can tell a connector apart from a token you minted by hand.
- It expires after 30 days and the client refreshes it silently. Refreshing rotates both credentials and revokes the token it replaced, so a client that refreshes often does not accumulate live credentials.
- It is always `read`. The authorization server cannot issue `write` at all, so approving a consent screen can never be the thing that grants an outside application the ability to change your record.

Revoking one is the same operation as revoking any other token, and disconnects the client on its next request.

## Why not session tokens

A browser session belongs to a person sitting in the app. It can be killed from Settings — change the password, or use **Sign out other devices** — but it is still the wrong shape for a credential that lives inside a third party's cloud and unlocks a complete medical history.

So `/api/mcp` accepts only an API token. A session presented there is rejected. Identifying a user any other way is rejected too.

## Related

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