# auth.md

You are an agent. This service supports **agentic registration**: discover → register → (claim if needed) → exchange for an access_token → call API → handle revocation. Follow the steps in order; do not skip ahead.

## Step 1 — Discover

Discovery is two hops — you may have already done this.

The 401 response that pointed you here also carries a `WWW-Authenticate` header with the PRM URL:

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

Pull the `resource_metadata` value from that header and fetch it (1a). If you don't have the 401 in hand, the conventional path on the resource server is `/.well-known/oauth-protected-resource`.

### 1a. Fetch the Protected Resource Metadata

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

Response shape:

```json
{
  "resource": "https://seyhunakyurek.com/",
  "resource_name": "Seyhun Akyurek Portfolio",
  "resource_logo_uri": "https://seyhunakyurek.com/logo.png",
  "authorization_servers": ["https://seyhunakyurek.com/"],
  "scopes_supported": ["read:portfolio", "write:messages"],
  "bearer_methods_supported": ["header"]
}
```

What each field tells you:

- `resource` — the canonical URL of the API you're trying to call. Use this as the `aud` when minting an ID-JAG.
- `resource_name` / `resource_logo_uri` — display name and logo for the service. Surface these to the user when asking for consent.
- `authorization_servers` — base URLs of the OAuth Authorization Server(s) for this resource. The `agent_auth` block lives on one of these (see 1b).
- `scopes_supported` — scopes the resource server understands. The access_token you receive at Step 5 will be scoped to some subset.
- `bearer_methods_supported` — how you'll send the access_token in Step 6 (`"header"` = `Authorization: Bearer …`).

### 1b. Fetch the Authorization Server metadata

```http
GET <authorization_servers[0]>/.well-known/oauth-authorization-server
```

Response shape:

```json
{
  "resource": "https://seyhunakyurek.com/",
  "authorization_servers": ["https://seyhunakyurek.com/"],
  "scopes_supported": ["read:portfolio", "write:messages"],
  "bearer_methods_supported": ["header"],

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

  "agent_auth": {
    "skill": "https://seyhunakyurek.com/auth.md",
    "register_uri": "https://seyhunakyurek.com/agent/identity",
    "identity_endpoint": "https://seyhunakyurek.com/agent/identity",
    "claim_endpoint": "https://seyhunakyurek.com/agent/identity/claim",
    "events_endpoint": "https://seyhunakyurek.com/agent/event/notify",
    "identity_types_supported": ["anonymous", "identity_assertion"],
    "identity_assertion": {
      "assertion_types_supported": [
        "urn:ietf:params:oauth:token-type:id-jag"
      ]
    },
    "credential_types_supported": ["access_token"],
    "events_supported": [
      "https://schemas.workos.com/events/agent/auth/identity/assertion/revoked"
    ],
    "revocation_uri": "https://seyhunakyurek.com/oauth2/revoke"
  }
}
```

The outer fields restate the PRM. The top-level OAuth endpoints (`issuer`, `token_endpoint`, `revocation_endpoint`, `grant_types_supported`) are standard RFC 8414 / RFC 7009 / RFC 7523 fields. The `agent_auth` block is the profile-specific bootstrap surface — read it in full. Every field is relevant:

- `issuer` — the canonical issuer URL of this authorization server. Validate the `iss` claim of any token the AS signs against this.
- `token_endpoint` — where you exchange a service-signed identity assertion for an access_token (Step 5).
- `revocation_endpoint` — where you POST to revoke an access_token (RFC 7009).
- `grant_types_supported` — lists the grant types accepted at `/oauth2/token`. `urn:ietf:params:oauth:grant-type:jwt-bearer` (RFC 7523) is for exchanging your identity_assertion for an access_token (Step 5). `urn:workos:agent-auth:grant-type:claim` is the polling grant for the claim ceremony (Step 4c).
- `agent_auth.skill` — the URL of this document.
- `agent_auth.register_uri` — where you POST to register (Step 3).
- `agent_auth.identity_endpoint` — alias for register_uri.
- `agent_auth.claim_endpoint` — where you POST the claim invite for anonymous registrations (Step 4) and where the agent polls for ceremony completion at `/view`.
- `agent_auth.events_endpoint` — where the provider POSTs a Security Event Token (RFC 8417) per RFC 8935 push delivery to notify the service of upstream identity events. You don't call this; it tells you what to expect.
- `agent_auth.identity_types_supported` — which registration methods this service accepts. Pick yours from Step 2.
- `agent_auth.identity_assertion.assertion_types_supported` — which assertion types this service accepts under the `identity_assertion` shape (currently ID-JAG).
- `agent_auth.credential_types_supported` — types of credentials issued (access_token).
- `agent_auth.events_supported` — event schemas this service can ingest (currently revocation). Informational; you don't act on these directly.
- `agent_auth.revocation_uri` — where you POST to revoke an access_token.

## Step 2 — Pick a method

Use this decision tree:

1. **You have a session tied to a user identity and can exchange it for an ID-JAG, audience-bound to this service** → identity_assertion + id-jag.
2. **You have neither** → anonymous. Claim ceremony optional; deferred until the user wants to take ownership.

