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
}
Discovery flow diagram showing agent fetching PRM then AS metadata

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/json

Dispatch by Type

typeFlowDescription
agent_verifiedAgent VerifiedAgent proves identity via ID-JAG credential
user_claimedUser ClaimedAnonymous 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"
}
Agent Verified flow: agent presents ID-JAG, provider validates signature, returns access token

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"
}
User Claimed anonymous flow: agent registers without email, receives claimable token

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]"
}
User Claimed email flow: agent provides email, provider sends OTP, agent completes claim

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:

  1. Check the WWW-Authenticate header for the PRM URL.
  2. Re-fetch the PRM to confirm the AS endpoint.
  3. Re-register using the same flow (agent_verified or user_claimed).
  4. 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"
}
CodeWhereWhat to do
invalid_signatureRegistration (agent_verified)The ID-JAG signature failed validation. Ensure the credential is correctly signed by a trusted issuer.
replay_detectedRegistrationThe credential has already been used. Generate a fresh ID-JAG with a new jti.
audience_mismatchRegistrationThe audience field does not match the resource server. Use the value from PRM.
credential_expiredRegistrationThe ID-JAG exp claim is in the past. Issue a new credential.
anonymous_not_enabledRegistration (user_claimed, anonymous)This provider does not support anonymous registration. Use verified_email mode instead.
verified_email_not_enabledRegistration (user_claimed, verified_email)This provider does not support email-based registration. Check AS metadata for supported types.
issuer_not_enabledRegistration (agent_verified)The ID-JAG issuer is not in the provider's trust list. Contact the provider to add your issuer.
unsupported_credential_typeRegistrationThe type field is not recognized. Check AS metadata for credential_types_supported.
rate_limitedAny endpointToo many requests. Respect Retry-After header and implement exponential backoff.
invalid_claim_tokenClaim ceremonyThe claim token is malformed or does not exist. Re-register to obtain a new one.
otp_invalidClaim ceremony (verify)The OTP code is incorrect. Prompt the user to re-enter. Limited attempts before lockout.
otp_expiredClaim ceremony (verify)The OTP has expired. Call /agent/claim/trigger again to send a new OTP.
claim_expiredClaim ceremonyThe claim window has closed. Re-register to get a new claimable credential.
previously_claimedClaim ceremonyThis 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."
}