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.
This commit is contained in:
Tim Krampitz
2026-07-26 14:00:58 +02:00
parent 070727d5cd
commit 01046b01e4
202 changed files with 31290 additions and 0 deletions

357
.github/skills/auth-md/SKILL.md vendored Normal file
View File

@@ -0,0 +1,357 @@
---
name: auth-md
description: >
Generate, validate, and explain `auth.md` files — the open protocol that lets AI agents
register for services on behalf of users. Use this skill whenever the user wants to make
their app agent-ready by publishing an `auth.md`, generate Protected Resource Metadata
(RFC 9728), validate an existing `auth.md` against the protocol specification, implement
agent registration endpoints, understand how the auth.md protocol works, or configure
authentication flows for agents. Trigger on mentions of "auth.md", "agent registration",
"agent auth", "make my app agent-ready", "ID-JAG", "identity_assertion flow",
"service_auth flow", "protected resource metadata", "claim ceremony", "agentic registration",
"CIMD", "Client ID Metadata Document", "oauth-id-jag", "agent revocation",
"agent credential", "agent discovery", "token exchange", "interaction_required",
"/.well-known/oauth-protected-resource", "/.well-known/oauth-authorization-server",
"/agent/identity", "/oauth2/token", or any variation of AI agent authentication/registration in APIs.
metadata:
author: https://ft.ia.br
version: "2.0"
date: 2026-06-27
repository: https://github.com/fabricioctelles/skills
license: Apache 2.0
category: library-and-api-reference
---
# auth-md
Generate, validate, and explain the **auth.md** protocol — the open standard that lets AI agents register for services on behalf of users, without signup forms.
---
## Protocol Context
auth.md is a Markdown file published at a service's root (typically `https://service.com/auth.md`) that instructs agents on how to register. It works simultaneously as human-readable documentation and as a discoverable runtime artifact for agents.
The protocol extends RFC 9728 (OAuth 2.0 Protected Resource Metadata) with an `agent_auth` block in the Authorization Server metadata. Registration returns an `identity_assertion` (service-signed JWT) that the agent exchanges at `/oauth2/token` for an `access_token`. Three registration methods are supported:
| Flow | Mechanism | When to use |
|------|-----------|-------------|
| **identity_assertion** | Provider signs an ID-JAG (with `auth_time`) asserting user identity. Service verifies JWKS, returns `identity_assertion`. Agent exchanges at `/oauth2/token`. | Service does JIT provisioning from OIDC/SAML; wants zero-friction registration. |
| **service_auth** | Email hint + browser-based ceremony. Agent receives `user_code` + `verification_uri`; user signs in and types code. Agent polls `/oauth2/token`. | Agents on platforms that can't mint ID-JAGs; self-serve without trust list. |
| **anonymous** | No identity upfront. Immediate `identity_assertion` with pre-claim scopes. Optional deferred claim for scope upgrade. | Agent needs basic access immediately; human ownership binding deferred. |
### Protocol Endpoints
| Endpoint | Purpose |
|----------|---------|
| `/.well-known/oauth-protected-resource` | Discovery — resource metadata (RFC 9728) |
| `/.well-known/oauth-authorization-server` | Discovery — AS metadata with `agent_auth` block |
| `POST /agent/identity` | Registration — dispatches on `type` field |
| `POST /agent/identity/claim` | Claim initiation (anonymous deferred, or re-initiate expired user_code) |
| `POST /oauth2/token` | Token exchange (JWT-bearer grant) + claim polling (claim grant) |
| `POST /oauth2/revoke` | Credential-layer revocation (RFC 7009) |
| `events_endpoint` | Registration-layer revocation (receives SETs, RFC 8935) |
### Token Lifecycle
Registration **never** returns an `access_token` directly. The flow is:
1. Registration → `identity_assertion` (service-signed JWT, reusable until expiry)
2. Exchange → `POST /oauth2/token` with `grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer``access_token`
3. Refresh → re-exchange same `identity_assertion` when access_token expires
4. Expired assertion → restart at registration (Step 3)
### Claim Ceremony (v2 — Browser-Based)
The claim ceremony uses RFC 8628-style device authorization:
1. Registration returns `user_code` + `verification_uri` in a `claim` block
2. Agent surfaces both to the user
3. User opens `verification_uri`, signs in to the service, types the 6-digit code
4. Agent polls `POST /oauth2/token` with `grant_type=urn:workos:agent-auth:grant-type:claim` + `claim_token`
5. On success: receives `access_token` + fresh `identity_assertion`
---
## Operation Modes
| Parameter | Default | Description |
|-----------|---------|-------------|
| `mode` | `generate` | `generate` = create auth.md + metadata; `validate` = check existing auth.md; `explain` = explain the protocol |
| `validation_level` | `basic` | `basic` = structure + fields + consistency (offline); `full` = basic + live endpoint fetch |
| `flows` | `all` | Which flows to include: `identity_assertion`, `service_auth`, `anonymous`, `all` |
| `role` | `app` | Perspective: `app` = service accepting registrations; `provider` = platform minting ID-JAGs |
---
## Workflow: Generate
### 1. Scan the codebase
Look for:
- Existing API routes and authentication patterns
- Defined scopes/permissions
- Framework (Express, Django, Rails, FastAPI, NestJS, etc.)
- Base URL and auth server URL configuration
- Existing authentication middleware
- User models and provisioning mechanisms
### 2. Ask the user only what cannot be inferred
- Which flows to support (identity_assertion, service_auth, anonymous, or combination)
- Pre-claim scopes vs post-claim scopes (if anonymous)
- Trusted agent providers and trust list policy (if identity_assertion)
- Whether the service already does JIT provisioning or requires manual onboarding
- `idJagMaxAuthAgeSeconds` value (default: 3600)
- Desired rate limiting policy
### 3. Generate artifacts
Produce three artifacts:
**a) `auth.md`** — Markdown file following the protocol template (see `references/protocol-template.md`). Must contain:
- Title and intro addressed to the agent
- Step 1 — Discover (two hops: PRM → AS metadata)
- Step 2 — Pick a method (decision tree)
- Step 3 — Register (one subsection per supported method)
- Step 4 — Claim ceremony (if service_auth or anonymous with claim)
- Step 5 — Exchange the assertion (POST /oauth2/token with jwt-bearer grant)
- Step 6 — Use the access_token
- Errors (complete table with all applicable codes)
- Revocation (two layers)
**b) `oauth-protected-resource.json`** — JSON for `/.well-known/oauth-protected-resource` with `resource_name` and `resource_logo_uri`
**c) `oauth-authorization-server.json`** — JSON for `/.well-known/oauth-authorization-server` with:
- `issuer`, `token_endpoint`, `revocation_endpoint`, `grant_types_supported`
- Complete `agent_auth` block with `identity_endpoint`, `claim_endpoint`, `events_endpoint`
### 4. Generate implementation guidance
"Next Steps" section with:
- How to serve `auth.md` at the domain root
- How to serve metadata at the well-known paths
- How to add `WWW-Authenticate` header to 401 responses
- Endpoint implementation guidance (without generating framework-specific code unless requested)
- Token exchange implementation at `/oauth2/token`
- Claim page hosting (verification_uri → login → code input → confirm)
- Recommended rate limiting configuration
- Recommended audit events
- Security considerations (token hashing, auth_time validation, replay protection, claim_token handling)
### 5. Generate Agent Provider guide (if role=provider)
When the user is an agent provider (not an app), generate:
- How to mint audience-specific ID-JAGs with `auth_time`
- Token structure (header + payload with required and optional claims)
- How to publish JWKS
- Optionally: how to publish a CIMD (Client ID Metadata Document)
- How to implement revocation (POST SET to events_endpoint)
- How to present consent to the user before asserting identity (using `resource_name` + `resource_logo_uri`)
---
## Workflow: Validate
### 1. Load the auth.md
From a local file path or URL.
### 2. Run validation at the requested level
**Basic (offline):**
- All required headings present (Step 16, Errors, Revocation)
- At least one flow documented
- Valid JSON in fenced code blocks for request/response shapes
- AS metadata contains `identity_endpoint`, `token_endpoint`, `grant_types_supported`
- Error table with standard error codes
- Consistency: flows in prose match `identity_types_supported` in metadata JSON
- No unreplaced placeholders
**Full (live):**
- All basic checks, plus:
- Fetch `/.well-known/oauth-protected-resource` from the declared base URL
- Verify `agent_auth` block exists in AS metadata
- Fetch `/.well-known/oauth-authorization-server` and verify consistency
- Check that `identity_endpoint`, `token_endpoint`, `revocation_endpoint` respond (accept 400/401/422, reject 404/405)
- Verify API returns 401 with `WWW-Authenticate` containing `resource_metadata`
### 3. Report results
Checklist with ✅/❌ per rule, grouped by category:
- **Structure** — headings and order
- **Fields** — required fields in JSONs
- **Consistency** — cross-references between prose and metadata
- **Format** — valid JSON, valid HTTP, no placeholders
- **Endpoints** (full only) — reachability and correct responses
Include severity: 🔴 Error (agents will fail), 🟡 Warning (degraded experience), 🟢 Info (suggestion).
See `references/validation-rules.md` for the complete ruleset.
---
## Workflow: Explain
When the user wants to understand the protocol without generating or validating:
1. Identify what the user wants to know (overview, specific flow, specific endpoint, security, etc.)
2. Explain using the protocol context above and the references
3. Use text-based sequence diagrams when helpful
4. Point to official documentation when relevant
---
## User Matching and JIT Provisioning
The identity_assertion flow needs to decide which service user a registration represents. Recommended resolution order:
1. **Delegation record match** — if `(iss, sub)` has a delegation on file, route to same user
2. **Verified email match** — if a user exists with same verified email BUT no `(iss, sub)` delegation → `interaction_required` (401) with claim block for user to confirm linking
3. **Verified phone match** — same pattern
4. **No match → JIT** — create a new user per provisioning policy, or refuse
Reject ID-JAGs with neither a verified email nor a verified phone — there's no basis for matching.
---
## Rate Limiting
The `/agent/identity` endpoint is unauthenticated for anonymous registration. Implement two tiers:
1. **Per-IP** (checked first) — prevents a single source from consuming the tenant's budget. Default: 5/hour anonymous, 60/hour identity_assertion.
2. **Per-tenant** (checked second) — global cap across IPs. Default: 100/hour anonymous, 1000/hour identity_assertion.
Also rate-limit `/oauth2/token` polling — enforce `interval` from the claim block, reject with `slow_down` if too fast.
---
## Recommended Audit Events
| Event | When | Data |
|-------|------|------|
| `registration.created` | Successful POST /agent/identity | registration_id, registration_type, iss, sub |
| `registration.interaction_required` | 401 interaction_required | registration_id, iss, sub, matched_user_id |
| `registration.login_required` | 401 login_required | iss, sub, auth_time, max_age |
| `claim.initiated` | /agent/identity/claim called | registration_id, email |
| `claim.completed` | User submitted correct user_code | registration_id, claimed_by_user_id |
| `claim.expired` | user_code window or registration expired | registration_id |
| `token.exchanged` | /oauth2/token jwt-bearer success | registration_id, access_token_id |
| `token.revoked` | /oauth2/revoke called | access_token_id |
| `registration.revoked` | SET processed at events_endpoint | registration_id, iss, sub |
---
## Security Considerations
- **auth_time validation** — `auth_time` is required in ID-JAGs. Service validates against `idJagMaxAuthAgeSeconds`. If too old, returns `login_required` (401) — agent must get user to re-authenticate at provider.
- **claim_token handling** — returned exactly once in the registration response. Agent holds in memory only for ceremony duration. Do not persist past Step 4.
- **Token hashing** — `claim_token` is a bearer secret. Store only SHA-256 hash server-side.
- **Consent UX** — surface `resource_name` and `resource_logo_uri` from PRM to the user before asserting identity. This is the user's only consent gate.
- **Two revocation layers** — credential layer (agent-callable, `/oauth2/revoke`, kills one access_token) vs registration layer (provider-driven SETs at `events_endpoint`, kills identity_assertion + all derived tokens).
- **Replay protection** — cache `jti` values with TTL of at least `exp - iat` + clock skew (typically 6 min).
- **CIMD resolution** — if `client_id` is a URL, fetch as Client ID Metadata Document and verify `jwks_uri`.
- **Bulk revocation** — provide operator-facing mechanism to revoke all outstanding identity_assertions for a tenant.
---
## Error Codes Reference
| Code | Where | Meaning |
|------|-------|---------|
| `anonymous_not_enabled` | `/agent/identity` | Service doesn't accept anonymous |
| `service_auth_not_enabled` | `/agent/identity` | service_auth disabled |
| `issuer_not_enabled` | `/agent/identity` | Provider not on trust list |
| `invalid_request` | `/agent/identity` | Body/claim/signature/jti/aud problems |
| `interaction_required` (401) | `/agent/identity` | ID-JAG matched account, no delegation — claim needed |
| `login_required` (401) | `/agent/identity` | auth_time too old — re-authenticate at provider |
| `invalid_claim_token` | `/agent/identity/claim` | Token wrong or expired |
| `claimed_or_in_flight` | `/agent/identity/claim` | Already claimed or wrong endpoint |
| `claim_expired` | `/agent/identity/claim` | Registration expired |
| `invalid_grant` | `/oauth2/token` | Assertion expired/revoked |
| `invalid_client` | `/oauth2/token` | client_id not recognized |
| `unsupported_grant_type` | `/oauth2/token` | Not jwt-bearer or claim grant |
| `authorization_pending` | `/oauth2/token` (claim) | User hasn't completed ceremony |
| `expired_token` | `/oauth2/token` (claim) | user_code window closed |
| `slow_down` | `/oauth2/token` (claim) | Polling too fast |
| `rate_limited` (429) | any | Back off and retry |
---
## Agent Readiness Scanner Check
The [isitagentready.com](https://isitagentready.com) scanner validates auth.md as the `authMd` check. Pass criteria:
1. `/auth.md` served from site root with HTTP 200
2. Content is Markdown with H1 heading containing "auth.md"
3. Optionally validates OAuth Protected Resource Metadata at `/.well-known/oauth-protected-resource`
4. Optionally validates Authorization Server metadata at `/.well-known/oauth-authorization-server`
**To pass the check minimally:**
```markdown
# auth.md
This service accepts AI agent registrations.
## Authentication
Agents can register via POST /agent/identity with a valid ID-JAG.
See below for supported methods.
```
**To pass with full marks (all metadata):**
- Serve `/auth.md` with proper heading
- Publish `/.well-known/oauth-protected-resource` with `resource`, `resource_name`, `resource_logo_uri`, `authorization_servers`, `scopes_supported`, `bearer_methods_supported: ["header"]`
- Publish `/.well-known/oauth-authorization-server` with `issuer`, `token_endpoint`, `revocation_endpoint`, `grant_types_supported`, and `agent_auth` block containing `skill`, `identity_endpoint`, `claim_endpoint`, `events_endpoint`, and registration methods
**Scan command:**
```bash
curl -s -X POST 'https://isitagentready.com/api/scan' \
-H 'Content-Type: application/json' \
-d '{"url":"https://YOUR-DOMAIN/","enabledChecks":["authMd"]}' | jq '.checks.discovery.authMd'
```
---
## Quality Checklist
Before delivering output, verify:
- [ ] Generated `auth.md` contains all required steps (1-6) + Errors + Revocation
- [ ] AS metadata includes `issuer`, `token_endpoint`, `revocation_endpoint`, `grant_types_supported`
- [ ] `agent_auth` block includes `identity_endpoint`, `claim_endpoint`, `events_endpoint`
- [ ] `identity_types_supported` matches the flows the user chose
- [ ] `scopes_supported` reflects actual API scopes found in codebase
- [ ] Base URLs are consistent between auth.md and metadata JSON
- [ ] Error codes table includes all standard codes for the supported flows
- [ ] Step 5 documents token exchange at `/oauth2/token` with jwt-bearer grant
- [ ] Revocation section documents both layers (credential + registration)
- [ ] No unreplaced placeholder values (`{{...}}`, `<your-...>`, `[YOUR_...]`)
- [ ] Validation report covers all rules for the requested level
- [ ] Rate limiting documented (including /oauth2/token polling)
- [ ] Security considerations included (auth_time, claim_token, consent UX)
- [ ] If role=provider: ID-JAG structure with `auth_time` documented
---
## References
- `references/protocol-template.md` — Complete auth.md template with all sections and placeholders
- `references/validation-rules.md` — Full validation ruleset with error messages and severities
- `references/metadata-schema.md` — JSON schema for PRM, AS metadata, ID-JAG, and identity_assertion
- `references/example-auth-md.md` — Working example of a complete auth.md file (Acme Notes)
- `references/implementation-guide.md` — Server-side implementation guide with token exchange, claim ceremony, revocation, and security
---
## Updating Protocol Knowledge
This skill ships with a snapshot of the auth.md protocol specification (v2, June 2026). When possible, fetch the latest version from:
- Skill Home and Doc Hub: `https://auth-md.com`
- Spec: `https://raw.githubusercontent.com/workos/auth.md/refs/heads/main/AUTH.md`
- Docs overview: `https://workos.com/auth-md/docs`
- Apps guide: `https://workos.com/auth-md/docs/apps`
- Agent providers guide: `https://workos.com/auth-md/docs/agent-providers`
- File anatomy: `https://workos.com/auth-md/docs/auth-md`
If fetch fails, use the bundled `references/` as the source of truth.

View File

@@ -0,0 +1,403 @@
# Complete Example: auth.md
A working example of an `auth.md` file for the fictional "Acme Notes" service, updated to protocol v2 (June 2026).
---
## Example Context
- **Service:** Acme Notes (collaborative notes app)
- **Resource Server:** `https://api.acmenotes.com`
- **Auth Server:** `https://auth.acmenotes.com`
- **Supported flows:** identity_assertion (ID-JAG), service_auth, anonymous
- **Scopes:** `notes.read`, `notes.write`, `notes.admin`
---
## The File
```markdown
# 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.
The resource server is `https://api.acmenotes.com` and the authorization server is `https://auth.acmenotes.com`.
## 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="https://api.acmenotes.com/.well-known/oauth-protected-resource"
\```
### 1a. Fetch the Protected Resource Metadata
\```http
GET /.well-known/oauth-protected-resource
\```
Response:
\```json
{
"resource": "https://api.acmenotes.com/",
"resource_name": "Acme Notes",
"resource_logo_uri": "https://acmenotes.com/logo.png",
"authorization_servers": ["https://auth.acmenotes.com/"],
"scopes_supported": ["notes.read", "notes.write", "notes.admin"],
"bearer_methods_supported": ["header"]
}
\```
### 1b. Fetch the Authorization Server metadata
\```http
GET /.well-known/oauth-authorization-server
\```
Response:
\```json
{
"resource": "https://api.acmenotes.com/",
"authorization_servers": ["https://auth.acmenotes.com/"],
"scopes_supported": ["notes.read", "notes.write", "notes.admin"],
"bearer_methods_supported": ["header"],
"issuer": "https://auth.acmenotes.com",
"token_endpoint": "https://auth.acmenotes.com/oauth2/token",
"revocation_endpoint": "https://auth.acmenotes.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://acmenotes.com/auth.md",
"identity_endpoint": "https://auth.acmenotes.com/agent/identity",
"claim_endpoint": "https://auth.acmenotes.com/agent/identity/claim",
"events_endpoint": "https://auth.acmenotes.com/agent/event/notify",
"identity_types_supported": ["anonymous", "identity_assertion", "service_auth"],
"identity_assertion": {
"assertion_types_supported": [
"urn:ietf:params:oauth:token-type:id-jag"
]
},
"events_supported": [
"https://schemas.workos.com/events/agent/auth/identity/assertion/revoked"
]
}
}
\```
## Step 2 — Pick a method
1. **You have a session tied to a user identity and can exchange it for an ID-JAG** → 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.
Cross-check against the `agent_auth` block before proceeding.
## Step 3 — Register
Before sending an `identity_assertion` or `service_auth` body, surface "Acme Notes" and its logo to the user and confirm consent. Skip for `anonymous`.
### identity_assertion + id-jag
Mint the ID-JAG with:
- `aud` = `https://api.acmenotes.com/` (from PRM `resource`)
- `auth_time` = epoch seconds of user's last authentication at your provider (**required**)
\```http
POST /agent/identity
Host: auth.acmenotes.com
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_01ABC123DEF456",
"registration_type": "identity_assertion",
"identity_assertion": "eyJhbGciOiJFUzI1NiJ9...",
"assertion_expires": "2026-05-22T14:00:00.000Z",
"scopes": ["notes.read", "notes.write"]
}
\```
Keep `identity_assertion` and go to Step 5.
Response — confirmation required (401, `interaction_required`):
\```json
{
"error": "interaction_required",
"error_description": "ID-JAG email matches an existing account but no delegation on file for (iss, sub).",
"registration_id": "reg_01ABC123DEF456",
"registration_type": "identity_assertion",
"claim_url": "https://auth.acmenotes.com/agent/identity/claim",
"claim_token": "clm_xYz789AbC012dEf",
"claim_token_expires": "2026-05-22T13:30:00.000Z",
"post_claim_scopes": ["notes.read", "notes.write"],
"claim": {
"user_code": "847291",
"expires_in": 600,
"verification_uri": "https://auth.acmenotes.com/login?return_to=%2Fclaim%3Fclaim_attempt_token%3Dcat_abc123",
"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 7200s old; max allowed is 3600s. Re-authenticate at the provider.",
"max_age": 3600
}
\```
Re-authenticate the user at your provider and mint a fresh ID-JAG.
### service_auth
\```http
POST /agent/identity
Host: auth.acmenotes.com
Content-Type: application/json
{
"type": "service_auth",
"login_hint": "jane@example.com"
}
\```
Response (200):
\```json
{
"registration_id": "reg_01DEF789GHI012",
"registration_type": "service_auth",
"claim_url": "https://auth.acmenotes.com/agent/identity/claim",
"claim_token": "clm_aBc123DeF456gHi",
"claim_token_expires": "2026-05-22T13:30:00.000Z",
"post_claim_scopes": ["notes.read", "notes.write"],
"claim": {
"user_code": "592841",
"expires_in": 600,
"verification_uri": "https://auth.acmenotes.com/login?return_to=%2Fclaim%3Fclaim_attempt_token%3Dcat_def456",
"interval": 5
}
}
\```
No `identity_assertion` yet. Go to Step 4.
### anonymous
\```http
POST /agent/identity
Host: auth.acmenotes.com
Content-Type: application/json
{
"type": "anonymous"
}
\```
Response (200):
\```json
{
"registration_id": "reg_01GHI345JKL678",
"registration_type": "anonymous",
"identity_assertion": "eyJhbGciOiJFUzI1NiJ9...",
"assertion_expires": "2026-05-22T14:00:00.000Z",
"pre_claim_scopes": ["notes.read"],
"claim_url": "https://auth.acmenotes.com/agent/identity/claim",
"claim_token": "clm_mNo345PqR678sTu",
"claim_token_expires": "2026-05-22T13:30:00.000Z",
"post_claim_scopes": ["notes.read", "notes.write"]
}
\```
Exchange `identity_assertion` at `/oauth2/token` for an access_token with `pre_claim_scopes` (Step 5). To upgrade scopes, go to Step 4.
## Step 4 — Claim ceremony
### 4a. Get the ceremony materials
For **service_auth** and **interaction_required** responses, you already have the `claim` block. Skip to 4b.
For **anonymous**, initiate:
\```http
POST /agent/identity/claim
Host: auth.acmenotes.com
Content-Type: application/json
{
"claim_token": "clm_mNo345PqR678sTu",
"email": "jane@example.com"
}
\```
Response (200):
\```json
{
"registration_id": "reg_01GHI345JKL678",
"claim_attempt_id": "cla_001",
"status": "initiated",
"expires_at": "2026-05-22T13:40:00.000Z",
"claim_attempt": {
"user_code": "374918",
"expires_in": 600,
"verification_uri": "https://auth.acmenotes.com/login?return_to=%2Fclaim%3Fclaim_attempt_token%3Dcat_ghi789",
"interval": 5
}
}
\```
### 4b. Hand off to the user
Surface to the user:
> Open this link, sign in (or sign up), and enter this 6-digit code: **374918**
> https://auth.acmenotes.com/login?return_to=%2Fclaim%3Fclaim_attempt_token%3Dcat_ghi789
The user signs in to Acme Notes, sees a confirmation page, and types the code.
### 4c. Poll for completion
\```http
POST /oauth2/token
Host: auth.acmenotes.com
Content-Type: application/x-www-form-urlencoded
grant_type=urn:workos:agent-auth:grant-type:claim&claim_token=clm_mNo345PqR678sTu
\```
Response while waiting:
\```json
{
"error": "authorization_pending",
"error_description": "User has not completed the claim ceremony yet."
}
\```
Response on success:
\```json
{
"access_token": "eyJhbGciOiJSUzI1NiJ9...",
"token_type": "Bearer",
"expires_in": 3600,
"scope": "notes.read notes.write",
"identity_assertion": "eyJhbGciOiJFUzI1NiJ9...",
"assertion_expires": "2026-05-22T15:00:00.000Z"
}
\```
Use `access_token` immediately. Cache `identity_assertion` for refresh via Step 5.
If `user_code` window expires:
\```json
{
"error": "expired_token",
"error_description": "The user_code window has closed."
}
\```
Re-call `POST /agent/identity/claim` with same `claim_token` and `email` for a fresh code. If that returns `claim_expired`, restart at Step 3.
## Step 5 — Exchange the assertion
\```http
POST /oauth2/token
Host: auth.acmenotes.com
Content-Type: application/x-www-form-urlencoded
grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer&assertion=eyJhbGciOiJFUzI1NiJ9...&resource=https://api.acmenotes.com/
\```
Response (200):
\```json
{
"access_token": "eyJhbGciOiJSUzI1NiJ9...",
"token_type": "Bearer",
"expires_in": 3600,
"scope": "notes.read notes.write"
}
\```
Same `identity_assertion` can be re-used until it expires. If `invalid_grant`, restart at Step 3.
## Step 6 — Use the access_token
\```http
GET /api/notes
Host: api.acmenotes.com
Authorization: Bearer eyJhbGciOiJSUzI1NiJ9...
\```
**Refresh:** When access_token expires, re-call Step 5 with same identity_assertion. When identity_assertion expires or Step 5 returns `invalid_grant`, restart at Step 3.
On 401 for a previously-working access_token: try Step 5 once. If that also fails, restart at Step 1.
Full API docs: https://docs.acmenotes.com/
## 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, ID-JAG 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 |
| `claim_expired` | `/agent/identity/claim` | Restart at Step 3 |
| `invalid_grant` | `/oauth2/token` | Assertion expired/revoked. Restart at Step 3 |
| `unsupported_grant_type` | `/oauth2/token` | Use one of the two supported grants |
| `authorization_pending` | `/oauth2/token` (claim) | User hasn't finished. Honor `interval` |
| `expired_token` | `/oauth2/token` (claim) | user_code window closed. Re-initiate or restart |
| `slow_down` | `/oauth2/token` (claim) | Add ≥5s to interval |
| `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 `https://auth.acmenotes.com/oauth2/revoke`. Kills one access_token. Identity assertion intact — re-run Step 5.
- **Registration layer (RFC 8935):** Provider POSTs a Security Event Token to `events_endpoint`. Invalidates identity_assertion and all derived access_tokens. Agent discovers via `invalid_grant` at `/oauth2/token` — restart at Step 3.
On 401 for a previously-working access_token: try Step 5 once. If `/oauth2/token` succeeds, credential-layer revocation. If `invalid_grant`, registration-layer — restart at Step 3.
```
---
## Notes on This Example
1. **All three flows** documented — in production, delete sections for unsupported flows
2. **Token exchange step** (Step 5) — registration never returns access_token directly; always returns identity_assertion
3. **Browser-based claim ceremony** — user_code + verification_uri replaces OTP-via-email
4. **`interaction_required` response** — shows the 401 path when ID-JAG email matches existing account without delegation
5. **`login_required` response** — shows `auth_time` freshness enforcement
6. **Two revocation layers** — credential (agent-callable) vs registration (provider-driven)
7. **Claim polling at `/oauth2/token`** — uses profile-specific grant URN to avoid collision with standard RFC 8628

View File

@@ -0,0 +1,433 @@
# Server-Side Implementation Guide
Detailed guidance for implementing auth.md protocol endpoints on the backend (v2, June 2026). Covers discovery, registration, claim ceremony, token exchange, revocation, security, rate limiting, and audit events.
---
## Table of Contents
1. [Minimum Implementation](#minimum-implementation)
2. [Discovery Documents](#discovery-documents)
3. [POST /agent/identity — Registration Handler](#post-agentidentity--registration-handler)
4. [ID-JAG Verification](#id-jag-verification)
5. [Claim Ceremony](#claim-ceremony)
6. [POST /oauth2/token — Token Endpoint](#post-oauth2token--token-endpoint)
7. [Revocation](#revocation)
8. [User Matching and JIT Provisioning](#user-matching-and-jit-provisioning)
9. [Rate Limiting](#rate-limiting)
10. [Security](#security)
11. [Audit Events](#audit-events)
12. [Deploy Checklist](#deploy-checklist)
---
## Minimum Implementation
1. Publish `/.well-known/oauth-protected-resource` with `resource_name` and `resource_logo_uri`
2. Publish `/.well-known/oauth-authorization-server` with `issuer`, `token_endpoint`, `revocation_endpoint`, `grant_types_supported`, and `agent_auth` block
3. Return `WWW-Authenticate: Bearer resource_metadata="..."` on 401 responses
4. Host `POST /agent/identity` that dispatches on the `type` field
5. For identity_assertion: maintain a trust list and verify ID-JAG signatures via JWKS, validate `auth_time`
6. For service_auth: return `claim` block with `user_code` + `verification_uri`
7. For anonymous: issue `identity_assertion` immediately with pre-claim scopes
8. Host `POST /agent/identity/claim` for deferred claim initiation
9. Implement `POST /oauth2/token` handling both grant types (jwt-bearer exchange + claim polling)
10. Implement `POST /oauth2/revoke` for credential-layer revocation (RFC 7009)
11. Accept SETs at `events_endpoint` for registration-layer revocation (RFC 8935)
12. Record audit events for every state change
---
## Discovery Documents
### Serving the PRM
```
GET /.well-known/oauth-protected-resource
→ 200 OK
→ Content-Type: application/json
```
Must include `resource_name` and `resource_logo_uri` — agents surface these to the user for consent before asserting identity.
Cache aggressively: `Cache-Control: public, max-age=3600`.
### Serving the AS Metadata
```
GET /.well-known/oauth-authorization-server
→ 200 OK
→ Content-Type: application/json
```
Must include standard OAuth fields (`issuer`, `token_endpoint`, `revocation_endpoint`, `grant_types_supported`) plus the `agent_auth` block with `identity_endpoint`, `claim_endpoint`, `events_endpoint`.
### WWW-Authenticate Header
On every 401 response from the API:
```http
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer resource_metadata="https://api.service.com/.well-known/oauth-protected-resource"
```
---
## POST /agent/identity — Registration Handler
All registration requests share the same endpoint and dispatch on the `type` field:
```
POST /agent/identity
Content-Type: application/json
```
### Dispatch
| `type` | Flow | Returns |
|--------|------|---------|
| `identity_assertion` | ID-JAG verified | `identity_assertion` (service-signed JWT) |
| `service_auth` | Email hint + browser ceremony | `claim` block (ceremony materials) |
| `anonymous` | No identity | `identity_assertion` + `claim_token` for deferred claim |
### Handler: identity_assertion + id-jag
1. Decode the ID-JAG header to obtain `kid` and `alg`
2. Look up the issuer (`iss`) in the trust list. Reject if unknown → `issuer_not_enabled`
3. Fetch JWKS from the provider (see ID-JAG Verification section for caching)
4. Verify signature → `invalid_request` if fails
5. Validate claims:
- `aud` matches the `resource` from PRM → `invalid_request`
- `exp` is in the future → `invalid_request`
- `iat` not unreasonably in the future (~1-2 min skew)
- `jti` not seen recently → `invalid_request` (replay)
- `auth_time` present and within `idJagMaxAuthAgeSeconds``login_required` (401) if too old
- At least `email_verified` or `phone_number_verified` is `true``invalid_request`
6. Match or provision the user (see User Matching)
7. Check delegation: if `(iss, sub)` is known OR JIT-provisioned without collision → success
8. If email/phone collision with existing account but no delegation on file → `interaction_required` (401) with `claim` block
9. On success: sign an `identity_assertion` JWT and return it
### Handler: service_auth
1. Validate `login_hint` (email format)
2. Create a registration row with type `service_auth`
3. Generate `claim_token` (returned to agent once), `user_code` (6-digit), `claim_attempt_token` (embedded in verification_uri)
4. Store SHA-256 hashes of `claim_token`
5. Build `verification_uri` pointing to the service's login page with `return_to` parameter
6. Return the registration response with `claim` block containing `user_code`, `verification_uri`, `expires_in`, `interval`
### Handler: anonymous
1. Apply rate limits
2. Create a registration row
3. Sign an `identity_assertion` JWT with pre-claim scopes
4. Generate `claim_token` for deferred claim. Store only SHA-256 hash.
5. Return `identity_assertion` + `claim_token` + pre/post claim scopes
---
## ID-JAG Verification
### Trust List
Maintain a registry of providers. Minimum entry: issuer URL. Richer entries can pin JWKS URI, CIMD URL, or attestation policy.
### JWKS Fetching
- Fetch `{iss}/.well-known/jwks.json` on first use
- Cache per `Cache-Control`, with floor 10 min, ceiling 24h
- On `kid` miss, refetch once before rejecting
### CIMD Resolution
If `client_id` is a URL:
1. Fetch as OAuth Client ID Metadata Document
2. Verify `jwks_uri` matches the one used to verify signature
### auth_time Validation
- `auth_time` is **required** in ID-JAGs
- Compare `now() - auth_time` against `idJagMaxAuthAgeSeconds` (service-configured, e.g., 3600)
- If too old: return `login_required` (401) with `max_age` in the response
- Agent must re-authenticate user at provider and mint fresh ID-JAG
### Replay Protection
- Cache `jti` values with TTL of at least `exp - iat` + clock skew (typically 6 min)
- Reject on collision with `invalid_request`
---
## Claim Ceremony
The v2 claim ceremony is browser-based, borrowing from RFC 8628 device authorization.
### POST /agent/identity/claim (anonymous deferred claim)
1. Hash `claim_token`, look up registration
2. Reject if not found → `invalid_claim_token`, already claimed → `claimed_or_in_flight`, expired → `claim_expired`
3. Generate `user_code` (6-digit), `claim_attempt_token`
4. Build `verification_uri` with embedded `claim_attempt_token`
5. Return `claim_attempt` block with `user_code`, `verification_uri`, `expires_in`, `interval`
### Service-Hosted Claim Page
When the user opens `verification_uri`:
1. Redirect to login if not authenticated
2. After login, show claim page displaying:
- The user's identity ("You're signed in as jane@example.com")
- The requesting agent/provider info (from PRM `resource_name`)
- Input field for the 6-digit code
3. On correct code submission: mark claim as complete, associate registration with user
4. Incorrect code: show error, allow retry (up to limit)
### Claim Completion Flow
When the user submits the correct `user_code` on the claim page:
1. Mark the claim as complete in the database
2. For anonymous: sign a new `identity_assertion` (v2) carrying user claims, superseding the pre-claim one
3. For service_auth: sign the first `identity_assertion` for this registration
4. The next poll at `/oauth2/token` with the claim grant returns the access_token + identity_assertion
---
## POST /oauth2/token — Token Endpoint
Handles two grant types at the same endpoint:
### Grant: urn:ietf:params:oauth:grant-type:jwt-bearer (Token Exchange)
```
POST /oauth2/token
Content-Type: application/x-www-form-urlencoded
grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer
&assertion=<identity_assertion>
&resource=<resource_url> (optional but recommended)
```
Processing:
1. Verify the `identity_assertion` JWT signature (service's own key)
2. Validate `exp` not passed → `invalid_grant` if expired
3. Check assertion not revoked → `invalid_grant` if revoked
4. Mint a fresh `access_token` scoped to the assertion's scopes
5. Return standard OAuth token response
### Grant: urn:workos:agent-auth:grant-type:claim (Claim Polling)
```
POST /oauth2/token
Content-Type: application/x-www-form-urlencoded
grant_type=urn:workos:agent-auth:grant-type:claim
&claim_token=<claim_token>
```
Processing:
1. Hash `claim_token`, look up registration
2. If claim not yet completed → return `authorization_pending`
3. If user_code window expired → return `expired_token`
4. If polling too fast → return `slow_down`
5. If claim completed → mint access_token + return along with new `identity_assertion`
Why a profile-specific grant URN? So this doesn't collide with services that also implement standard RFC 8628 device authorization at the same token endpoint.
### Error Responses
| Error | HTTP | Condition |
|-------|------|-----------|
| `invalid_grant` | 400 | Assertion expired, revoked, or invalid |
| `invalid_client` | 401 | client_id not recognized |
| `unsupported_grant_type` | 400 | Not one of the two supported grants |
| `authorization_pending` | 400 | Claim polling — user hasn't finished |
| `expired_token` | 400 | user_code window closed |
| `slow_down` | 400 | Polling too fast — add ≥5s |
---
## Revocation
### Credential Layer — POST /oauth2/revoke (RFC 7009)
Agent-callable. Kills one access_token.
```
POST /oauth2/revoke
Content-Type: application/x-www-form-urlencoded
token=<access_token>&token_type_hint=access_token
```
- Return 200 on success (idempotent)
- The underlying `identity_assertion` remains valid — agent can re-exchange for a new access_token
### Registration Layer — Events Endpoint (RFC 8935)
Provider-driven. Receives Security Event Tokens.
```
POST /agent/event/notify
Content-Type: application/secevent+jwt
<SET JWT>
```
Processing:
1. Verify SET signature against issuer's JWKS
2. Enforce `jti` uniqueness
3. Match `events` key to supported schemas
4. For `https://schemas.workos.com/events/agent/auth/identity/assertion/revoked`:
- Invalidate all identity_assertions for `(iss, sub, aud)`
- Invalidate all derived access_tokens
5. Return 200 on success, 400 on verification failure
### Bulk Revocation
Provide an operator-facing mechanism to revoke all outstanding identity_assertions and access_tokens for a tenant in one shot — for incident response.
---
## User Matching and JIT Provisioning
Resolution order:
1. **Delegation record match** — if `(iss, sub)` has a delegation on file, route to that user. Strongest identifier.
2. **Verified email match** — if a user exists with same verified email:
- If delegation exists for `(iss, sub)` → direct match (already covered above)
- If NO delegation for `(iss, sub)``interaction_required` (401). User must confirm linking.
3. **Verified phone match** — same pattern.
4. **No match → JIT** — create a new user per provisioning policy, or refuse.
Reject ID-JAGs with neither verified email nor verified phone.
---
## Rate Limiting
### Two Tiers
| Tier | Checked | Default Anonymous | Default identity_assertion |
|------|---------|-------------------|---------------------------|
| Per-IP | First | 5/hour | 60/hour |
| Per-tenant | Second | 100/hour | 1000/hour |
### Implementation
- Sliding-window counter with shared store
- Fail open on store errors
- Return 429 with `Retry-After` header
- Also rate-limit `/oauth2/token` polling (respect `interval` from claim block, reject with `slow_down`)
---
## Security
### Token Hashing
| Token | Storage | Plaintext leaves server |
|-------|---------|------------------------|
| `claim_token` | SHA-256 hash | Once, in the registration response |
| `user_code` | Stored for comparison | Displayed on claim page when user submits |
| `identity_assertion` | Full JWT stored (or just signature hash for lookup) | In registration response |
### claim_token
- Returned **exactly once** to the agent in the registration response
- Agent holds in memory for ceremony duration — must not persist past Step 4
- High-entropy: prefix `clm_` + 25+ chars base62
### auth_time Enforcement
- Service configures `idJagMaxAuthAgeSeconds` (default: 3600)
- Reject ID-JAGs where `now() - auth_time > idJagMaxAuthAgeSeconds`
- Return `login_required` (401) with `max_age` field
### Consent UX
- Surface `resource_name` and `resource_logo_uri` from PRM to user before identity assertion
- The claim page should display who is requesting access (provider name from CIMD or ID-JAG metadata)
### user_code Security
- 6-digit numeric code
- 10-minute TTL (configurable via `expires_in`)
- Tight retry limits (3-5 attempts) on the claim page
- Code is tied to the `claim_attempt_token` embedded in `verification_uri`
### Replay Protection
- `jti` cache mandatory for ID-JAGs
- `jti` uniqueness enforced for SETs at events_endpoint
- Shared store required for multi-replica deployments
### Trust List Discipline
Treat the trusted-providers list as security-critical configuration. Changes should be audited.
---
## Audit Events
### Recommended Events
| Event | When | Minimum Data |
|-------|------|--------------|
| `registration.created` | Successful POST /agent/identity | registration_id, registration_type, iss, sub (if ID-JAG) |
| `registration.interaction_required` | 401 interaction_required returned | registration_id, iss, sub, matched_user_id |
| `registration.login_required` | 401 login_required returned | iss, sub, auth_time, max_age |
| `claim.initiated` | /agent/identity/claim called | registration_id, email |
| `claim.completed` | User submitted correct user_code | registration_id, claimed_by_user_id |
| `claim.expired` | user_code window or registration expired | registration_id |
| `token.exchanged` | /oauth2/token jwt-bearer success | registration_id, access_token_id |
| `token.revoked` | /oauth2/revoke called | access_token_id |
| `registration.revoked` | SET processed at events_endpoint | registration_id, iss, sub |
| `registration.expired` | Unclaimed registration past TTL | registration_id |
---
## Deploy Checklist
### Before publishing
- [ ] PRM served at `/.well-known/oauth-protected-resource` with `resource_name` and `resource_logo_uri`
- [ ] AS metadata served at `/.well-known/oauth-authorization-server` with `issuer`, `token_endpoint`, `revocation_endpoint`, `grant_types_supported`, and `agent_auth` block
- [ ] `agent_auth` contains `identity_endpoint`, `claim_endpoint`, `events_endpoint`
- [ ] `auth.md` served at the domain root
- [ ] API returns `WWW-Authenticate` header on 401s
- [ ] `POST /agent/identity` dispatches correctly by `type`
- [ ] Trust list configured (if identity_assertion)
- [ ] JWKS fetching with cache (if identity_assertion)
- [ ] `auth_time` validation against `idJagMaxAuthAgeSeconds` (if identity_assertion)
- [ ] `POST /agent/identity/claim` generates user_code + verification_uri (if service_auth/anonymous)
- [ ] Claim page served at verification_uri (login → code input → confirm)
- [ ] `POST /oauth2/token` handles jwt-bearer grant (assertion → access_token)
- [ ] `POST /oauth2/token` handles claim grant (polling → access_token + identity_assertion)
- [ ] `POST /oauth2/revoke` kills access_tokens (RFC 7009)
- [ ] Events endpoint accepts SETs for registration revocation
- [ ] Rate limiting active on `/agent/identity` and `/oauth2/token`
- [ ] claim_token stored as SHA-256 hash
- [ ] Replay protection for `jti` implemented
- [ ] Audit events being recorded
### Recommended tests
- [ ] identity_assertion: valid ID-JAG with fresh auth_time → identity_assertion returned
- [ ] identity_assertion: expired ID-JAG → `invalid_request`
- [ ] identity_assertion: auth_time too old → `login_required` (401)
- [ ] identity_assertion: email collision without delegation → `interaction_required` (401) with claim block
- [ ] identity_assertion: repeated `jti``invalid_request` (replay)
- [ ] identity_assertion: unknown issuer → `issuer_not_enabled`
- [ ] service_auth: valid email → registration with claim block (user_code + verification_uri)
- [ ] anonymous: registration → identity_assertion with pre_claim_scopes
- [ ] anonymous: claim initiation → claim_attempt with user_code
- [ ] /oauth2/token jwt-bearer: valid assertion → access_token
- [ ] /oauth2/token jwt-bearer: expired assertion → `invalid_grant`
- [ ] /oauth2/token claim: before completion → `authorization_pending`
- [ ] /oauth2/token claim: after completion → access_token + identity_assertion
- [ ] /oauth2/token claim: after window expires → `expired_token`
- [ ] /oauth2/token claim: too-fast polling → `slow_down`
- [ ] /oauth2/revoke: valid access_token → 200
- [ ] events_endpoint: valid SET → registrations revoked
- [ ] Rate limit exceeded → 429
- [ ] 401 on API → WWW-Authenticate header present

View File

@@ -0,0 +1,370 @@
# Metadata Schema
JSON structure reference for the discovery documents and tokens required by the auth.md protocol (v2, June 2026).
---
## Protected Resource Metadata (PRM)
Served at: `{resource_server}/.well-known/oauth-protected-resource`
Defined by [RFC 9728](https://datatracker.ietf.org/doc/html/rfc9728).
```json
{
"resource": "https://api.example.com/",
"resource_name": "Example Service",
"resource_logo_uri": "https://example.com/logo.png",
"authorization_servers": ["https://auth.example.com/"],
"scopes_supported": ["read", "write", "admin"],
"bearer_methods_supported": ["header"]
}
```
### Fields
| Field | Required | Type | Description |
|-------|----------|------|-------------|
| `resource` | ✅ | string (URL) | Canonical URL of the API. Used as `aud` in ID-JAGs. |
| `resource_name` | ✅ | string | Display name for consent prompts. Surface to user before asserting identity. |
| `resource_logo_uri` | Recommended | string (URL) | Logo for consent UI. Surface alongside `resource_name`. |
| `authorization_servers` | ✅ | string[] | Base URLs of OAuth AS(s). Agent fetches AS metadata from here. |
| `scopes_supported` | ✅ | string[] | All scopes the resource server understands. |
| `bearer_methods_supported` | ✅ | string[] | How credentials are presented. Typically `["header"]`. |
---
## Authorization Server Metadata
Served at: `{authorization_server}/.well-known/oauth-authorization-server`
Combines standard [RFC 8414](https://datatracker.ietf.org/doc/html/rfc8414) fields with the `agent_auth` profile block.
```json
{
"resource": "https://api.example.com/",
"authorization_servers": ["https://auth.example.com/"],
"scopes_supported": ["read", "write", "admin"],
"bearer_methods_supported": ["header"],
"issuer": "https://auth.example.com",
"token_endpoint": "https://auth.example.com/oauth2/token",
"revocation_endpoint": "https://auth.example.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://example.com/auth.md",
"identity_endpoint": "https://auth.example.com/agent/identity",
"claim_endpoint": "https://auth.example.com/agent/identity/claim",
"events_endpoint": "https://auth.example.com/agent/event/notify",
"identity_types_supported": ["anonymous", "identity_assertion", "service_auth"],
"identity_assertion": {
"assertion_types_supported": [
"urn:ietf:params:oauth:token-type:id-jag"
]
},
"events_supported": [
"https://schemas.workos.com/events/agent/auth/identity/assertion/revoked"
]
}
}
```
### Top-Level OAuth Fields
| Field | Required | Type | Description |
|-------|----------|------|-------------|
| `issuer` | ✅ | string (URL) | Canonical issuer URL of this AS. Validate `iss` claim of any token the AS signs against this. |
| `token_endpoint` | ✅ | string (URL) | Where agents exchange identity assertions for access_tokens (Step 5) and poll during claim ceremony (Step 4c). |
| `revocation_endpoint` | ✅ | string (URL) | Where agents POST to revoke an access_token ([RFC 7009](https://datatracker.ietf.org/doc/html/rfc7009)). |
| `grant_types_supported` | ✅ | string[] | Grant types accepted at `token_endpoint`. Must include `urn:ietf:params:oauth:grant-type:jwt-bearer` (token exchange) and `urn:workos:agent-auth:grant-type:claim` (claim polling). |
### `agent_auth` Block Fields
| Field | Required | Type | Description |
|-------|----------|------|-------------|
| `skill` | Recommended | string (URL) | URL of the auth.md file. |
| `identity_endpoint` | ✅ | string (URL) | POST endpoint for registration (Step 3). |
| `claim_endpoint` | Conditional | string (URL) | POST endpoint for claim initiation. Required if `service_auth` or `anonymous` supported. |
| `events_endpoint` | Recommended | string (URL) | Receives Security Event Tokens ([RFC 8417](https://datatracker.ietf.org/doc/html/rfc8417)) from providers for registration-layer revocation. |
| `identity_types_supported` | ✅ | string[] | `"anonymous"`, `"identity_assertion"`, `"service_auth"`, or combination. |
| `identity_assertion` | Conditional | object | Required if `"identity_assertion"` in identity_types_supported. |
| `identity_assertion.assertion_types_supported` | ✅ (if identity_assertion) | string[] | `"urn:ietf:params:oauth:token-type:id-jag"`. |
| `events_supported` | Recommended | string[] | Event schemas this service can ingest (currently revocation). Informational. |
### identity_types → Flow Mapping
| `identity_types_supported` | Flow | Ceremony |
|---|---|---|
| `identity_assertion` | ID-JAG verified by provider | None (unless `interaction_required`) |
| `service_auth` | Email hint, browser-based ceremony | RFC 8628-style: user_code + verification_uri |
| `anonymous` | No identity upfront | Optional deferred claim |
---
## ID-JAG Token (Identity Assertion JWT Authorization Grant)
Defined by [draft-ietf-oauth-identity-assertion-authz-grant](https://datatracker.ietf.org/doc/html/draft-ietf-oauth-identity-assertion-authz-grant).
### Header
```json
{
"typ": "oauth-id-jag+jwt",
"alg": "ES256",
"kid": "<provider-key-id>"
}
```
### Payload
```json
{
"iss": "https://api.agent-provider.com",
"sub": "<opaque-user-identifier>",
"aud": "https://api.example.com",
"client_id": "<issuer-url-or-cimd-url>",
"jti": "<unique-token-id>",
"iat": 1716400000,
"exp": 1716400300,
"auth_time": 1716399000,
"email": "user@example.com",
"email_verified": true,
"amr": ["mfa"],
"name": "Jane Smith",
"phone_number": "+15553805188",
"phone_number_verified": false,
"resource": "https://api.example.com",
"agent_platform": "cursor",
"agent_context_id": "chat-abc123"
}
```
### Required Claims
| Claim | Description |
|-------|-------------|
| `iss` | Provider's issuer URL (must be on service's trust list) |
| `sub` | Opaque user identifier at the provider |
| `aud` | Service's `resource` URL from the PRM |
| `client_id` | Provider identity (issuer URL or CIMD URL) |
| `jti` | Unique token ID for replay protection |
| `iat` | Issuance time (epoch seconds) |
| `exp` | Expiration (typically iat + 5 minutes) |
| `auth_time` | Epoch seconds when the user last authenticated at the provider. **Required.** Service rejects ID-JAGs whose auth_time is older than its `idJagMaxAuthAgeSeconds` window. |
| `email` + `email_verified: true` OR `phone_number` + `phone_number_verified: true` | At least one verified contact required |
### Optional Claims
| Claim | Description |
|-------|-------------|
| `amr` | Authentication methods reference (e.g., `["mfa"]`) |
| `name` | User's display name |
| `phone_number` | User's phone number |
| `phone_number_verified` | Whether phone is verified |
| `resource` | Resource server URL (informational) |
| `agent_platform` | Agent platform (e.g., `"cursor"`, `"chatgpt"`) |
| `agent_context_id` | Agent context/chat ID |
---
## Client ID Metadata Document (CIMD)
Optional document that decouples provider identity from signing keys. Defined by [draft-ietf-oauth-client-id-metadata-document](https://datatracker.ietf.org/doc/draft-ietf-oauth-client-id-metadata-document/).
Hosted at the URL used as `client_id` in the ID-JAG.
```json
{
"client_id": "https://api.agent-provider.com/agent-auth.json",
"client_name": "Agent Provider",
"logo_uri": "https://agent-provider.com/logo.png",
"client_uri": "https://agent-provider.com",
"tos_uri": "https://agent-provider.com/tos",
"policy_uri": "https://agent-provider.com/privacy",
"token_endpoint_auth_method": "private_key_jwt",
"jwks_uri": "https://agent-provider.com/.well-known/jwks.json",
"scope": "openid email profile"
}
```
---
## Identity Assertion JWT (Service-Signed)
After successful registration, the service returns an `identity_assertion` — a JWT signed by the service that the agent exchanges at `/oauth2/token` for an access_token.
The identity_assertion is:
- Reusable until it expires (`assertion_expires`)
- Exchanged via `POST /oauth2/token` with `grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer`
- Replaced by a fresh one after claim ceremony completion (v2 carries user claims)
---
## Registration Response Shapes
### identity_assertion + id-jag (no confirmation needed)
```json
{
"registration_id": "reg_...",
"registration_type": "identity_assertion",
"identity_assertion": "<service-signed-jwt>",
"assertion_expires": "2026-05-04T13:00:00.000Z",
"scopes": ["read", "write"]
}
```
### identity_assertion + id-jag (interaction_required — 401)
```json
{
"error": "interaction_required",
"error_description": "...",
"registration_id": "reg_...",
"registration_type": "identity_assertion",
"claim_url": "https://auth.example.com/agent/identity/claim",
"claim_token": "clm_...",
"claim_token_expires": "...",
"post_claim_scopes": ["read", "write"],
"claim": {
"user_code": "123456",
"expires_in": 600,
"verification_uri": "https://auth.example.com/login?return_to=...",
"interval": 5
}
}
```
### service_auth
```json
{
"registration_id": "reg_...",
"registration_type": "service_auth",
"claim_url": "https://auth.example.com/agent/identity/claim",
"claim_token": "clm_...",
"claim_token_expires": "2026-05-21T17:31:25.994Z",
"post_claim_scopes": ["read", "write"],
"claim": {
"user_code": "123456",
"expires_in": 600,
"verification_uri": "https://auth.example.com/login?return_to=...",
"interval": 5
}
}
```
### anonymous
```json
{
"registration_id": "reg_...",
"registration_type": "anonymous",
"identity_assertion": "<service-signed-jwt>",
"assertion_expires": "2026-05-04T13:00:00.000Z",
"pre_claim_scopes": ["read"],
"claim_url": "https://auth.example.com/agent/identity/claim",
"claim_token": "clm_...",
"claim_token_expires": "2026-05-21T17:26:32.915Z",
"post_claim_scopes": ["read", "write"]
}
```
### Claim Ceremony Initiation (anonymous → POST /agent/identity/claim)
```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://auth.example.com/login?return_to=...",
"interval": 5
}
}
```
### Token Exchange Response (POST /oauth2/token — JWT-bearer grant)
```json
{
"access_token": "<token>",
"token_type": "Bearer",
"expires_in": 3600,
"scope": "read write"
}
```
### Claim Polling Success (POST /oauth2/token — claim grant)
```json
{
"access_token": "<token>",
"token_type": "Bearer",
"expires_in": 3600,
"scope": "read write",
"identity_assertion": "<service-signed-jwt-v2>",
"assertion_expires": "2026-05-21T18:31:25.994Z"
}
```
---
## Error Response Shape
```json
{
"error": "<error_code>",
"error_description": "<human-readable description>"
}
```
---
## Revocation
Two independent layers:
### Credential Layer (RFC 7009) — Agent-Callable
```http
POST /oauth2/revoke
Content-Type: application/x-www-form-urlencoded
token=<access_token>&token_type_hint=access_token
```
Kills one access_token. 200 on success, idempotent. The `identity_assertion` remains intact — re-run Step 5 for a fresh access_token.
### Registration Layer (RFC 8935 SET delivery) — Provider-Driven
Provider POSTs a Security Event Token (`Content-Type: application/secevent+jwt`) to the service's `events_endpoint`. The service invalidates the identity_assertion and all derived access_tokens.
Agent discovers this when `/oauth2/token` returns `invalid_grant` — restart at Step 3.
### SET Payload
```json
{
"iss": "https://api.agent-provider.com",
"sub": "<opaque-user-identifier>",
"aud": "https://auth.example.com",
"jti": "<unique-identifier>",
"iat": 1716400000,
"events": {
"https://schemas.workos.com/events/agent/auth/identity/assertion/revoked": {}
}
}
```
### Processing
1. Verify signature against the issuer's JWKS (same trust path as ID-JAG verification)
2. Enforce `jti` uniqueness for replay protection
3. Find all identity_assertions and access_tokens for `(iss, sub, aud)` and invalidate them
4. Return 200 on success, 400 on verification failure

View File

@@ -0,0 +1,451 @@
# 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
```markdown
# 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`

View File

@@ -0,0 +1,162 @@
# Validation Rules
Complete ruleset for validating `auth.md` files against the protocol specification (v2, June 2026).
---
## Level: Basic (Offline)
### Structure Rules
| ID | Rule | Error Message | Severity |
|----|------|---------------|----------|
| S01 | Document starts with `# auth.md` heading | Missing required top-level heading `# auth.md` | 🔴 |
| S02 | Contains `## Step 1 — Discover` section | Missing required section: Step 1 — Discover | 🔴 |
| S03 | Contains `## Step 2 — Pick a method` section | Missing required section: Step 2 — Pick a method | 🔴 |
| S04 | Contains `## Step 3 — Register` section | Missing required section: Step 3 — Register | 🔴 |
| S05 | Contains `## Step 4 — Claim ceremony` section (if service_auth or anonymous with claim) | Missing required section: Step 4 — Claim ceremony (required when service_auth or anonymous claim is supported) | 🔴 |
| S06 | Contains `## Step 5 — Exchange the assertion` section | Missing required section: Step 5 — Exchange the assertion | 🔴 |
| S07 | Contains `## Step 6 — Use the access_token` section | Missing required section: Step 6 — Use the access_token | 🔴 |
| S08 | Contains `## Errors` section | Missing required section: Errors | 🔴 |
| S09 | Contains `## Revocation` section | Missing required section: Revocation | 🔴 |
| S10 | Sections appear in correct order (Step 1 → 2 → 3 → 4 → 5 → 6 → Errors → Revocation) | Sections are out of order. Expected sequence: Step 1, 2, 3, 4, 5, 6, Errors, Revocation | 🟡 |
| S11 | Intro declares real hostnames (resource server and auth server) | Intro should declare real hostnames for resource and auth servers | 🟢 |
### Field Rules
| ID | Rule | Error Message | Severity |
|----|------|---------------|----------|
| F01 | Step 1 contains at least one fenced JSON block with `resource` field | Step 1 must include Protected Resource Metadata JSON with `resource` field | 🔴 |
| F02 | PRM JSON contains `resource_name` | Missing `resource_name` in PRM — agents need this for consent prompts | 🟡 |
| F03 | PRM JSON contains `resource_logo_uri` | Missing `resource_logo_uri` — recommended for consent UX | 🟢 |
| F04 | JSON metadata contains `authorization_servers` array | Missing `authorization_servers` — agents can't discover the auth endpoint | 🟡 |
| F05 | JSON metadata contains `scopes_supported` array with ≥1 scope | Missing or empty `scopes_supported` | 🟡 |
| F06 | AS metadata contains `agent_auth` block | Missing `agent_auth` block in Authorization Server metadata | 🔴 |
| F07 | `agent_auth` contains `identity_endpoint` | Missing `identity_endpoint` in agent_auth block | 🔴 |
| F08 | `agent_auth` contains `identity_types_supported` with ≥1 type | Missing or empty `identity_types_supported` | 🔴 |
| F09 | If `identity_assertion` in identity_types_supported, `assertion_types_supported` must exist | Declared `identity_assertion` support but missing `assertion_types_supported` | 🟡 |
| F10 | Step 3 contains at least one `POST /agent/identity` request example | Step 3 must include at least one registration request example | 🔴 |
| F11 | Errors section contains a table with `Code`, `Where`, and `What to do` columns | Errors section must contain a table with Code, Where, and What to do columns | 🟡 |
| F12 | `agent_auth` contains `skill` pointing to auth.md URL | Missing `skill` field in agent_auth — recommended for discoverability | 🟢 |
| F13 | `agent_auth` contains `events_supported` | Missing `events_supported` — recommended for revocation support | 🟢 |
| F14 | AS metadata contains `token_endpoint` | Missing `token_endpoint` — required for token exchange and claim polling | 🔴 |
| F15 | AS metadata contains `revocation_endpoint` | Missing `revocation_endpoint` — required for credential-layer revocation | 🟡 |
| F16 | AS metadata contains `grant_types_supported` with both required URNs | Missing or incomplete `grant_types_supported` — must include jwt-bearer and claim grant URNs | 🟡 |
| F17 | If `service_auth` or `anonymous` in identity_types_supported, `claim_endpoint` must exist | service_auth/anonymous requires `claim_endpoint` for ceremony initiation | 🟡 |
| F18 | `agent_auth` contains `events_endpoint` | Missing `events_endpoint` — recommended for registration-layer revocation | 🟢 |
| F19 | Step 5 contains `POST /oauth2/token` with jwt-bearer grant | Step 5 must document the token exchange via /oauth2/token | 🔴 |
### Consistency Rules
| ID | Rule | Error Message | Severity |
|----|------|---------------|----------|
| C01 | Flows documented in Step 3 match `identity_types_supported` in metadata | Mismatch: Step 3 documents flows not declared in metadata (or vice versa) | 🟡 |
| C02 | If only identity_assertion flow without claim: Step 4 may be absent | Step 4 present but only identity_assertion flow without claim is supported | 🟡 |
| C03 | `identity_endpoint` path matches the POST path in Step 3 examples | Registration endpoint in metadata doesn't match the POST path in Step 3 | 🟡 |
| C04 | `claim_endpoint` path matches the POST path in Step 4 examples (if present) | Claim endpoint in metadata doesn't match the POST path in Step 4 | 🟡 |
| C05 | Scopes in response examples are subset of `scopes_supported` | Response example contains scopes not listed in `scopes_supported` | 🟡 |
| C06 | `resource` URL is consistent across all JSON blocks | Different `resource` URLs found in metadata blocks — must be consistent | 🟡 |
| C07 | All URLs use HTTPS scheme | Non-HTTPS URL found — all endpoints must use HTTPS | 🟡 |
| C08 | Error codes in table match standard protocol error codes (see list below) | Non-standard error code found | 🟡 |
| C09 | `aud` in ID-JAG examples matches the `resource` from PRM | ID-JAG `aud` example doesn't match the resource URL | 🟡 |
| C10 | `assertion_types_supported` includes types used in Step 3 examples | Step 3 uses assertion types not declared in metadata | 🟡 |
| C11 | `token_endpoint` in metadata matches the `/oauth2/token` path used in Steps 4c and 5 | Token endpoint in metadata doesn't match the POST path in Steps 4c/5 | 🟡 |
| C12 | `grant_types_supported` includes `urn:ietf:params:oauth:grant-type:jwt-bearer` | Missing jwt-bearer grant in grant_types_supported — required for token exchange | 🟡 |
| C13 | `grant_types_supported` includes `urn:workos:agent-auth:grant-type:claim` (if claim ceremony exists) | Missing claim grant in grant_types_supported — required for ceremony polling | 🟡 |
### Format Rules
| ID | Rule | Error Message | Severity |
|----|------|---------------|----------|
| X01 | All JSON in fenced code blocks is valid JSON | Invalid JSON in fenced code block at section: {section} | 🟡 |
| X02 | HTTP request examples use valid HTTP method + path | Invalid HTTP request format. Expected: METHOD /path | 🟡 |
| X03 | No unreplaced placeholder patterns (`{{...}}`, `<your-...>`, `[YOUR_...]`) | Unreplaced placeholder found: {placeholder} | 🟡 |
| X04 | Fenced code blocks have language hint (`http`, `json`) | Code block missing language hint — agents use these to identify request shapes | 🟢 |
---
## Level: Full (Live)
All Basic rules, plus:
### Endpoint Rules
| ID | Rule | Error Message | Severity |
|----|------|---------------|----------|
| E01 | `GET {base_url}/.well-known/oauth-protected-resource` returns 200 with valid JSON | Protected Resource Metadata endpoint not reachable or returns invalid response | 🔴 |
| E02 | PRM response contains `authorization_servers` pointing to AS with `agent_auth` block | No `agent_auth` block discoverable through PRM → AS metadata chain | 🔴 |
| E03 | `GET {auth_server}/.well-known/oauth-authorization-server` returns 200 with valid JSON | Authorization Server metadata endpoint not reachable | 🔴 |
| E04 | AS metadata `agent_auth` block matches declarations in auth.md | Live AS metadata `agent_auth` block differs from auth.md declarations | 🟡 |
| E05 | `identity_endpoint` accepts POST (returns 400/401/422, not 404/405) | Registration endpoint returns 404 or 405 — not implemented | 🔴 |
| E06 | `claim_endpoint` accepts POST (if service_auth or anonymous supported) | Claim endpoint returns 404 or 405 — not implemented | 🔴 |
| E07 | `token_endpoint` accepts POST (returns 400/401, not 404/405) | Token endpoint returns 404 or 405 — not implemented | 🔴 |
| E08 | `revocation_endpoint` accepts POST (returns 200/400, not 404/405) | Revocation endpoint returns 404 or 405 — not implemented | 🟡 |
| E09 | API base URL returns 401 with `WWW-Authenticate` header containing `resource_metadata` | API does not return WWW-Authenticate header with resource_metadata on 401 | 🟡 |
---
## Standard Protocol Error Codes
Complete list of error codes the Errors table should cover (per supported flows):
### Registration Endpoint (`/agent/identity`)
- `anonymous_not_enabled`
- `service_auth_not_enabled`
- `issuer_not_enabled`
- `invalid_request` (body shape, missing claims, signature, jti, aud, unverified identity)
- `interaction_required` (401) — ID-JAG matched account but no (iss,sub) delegation
- `login_required` (401) — auth_time missing or older than max_age
- `rate_limited` (429)
### Claim Endpoint (`/agent/identity/claim`)
- `invalid_claim_token`
- `claimed_or_in_flight`
- `claim_expired`
### Token Endpoint (`/oauth2/token`)
- `invalid_grant` — assertion expired/revoked/replayed
- `invalid_client`
- `unsupported_grant_type`
- `authorization_pending` — claim polling, user hasn't completed
- `expired_token` — user_code window closed
- `slow_down` — polling too fast
### All Endpoints
- `rate_limited` (429)
---
## Validation Report Format
```markdown
# Validation Report — auth.md
**File:** {path_or_url}
**Level:** {basic|full}
**Date:** {timestamp}
## Summary
- ✅ {n} rules passed
- ❌ {n} rules failed
- ⚠️ {n} warnings
## Structure
| Status | ID | Rule |
|--------|-----|-------|
| ✅ | S01 | Heading `# auth.md` present |
| ❌ | S06 | Step 5 section missing |
## Fields
...
## Consistency
...
## Format
...
## Endpoints (full only)
...
```