Anatomy

The auth.md file

auth.md is a single Markdown file an application publishes at a well-known location to tell agents how to register. This page walks through where to host it, the sections it should contain, and how agents are expected to parse it.

Example

See the example auth.md for a complete template you can adapt.

Where to host it

Serve the file at your service root: https://service.example.com/auth.md. Agents discover it the same way humans do — by reading documentation, following SDK pointers, or by examining a WWW-Authenticate header you return on 401 responses. The structured Protected Resource Metadata (PRM) at /.well-known/oauth-protected-resource remains the authoritative source for endpoint URLs and supported flows; auth.md is the prose companion that points back at it.

Sections in order

A well-formed auth.md is organized as a numbered walkthrough an agent follows top to bottom. Here is what each section should cover:

Title and intro

A one-line title (# auth.md) followed by a short preamble addressed to the agent. State that the service supports agentic registration and name the real hostnames you'll use throughout the file — your resource server (the API base, e.g. https://api.yourservice.example.com) and your authorization server (where registration and claim live, e.g. https://auth.yourservice.example.com) — so the agent knows which host each example targets.

Discover

A two-hop discovery walkthrough. Show the agent how to:

  • Pull the PRM URL from the WWW-Authenticate: Bearer resource_metadata="…" header on a 401, or fall back to the conventional /.well-known/oauth-protected-resource path.
  • Fetch the Protected Resource Metadata and read resource, resource_name, authorization_servers, scopes_supported, and bearer_methods_supported.
  • Fetch the Authorization Server metadata at /.well-known/oauth-authorization-server and read both the standard OAuth fields — issuer, token_endpoint, revocation_endpoint, grant_types_supported — and the agent_auth block in full: skill, identity_endpoint, claim_endpoint, events_endpoint, identity_types_supported, identity_assertion.assertion_types_supported, and events_supported.

Document every field. The agent uses these to decide what to call next.

Pick a method

A decision tree mapping what the agent has on hand to the right registration method:

  • A session it can exchange for an ID-JAG bound to this service's audience → identity_assertion.
  • Only the user's email → service_auth (claim ceremony required).
  • Neither → anonymous (claim ceremony optional, deferred until the user wants to take ownership).

For identity_assertion, tell the agent to cross-check its assertion type against identity_assertion.assertion_types_supported before sending — if it isn't listed, pick another method or stop. For service_auth and anonymous, identity_types_supported is informational; the agent sends the body and falls back on the *_not_enabled error if the service opted out.

Register

One subsection per registration type your service accepts. For each: show the exact POST /agent/identity body and the success response in full. Use fenced http and json code blocks so the agent can extract the templates directly. Every path returns a service-signed identity_assertion (never a credential directly) that the agent exchanges at /oauth2/token for an access_token.

  • identity_assertion — when (iss, sub) is known or the user is JIT-provisioned, completes immediately; the response carries identity_assertion, assertion_expires, and scopes. When the verified email matches an existing account, the service returns 401 interaction_required with a claim block (first-link step-up); when auth_time is stale, 401 login_required.
  • service_auth — body is { "type": "service_auth", "login_hint": "<email>" }. Returns a claim_token and a claim block (user_code, verification_uri) but no assertion yet; the assertion is minted when the user completes the ceremony.
  • anonymous — returns a pre-claim identity_assertion plus a claim_token, so the agent can exchange for an access_token and start working at pre_claim_scopes immediately.

Claim ceremony

The device-authorization-shaped ceremony (RFC 8628 vocabulary), broken into three substeps. The service never emails a code — the user_code travels from the agent to the user, and the user authenticates on a page the service owns.

  • 4a. Get the ceremony materialsservice_auth registrations already have the claim block from the registration response. For anonymous, POST /agent/identity/claim with the claim_token and the user's email to mint a claim_attempt block.
  • 4b. Hand off to the user — the agent surfaces verification_uri and user_code in one message. The user opens the link, signs in (or signs up) at the service, lands on the claim page, and types the code there — not back to the agent.
  • 4c. Poll for completionPOST /oauth2/token with grant_type=urn:workos:agent-auth:grant-type:claim and the claim_token. Returns authorization_pending while waiting; on success a standard token response plus an identity_assertion. If the user_code window lapses (expired_token), re-call /agent/identity/claim for a fresh code.

Exchange the assertion

Show the RFC 7523 JWT-bearer exchange at token_endpoint: grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer&assertion=<identity_assertion>, with an optional resource to pin the token to the API. The response is a standard OAuth token envelope (access_token, token_type, expires_in, scope) with no refresh_token. The same assertion can be re-exchanged until it expires.

Use the access_token

Show that the access_token is presented as Authorization: Bearer <access_token>. Document refresh: when the access_token expires, re-exchange the same identity_assertion; when the assertion itself expires or /oauth2/token returns invalid_grant, restart at registration. There is no OAuth refresh_token — the two-step pattern (assertion → access_token) replaces it.

Errors

A table mapping each error code to the endpoint that returns it and the action the agent should take. Cover the /agent/identity codes (invalid_issuer, invalid_signature, expired, replay_detected, invalid_audience, invalid_client_id, missing_verified_email, invalid_request, the *_not_enabled family, and the 401s interaction_required and login_required); the /agent/identity/claim codes (invalid_claim_token, claimed_or_in_flight, claim_expired); and the /oauth2/token OAuth codes (invalid_grant, invalid_client, unsupported_grant_type, and the claim grant's authorization_pending, expired_token, slow_down).

Revocation

Spell out the two independent layers that can kill what the agent holds:

  • Credential layer (RFC 7009, revocation_endpoint) — agent-callable. POST the access_token to /oauth2/revoke to kill it; the identity_assertion survives, so the agent can re-exchange for a fresh access_token.
  • Registration layer (RFC 8935 SET delivery, events_endpoint) — provider-driven. The provider POSTs a secevent+jwt to the service, invalidating the assertion and every access_token derived from it. The agent discovers this as invalid_grant on the next exchange and restarts at registration.

How agents parse it

Agents are expected to treat auth.md as both prose and structured data:

  • Scan headings to find sections relevant to the current step (registration vs. operation).
  • Read the Discovery section first to learn which flows are supported before committing to one.
  • Extract code blocks as templates for HTTP requests. Use fenced blocks with language hints (http, json) so the agent can identify request shapes without ambiguity.
  • Treat the PRM as authoritative if anything in this file conflicts with it. The PRM is the runtime source of truth.

Keep the file conservative in length and high in signal. Anything an agent doesn't need to register or operate against your API belongs in your main documentation, not in auth.md.