auth.md Protocol Specification v1 — May 2026
Source: github.com/workos/auth.md
1. Discovery
Agents discover auth.md support via the Protected Resource Metadata (PRM) document at /.well-known/oauth-protected-resource. This follows RFC 9728.
When an agent receives a 401 Unauthorized with a WWW-Authenticate header, it fetches the PRM to locate the Authorization Server (AS) metadata.
PRM Response
{
"resource": "https://api.example.com",
"authorization_servers": [
"https://auth.example.com"
],
"bearer_methods_supported": ["header"],
"scopes_supported": ["read", "write"]
}AS Metadata Response
The agent then fetches /.well-known/oauth-authorization-server on the AS:
{
"issuer": "https://auth.example.com",
"agent_registration_endpoint": "https://auth.example.com/agent/auth",
"agent_revocation_endpoint": "https://auth.example.com/agent/revoke",
"scopes_supported": ["read", "write"],
"credential_types_supported": ["agent_verified", "user_claimed"],
"claim_ceremony_supported": true
}2. Registration
All registration flows begin with a POST to the agent_registration_endpoint. The server dispatches on the type field in the request body.
Endpoint
POST /agent/auth
Content-Type: application/jsonDispatch by Type
| type | Flow | Description |
|---|---|---|
agent_verified | Agent Verified | Agent proves identity via ID-JAG credential |
user_claimed | User Claimed | Anonymous or email-based registration on behalf of a user |
3. Agent Verified Flow
The Agent Verified flow uses an ID-JAG (JSON Agent Grant) credential — a signed JWT issued by a trusted agent platform (e.g., OpenAI, Anthropic, Google). The provider validates the signature against a trust list of known issuers.
Request
POST /agent/auth
Content-Type: application/json
{
"type": "agent_verified",
"credential": "eyJhbGciOiJFUzI1NiIs...",
"audience": "https://api.example.com",
"scope": ["read", "write"]
}Successful Response (201)
{
"access_token": "authmd_live_abc123...",
"token_type": "Bearer",
"expires_in": 3600,
"scope": "read write",
"agent_id": "agent_01HXYZ...",
"issued_at": "2026-05-22T12:00:00Z"
}4. User Claimed Flow
The User Claimed flow allows an agent to register on behalf of a user. It supports two modes: anonymous start (no email required upfront) and email required (email provided at registration time).
Anonymous Start Request
POST /agent/auth
Content-Type: application/json
{
"type": "user_claimed",
"mode": "anonymous",
"audience": "https://api.example.com",
"scope": ["read"]
}Anonymous Start Response (201)
{
"access_token": "authmd_anon_xyz789...",
"token_type": "Bearer",
"expires_in": 86400,
"scope": "read",
"agent_id": "agent_01HABC...",
"claimable": true,
"claim_token": "claim_01HDEF...",
"claim_expires_at": "2026-05-29T12:00:00Z"
}Email Required Request
POST /agent/auth
Content-Type: application/json
{
"type": "user_claimed",
"mode": "verified_email",
"email": "[email protected]",
"audience": "https://api.example.com",
"scope": ["read", "write"]
}Email Required Response (202)
{
"status": "pending_verification",
"claim_token": "claim_01HGHI...",
"claim_expires_at": "2026-05-22T12:10:00Z",
"message": "OTP sent to [email protected]"
}5. Claim Ceremony
The Claim Ceremony upgrades an anonymous credential to a user-owned credential. It consists of three steps: trigger, OTP verification, and complete.
Step 1: Trigger Claim
POST /agent/claim/trigger
Content-Type: application/json
{
"claim_token": "claim_01HDEF...",
"email": "[email protected]"
}// Response 200
{
"status": "otp_sent",
"expires_at": "2026-05-22T12:10:00Z"
}Step 2: Submit OTP
POST /agent/claim/verify
Content-Type: application/json
{
"claim_token": "claim_01HDEF...",
"otp": "847291"
}// Response 200
{
"status": "verified",
"message": "OTP accepted. Call /agent/claim/complete to finalize."
}Step 3: Complete Claim
POST /agent/claim/complete
Content-Type: application/json
{
"claim_token": "claim_01HDEF..."
}// Response 200
{
"access_token": "authmd_live_claimed_456...",
"token_type": "Bearer",
"expires_in": 3600,
"scope": "read write",
"agent_id": "agent_01HABC...",
"user_email": "[email protected]",
"claimed_at": "2026-05-22T12:05:00Z"
}6. Credential Usage
After registration, agents use the issued access_token as a Bearer token in the Authorization header.
Authenticated Request
GET /api/resource
Authorization: Bearer authmd_live_abc123...Token Expiry
Tokens expire after the duration specified in expires_in (seconds). Agents MUST track expiry and re-register before the token expires.
401 Recovery
When a request returns 401 Unauthorized, the agent SHOULD:
- Check the
WWW-Authenticateheader for the PRM URL. - Re-fetch the PRM to confirm the AS endpoint.
- Re-register using the same flow (agent_verified or user_claimed).
- Retry the original request with the new token.
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer resource_metadata="https://api.example.com/.well-known/oauth-protected-resource"7. Errors
All error responses use a consistent JSON shape:
{
"error": "error_code",
"error_description": "Human-readable explanation"
}| Code | Where | What to do |
|---|---|---|
invalid_signature | Registration (agent_verified) | The ID-JAG signature failed validation. Ensure the credential is correctly signed by a trusted issuer. |
replay_detected | Registration | The credential has already been used. Generate a fresh ID-JAG with a new jti. |
audience_mismatch | Registration | The audience field does not match the resource server. Use the value from PRM. |
credential_expired | Registration | The ID-JAG exp claim is in the past. Issue a new credential. |
anonymous_not_enabled | Registration (user_claimed, anonymous) | This provider does not support anonymous registration. Use verified_email mode instead. |
verified_email_not_enabled | Registration (user_claimed, verified_email) | This provider does not support email-based registration. Check AS metadata for supported types. |
issuer_not_enabled | Registration (agent_verified) | The ID-JAG issuer is not in the provider's trust list. Contact the provider to add your issuer. |
unsupported_credential_type | Registration | The type field is not recognized. Check AS metadata for credential_types_supported. |
rate_limited | Any endpoint | Too many requests. Respect Retry-After header and implement exponential backoff. |
invalid_claim_token | Claim ceremony | The claim token is malformed or does not exist. Re-register to obtain a new one. |
otp_invalid | Claim ceremony (verify) | The OTP code is incorrect. Prompt the user to re-enter. Limited attempts before lockout. |
otp_expired | Claim ceremony (verify) | The OTP has expired. Call /agent/claim/trigger again to send a new OTP. |
claim_expired | Claim ceremony | The claim window has closed. Re-register to get a new claimable credential. |
previously_claimed | Claim ceremony | This credential has already been claimed by a user. It cannot be claimed again. |
8. Revocation
Tokens can be revoked by either the provider or the user.
Provider-Driven Revocation
Providers MAY revoke tokens at any time (abuse detection, policy change, user request via dashboard). The agent will receive a 401 on next use and must re-register.
User-Driven Revocation
Users can revoke agent access through the provider's dashboard or via the revocation endpoint:
POST /agent/revoke
Content-Type: application/json
Authorization: Bearer authmd_live_abc123...
{
"agent_id": "agent_01HXYZ..."
}// Response 200
{
"status": "revoked",
"agent_id": "agent_01HXYZ...",
"revoked_at": "2026-05-22T14:00:00Z"
}9. Security
Token Hashing
Providers MUST store tokens as SHA-256 hashes, never in plaintext. The raw token is returned only once at issuance. Lookup is performed by hashing the presented token and comparing against stored hashes.
OTP Entropy
OTP codes MUST be at least 6 digits generated from a cryptographically secure random source (minimum 20 bits of entropy). OTPs expire after a maximum of 10 minutes and are single-use.
Replay Protection
Providers MUST track the jti (JWT ID) of each ID-JAG credential and reject any credential whose jti has been seen before. The replay window SHOULD be at least as long as the credential's maximum lifetime.
Trust List
Providers maintain a trust list of approved ID-JAG issuers. Each entry contains the issuer's iss URL and public key (or JWKS endpoint). Only credentials from trusted issuers are accepted in the agent_verified flow.
// Example trust list entry
{
"issuer": "https://platform.openai.com",
"jwks_uri": "https://platform.openai.com/.well-known/jwks.json",
"enabled": true,
"added_at": "2026-01-15T00:00:00Z"
}10. Rate Limiting
Providers MUST implement rate limiting to prevent abuse. Two layers are recommended:
Per-IP Rate Limiting
Applies to all unauthenticated endpoints (registration, claim trigger). Recommended: 20 requests per minute per IP address.
Per-Tenant Rate Limiting
Applies to authenticated requests using a valid token. Limits are scoped to the agent_id. Recommended: 1000 requests per hour per agent.
Response Headers
HTTP/1.1 429 Too Many Requests
Retry-After: 30
X-RateLimit-Limit: 20
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1716400000
{
"error": "rate_limited",
"error_description": "Too many requests. Retry after 30 seconds."
}