OAuth 2.1 + DCR

When Require auth is on, each MCP install (each Claude / ChatGPT user,
effectively) gets its own OAuth client via Dynamic Client Registration —
no human login, no consent screen by default. With a branded consent
screen for email capture if you enable it.

The flow

Agent                                              aimiles
  │   GET /.well-known/oauth-protected-resource/...  │
  ├──────────────────────────────────────────────────▶│
  │◀──────────────────────────────────────────────────┤  PRM (RFC 9728)
  │   GET /.well-known/oauth-authorization-server     │
  ├──────────────────────────────────────────────────▶│
  │◀──────────────────────────────────────────────────┤  ASM (RFC 8414)
  │   POST /api/oauth/register  (RFC 7591)            │
  ├──────────────────────────────────────────────────▶│
  │◀──────────────────────────────────────────────────┤  client_id (+ optional secret)
  │   GET /api/oauth/authorize  ?code_challenge=…     │
  ├──────────────────────────────────────────────────▶│  (branded consent screen)
  │◀──────────────────────────────────────────────────┤  302 redirect with code
  │   POST /api/oauth/token  + code_verifier (PKCE)   │
  ├──────────────────────────────────────────────────▶│
  │◀──────────────────────────────────────────────────┤  access_token (1h, opaque)
  │   POST /api/mcp/<pubId>   Authorization: Bearer   │
  ├──────────────────────────────────────────────────▶│  meter keyed by client_id

Standards we implement

  • RFC 7591 — Dynamic Client Registration
  • RFC 8414 — Authorization Server Metadata
  • RFC 9728 — Protected Resource Metadata
  • OAuth 2.1 — auth_code + PKCE (S256), no implicit grant
  • RFC 8707 — Resource indicators (token bound to one publisher)

Token storage

We store sha256(token) only — a DB leak doesn't yield usable tokens.
Tokens are 1h; refresh tokens (30d) rotate on each use.

Per-publisher consent

The consent screen at /api/oauth/authorize renders with your logo,
primary color, and font — set on the Publisher row during onboarding.

It captures:

  • Reader's email (lands on OAuthClient.email, attributes every
    subsequent McpCall to that person)
  • Terms acceptance
  • Optional: "Already a subscriber? Link my subscription" button if you've
    configured subscription linking

Identity keying

The meter ledger uses oauth:{client_id} as the install identity when a
bearer token is present; falls back to sess:{mcp-session-id} otherwise.

Same install across multiple agent sessions = same client_id = same
meter row. That's the whole point of OAuth here.