# auth.md

You are an agent. The Vertical Tribe publishes Himalayan trek and expedition data from Dehradun, Uttarakhand, India.

The public read API at `https://theverticaltribe.com/api/v1/` does not require registration or a token. Call it with GET. This file is the registration walkthrough for agents that still want a scoped credential, and the discovery map for the OAuth documents this origin publishes.

Resource server: `https://theverticaltribe.com/`
Authorization server: `https://theverticaltribe.com`
Public API: `https://theverticaltribe.com/api/v1/`
Human docs: `https://theverticaltribe.com/developers/`
OpenAPI: `https://theverticaltribe.com/openapi.json`

## Step 1 — Discover

Discovery is two hops.

If a 401 pointed you here, read `WWW-Authenticate: Bearer resource_metadata="…"` and fetch that URL. If you do not have a 401, the conventional path is:

```http
GET /.well-known/oauth-protected-resource
```

### 1a. Protected Resource Metadata

```json
{
  "resource": "https://theverticaltribe.com/",
  "resource_name": "The Vertical Tribe",
  "authorization_servers": ["https://theverticaltribe.com"],
  "scopes_supported": ["public.read"],
  "bearer_methods_supported": ["header"]
}
```

- `resource` — audience for any assertion you mint.
- `authorization_servers` — issuer base URL. Fetch RFC 8414 metadata from it.
- `scopes_supported` — the only scope this origin advertises. The public GET API ignores the header.
- `bearer_methods_supported` — `header` means `Authorization: Bearer …`.

The public catalogue, peak dataset and departure list stay unauthenticated even if you present a bearer token.

### 1b. Authorization Server metadata

```http
GET https://theverticaltribe.com/.well-known/oauth-authorization-server
```

Also published at `/.well-known/openid-configuration`. `issuer` is `https://theverticaltribe.com` and matches `authorization_servers[0]`.

Read `issuer`, `authorization_endpoint`, `token_endpoint`, `jwks_uri`, `revocation_endpoint`, `grant_types_supported`, `response_types_supported`, and the `agent_auth` block in full:

- `agent_auth.skill` — this file.
- `agent_auth.register_uri` / `identity_endpoint` — `POST /agent/auth` (also `POST /agent/identity`).
- `agent_auth.claim_uri` / `claim_endpoint` — `POST /agent/identity/claim`.
- `agent_auth.revocation_uri` — `POST /oauth/revoke`.
- `agent_auth.identity_types_supported` — `anonymous`, `identity_assertion`.
- `agent_auth.identity_assertion.assertion_types_supported` — ID-JAG and `verified_email`.
- `agent_auth.identity_assertion.credential_types_supported` / `anonymous.credential_types_supported` — `access_token`.

## Step 2 — Pick a method

1. You can mint an ID-JAG audience-bound to `https://theverticaltribe.com/` → `identity_assertion`.
2. You only have the user's email → this origin does not advertise `service_auth`. Stop or use the public GET API.
3. You have neither → `anonymous`, or skip registration and GET `/api/v1/`.

If your assertion type is not in `identity_assertion.assertion_types_supported`, pick another method or use the public API.

## Step 3 — Register

Surface `resource_name` from the PRM and the `public.read` scope before asserting a user identity. Skip that consent gate for `anonymous`.

Registration is `POST` JSON to `register_uri`. Passive scanners must not POST here — registration can create a record.

### identity_assertion

```http
POST /agent/identity
Content-Type: application/json

{
  "type": "identity_assertion",
  "assertion_type": "urn:ietf:params:oauth:token-type:id-jag",
  "assertion": "<ID-JAG JWT>"
}
```

`aud` must be the PRM `resource`. `iss` is your provider. Include a fresh `jti` and near-term `exp`.

### anonymous

```http
POST /agent/auth
Content-Type: application/json

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

A successful registration returns a service-signed `identity_assertion`. Exchange it at `token_endpoint` with `grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer`.

These endpoints exist so agents can discover the method. They do not mint a live access token today. The public API does not need one. For a write path (a booking enquiry) POST `/api/bookings` as documented at `/developers/`, or send a human to `/book/`. Seats are confirmed by a guide, not by a token.

## Step 4 — Claim ceremony

`claim_uri` is `https://theverticaltribe.com/agent/identity/claim`. If a registration response includes a `claim` block, show the user `verification_uri` and `user_code`. The user types the code on a page this origin owns. Poll `token_endpoint` with `grant_type=urn:workos:agent-auth:grant-type:claim`.

There is no email of the code from this service.

## Step 5 — Exchange the assertion

```http
POST /oauth/token
Content-Type: application/x-www-form-urlencoded

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

## Step 6 — Use the access_token

```http
GET /api/v1/trips
Authorization: Bearer <access_token>
```

The GET API answers without the header. When a token is present it is ignored on `/api/v1/*`.

Refresh: re-exchange the assertion. There is no refresh_token. `invalid_grant` means start at Step 3.

## Errors

| Code | Where | What to do |
| --- | --- | --- |
| `anonymous_not_enabled` | `/agent/identity` | Use the public GET API. |
| `invalid_request` | `/agent/identity` | Fix the body. |
| `invalid_grant` | `/oauth/token` | Restart at Step 3. |
| `unsupported_grant_type` | `/oauth/token` | Use a grant listed in AS metadata. |
| `authorization_pending` | `/oauth/token` | Wait `interval` and poll. |

## Revocation

- Credential layer: `POST /oauth/revoke` with `token=<access_token>&token_type_hint=access_token` ([RFC 7009](https://datatracker.ietf.org/doc/html/rfc7009)).
- Registration layer: providers POST a SET to `agent_auth.events_endpoint`. You learn this as `invalid_grant` on the next exchange.

Questions: hello@theverticaltribe.com
