Claude Skill

auth-md

Generate, validate, and explain `auth.md` files — the open protocol for AI agent registration. Use when making apps agent-ready, generating Protected Resource Metadata (RFC 9728), validating auth.md files, or implementing agent registration endpoints. Triggers on "auth.md", "agen

LLM Mart · 0 points · 9 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download fabricioctelles-skills-skills_auth-md-2b9f704.zip · 27 KB
Part of fabricioctelles/skills — 16 skills

Install

skills CLI npx skills add https://github.com/fabricioctelles/skills/tree/main/skills/auth-md
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install fabricioctelles-skills@llmmart
Git git clone https://github.com/fabricioctelles/skills.git

The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole fabricioctelles/skills collection as a plugin from our marketplace. Git is the plain clone.

Skill manifest

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 1–6, 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 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:

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

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.

Files (skills)
  • references
    • example-auth-md.md 12.3 KB
      # 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
      
    • implementation-guide.md 16.7 KB
      # 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
      
    • metadata-schema.md 12 KB
      # 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
      
    • protocol-template.md 14.9 KB
      # 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`
      
    • validation-rules.md 10 KB
      # 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)
      ...
      ```
      
  • SKILL.md 17.2 KB
    ---
    name: auth-md
    description: >
      Generate, validate, and explain `auth.md` files — the open protocol for AI agent registration.
      Use when making apps agent-ready, generating Protected Resource Metadata (RFC 9728),
      validating auth.md files, or implementing agent registration endpoints. Triggers on
      "auth.md", "agent registration", "agent auth", "make my app agent-ready", "ID-JAG",
      "identity_assertion", "service_auth", "protected resource metadata", "agentic registration".
    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 1–6, 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.
    

Comments (0)

Sign in to join the conversation.

No comments yet.

Reviews (0)

No reviews yet.

Related