# auth.md

> **AgentSIM is the control plane for agents hitting the real world's auth walls. SMS OTP is the first challenge type.**
>
> Connector 0 provisions a synthetic or programmable US test number, receives or accepts an injected message, parses the OTP, and releases the session over SDK, REST, webhooks, or MCP.

This file tells agents how to register against AgentSIM on behalf of a user,
which flows we support, and what scopes they can ask for. It is the prose
companion to our Protected Resource Metadata (PRM); if anything here conflicts
with the PRM, the PRM wins.

Hostnames used throughout:

| Role | Host |
| --- | --- |
| Resource server (the API) | `https://api.agentsim.dev` |
| Authorization server (registration + tokens) | `https://api.agentsim.dev` |
| This file | `https://agentsim.dev/auth.md` |
| Agent overview | `https://agentsim.dev/agents` |
| Human docs | `https://docs.agentsim.dev` |
| Dashboard / claim page | `https://console.agentsim.dev` |

> ## Implementation status
>
> This document is the **published spec** for AgentSIM's agent registration.
> The `/.well-known/oauth-protected-resource` and
> `/.well-known/oauth-authorization-server` discovery documents are implemented
> on the API host. `service_auth` registration, claim-grant token polling,
> JWT-bearer refresh, token revocation, and registration revocation events are implemented.
>
> Live flow availability:
> - ✅ `service_auth` (user-claimed, email)
> - 🗓 `identity_assertion` (agent-verified / ID-JAG) — requires a trusted-provider list.
>
> After claim, `/oauth2/token` returns an opaque `agt_…` bearer access token plus
> a service-signed `identity_assertion`. Re-exchange the assertion to refresh.
> Existing `x-api-key: asm_live_…` keys continue to work.

## 1. Discover

Two hops, both on the API host:

1. On any `401` from the API we return:
   ```http
   WWW-Authenticate: Bearer resource_metadata="https://api.agentsim.dev/.well-known/oauth-protected-resource"
   ```
   Fall back to the conventional `GET /.well-known/oauth-protected-resource` path if the header is absent.

2. Read the PRM, then fetch the Authorization Server metadata at
   `/.well-known/oauth-authorization-server`. The fields an agent needs to
   decide what to call next:

   ```json
   {
     "resource": "https://api.agentsim.dev/",
     "resource_name": "AgentSIM",
     "authorization_servers": ["https://api.agentsim.dev/"],
     "scopes_supported": ["sessions:write", "sessions:read", "otp:read", "usage:read", "keys:manage"],
     "bearer_methods_supported": ["header"],

     "issuer": "https://api.agentsim.dev",
     "token_endpoint": "https://api.agentsim.dev/oauth2/token",
     "revocation_endpoint": "https://api.agentsim.dev/oauth2/revoke",
     "grant_types_supported": [
       "urn:ietf:params:oauth:grant-type:jwt-bearer",
       "urn:workos:agent-auth:grant-type:claim"
     ],

     "agent_auth": {
       "skill": "https://agentsim.dev/auth.md",
       "identity_endpoint": "https://api.agentsim.dev/agent/identity",
       "claim_endpoint": "https://api.agentsim.dev/agent/identity/claim",
       "events_endpoint": "https://api.agentsim.dev/agent/event/notify",
       "identity_types_supported": ["service_auth"],
       "identity_assertion": { "assertion_types_supported": [] },
       "events_supported": [
         "https://schemas.workos.com/events/agent/auth/identity/assertion/revoked"
       ]
     }
   }
   ```

## 2. Pick a method

| What the agent has | Use |
| --- | --- |
| Only wants to start working now, user will claim ownership later | not supported yet |
| The user's email; no usage until the user confirms | `service_auth` |
| A trusted-provider identity assertion (ID-JAG) | not supported yet — fall back to `service_auth` |

`service_auth` withholds credentials until the user completes the claim ceremony.

## 3. Register

All registrations are `POST https://api.agentsim.dev/agent/identity`,
dispatched on `type`.

### `service_auth`

```json
{ "type": "service_auth", "login_hint": "user@example.com" }
```
```json
{
  "registration_id": "reg_01ABC…",
  "registration_type": "service_auth",
  "claim_url": "/agent/identity/claim",
  "claim_token": "clm_…",
  "claim_token_expires": "2026-06-17T13:00:00.000Z",
  "post_claim_scopes": ["sessions:write", "sessions:read", "otp:read", "usage:read"],
  "claim": {
    "user_code": "123456",
    "expires_in": 600,
    "verification_uri": "https://console.agentsim.dev/claim?claim_attempt_token=cla_…",
    "interval": 5
  }
}
```

No assertion is issued until the ceremony completes.

## 4. Claim ceremony

**4a. Get the ceremony materials.** `service_auth` already has the `claim`
block above. Anonymous claim start is not implemented yet.

