# auth.md

You are an agent. This service supports **agentic registration** (the auth.md profile): discover → register → claim → exchange for an access_token → call the API → handle revocation. Follow the steps in order. If you can instead open a browser window for the user, the OAuth authorization-code path at the end of this document is the better fit for a relayed MCP endpoint.

## Audience

Software agents and developer tools that call webmcp.fast. There are two protected surfaces, and one credential never works on the other:

- **A relayed endpoint**, `https://<handle>.webmcp.fast/<device>/<server>/mcp`: one local MCP server on one of a user's devices. Scope `mcp`. Only OAuth authorization code (with the user on the consent screen) or a connector token opens it. **Agent registration below can never reach a relayed endpoint.**
- **The management MCP server**, `https://webmcp.fast/mcp`: read-only. `check_handle`, `start_setup`, `setup_status` are anonymous; `list_devices` and `connect_url` need scope `manage`, which registration below can give you once the user claims you.

The public site, the docs and every discovery document need no credential.

## Step 1 — Discover

An unauthenticated request to a protected endpoint answers `401` with `WWW-Authenticate: Bearer resource_metadata="..."`. Fetch that URL (1a), then the authorization server it names (1b).

### 1a. Protected Resource Metadata (RFC 9728)

```http
GET https://webmcp.fast/.well-known/oauth-protected-resource/mcp
```

`resource` is `https://webmcp.fast/mcp`, `authorization_servers` is `["https://webmcp.fast"]`, `scopes_supported` is `["manage"]`, `bearer_methods_supported` is `["header"]`, and `agent_auth_uri` points at this document. Relayed endpoints have their own document at `https://<handle>.webmcp.fast/.well-known/oauth-protected-resource/<device>/<server>/mcp`.

### 1b. Authorization Server Metadata (RFC 8414)

```http
GET https://webmcp.fast/.well-known/oauth-authorization-server
```

The standard fields: `issuer` (`https://webmcp.fast`), `token_endpoint` (`https://webmcp.fast/oauth/token`), `revocation_endpoint` (also `https://webmcp.fast/oauth/token`, RFC 7009), `registration_endpoint` (`https://webmcp.fast/oauth/register`, RFC 7591) and `grant_types_supported`, which includes `urn:ietf:params:oauth:grant-type:jwt-bearer` (Step 5) and `urn:workos:agent-auth:grant-type:claim` (Step 4c). The profile-specific block:

```json
"agent_auth": {
  "skill": "https://webmcp.fast/auth.md",
  "identity_endpoint": "https://webmcp.fast/agent/identity",
  "claim_endpoint": "https://webmcp.fast/agent/identity/claim",
  "identity_types_supported": ["anonymous"],
  "credential_types_supported": ["access_token"],
  "events_supported": [],
  "anonymous": { "pre_claim_scopes": ["setup"], "post_claim_scopes": ["manage"] }
}
```

If the block is missing, anonymous registration is switched off on this deployment and `POST /agent/identity` answers `anonymous_not_enabled`. There is no `events_endpoint`: no upstream identity provider is trusted, so there is nothing to be notified about.

## Step 2 — Pick a method

Only `anonymous` is supported. `identity_assertion` (ID-JAG) answers `issuer_not_enabled` and `service_auth` answers `service_auth_not_enabled`; do not send them. Anonymous registration needs nothing from the user, so there is no consent to collect before Step 3. The claim ceremony (Step 4) is where the user decides.

## Step 3 — Register

```http
POST https://webmcp.fast/agent/identity
Content-Type: application/json

{ "type": "anonymous" }
```

Response (200):

```json
{
  "registration_id": "reg_...",
  "registration_type": "anonymous",
  "identity_assertion": "<service-signed JWT, HS256>",
  "assertion_expires": "<24 hours from now>",
  "pre_claim_scopes": ["setup"],
  "claim_url": "https://webmcp.fast/agent/identity/claim",
  "claim_token": "clm_...",
  "claim_token_expires": "<24 hours from now>",
  "post_claim_scopes": ["manage"]
}
```

`claim_token` is returned exactly once and stored only as a hash; hold it in memory for the ceremony and do not persist or log it. Limited to 10 registrations per hour per IP.

**What `setup` is worth: nothing beyond what you already have.** A pre-claim access_token identifies you as a registered agent and that is all. The management tools `check_handle`, `start_setup`, `setup_status` are anonymous anyway, and a `setup` token is refused (`401`, `error="insufficient_scope"`) by `list_devices` and `connect_url`. It cannot reach a relayed endpoint. If you only need the anonymous tools, you do not need to register at all. Register when you want the user to claim you.

## Step 4 — Claim ceremony

The end goal: a signed-in user confirms a 6-digit `user_code` **you supply them**, on a page this service owns. The code travels from you to the user; the user signs in with the code webmcp.fast emails them (a different code, which you must never ask for) and types your code into the page. That binds your registration to their account.

