# auth.md — agent authentication for namailu.cz

Audience: autonomous agents and the people who deploy them.
Human-readable versions: <https://www.namailu.cz/mcp-manual>, <https://www.namailu.cz/api-manual>.

## What never changes here

A human issues every credential. You can start the process yourself, but nothing you receive before a person confirms grants access to any mailbox. There are no pre-claim scopes: for mail, even read-only is somebody's correspondence.

## Step 1 — Discover

- Protected resource metadata (RFC 9728): <https://mcp.namailu.cz/.well-known/oauth-protected-resource>
- Authorization server metadata (RFC 8414): <https://id.namailu.cz/.well-known/oauth-authorization-server> — read the `agent_auth` block in it.
- Resource (RFC 8707 `resource` parameter): `https://mcp.namailu.cz`
- Endpoint (Streamable HTTP): `https://mcp.namailu.cz/mcp`, or `https://www.namailu.cz/mcp/rpc` if you only know the website. Each host has its own resource identifier — use the one from the metadata you read.
- Scopes: `mcp:mail.messages.read`, `mcp:mail.read`, `mcp:mail.send`

## Step 2 — Register

```http
POST https://id.namailu.cz/agent/identity
Content-Type: application/json

{"type": "service_auth", "login_hint": "person@example.com",
 "client_name": "What you are, in plain words"}
```

You get back `claim_token` (yours, keep it in memory only), and a `claim` block with `user_code`, `verification_uri`, `expires_in` and `interval`. Nothing in that response is a credential.

`login_hint` is the person you are acting for. We never send mail to it — it exists so that **only that account** can finish the ceremony. The response is identical whether or not the address has an account here, so do not read anything into it.

Not supported, deliberately:

- `anonymous` → `anonymous_not_enabled`. It would hand a token to an unidentified caller before any human agreed.
- `identity_assertion` / ID-JAG → `issuer_not_enabled`. It would mean a list of outside providers whose word opens somebody's mailbox.
- **Dynamic Client Registration (RFC 7591)** — no `/register` endpoint; do not try to create a client automatically.

## Step 3 — Claim ceremony

Show the person one message with `verification_uri` and the `user_code`, and be explicit that the code goes into the page they land on, not back to you. They sign in, type the code, pick which agent mailboxes you may use, and confirm with their second factor.

They can pick fewer mailboxes than you hoped for, and an account without 2FA cannot confirm an agent at all. Both are the point, not a fault.

If the code expires while they are still deciding, ask for a fresh one:

```http
POST https://id.namailu.cz/agent/identity/claim
Content-Type: application/json

{"claim_token": "clm_…"}
```

## Step 4 — Poll

```http
POST https://id.namailu.cz/token
Content-Type: application/x-www-form-urlencoded

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

Honor `interval`. `authorization_pending` means keep waiting; `slow_down` means you are polling too fast; `expired_token` means the code window closed — go back to Step 3. On success you get an `access_token` and an `identity_assertion`.

## Step 5 — Refresh

There is no refresh token. When the access token expires, exchange the assertion again:

```http
POST https://id.namailu.cz/token
Content-Type: application/x-www-form-urlencoded

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

`invalid_grant` means the person withdrew consent or it expired — start over at Step 2. Do not retry in a loop.

## Step 6 — Use it

`Authorization: Bearer <access token>` against `https://mcp.namailu.cz/mcp` (tokens are audience-bound to `https://mcp.namailu.cz`) (`bearer_methods_supported: ["header"]`). Scopes come from what the person granted, not from what you asked for — re-check them instead of assuming.

## Alternative — a person already runs you

Then skip all of the above and ask them for a mailbox-scoped API key:

- Resource: `https://api.namailu.cz/v1` · description: <https://api.namailu.cz/openapi.json>
- Credential use: `Authorization: Bearer <api key>`
- The person creates the agent mailbox and its key at <https://portal.namailu.cz/mailboxes>.
- The key is bound to exactly one mailbox: it can never reach another mailbox in the organisation, even if that mailbox belongs to the same person. It is shown once and can be revoked at any time.

## Alternative — delegated MCP with a browser

If you can open a browser for the person, the ordinary OAuth 2.1 authorization code flow with PKCE `S256` also works; identify your client with a client ID metadata document (CIMD). Same resource, same scopes.

## Machine-readable

```json
{
  "agent_auth": {
    "skill": "https://www.namailu.cz/auth.md",
    "register_uri": "https://id.namailu.cz/agent/identity",
    "identity_endpoint": "https://id.namailu.cz/agent/identity",
    "claim_endpoint": "https://id.namailu.cz/agent/identity/claim",
    "token_endpoint": "https://id.namailu.cz/token",
    "identity_types_supported": [
      "service_auth"
    ],
    "dynamic_client_registration": false,
    "operator_uri": "https://portal.namailu.cz/mailboxes",
    "methods": [
      {
        "type": "agentic-registration",
        "resource": "https://mcp.namailu.cz",
        "endpoint": "https://mcp.namailu.cz/mcp",
        "metadata": "https://id.namailu.cz/.well-known/oauth-authorization-server",
        "authorization_server": "https://id.namailu.cz",
        "grant_types": [
          "urn:workos:agent-auth:grant-type:claim",
          "urn:ietf:params:oauth:grant-type:jwt-bearer"
        ],
        "scopes_supported": [
          "mcp:mail.messages.read",
          "mcp:mail.read",
          "mcp:mail.send"
        ],
        "bearer_methods_supported": [
          "header"
        ],
        "credential": "OAuth access token after a human completes the claim ceremony",
        "how": "POST https://id.namailu.cz/agent/identity with {\"type\": \"service_auth\", \"login_hint\": \"<the person you work for>\"}, show them the returned verification_uri and 6-digit user_code, and poll the token endpoint until they confirm."
      },
      {
        "type": "oauth2-mcp",
        "resource": "https://mcp.namailu.cz",
        "endpoint": "https://mcp.namailu.cz/mcp",
        "metadata": "https://mcp.namailu.cz/.well-known/oauth-protected-resource",
        "authorization_server": "https://id.namailu.cz",
        "authorization_server_metadata": "https://id.namailu.cz/.well-known/oauth-authorization-server",
        "scopes_supported": [
          "mcp:mail.messages.read",
          "mcp:mail.read",
          "mcp:mail.send"
        ],
        "bearer_methods_supported": [
          "header"
        ],
        "code_challenge_methods_supported": [
          "S256"
        ],
        "credential": "OAuth 2.1 access token",
        "how": "A signed-in mailbox owner approves the grant in the browser; the client is identified by a client ID metadata document (CIMD) URL. Start at https://portal.namailu.cz/mailboxes."
      },
      {
        "type": "api-key",
        "resource": "https://api.namailu.cz/v1",
        "metadata": "https://api.namailu.cz/openapi.json",
        "credential": "Bearer API key, scoped to a single mailbox",
        "how": "A signed-in person creates an agent mailbox and generates its key at https://portal.namailu.cz/mailboxes. The key is shown once and can be revoked at any time."
      }
    ]
  }
}
```

## Notes for scanners

- Do not probe `POST /agent/auth` during passive scans — no such endpoint exists. `POST /agent/identity` does, and every call to it creates a pending registration that a human then has to ignore; please do not call it unless you mean to register.
- Everything linked from this document is public. Anything else is behind a login and returns `401`/`403` by design, not by misconfiguration.