**4b. Hand off to the user.** Surface `verification_uri` and `user_code` in a
single message. The user signs in at `console.agentsim.dev` (email/password,
Google, or GitHub via our existing better-auth) and types the code on the
claim page — not back to the agent. Only the signed-in user matching the
registration's `claim_email` may complete it.

**4c. Poll for completion** at the token endpoint with the claim grant:

```http
POST /oauth2/token HTTP/1.1
Host: api.agentsim.dev
Content-Type: application/x-www-form-urlencoded

grant_type=urn:workos:agent-auth:grant-type:claim&claim_token=clm_…
```
Pending → `{ "error": "authorization_pending" }`. On completion → an opaque
bearer token plus a service-signed assertion:

```json
{
  "access_token": "agt_…",
  "token_type": "Bearer",
  "expires_in": 3600,
  "scope": "sessions:write sessions:read otp:read usage:read",
  "identity_assertion": "<service-signed JWT>",
  "assertion_expires": "2026-06-18T13:00:00.000Z"
}
```

If the `user_code` lapses → `{ "error": "expired_token" }`; restart at
`service_auth` registration for a fresh code.

## 5. Refresh

Re-exchange the `identity_assertion` until it expires:

```http
POST /oauth2/token HTTP/1.1
Host: api.agentsim.dev
Content-Type: application/x-www-form-urlencoded

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

```json
{ "access_token": "agt_…", "token_type": "Bearer", "expires_in": 3600, "scope": "sessions:write sessions:read otp:read usage:read" }
```

## 6. Use the access_token

```http
Authorization: Bearer <access_token>
```

Scopes map to the API surface (basePath `/v1`):

| Scope | Grants |
| --- | --- |
| `sessions:write` | `POST /v1/sessions`, `DELETE /v1/sessions/:id`, `POST /v1/sessions/:id/reroute` (billable) |
| `sessions:read` | `GET /v1/sessions/:id`, `GET /v1/sessions/:id/messages` |
| `otp:read` | `POST /v1/sessions/:id/wait`, message OTP bodies |
| `usage:read` | `GET /v1/usage`, `GET /v1/usage/summary` |
| `keys:manage` | `GET/POST/DELETE /v1/api-keys` (not granted by default; request at registration) |

Refresh: when the access_token expires, re-exchange the assertion (§5). If the assertion expires or `/oauth2/token` returns `invalid_grant`, restart at registration (§3).

## 7. Errors

| Code | From | Agent action |
| --- | --- | --- |
| `invalid_request` | `/agent/identity` | Fix the body and retry |
| `anonymous_not_enabled` / `service_auth_not_enabled` | `/agent/identity` | We opted out of that type; pick another |
| `authorization_pending` | `/oauth2/token` (claim grant) | Keep polling at `interval` |
| `expired_token` | `/oauth2/token` (claim grant) | User-code window lapsed; restart registration |
| `invalid_grant` | `/oauth2/token` | Token expired or revoked; restart at registration |
| `unsupported_grant_type`, `invalid_client` | `/oauth2/token` | Caller error; do not retry blindly |

## 8. Revocation

Two independent layers:

- **Credential layer** (agent-callable, `revocation_endpoint`):
  ```http
  POST /oauth2/revoke HTTP/1.1
  Content-Type: application/x-www-form-urlencoded

  token=<access_token>&token_type_hint=access_token
  ```
  Kills the access_token.
- **Registration layer** (provider-driven, `events_endpoint`): providers POST a signed `secevent+jwt` to `/agent/event/notify`. An `…/identity/assertion/revoked` event revokes the registration and all access tokens derived from it.

## Scopes, pricing, and limits

- **Pricing:** https://agentsim.dev#pricing (Hobby free / Builder $0.99 per
  session / Enterprise). One `POST /v1/sessions` = one billable session;
  `OtpTimeoutError` sessions are not billed.
- **Rate limits:** 60 provisions/minute per credential; 50 concurrent active
  sessions per credential.
- **Supported services:** https://docs.agentsim.dev/supported-services —
  programmable US numbers work on Discord, GitHub, Notion, Linear, Slack,
  Auth0/Supabase test flows, and web3/dev tooling; not yet on Google, Meta,
  Stripe, Coinbase, or banks.
- **Terms / Privacy:** https://agentsim.dev/terms · https://agentsim.dev/privacy
- **Integration contact:** contact@agentsim.dev

## References

- Protocol home: https://workos.com/auth-md
- Spec repo: https://github.com/workos/auth.md
- RFCs: [9728](https://datatracker.ietf.org/doc/html/rfc9728) (PRM),
  [8414](https://datatracker.ietf.org/doc/html/rfc8414) (AS metadata),
  [7523](https://datatracker.ietf.org/doc/html/rfc7523) (JWT-bearer),
  [8628](https://datatracker.ietf.org/doc/html/rfc8628) (device authz),
  [7009](https://datatracker.ietf.org/doc/html/rfc7009) (revocation),
  [8417](https://datatracker.ietf.org/doc/html/rfc8417) /
  [8935](https://datatracker.ietf.org/doc/html/rfc8935) (SET delivery).