### 4a. Get the ceremony materials

```http
POST https://webmcp.fast/agent/identity/claim
Content-Type: application/json

{ "claim_token": "clm_...", "email": "user@example.com" }
```

Response (200):

```json
{
  "registration_id": "reg_...",
  "claim_attempt_id": "cla_...",
  "status": "initiated",
  "expires_at": "<10 minutes from now>",
  "claim_attempt": {
    "user_code": "123456",
    "expires_in": 600,
    "verification_uri": "https://webmcp.fast/agent/claim?claim_attempt_token=...",
    "interval": 5
  }
}
```

`email` binds the registration to the human you act for: only a user signed in with exactly that address can complete the ceremony, so an intercepted `user_code` is useless to anyone else. The `verification_uri` carries an opaque `claim_attempt_token`, never the `user_code`. Limited to 30 calls per 15 minutes per IP and 10 per registration.

### 4b. Hand off to the user

Give the user the link and the code in one message, for example:

> Open this link, sign in as user@example.com, and enter this 6-digit code on the page: **123456**
> https://webmcp.fast/agent/claim?claim_attempt_token=...

Be explicit that the code goes into the page they land on after signing in, not back to you. They will: open the link; sign in with the 6-digit code webmcp.fast emails them (which creates the account if there is none); land on the claim page, which says "signed in as <email>" and spells out what you will and will not be able to do; type your code; press Approve or Deny.

### 4c. Poll for completion

```http
POST https://webmcp.fast/oauth/token
Content-Type: application/x-www-form-urlencoded

grant_type=urn:workos:agent-auth:grant-type:claim&claim_token=clm_...
```

While waiting (RFC 8628 vocabulary): `{"error": "authorization_pending"}`. Poll no faster than `interval` seconds; faster gets `slow_down`, and you should add 5 seconds. On success:

```json
{
  "access_token": "wma_...",
  "token_type": "Bearer",
  "expires_in": 3600,
  "scope": "manage",
  "identity_assertion": "<service-signed JWT, v2>",
  "assertion_expires": "<30 days from now>"
}
```

Use `access_token` at once; cache the v2 `identity_assertion` for refreshes (Step 5). Completion **revokes every pre-claim access_token** and supersedes the v1 assertion: the v2 carries the user's `email` and `email_verified: true`, the v1 did not. The success response is handed over once; after that the claim grant answers `invalid_grant` and the v2 assertion is your refresh path.

If the code window passes: `{"error": "expired_token"}`. Call `POST /agent/identity/claim` again with the same `claim_token` and `email` for a fresh `user_code` and link (this also works after five wrong codes locked the attempt). If that answers `410 claim_expired`, the 24-hour registration window has closed: start over at Step 3. `access_denied` means the user pressed Deny: stop, and only start again if they ask.

Wrong codes are counted and shown to the user on the page, never to you; five wrong codes lock that attempt.

## Step 5 — Exchange the assertion

```http
POST https://webmcp.fast/oauth/token
Content-Type: application/x-www-form-urlencoded

grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer&assertion=<identity_assertion>&resource=https://webmcp.fast/mcp
```

`resource` is optional; if present it must be exactly `https://webmcp.fast/mcp` (RFC 8707), the only resource these grants are issued for. Response (200):

```json
{ "access_token": "wma_...", "token_type": "Bearer", "expires_in": 3600, "scope": "setup" }
```

`scope` is `setup` before the claim and `manage` after it. The same assertion mints as many access_tokens as you need until it expires (v1: 24 hours; v2: 30 days). `invalid_grant` means the assertion is expired, revoked (the user disconnected you on the dashboard) or superseded (a v1 after the claim completed): restart at Step 3, and re-do the claim ceremony if you need `manage` again. No `client_id` is needed for these two grants.

## Step 6 — Use the access_token

```http
POST https://webmcp.fast/mcp
Authorization: Bearer wma_...
Content-Type: application/json
Accept: application/json, text/event-stream

{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"list_devices","arguments":{}}}
```

Access tokens start with `wma_`, are opaque, stored hashed, and last one hour. When one expires, repeat Step 5 with the same assertion; when the assertion expires or answers `invalid_grant`, restart at Step 3. There is no OAuth refresh_token in this flow.

A `wma_` token sent to a relayed endpoint (`https://<handle>.webmcp.fast/<device>/<server>/mcp`) is refused with `401`, whatever its scope. Those endpoints take OAuth `mcp` tokens and connector tokens only.

## Errors

Errors at `/agent/identity` and `/agent/identity/claim` are JSON `{error, message, hint, docs}` (plus `error_description`), like the rest of the API. Errors at `/oauth/token` use the RFC 6749 shape `{error, error_description}`.