For `identity_assertion`, check that your assertion type is in `agent_auth.identity_assertion.assertion_types_supported`, if not listed then stop. For `anonymous`, `identity_types_supported` is informational — send the body and fall back on the `*_not_enabled` error if the service opted out.

## Step 3 — Register

Before sending an `identity_assertion` body, surface the service's `resource_name` and `resource_logo_uri` (from Step 1a) and the scope set you'll be acting under, and confirm with the user. This is the user's only consent gate before their identity is asserted to the service. Skip this for `anonymous` — there is no user identity to assert.

### identity_assertion + id-jag

Before minting the ID-JAG, confirm your provider is on this service's trust list. If it isn't, fall back to `anonymous`.

Mint the assertion with:

- `aud` = the `resource` from the PRM
- `iss` = your provider's issuer URL (must be on the trust list above)
- `email_verified: true` OR `phone_number_verified: true`
- Fresh `jti`
- Near-term `exp` (~5 minutes)
- `auth_time` — epoch seconds when the user last authenticated at your provider. **Required.**

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

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

The response has two shapes:

**No confirmation needed** — `(iss, sub)` is known:

```json
{
  "registration_id": "reg_...",
  "registration_type": "identity_assertion",
  "identity_assertion": "<service-signed JWT>",
  "assertion_expires": "2026-05-04T13:00:00.000Z",
  "scopes": ["read:portfolio", "write:messages"]
}
```

Keep `identity_assertion` and go to Step 5.

### anonymous

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

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

Response (200):

```json
{
  "registration_id": "reg_...",
  "registration_type": "anonymous",
  "identity_assertion": "<service-signed JWT>",
  "assertion_expires": "2026-05-04T13:00:00.000Z",
  "pre_claim_scopes": ["read:portfolio"],
  "claim_url": "https://seyhunakyurek.com/agent/identity/claim",
  "claim_token": "clm_...",
  "claim_token_expires": "2026-05-21T17:26:32.915Z",
  "post_claim_scopes": ["read:portfolio", "write:messages"]
}
```

The `identity_assertion` exchanges at `/oauth2/token` for an access_token with `pre_claim_scopes` immediately. If you also want a human to take ownership and unlock `post_claim_scopes`, go to Step 4. Otherwise skip to Step 5.

## Step 4 — Claim ceremony

The end goal: get a signed-in user to confirm a 6-digit `user_code` **you supply them**.

### 4a. Get the ceremony materials

For **anonymous** registrations, ask the service to start a ceremony:

```http
POST /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": "2026-05-21T17:31:25.994Z",
  "claim_attempt": {
    "user_code": "123456",
    "expires_in": 600,
    "verification_uri": "https://seyhunakyurek.com/claim?claim_attempt_token=...",
    "interval": 5
  }
}
```

Surface `verification_uri` + `user_code` to the user; poll the standard `token_endpoint` from AS metadata with the claim grant (see 4c).

### 4b. Hand off to the user

Surface `verification_uri` and `user_code` to the user in a single message. Suggested copy:

> Open this link, sign in (or sign up), and enter this 6-digit code: **123456**
> https://seyhunakyurek.com/claim?claim_attempt_token=...

### 4c. Poll for completion

Poll the standard `token_endpoint` (from AS metadata) with the profile-specific claim grant, passing your `claim_token`:

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

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

Response while waiting:

```json
{ "error": "authorization_pending", "error_description": "..." }
```

Response on success:

```json
{
  "access_token": "<post-claim access_token>",
  "token_type": "Bearer",
  "expires_in": 3600,
  "scope": "read:portfolio write:messages",
  "identity_assertion": "<service-signed JWT>",
  "assertion_expires": "2026-05-21T18:31:25.994Z"
}
```

## Step 5 — Exchange the assertion

POST the `identity_assertion` to the AS metadata's `token_endpoint` with the RFC 7523 JWT-bearer grant.

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

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

Response (200):

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

Extract `access_token` and go to Step 6.

## Step 6 — Use the access_token

Present the `access_token` as a bearer token:

```http
GET /api/resource
Authorization: Bearer <access_token>
```

**Refresh.** When the access_token expires, re-call Step 5 with the same `identity_assertion`. When the identity assertion itself expires or `/oauth2/token` returns `invalid_grant`, restart at Step 3.

## Errors

| Code | Where | What to do |
|------|-------|------------|
| `anonymous_not_enabled` | `/agent/identity` | This service doesn't accept anonymous. Pick another method. |
| `invalid_request` | `/agent/identity` | Body shape, missing claims, ID-JAG signature problems. Fix the input. |
| `invalid_grant` | `/oauth2/token` | Assertion expired, revoked, or otherwise failed. Restart at Step 3. |
| `authorization_pending` | `/oauth2/token` | User hasn't completed the ceremony yet. Honor `interval`; retry. |
| `expired_token` | `/oauth2/token` | User_code window or outer claim window closed. Re-initiate or restart. |
| `slow_down` | `/oauth2/token` | Polling too fast. Add at least 5s to your `interval` and retry. |

## Revocation

POST `token=<access_token>&token_type_hint=access_token` (form-encoded) to the `revocation_endpoint` to kill one access_token. 200 on success, idempotent.

```http
POST /oauth2/revoke
Content-Type: application/x-www-form-urlencoded

token=<access_token>&token_type_hint=access_token
```
