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

# Authentication

> API keys and OAuth 2.1 — two ways credentials reach the server

Every API request carries a bearer token. Marfa accepts two token types, both sent in the `Authorization` header:

```http theme={null}
Authorization: Bearer <token>
```

The token prefix indicates its kind:

| Prefix         | Kind                                                    |
| -------------- | ------------------------------------------------------- |
| `marfa_k1_...` | API key                                                 |
| `marfa_at_...` | OAuth access token                                      |
| `marfa_rt_...` | OAuth refresh token (used only at the refresh endpoint) |

## API keys

Bearer tokens, suitable for development, automation, server-to-server calls, and app-to-Marfa in self-hosted deployments.

### Creating keys

Call [`POST /keys`](/api-reference/keys/create-an-api-key) with the label, role, and the three permission maps (types, extensions, edges). The response contains the plaintext key **once** — the server never shows it again. Store it securely. Rotating means revoking the old key and creating a new one.

<Accordion title="Example request and response">
  ```json theme={null}
  POST /keys
  {
    "label": "my-app",
    "role": "member",
    "source": "My App",
    "default_tier": "library",
    "type_permissions": { "core.note": "write", "core.bookmark": "read" },
    "extension_permissions": { "my-app.*": "write" },
    "edge_permissions": { "parent-of": "write", "about": "write" }
  }
  ```

  ```json theme={null}
  {
    "id": "019d...",
    "key": "marfa_k1_abc...",
    "label": "my-app",
    "role": "member",
    "source": "My App",
    "default_tier": "library",
    ...
  }
  ```
</Accordion>

### Key fields

Full request and response shapes live in the [Keys API reference](/api-reference/keys/create-an-api-key). A few fields carry rules worth calling out:

* **`role`** — `admin` bypasses every permission check. `member` is scoped by the three permission maps.
* **`source`** — stamped onto every item written by this key. Unique per tenant; clients can't override.
* **`default_tier`** — stamps onto items when the client omits the field. Clients can override per item. See [Tier](/concepts/library).
* **`type_permissions`**, **`extension_permissions`**, **`edge_permissions`** — see [Permissions](/api/permissions) for resolution rules and the dual-gate.

### Bootstrap

On a fresh server with no keys, the first `POST /keys` succeeds without auth (bootstrap mode). Once any key exists, bootstrap mode disables — further key creation requires an admin token.

If all keys are later revoked, bootstrap mode does not re-enable automatically. Recovery requires server-side admin intervention (dropping and recreating the keys table, typically).

### Managing keys

List, retrieve, update, and revoke via the [Keys endpoints](/api-reference/keys/list-api-keys). Admin-scoped — non-admin keys cannot manage other keys.

### Revocation

[`DELETE /keys/{id}`](/api-reference/keys/revoke-an-api-key) revokes immediately. Revoked tokens return `401 unauthorized` on the next request.

## OAuth 2.1

Authorization Code flow with PKCE. The "Sign in with Marfa" flow for third-party apps.

The OAuth-protocol surface is owned by the `@better-auth/oauth-provider` plugin mounted on Marfa's Better Auth instance. Endpoints land under `/auth/oauth2/*`; discovery + JWKS are re-published at the standard well-known paths. The consent screen and grants management remain Marfa-owned and sit on top of the plugin's tables.

### High-level flow

<Steps>
  <Step title="Client registration">
    An app registers as an OAuth client. Two paths: dynamic registration via `POST /auth/oauth2/register` (RFC 7591, open — `allowUnauthenticatedClientRegistration: true`), or admin-side via `POST /auth/clients`. Public clients (native apps, SPAs) use PKCE without a client secret; confidential clients (server apps) receive a `client_secret`.
  </Step>

  <Step title="Authorization request">
    The app redirects the user to `GET /auth/authorize` with `client_id`, `redirect_uri`, `scope`, `state`, `code_challenge`, and `code_challenge_method=S256`. If the user has no active session, the server redirects to `/auth/sign-in` (a server-rendered HTML form) preserving the original authorize URL as a return target. After sign-in, the user lands on the consent screen listing exactly which types, extensions, and edges the app is requesting.
  </Step>

  <Step title="Consent">
    The user approves (or modifies) the scope. The consent form POSTs to `/auth/authorize/decision`, which forwards to the plugin's `/auth/oauth2/consent` and redirects back to `redirect_uri` with an authorization code.
  </Step>

  <Step title="Token exchange">
    The app exchanges the code at `POST /auth/oauth2/token` with the `code_verifier`. Response: access token, refresh token, scope granted. (`POST /auth/token` is preserved as a back-compat alias.)
  </Step>

  <Step title="Use the access token">
    The app calls the API with `Authorization: Bearer marfa_at_...`. Access tokens are short-lived (default 1 hour).
  </Step>

  <Step title="Refresh">
    Before the access token expires, the app calls `POST /auth/oauth2/token` with `grant_type=refresh_token` to get a new pair. Refresh token rotation: the old refresh token is invalidated.
  </Step>