| Code | Where | What to do |
|---|---|---|
| `anonymous_not_enabled` | `/agent/identity` | Registration is switched off here. Use OAuth or a connector token. |
| `service_auth_not_enabled` | `/agent/identity` | Not offered. Send `{"type": "anonymous"}`. |
| `issuer_not_enabled` | `/agent/identity` | No provider is trusted for ID-JAGs. Send `{"type": "anonymous"}`. |
| `invalid_request` | `/agent/identity`, `/agent/identity/claim` | Body shape, missing field or invalid email. Fix the input. |
| `invalid_claim_token` | `/agent/identity/claim` | Unknown or revoked `claim_token`. Restart at Step 3. |
| `claimed_or_in_flight` (409) | `/agent/identity/claim` | Already claimed. Collect the credential with the claim grant or exchange the v2 assertion. |
| `claim_expired` (410) | `/agent/identity/claim` | The 24-hour window closed. Restart at Step 3. |
| `invalid_grant` | `/oauth/token` | Assertion or claim_token expired, revoked or superseded. Restart at Step 3. |
| `invalid_target` | `/oauth/token` | `resource` is not `https://webmcp.fast/mcp`. |
| `unsupported_grant_type` | `/oauth/token` | Not one of the advertised grants. |
| `authorization_pending` | `/oauth/token` (claim grant) | Keep polling at `interval`. |
| `slow_down` | `/oauth/token` (claim grant) | Add 5 seconds to `interval`. |
| `expired_token` | `/oauth/token` (claim grant) | Re-call `/agent/identity/claim`; on `claim_expired`, restart at Step 3. |
| `access_denied` | `/oauth/token` (claim grant) | The user declined. Stop. |
| `rate_limited` (429) | any | Wait `Retry-After` seconds. Limits are advertised in `RateLimit-Policy`. |

Retry policy: 5xx, back off and retry the same request; 4xx, do not retry the same payload, act on the table; 401 on a previously working access_token, run Step 5 once, and if that fails restart at Step 1.

## Revocation

- **Credential layer (RFC 7009).** `POST https://webmcp.fast/oauth/token` with `token=wma_...&token_type_hint=access_token` (form-encoded) kills that one access_token. `200` on success, idempotent. Your assertion is intact; run Step 5 for a fresh token.
- **Registration layer.** The user presses Disconnect under *Setup agents* on https://webmcp.fast/app. Every assertion and access_token of that registration dies; the next exchange answers `invalid_grant`. Restart at Step 3 if the user wants you back. There is no provider-driven event feed (`events_supported` is empty).

## The other credentials

### OAuth authorization code with PKCE S256 (relayed endpoints, scope `mcp`)

For an agent that can send the user to a browser. Register a client at `POST https://webmcp.fast/oauth/register` (RFC 7591, no approval step) or use a Client ID Metadata Document URL as the `client_id`, then:

1. Generate a PKCE `code_verifier` and S256 `code_challenge`.
2. Open `https://webmcp.fast/oauth/authorize` with `response_type=code`, your `client_id`, a registered `redirect_uri`, `scope=mcp` (or `manage` with `resource=https://webmcp.fast/mcp`), the challenge, `code_challenge_method=S256`, `state`, and `resource` set to the exact MCP URL (RFC 8707).
3. The user signs in with a 6-digit code emailed to them and sees a consent screen naming that one endpoint. **This step is for the human.** An agent is never given the code and must not ask for it.
4. Exchange the returned code at `POST https://webmcp.fast/oauth/token` with `grant_type=authorization_code`, `client_id`, the same `redirect_uri` and the `code_verifier`.

Access tokens last one hour; refresh tokens rotate on use; a grant lasts until the user disconnects it on the dashboard or removes the server or device. Send the token as `Authorization: Bearer` to `POST https://<handle>.webmcp.fast/<device>/<server>/mcp`. The token's audience must equal the URL it is sent to.

### Connector tokens

For a harness that cannot open a sign-in window, the user creates a connector token on https://webmcp.fast/app. It starts with `wmf_`, is shown once, is bound to one handle, device and server, and is sent as `Authorization: Bearer wmf_...`. It is revoked from the same page.

## Scopes

| Scope | Grants | How it is obtained |
|---|---|---|
| `mcp` | One relayed endpoint, named by `resource` and confirmed on the consent screen. | OAuth authorization code only. |
| `manage` | Reading the user's handles, devices and connector URLs through the management MCP server. | OAuth authorization code, or agent registration after the claim ceremony. |
| `setup` | Nothing beyond the anonymous management tools. Identifies a registered agent. | Agent registration, before the claim. |

## The daemon

The daemon does not use OAuth. It pairs once (a device authorization flow shaped after RFC 8628, `POST https://webmcp.fast/api/v1/device/start` and `/poll`, approved by the human at https://webmcp.fast/activate) and then authenticates its WebSocket with an Ed25519 signature over a server challenge. See https://webmcp.fast/openapi.json.

## Contact

support@webmcp.fast. See also the [security model](https://webmcp.fast/docs/security-model), the [protocol](https://webmcp.fast/docs/api) and the [privacy policy](https://webmcp.fast/privacy).
