Files
SlipItIn/.github/skills/auth-md/references/protocol-template.md
Tim Krampitz 01046b01e4 Neue Skills, Referenzen & OpenWiki-Doku integriert
Umfangreiche Erweiterung der Skill-Bibliothek: Neue Skills für Humanisierung (Englisch/PT-BR), Design-Validierung, AI-SEO und Coolify-Deployment inkl. Regelwerke, Presets, Pattern-Referenzen, Testfälle und Automatisierungsskripte. Zusätzliche Skills für Revenue-Centric Design, Pier Cloud, OKF, Lebenslauf- und LinkedIn-Optimierung sowie zahlreiche Referenzdateien, Checklisten und YAML/JSON/Markdown-Templates. Einführung einer vollständigen OpenWiki-Dokumentation mit Architektur-, Domain- und Workflow-Beschreibungen, zentralem Index und automatisierten Updates. Modularer Aufbau, restriktive Lizenzen und umfassende Qualitäts- und Evaluationsmechanismen für alle neuen Inhalte.
2026-07-26 14:00:58 +02:00

15 KiB

Protocol Template

Canonical template for generating an auth.md file (v2, June 2026). Replace all {{placeholders}} with service-specific values. Delete sections for flows the service does not support.


File Structure

An auth.md is organized as a numbered walkthrough the agent follows top to bottom:

  1. Title and intro — addressed to the agent, declares real hostnames (resource server + auth server)
  2. Step 1 — Discover — two-hop discovery (PRM → AS metadata)
  3. Step 2 — Pick a method — decision tree
  4. Step 3 — Register — one subsection per supported method
  5. Step 4 — Claim ceremony — browser-based user_code ceremony (if service_auth or anonymous claim)
  6. Step 5 — Exchange the assertion — POST identity_assertion to /oauth2/token for access_token
  7. Step 6 — Use the access_token — how to use and refresh
  8. Errors — error codes table
  9. Revocation — two-layer revocation model

Complete Template

# 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.

Examples use placeholder hosts: `{{base_url}}` (the resource server hosting the API you want to call) and `{{auth_server_url}}` (the authorization server that handles registration).

## Step 1 — Discover

Discovery is two hops. The 401 response that pointed you here carries a `WWW-Authenticate` header with the PRM URL:

