# auth.md

You are an agent. Sorted Travel destination ranking, facts, weather, and visa tools are a free tier with **zero-auth access**. Profile preference tools use OAuth 2.0. This document is the WorkOS auth.md skill advertised as `agent_auth.skill`. Follow discovery, then pick a method.

## Step 1 — Discover

A 401 carries `WWW-Authenticate` with the RFC 9728 protected-resource metadata URL:

```http
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer resource_metadata="https://sorted.travel/.well-known/oauth-protected-resource"
```

If you do not have a 401, fetch the conventional path on this host.

### 1a. Fetch the Protected Resource Metadata

```http
GET https://sorted.travel/.well-known/oauth-protected-resource
```

- `resource` — `https://sorted.travel/mcp`.
- `authorization_servers` — `["https://sorted.travel"]`.
- `scopes_supported` — `profile.read`, `profile.write` (profile MCP tools only).
- `bearer_methods_supported` — `header`.

The same document is also at `https://sorted.travel/.well-known/oauth-protected-resource/mcp`.

### 1b. Fetch the Authorization Server metadata

```http
GET https://sorted.travel/.well-known/oauth-authorization-server
```

Read the RFC 8414 fields (`issuer`, `token_endpoint`, `revocation_endpoint`, `grant_types_supported`) and the WorkOS `agent_auth` block in full:

- `agent_auth.skill` — this document (`https://sorted.travel/auth.md`).
- `agent_auth.identity_endpoint` — `https://sorted.travel/agent/identity`.
- `agent_auth.identity_types_supported` — `["anonymous"]` only. Do not send `identity_assertion` or `service_auth`. The ID-JAG URN `urn:ietf:params:oauth:token-type:id-jag` is **not** advertised (it would live under `identity_assertion.assertion_types_supported`, not at the top level).

Human docs: https://sorted.travel/developers#oauth

## Step 2 — Pick a method

1. **Destination tools** (`GET https://sorted.travel/api/v1/status`, `POST https://sorted.travel/mcp` ranking/facts/weather/visa) — no credential. Skip registration.
2. **You have neither a user session nor an email, and a host still wants an agent-auth token** → [anonymous](#anonymous).
3. **You need `profile.read` / `profile.write`** — do **not** use agent-auth. Use OAuth 2.0 authorization code + PKCE at `https://sorted.travel/oauth/authorize` (RFC 8414 `authorization_endpoint`).
4. **identity_assertion + id-jag** — not enabled. Stop if that is your only method.
5. **service_auth** — not enabled.

## Step 3 — Register

### identity_assertion + id-jag

Not accepted. `POST https://sorted.travel/agent/identity` with `type: identity_assertion` returns `issuer_not_enabled`.

### service_auth

Not accepted. `POST https://sorted.travel/agent/identity` with `type: service_auth` returns `service_auth_not_enabled`.

### anonymous

```http
POST https://sorted.travel/agent/identity
Content-Type: application/json

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

Success:

```json
{
  "registration_id": "reg_...",
  "registration_type": "anonymous",
  "identity_assertion": "<service-signed JWT>",
  "assertion_expires": "2026-09-13T12:00:00.000Z",
  "pre_claim_scopes": []
}
```

The assertion exchanges at the token endpoint for an access_token with **no profile scopes**. Destination APIs still work with no token. GET `https://sorted.travel/agent/identity` returns 401 with `WWW-Authenticate` so you can bootstrap discovery.

## Step 4 — Claim ceremony

Skip claim. This service does not advertise `claim_endpoint` and does not upgrade anonymous registrations to `profile.read` / `profile.write`. There is no `user_code` flow and no `urn:workos:agent-auth:grant-type:claim` grant. For a signed-in traveler profile, use authorization code + PKCE.

## Step 5 — Exchange the assertion

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

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

Response:

```json
{
  "access_token": "<token>",
  "token_type": "Bearer",
  "expires_in": 3600,
  "scope": ""
}
```

There is no refresh_token in this flow. Re-exchange the same `identity_assertion` until it expires; then restart at Step 3.

## Step 6 — Use the access_token

```http
GET https://sorted.travel/api/v1/status
Authorization: Bearer <access_token>
```

Optional on destination routes. Profile MCP tools require authorization-code tokens with `profile.read` or `profile.write`, not this anonymous token.

Sandbox: https://sorted.travel/sandbox. Self-serve API keys (optional): POST https://sorted.travel/api/v1/api-keys.

## Errors

| Code | Where | What to do |
| --- | --- | --- |
| `issuer_not_enabled` | `/agent/identity` | identity_assertion / id-jag is not enabled. Use anonymous or authorization_code. |
| `service_auth_not_enabled` | `/agent/identity` | service_auth is not enabled. |
| `invalid_request` | `/agent/identity` | Send `{ "type": "anonymous" }`. |
| `invalid_grant` | `/oauth/token` | Assertion expired or invalid. Restart at Step 3. |
| `unsupported_grant_type` | `/oauth/token` | Use jwt-bearer for agent-auth, or authorization_code / refresh_token for profile OAuth. |

## Revocation

POST `https://sorted.travel/oauth/revoke` with `token=<access_token>&token_type_hint=access_token` ([RFC 7009](https://datatracker.ietf.org/doc/html/rfc7009)). This service does not advertise `events_endpoint` (no ID-JAG SET push). After `invalid_grant`, restart at Step 3.

Contact: support@sorted.travel. Terms: https://sorted.travel/terms. Privacy: https://sorted.travel/privacy.