</Steps>

### Endpoint surface

| Endpoint                                                    | Spec / purpose                                                                                   |
| ----------------------------------------------------------- | ------------------------------------------------------------------------------------------------ |
| `POST /auth/oauth2/authorize` + `POST /auth/oauth2/consent` | Authorization Code + PKCE S256.                                                                  |
| `POST /auth/oauth2/token`                                   | `authorization_code`, `refresh_token`, `client_credentials`. Refresh rotation built in.          |
| `POST /auth/oauth2/userinfo`                                | OIDC userinfo.                                                                                   |
| `POST /auth/oauth2/revoke`                                  | RFC 7009 token revocation.                                                                       |
| `POST /auth/oauth2/introspect`                              | RFC 7662 token introspection.                                                                    |
| `POST /auth/oauth2/register`                                | RFC 7591 Dynamic Client Registration. Open to unauthenticated callers.                           |
| `GET /auth/jwks`                                            | JWKS for `id_token` signature verification.                                                      |
| `GET /.well-known/oauth-authorization-server`               | RFC 8414 authorisation-server metadata.                                                          |
| `GET /.well-known/openid-configuration`                     | OIDC discovery.                                                                                  |
| `GET /auth/authorize` + `POST /auth/authorize/decision`     | Marfa-owned consent screen + decision handler (forwards to the plugin's `/auth/oauth2/consent`). |
| `/auth/device*`                                             | Marfa-owned device-flow state machine; tokens land in the plugin's tables on issuance.           |
| `/auth/grants*`, `/auth/sessions*`, `/auth/security`        | Marfa-owned user-facing grants + session management.                                             |

### Scopes

Scopes use a verb-on-resource grammar that mirrors the permission maps:

| Scope                  | Meaning                                                                                     |
| ---------------------- | ------------------------------------------------------------------------------------------- |
| `<type>:read`          | Read items of this type (pattern matches subtypes).                                         |
| `<type>:write`         | Create / update items of this type.                                                         |
| `edge.<type>:read`     | Read edges of this type.                                                                    |
| `edge.<type>:write`    | Create / delete edges of this type (dual-gated with type write).                            |
| `metadata:read`        | Read tags.                                                                                  |
| `metadata:write`       | Write tags.                                                                                 |
| `metadata.<sub>:write` | Write a specific metadata sub-resource (today: `metadata.types:write` gates `POST /types`). |

Wildcards are allowed on type scopes: `core.bookmark.*:read` covers bookmark and all its subtypes; `*:read` covers everything.

Extension namespaces are not part of the OAuth scope grammar — extension reads and writes are governed by the credential's `extension_permissions` map (see [Permissions](/api/permissions)). OAuth tokens issued by the consent flow inherit a default extension grant from the requesting client's registration.

The consent UI renders the resolved type patterns literally — users see exactly what they're granting.

### Partial grants

Users can approve a subset of requested scopes. The returned `scope` parameter in the token response lists what was actually granted. Apps must handle receiving less than they asked for.

### Refresh token rotation

Each refresh invalidates the prior refresh token and issues a new one. Reusing an invalidated refresh token returns `400 token_reuse_detected` and signals that the token pair has been compromised — both access and refresh tokens are invalidated. The app must restart the authorization flow.

### Device Authorization Grant

For headless and CLI contexts where the Authorization Code flow's browser-redirect can't run, the platform implements RFC 8628 Device Authorization Grant.

<Steps>
  <Step title="Initiate">
    The CLI calls `POST /auth/device` with `client_id` and `scope`. Response carries `device_code`, a short human-readable `user_code`, the `verification_uri` to open in a browser, the `verification_uri_complete` form with the user code embedded as a query parameter (so the form pre-fills), and the `interval` between polls.
  </Step>

  <Step title="Display and poll">
    The CLI prints the URL and code; the user opens the URL on any browser-capable device, signs in if needed, enters or confirms the code, and approves. Meanwhile the CLI polls `POST /auth/device/token` at the declared interval. Visiting `GET /auth/device?user_code=XXX-XXXX` short-circuits straight to the consent screen.
  </Step>

  <Step title="Token issuance">
    Once the user approves, the next poll returns the same access + refresh token pair as the Authorization Code flow. When `openid` is among the granted scopes, an `id_token` is also returned.
  </Step>
</Steps>

While the user has not yet approved, polls return `400 authorization_pending`. Polling faster than the declared interval returns `400 slow_down` and bumps the interval up. Past the device-code expiry, polls return `400 expired_token`.

### Sign-in and sign-up surfaces

The platform serves server-rendered HTML at `/auth/sign-in` and (when sign-up is enabled) `/auth/sign-up`. These are the surfaces the consent flow redirects unauthenticated users to. They also stand alone for direct sign-in to the platform's own console.

### OIDC identity tokens

When `openid` is among the granted scopes, the token endpoint returns an `id_token` alongside the access and refresh tokens. The id token is a signed JWT carrying:

* Required claims: `iss`, `sub`, `aud`, `exp`, `iat`
* With `profile` scope: `name`, `preferred_username`, `picture`
* With `email` scope: `email`, `email_verified`

Verify signatures against the platform's JWKS at `/.well-known/jwks.json`. Algorithms supported are advertised in the OIDC discovery doc at `/.well-known/oauth-authorization-server` under `id_token_signing_alg_values_supported` (today: `RS256`).

For most clients the userinfo endpoint at `/auth/userinfo` is the simpler way to fetch claim data — it returns the same shape over a request rather than as a JWT. Use the id token when verifying identity statelessly without a server round-trip; use userinfo when you have a bearer in hand and want the current claim values.

### Token revocation

`POST /auth/oauth2/revoke` implements RFC 7009. Pass the access or refresh token as `token` in a form-encoded body. The server invalidates the token (and, for refresh tokens, the entire grant) and returns `200` regardless of whether the token was valid — per the spec, to avoid leaking token-existence information.

```bash theme={null}
curl -X POST https://marfa.example.com/auth/oauth2/revoke \
  -H 'Content-Type: application/x-www-form-urlencoded' \
  -d 'token=marfa_at_...'
```

Apps should call this on sign-out to ensure server-side state matches the user's intent. The endpoint is advertised in the OIDC discovery doc as `revocation_endpoint`.

### Token introspection

`POST /auth/oauth2/introspect` implements RFC 7662. Form-encoded body with `token` and (for confidential clients) `client_id` + `client_secret`. The server returns `{ active: boolean, scope?, client_id?, exp?, ... }` — `active: false` is the not-valid response, never an error.

### Grant lifecycle audit

The auth audit-row catalog carries two grant-lifecycle entries:

* `auth.grant.created` — emitted on consent acceptance (both code-flow via `/auth/authorize/decision` and device-flow via `/auth/device/consent`). `details.source` distinguishes the surface; `details.created` distinguishes first-consent from re-consent.
* `auth.grant.revoked` — emitted on `DELETE /auth/grants/:id` and the form-friendly `POST /auth/grants/:id/revoke`.

Both rows are written by Marfa route handlers, not by the plugin, so they carry the verified `client_id`, `user_id`, and grant item id without trusting form fields.

### Grants management

`/auth/grants` exposes the user's current OAuth grants — one row per `system.connection` of `kind: app`. Listing, inspecting, and revoking grants happens through this surface. Revoking a grant invalidates the associated tokens immediately. See [Connections](/concepts/connections) for the underlying type.

### Connector OAuth bootstrap

Distinct from the user-facing OAuth flow above: the platform also runs an OAuth callback at `/oauth/callback/:provider` for Integration connectors that need to establish OAuth tokens against an external service (Google Calendar, GitHub, etc.). The callback verifies a server-signed state, exchanges the upstream code for tokens, and stores them encrypted for the connector's later use through the OAuth proxy. End users see a normal upstream OAuth flow; the platform handles the bootstrap on the connector's behalf. See [Integrations](/concepts/integrations) for the wider context.

The bootstrap flow is hardened with two platform-side controls:

* **PKCE on the upstream authorize call.** The platform generates a code verifier per attempt, persists it inside the AES-GCM-signed state envelope, and sends `code_challenge` + `code_challenge_method=S256` on the authorize redirect. The verifier is replayed at token exchange. Connectors don't manage PKCE themselves — it's enforced by the platform regardless of what the upstream service expects, defending against authorization-code injection.
* **Redirect allow-list.** `MARFA_OAUTH_REDIRECT_ALLOWLIST` (comma-separated) lists the canonical `redirect_uri` values the bootstrap will accept. Requests whose `redirect_uri` doesn't match (after scheme + host + path normalisation) are rejected at `/oauth/start` with `400`. Required in production; misconfigured deployments fail loudly rather than accept arbitrary redirects.

## Choosing between API keys and OAuth

* **API keys** for: your own apps against your own tenant, server-to-server calls, scripts, CI, MCP servers and CLIs connecting to a known tenant.
* **OAuth** for: third-party apps where the user wants explicit consent per type, revocable without rotating an API key, and a flow that terminates at a login experience.

Both produce the same server behavior — permissions enforce identically regardless of credential kind.