\```http
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer resource_metadata="{{base_url}}/.well-known/oauth-protected-resource"
\```

### 1a. Fetch the Protected Resource Metadata

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

Response:

\```json
{
  "resource": "{{base_url}}/",
  "resource_name": "{{service_name}}",
  "resource_logo_uri": "{{logo_url}}",
  "authorization_servers": ["{{auth_server_url}}/"],
  "scopes_supported": [{{scopes_list}}],
  "bearer_methods_supported": ["header"]
}
\```

### 1b. Fetch the Authorization Server metadata

\```http
GET /.well-known/oauth-authorization-server
\```

Response:

\```json
{
  "resource": "{{base_url}}/",
  "authorization_servers": ["{{auth_server_url}}/"],
  "scopes_supported": [{{scopes_list}}],
  "bearer_methods_supported": ["header"],
  "issuer": "{{auth_server_url}}",
  "token_endpoint": "{{auth_server_url}}/oauth2/token",
  "revocation_endpoint": "{{auth_server_url}}/oauth2/revoke",
  "grant_types_supported": [
    "urn:ietf:params:oauth:grant-type:jwt-bearer",
    "urn:workos:agent-auth:grant-type:claim"
  ],
  "agent_auth": {
    "skill": "{{auth_md_url}}",
    "identity_endpoint": "{{auth_server_url}}/agent/identity",
    "claim_endpoint": "{{auth_server_url}}/agent/identity/claim",
    "events_endpoint": "{{auth_server_url}}/agent/event/notify",
    "identity_types_supported": [{{identity_types}}],
    "identity_assertion": {
      "assertion_types_supported": [{{assertion_types}}]
    },
    "events_supported": [
      "https://schemas.workos.com/events/agent/auth/identity/assertion/revoked"
    ]
  }
}
\```

## 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 only the user's email** → service_auth. Claim ceremony required.
3. **You have neither** → anonymous. Claim ceremony optional; deferred until the user wants to take ownership.

Before sending: cross-check your choice against the `agent_auth` block. If your type is not in `identity_types_supported`, pick another or stop.

## Step 3 — Register

Before sending an `identity_assertion` or `service_auth` 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. Skip this for `anonymous`.

### identity_assertion + id-jag

<!-- DELETE THIS SECTION IF NOT SUPPORTING ID-JAG FLOW -->

Mint the ID-JAG with:
- `aud` = the `resource` from the PRM
- `iss` = your provider's issuer URL (must be on trust list)
- `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": "<ID-JAG>"
}
\```

Response — no confirmation needed (200):

\```json
{
  "registration_id": "reg_...",
  "registration_type": "identity_assertion",
  "identity_assertion": "<service-signed-jwt>",
  "assertion_expires": "{{assertion_expiry}}",
  "scopes": [{{post_registration_scopes}}]
}
\```

Keep `identity_assertion` and go to Step 5.

Response — confirmation required (401, `interaction_required`):

\```json
{
  "error": "interaction_required",
  "error_description": "...",
  "registration_id": "reg_...",
  "registration_type": "identity_assertion",
  "claim_url": "{{auth_server_url}}/agent/identity/claim",
  "claim_token": "clm_...",
  "claim_token_expires": "...",
  "post_claim_scopes": [{{post_claim_scopes}}],
  "claim": {
    "user_code": "123456",
    "expires_in": 600,
    "verification_uri": "{{auth_server_url}}/login?return_to=...",
    "interval": 5
  }
}
\```

Surface `verification_uri` + `user_code` to the user (Step 4b) and poll (Step 4c).

Response — login required (401, `login_required`):

\```json
{
  "error": "login_required",
  "error_description": "auth_time is too old; re-authenticate at the provider.",
  "max_age": 3600
}
\```

Re-authenticate the user at your provider (`prompt=login`) and mint a fresh ID-JAG.

### service_auth

<!-- DELETE THIS SECTION IF NOT SUPPORTING SERVICE_AUTH FLOW -->

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

{
  "type": "service_auth",
  "login_hint": "user@example.com"
}
\```

Response (200):

\```json
{
  "registration_id": "reg_...",
  "registration_type": "service_auth",
  "claim_url": "{{auth_server_url}}/agent/identity/claim",
  "claim_token": "clm_...",
  "claim_token_expires": "{{claim_ttl}}",
  "post_claim_scopes": [{{post_claim_scopes}}],
  "claim": {
    "user_code": "123456",
    "expires_in": 600,
    "verification_uri": "{{auth_server_url}}/login?return_to=...",
    "interval": 5
  }
}
\```

No `identity_assertion` yet. Go to Step 4.

### anonymous

<!-- DELETE THIS SECTION IF NOT SUPPORTING ANONYMOUS FLOW -->

\```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": "{{assertion_expiry}}",
  "pre_claim_scopes": [{{pre_claim_scopes}}],
  "claim_url": "{{auth_server_url}}/agent/identity/claim",
  "claim_token": "clm_...",
  "claim_token_expires": "{{claim_ttl}}",
  "post_claim_scopes": [{{post_claim_scopes}}]
}
\```

The `identity_assertion` exchanges at `/oauth2/token` for an access_token with `pre_claim_scopes` immediately (Step 5). To upgrade scopes, go to Step 4.

## Step 4 — Claim ceremony

<!-- DELETE THIS ENTIRE SECTION IF ONLY SUPPORTING identity_assertion WITHOUT interaction_required -->

The end goal: get a signed-in user to confirm a 6-digit `user_code` **you supply them**. The code travels from you → user; the user authenticates to the service and types it into a page the service owns.

### 4a. Get the ceremony materials

For **service_auth** registrations and **interaction_required** responses, you already have the `claim` block from Step 3. Skip to 4b.

For **anonymous** registrations, initiate the 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": "{{claim_attempt_ttl}}",
  "claim_attempt": {
    "user_code": "123456",
    "expires_in": 600,
    "verification_uri": "{{auth_server_url}}/login?return_to=...",
    "interval": 5
  }
}
\```

### 4b. Hand off to the user

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

> Open this link, sign in (or sign up), and enter this 6-digit code: **123456**
> {{verification_uri}}

The user will:
1. Open `verification_uri`
2. Authenticate with the service (sign in or sign up)
3. Land on the claim page, see their identity displayed, type the `user_code`, and submit

### 4c. Poll for completion

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

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

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

Response while waiting:

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

Response on success:

\```json
{
  "access_token": "<token>",
  "token_type": "Bearer",
  "expires_in": 3600,
  "scope": "{{scopes}}",
  "identity_assertion": "<service-signed-jwt-v2>",
  "assertion_expires": "{{assertion_expiry}}"
}
\```

Use `access_token` immediately; cache `identity_assertion` for refresh via Step 5.

If the `user_code` window expires:

\```json
{
  "error": "expired_token",
  "error_description": "..."
}
\```

Re-call `POST /agent/identity/claim` with the same `claim_token` and `email` to mint a fresh `user_code`. If that returns `claim_expired`, restart at Step 3.

Honor `interval` (in seconds); on `slow_down` back off.

## Step 5 — Exchange the assertion

POST the `identity_assertion` to the 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={{base_url}}/
\```

Response (200):

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

The same `identity_assertion` can be re-used to mint additional access_tokens until it expires. If `/oauth2/token` returns `invalid_grant`, restart at Step 3.

## Step 6 — Use the access_token

Present as a bearer token:

\```http
GET /api/some-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. There is no refresh_token — the two-step pattern replaces it.

On 401 for a previously-working access_token: try Step 5 once. If that also fails, restart at Step 1.

Full API reference: `{{api_docs_url}}`

## Errors

| Code | Where | What to do |
|------|-------|------------|
| `anonymous_not_enabled` | `/agent/identity` | Pick another method from Step 2 |
| `service_auth_not_enabled` | `/agent/identity` | Pick another method |
| `issuer_not_enabled` | `/agent/identity` | Provider not on trust list. Pick another method |
| `invalid_request` | `/agent/identity` | Fix body shape, claims, signature, jti, aud problems |
| `interaction_required` (401) | `/agent/identity` (ID-JAG) | Body carries `claim` block; surface to user (Step 4) |
| `login_required` (401) | `/agent/identity` (ID-JAG) | Re-authenticate user at provider, mint fresh ID-JAG |
| `invalid_claim_token` | `/agent/identity/claim` | Restart at Step 3 |
| `claimed_or_in_flight` | `/agent/identity/claim` | Already claimed. Re-read Step 3 response |
| `claim_expired` | `/agent/identity/claim` | Registration expired. Restart at Step 3 |
| `invalid_grant` | `/oauth2/token` | Assertion expired/revoked. Restart at Step 3 |
| `invalid_client` | `/oauth2/token` | client_id not recognized |
| `unsupported_grant_type` | `/oauth2/token` | Use one of the two supported grant types |
| `authorization_pending` | `/oauth2/token` (claim) | User hasn't completed ceremony. Honor `interval` |
| `expired_token` | `/oauth2/token` (claim) | user_code window closed. Re-initiate or restart |
| `slow_down` | `/oauth2/token` (claim) | Add ≥5s to interval and retry |
| `rate_limited` (429) | any | Back off and retry |

## Revocation

Two independent layers:

- **Credential layer (RFC 7009):** POST `token=<access_token>&token_type_hint=access_token` to `{{auth_server_url}}/oauth2/revoke`. Kills one access_token. Identity assertion intact — re-run Step 5.
- **Registration layer (RFC 8935 SET delivery):** Provider POSTs a Security Event Token to `events_endpoint`. Service invalidates identity_assertion and all derived access_tokens. Agent discovers this when `/oauth2/token` returns `invalid_grant` — restart at Step 3.

On 401 for a previously-working access_token: try Step 5 once. If `/oauth2/token` succeeds, credential-layer revocation — fresh access_token works. If `invalid_grant`, registration-layer — restart at Step 3.

Placeholder Reference

Placeholder Description Example
{{base_url}} API base URL (resource server) https://api.acme.com
{{service_name}} Human-readable service name Acme Notes
{{logo_url}} Service logo URL https://acme.com/logo.png
{{auth_server_url}} Authorization server base URL https://auth.acme.com
{{auth_md_url}} URL where auth.md is hosted https://acme.com/auth.md
{{scopes_list}} JSON array of scope strings "notes.read", "notes.write"
{{identity_types}} Supported identity types "anonymous", "identity_assertion", "service_auth"
{{assertion_types}} Supported assertion types "urn:ietf:params:oauth:token-type:id-jag"
{{pre_claim_scopes}} Scopes before claim (anonymous) "notes.read"
{{post_claim_scopes}} Scopes after claim "notes.read", "notes.write"
{{post_registration_scopes}} Scopes after ID-JAG registration "notes.read", "notes.write"
{{assertion_expiry}} Identity assertion expiry (ISO) 2026-05-22T13:00:00.000Z
{{claim_ttl}} Claim token expiration (ISO) 2026-05-22T12:00:00.000Z
{{claim_attempt_ttl}} Claim attempt expiration 2026-05-22T12:10:00.000Z
{{api_docs_url}} Link to full API docs https://docs.acme.com/

Generation Rules

  1. Keep the file concise and high-signal — anything the agent doesn't need to register or operate belongs in main documentation, not in auth.md
  2. Use fenced code blocks with language hints (http, json) so agents can extract templates unambiguously
  3. Declare real hostnames in the intro — resource server and auth server — so the agent knows which host each example targets
  4. Delete sections for unsupported flows — don't leave empty sections or "N/A" markers
  5. The PRM is authoritative — if anything in auth.md conflicts with the PRM, the PRM wins
  6. Token exchange is always required — registration never returns an access_token directly; always returns an identity_assertion that must be exchanged at /oauth2/token