# auth.md

This file tells AI agents how to authenticate with **cinder.fi**'s services.
It implements the [auth.md protocol](https://github.com/workos/auth.md):
an agent can **register for its own credential** through a short,
user-approved claim ceremony — no pasted API keys required.

- **Resource server**: `https://mcp.cinder.fi/mcp` — cinder.fi's MCP server:
  19 tools for retirement projections, the cinder score, CPP/OAS/GIS and
  Social Security benefit modeling, withdrawal-order and Roth-conversion /
  RRSP-meltdown optimization, Monte Carlo simulation, historical backtests,
  and Canadian + US tax math (all 13 provinces, all 50 states).
- **Transports**: Streamable HTTP and SSE.
- **Authorization server**: `https://mcp.cinder.fi` (same origin as the
  resource server — spec-legal). It issues short-lived, scoped, revocable
  access tokens.

The whole flow is: **Discover → Register → Claim → Exchange → Use.**

## 1. Discover

Fetch the protected-resource metadata to learn which authorization server to
use, then the authorization-server metadata for its endpoints:

```http
GET https://mcp.cinder.fi/.well-known/oauth-protected-resource
GET https://mcp.cinder.fi/.well-known/oauth-authorization-server
GET https://mcp.cinder.fi/.well-known/jwks.json
```

The authorization-server document advertises the two grant types below, the
token and revocation endpoints, the JWKS (ES256 / P-256 public key used to
sign assertions), and an `agent_auth` block naming the registration endpoints
and supported identity types (`service_auth`, `anonymous`).

## 2. Register

`POST https://mcp.cinder.fi/agent/identity` with a JSON body. Pick one of two
identity types:

### service_auth — tie the credential to a specific user by email

```json
{
  "type": "service_auth",
  "login_hint": "user@example.com",
  "agent_name": "Your agent's name",
  "agent_provider": "your-company"
}
```

Response (`201`):

```json
{
  "registration_id": "…",
  "claim_token": "mcp_claim_…",
  "claim": {
    "user_code": "ABCD-2345",
    "verification_uri": "https://app.cinder.fi/#claim-agent",
    "expires_in": 600,
    "interval": 5
  }
}
```

Keep `claim_token` secret — it's how you poll for the result. Show the user
the `user_code` and `verification_uri`.

### anonymous — start immediately, claim later

```json
{ "type": "anonymous", "agent_name": "Your agent's name" }
```

The response additionally includes an `identity_assertion` (a 30-day JWT)
scoped to **free** tools. You can exchange it for an access token right away
(step 4) and use the free tier without any user involvement. If the user later
claims the same registration (step 3), a subsequent exchange upgrades the
credential to their tier.

## 3. Claim (the user approves)

Tell the user, in your own words:

> Go to **https://app.cinder.fi/#claim-agent**, sign in (or create a free
> account), and enter the code **`ABCD-2345`** to connect me.

The claim page leads with free-account signup — creating an account and
connecting the agent are the same action. Codes expire in 10 minutes; if one
lapses, request a fresh block with
`POST https://mcp.cinder.fi/agent/identity/claim` and the `claim_token`.

The **scopes the agent receives are derived from the approving user's tier**,
server-side. The agent never names its own scopes.

## 4. Exchange

### Poll the claim (service_auth)

`POST https://mcp.cinder.fi/oauth2/token`, form-encoded, no client auth:

```
grant_type=urn:workos:agent-auth:grant-type:claim
claim_token=mcp_claim_…
```

Poll no faster than `interval` seconds (5s). While the user hasn't acted you
get `400 {"error":"authorization_pending"}` (or `slow_down` if you poll too
fast). Once approved you get:

```json
{
  "identity_assertion": "<ES256 JWT>",
  "assertion_expires": "…",
  "scopes": ["tools:free", "tools:pro"]
}
```

### Exchange the assertion for an access token

The `identity_assertion` is a long-lived identity proof — **don't** send it to
`/mcp`. Exchange it for a short-lived access token:

```
grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer
assertion=<identity_assertion>
```

Response:

```json
{
  "access_token": "mcp_at_…",
  "token_type": "Bearer",
  "expires_in": 3600,
  "scope": "tools:free tools:pro"
}
```

## 5. Use

Send the access token as a Bearer credential on every MCP request:

```http
POST /mcp HTTP/1.1
Host: mcp.cinder.fi
Authorization: Bearer mcp_at_…
Content-Type: application/json
```

Access tokens live **1 hour**. When one expires, re-run the jwt-bearer
exchange (step 4) with your stored `identity_assertion` to mint a fresh
token — no new claim needed. Keep the assertion secret and re-exchange as
required; each exchange rotates your previous token.

## Tiers and capability

| | Free | Pro |
|---|---|---|
| Scope | `tools:free` | `tools:free` + `tools:pro` |
| Calls/day | 5 | Unlimited |
| Single-person tools | ✅ | ✅ |
| Couples tools | — | ✅ |
| Monte Carlo | — | ✅ |
| Optimizer / Backtest / Scenario compare | — | ✅ |

Tier is enforced server-side per call. Pro-only tools are filtered out of a
free key's `tools/list` entirely; if you invoke one anyway, the response names
the upgrade path (`https://cinder.fi/pricing/`) and — for an unclaimed
credential — the claim/signup URL, so you can prompt the user at the moment of
friction.

## Errors

| Status | `error` | Meaning | Agent action |
|---|---|---|---|
| `400` | `authorization_pending` | Claim not yet approved | Keep polling at `interval` |
| `400` | `slow_down` | Polling too fast | Back off, then retry |
| `400` | `expired_token` | Claim window lapsed | `POST /agent/identity/claim` for a fresh code |
| `400` | `invalid_grant` | Assertion invalid/expired or registration revoked | Re-register from step 2 |
| `401` | — | Missing/invalid/expired access token | Re-exchange (step 4), or re-register |
| `403` | — | Tool not on this credential's tier | Surface the upgrade path to the user |
| `429` | — | Free-tier daily quota exhausted | Wait for the next UTC day, or have the user upgrade/claim |
| `503` | `temporarily_unavailable` | Registration disabled on this deployment | Fall back to a user-mediated key (below) |

## Revocation

Exchange access tokens are revocable per [RFC 7009](https://www.rfc-editor.org/rfc/rfc7009):

```
POST https://mcp.cinder.fi/oauth2/revoke
token=mcp_at_…
```

The endpoint returns `200` even for unknown tokens. Users can also disconnect
an agent from the app. A revoked token fails with `401`; re-exchange or
re-register to continue.

## Consumer connectors (claude.ai, ChatGPT)

If you're a **consumer MCP client** with a browser — claude.ai Connectors,
ChatGPT — use the standard OAuth 2.1 **authorization code + PKCE** flow instead
of the agent grants above. It's fully supported and self-service:

1. Discover the endpoints from `/.well-known/oauth-authorization-server`
   (`authorization_endpoint`, `token_endpoint`, `registration_endpoint`).
2. **Register** via Dynamic Client Registration (RFC 7591):
   `POST /oauth2/register` with your `redirect_uris` → `client_id`.
3. Send the user to `GET /oauth2/authorize?response_type=code&client_id=…&redirect_uri=…&code_challenge=…&code_challenge_method=S256&state=…`.
   They sign in to cinder.fi and approve; you receive `code` at your redirect.
4. Exchange at `POST /oauth2/token` with
   `grant_type=authorization_code&code=…&code_verifier=…&client_id=…&redirect_uri=…`
   → a 1-hour `access_token` **and** a `refresh_token`.
5. Refresh with `grant_type=refresh_token&refresh_token=…` (rotated on use).

Scopes are set from the approving user's tier; PKCE (S256) is required.

## Fallback: user-mediated key

The classic path still works if you'd rather not run the protocol, or if
registration is temporarily disabled (`503`): ask the user to visit
`https://cinder.fi/mcp/` for a **free key by email** (5 calls/day) or a **Pro
key** (cinder.fi Pro, generated on that page or in **Settings → Advanced →
API**). Send it as `Authorization: Bearer <api-key>`. These keys are
long-lived secrets and don't expire on their own.

## More for agents

- Content index: `https://cinder.fi/llms.txt`
- MCP server details + signup: `https://cinder.fi/mcp/`
- Protocol source & roadmap: https://github.com/cinderfi/cinderfi/issues/1384
- Tool list and schemas: connect to the endpoint and call `tools/list`
