What is auth.md? WorkOS's open agent registration protocol, explained

WorkOS just shipped auth.md, a markdown file your service hosts at /auth.md that tells AI agents how to register on behalf of a user. Here is what it actually does, what it builds on, and what it does not solve.

May 26, 202619 min read

What is auth.md? WorkOS's open agent registration protocol, explained.

On May 25, 2026, WorkOS published auth.md, an open protocol that lets an AI agent register for your service on behalf of the user it is acting for. The spec is hosted at github.com/workos/auth.md, MIT licensed, and not tied to any WorkOS product. Any service can publish an auth.md file. Any agent can read one.

This post is a plain-English read of what auth.md is, what it actually does, what existing standards it builds on, and what it leaves open. I have no horse in this race beyond running authsome, which sits on a different layer of the same problem.

The pitch in one sentence

A service hosts a markdown file at https://yourservice.com/auth.md that tells agents how to sign up. The agent reads the file, follows the steps, and ends up with a credential that is scoped, tied to a real user, and independently revocable.

The framing WorkOS uses: "An agent hits your API. It gets a 401. What does it do next?" Today the answer is "give up, or beg a human to create an account and copy a key." auth.md is a structured answer to that 401.

Why a new protocol at all

There are two things that should not be controversial:

  1. OAuth assumes a human at a browser. The traditional 3-legged flow ends with a redirect to a consent screen. Agents do not have eyeballs.
  2. API keys are the worst credential. They are unscoped, attributable to nobody in particular, and revocation is "rotate the key and pray your other agents don't break."

Existing fixes patch one side or the other. Device code flow handles "no browser, just a TTY." Dynamic Client Registration (DCR, RFC 7591) lets a client register itself. PKCE handles public clients without a secret. None of these cover the specific problem of "an autonomous agent shows up at a service it has never seen before and wants to register a credential on behalf of a user it can prove it is acting for."

That is the gap auth.md fills. It is less a new auth flow and more an opinionated stitching together of RFC 9728 (Protected Resource Metadata) and the IETF ID-JAG draft (Identity Assertion Authorization Grant) into a workflow specifically for agent self-registration.

How discovery works

When an agent makes an unauthenticated call to a protected endpoint, the service returns a 401 with a WWW-Authenticate header:

http
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer resource_metadata="https://yourservice.com/.well-known/oauth-protected-resource"

That is bog-standard RFC 9728. The agent follows the pointer, gets back JSON describing the protected resource, and finds a reference to the authorization server. The authorization server metadata at /.well-known/oauth-authorization-server carries an agent_auth block that points at the auth.md endpoints.

The new bit is the markdown file itself, at the service's root: https://yourservice.com/auth.md. It is human-readable, machine-parseable as markdown, and meant to be the canonical procedural recipe for the agent. Sample sections in the reference AUTH.md:

  • Step 1: Discover
  • Step 2: Pick a method
  • Step 3: Register
  • Step 4: Claim ceremony
  • Step 5: Use the credential
  • Errors
  • Revocation

The choice to put procedural steps in markdown rather than a JSON schema is the most interesting design call. It bets that LLM-driven agents will read the file the same way a human developer would, which is probably right for the next 24 months and possibly wrong after that. We will come back to this.

The two flows

auth.md defines two registration paths. Pick one based on whether the agent's provider can vouch for which user it is acting for.

Agent verified flow

This is the elegant one. It assumes the agent's provider (OpenAI, Anthropic, Cursor, etc.) supports minting an ID-JAG, a JWT that asserts "this agent is acting on behalf of this verified user, audience-restricted to your service."

The flow:

  1. Agent requests an ID-JAG from its provider, with aud set to the target service.
  2. Agent POSTs the ID-JAG to /agent-auth on the service.
  3. Service decodes the JWT header, looks up the issuer in its list of trusted providers, fetches the provider's JWKS, verifies the signature, validates aud / exp / iat / jti / client_id.
  4. Service either finds a matching user (via prior delegation, verified email, or just-in-time provisioning) or rejects.
  5. Service returns a bearer credential synchronously.

No browser. No human in the loop. No refresh token either, by design. When the credential expires, the agent goes back to its provider, mints a fresh ID-JAG, and re-registers. The lack of refresh tokens is a freshness guarantee, not an oversight. Each registration is a fresh attestation from the agent's provider that this agent is still acting on behalf of this user.

The catch: this only works if the agent's provider supports ID-JAG. Today, that is not most of them. Anthropic, OpenAI, Cursor are named in the WorkOS materials as the kind of provider that would mint these. As of launch I am not aware of any of them publicly shipping ID-JAG endpoints. Watch this space.

User claimed flow

This is the fallback for when no trusted provider attestation exists. It is OTP-based and comes in two shapes:

  • Email required. Agent calls /agent-auth/claim with the user's email. Service sends an OTP. User pastes the 6-digit code into the agent. Agent calls /agent-auth/claim/complete with the code. Credential is issued.
  • Anonymous start. Agent gets an immediate credential at very narrow "pre-claim" scopes (think: read-only, browse the catalog, see what is on offer). The user can later upgrade those scopes by going through the OTP ceremony.

The user-claimed flow still has a human in the loop at first registration, which arguably defeats the autonomy story. But it is a meaningful improvement over "go to the website and paste your API key into the agent's config file." The OTP binds the credential to a verified email, which means revocation and audit are real.

What a real auth.md looks like

The spec is 24 hours old, so there are no production examples to point at yet. To make the abstract flow concrete, here is what a minimal auth.md setup would look like for a hypothetical "TaskCo" notes API.

The 401 that kicks off discovery:

http
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer resource_metadata="https://api.taskco.com/.well-known/oauth-protected-resource"
Content-Type: application/json

{"error": "invalid_token", "error_description": "Missing or invalid bearer token"}

The Protected Resource Metadata at /.well-known/oauth-protected-resource:

json
{
  "resource": "https://api.taskco.com",
  "authorization_servers": ["https://auth.taskco.com"],
  "bearer_methods_supported": ["header"],
  "agent_auth": {
    "spec": "https://api.taskco.com/auth.md",
    "register_uri": "https://auth.taskco.com/agent-auth",
    "claim_uri": "https://auth.taskco.com/agent-auth/claim",
    "claim_complete_uri": "https://auth.taskco.com/agent-auth/claim/complete",
    "trusted_providers": [
      "https://platform.openai.com",
      "https://api.anthropic.com",
      "https://cursor.com"
    ],
    "scopes_supported": ["tasks.read", "tasks.write", "projects.read"],
    "pre_claim_scopes": ["tasks.read"]
  }
}

The auth.md itself, at https://api.taskco.com/auth.md:

markdown
# TaskCo Agent Registration

You are an agent. TaskCo supports agentic registration: discover, register,
(claim if needed), call the API, handle revocation.

## Scopes

| Scope          | Description                          | Pre-claim |
|----------------|--------------------------------------|-----------|
| tasks.read     | Read tasks the user can see          | Yes       |
| tasks.write    | Create, edit, complete tasks         | No        |
| projects.read  | Read project memberships             | No        |

## Registration

### identity_assertion + ID-JAG

Send a POST to `https://auth.taskco.com/agent-auth` with form-encoded body:

    grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer
    assertion=<your-id-jag-jwt>
    requested_scopes=tasks.read tasks.write

Response: `{ access_token, token_type, expires_in, granted_scopes }`.

### anonymous + claim

POST `/agent-auth` with `grant_type=anonymous` returns a pre-claim credential
limited to `tasks.read`. Upgrade by triggering an OTP via `/agent-auth/claim`,
then submitting the user's 6-digit code at `/agent-auth/claim/complete`.

## Errors

| Code               | Meaning                                | Recovery               |
|--------------------|----------------------------------------|------------------------|
| invalid_assertion  | ID-JAG signature failed                | Re-mint from provider  |
| provider_untrusted | Issuer not in trusted_providers list   | Use anonymous flow     |
| claim_required     | Cannot write until claimed             | Run claim ceremony     |
| otp_invalid        | OTP wrong or expired                   | Re-trigger claim       |

## Revocation

OIDC backchannel logout tokens revoke at the source. Users can also revoke
any agent at `https://taskco.com/settings/agents`.

Three things to notice. The agent_auth block in the metadata is the machine-readable contract that lets an agent automate the entire flow without ever reading the markdown. The auth.md markdown is the human-and-LLM-readable contract; an agent that hits a service for the first time can render it for the user, or read it directly to decide what to ask for. The scope table is the most load-bearing thing in the file. Most of the design effort in publishing auth.md goes into deciding what those scopes are and which are safe pre-claim.

Walking the verified flow as an agent

If you are building the consumer side, the pseudo-code below is the shape of the agent-side flow. Plain Python with httpx, not coupled to any specific broker.

python
from urllib.parse import urlparse
import httpx

def register_via_authmd(service_url: str, provider_sdk) -> dict:
    """Discover an auth.md flow and complete verified registration."""

    # 1. Trigger the 401 to harvest the discovery hint.
    resp = httpx.get(f"{service_url}/api/me")
    assert resp.status_code == 401
    metadata_url = parse_www_authenticate(resp.headers["WWW-Authenticate"])

    # 2. Fetch the Protected Resource Metadata.
    prm = httpx.get(metadata_url).json()
    agent_auth = prm["agent_auth"]

    # 3. Sanity-check: does the service trust our provider?
    if provider_sdk.issuer_url not in agent_auth["trusted_providers"]:
        raise RuntimeError("provider not trusted; fall back to user-claimed")

    # 4. Ask our provider for an audience-restricted ID-JAG.
    id_jag = provider_sdk.mint_id_jag(
        audience=prm["resource"],
        scopes_requested=["tasks.read", "tasks.write"],
    )

    # 5. POST it to the service's register endpoint.
    registration = httpx.post(
        agent_auth["register_uri"],
        data={
            "grant_type": "urn:ietf:params:oauth:grant-type:jwt-bearer",
            "assertion": id_jag,
            "requested_scopes": "tasks.read tasks.write",
        },
    )
    registration.raise_for_status()
    return registration.json()

What this elides:

  • parse_www_authenticate is a regex over resource_metadata="..." in the header. Five lines.
  • provider_sdk.mint_id_jag does not exist yet for any major agent provider as of the spec's release. Once OpenAI, Anthropic, or Cursor ship a method like this, it would return an audience-restricted JWT signed by the provider's key.
  • The returned access_token should not be returned to caller code in production. It should land directly in a credential broker's vault, which then injects it on outbound HTTP calls so the agent process never sees the raw value. Same pattern as existing OAuth and API-key flows: the agent's Authorization header is rewritten at the proxy boundary, not stored in os.environ.

Total agent-side surface area: about 30 lines. Most of the actual work is on the service side.

What auth.md builds on

This is the part of the spec I find most reasonable: it does not reinvent the auth primitives.

Building blockUsed for
RFC 9728 Protected Resource MetadataDiscovery: how the agent finds the auth.md file
WWW-Authenticate: Bearer resource_metadata=…The 401 hint that triggers discovery
draft-ietf-oauth-identity-assertion-authz-grant-03 (ID-JAG)The signed assertion that the agent is acting for a specific user
draft-ietf-oauth-identity-chaining-12The broader identity-chaining framework ID-JAG profiles
OIDC Backchannel LogoutRevocation: providers can push a logout token to invalidate credentials
Bearer tokensThe credential format the agent uses to call the API

Nothing here is novel. The novelty is in the composition. Treat auth.md as glue, not new chemistry.

Warning

ID-JAG is still an IETF draft (current version 03, expires 2026-10-24), not a finalized RFC. The companion identity-chaining draft (12) is on the Standards Track but also unfinalized. Auth.md leans heavily on both. Co-author Aaron Parecki (Okta) is leading a parallel "Cross App Access" implementation push, which suggests momentum, but if either draft changes shape before ratification, services and providers will need to upgrade in lockstep. Plan for spec churn through at least mid-2027.

Where this fits next to OAuth, DCR, and MCP

auth.md is being released into a crowded conversation. Worth being clear about what it overlaps with and what it does not.

vs OAuth 3-legged. OAuth assumes the user is at a browser when registration happens. auth.md assumes the user is not. They solve different setups.

vs Dynamic Client Registration (RFC 7591). DCR is "the agent registers itself as an OAuth client." auth.md is "the agent registers itself for a credential on behalf of a user." DCR gives you a client_id and client_secret. auth.md gives you an access token. You could plausibly use DCR as a sub-step of auth.md.

vs MCP authentication. MCP (Model Context Protocol) servers expose tools to LLMs and have been steadily working through their own OAuth story. WorkOS has a separate "MCP Auth" product, Auth0 shipped "Auth for MCP" GA on May 6, 2026, and the auth.md spec does not deeply address how an MCP server would publish or consume one. The two could converge, but as of launch they are separate threads.

vs AAuth (Agentic Authorization OAuth 2.1 Extension). An IETF draft from May 2025 that takes the same problem from a different angle: extend OAuth 2.1 with explicit grant types for agents acting on behalf of users, rather than introducing a discovery-and-registration protocol on top. AAuth has not seen wide adoption. The two could coexist (AAuth handles the grant; auth.md handles the registration), but as of today no one has shipped that combination publicly.

vs raw API keys. This is the actual comparison most teams will make. auth.md credentials are scoped to a user and revocable per-agent. API keys are not. If you control the service and you can put auth.md on it, the upgrade is straightforward.

What it does not solve

Three honest gaps.

1. It does not store the credential for the agent. Once the service returns the bearer token, the agent has to keep it somewhere. If "somewhere" is process.env.SERVICE_TOKEN, you have rebuilt the API-key problem with extra steps. The credential needs to land in a vault, get refreshed when it expires (via re-mint, in the verified flow), and never end up in a stack trace or a subprocess env. This is the layer authsome and similar agent-side brokers cover. auth.md is silent on it, which is correct: it is a service-side protocol, not a client-side one.

2. It does not work without provider support. The clean flow is "agent provider mints ID-JAG." If your agent runs against a model API that does not support ID-JAG, you are in the user-claimed flow with OTPs. That is a real improvement over copy-pasting API keys, but it is not the autonomous magic the agent-verified flow promises.

3. It depends on services publishing the file. This is the chicken-and-egg problem that kills most protocol launches. Until a meaningful number of services host /auth.md, agents have no reason to look for it. Until agents look for it, services have no reason to host it. WorkOS has a strong vendor position to push this through their existing customer base, which is probably why this spec has a real shot where similar ideas have not.

Status check: who is actually using this on launch day

A reality check on adoption, since the launch was 24 hours ago and the temptation is to assume momentum that does not exist yet.

Spec age. The first commit to github.com/workos/auth.md landed on 2026-05-20 (Madison Packer, WorkOS). Public launch was 2026-05-25. The codebase is five days old.

Named launch partners: Cloudflare and Firecrawl. Both were called out by Michael Grinich's launch announcement. Issue #1 on the repo, filed by Samuel Colvin (FastAPI, Pydantic), notes that neither partner has a working auth.md at launch: firecrawl.dev/auth.md returns a messy HTML page with the wrong content type, and cloudflare.com/auth.md is an auto-generated markdown summary of /auth with no relationship to the spec. So "partner" in the launch materials is more aspirational than load-bearing on day one.

First external PR. PR #4 is from Aaron Parecki at Okta. Parecki is the co-author of the ID-JAG IETF draft that the verified flow depends on. The PR is small (a link to the spec), but the cross-company involvement is the more interesting signal: the IETF authority on the underlying primitive is paying attention.

First protocol extension proposal. Issue #3 proposes a did_key identity type that would let DID-based agents (AT Protocol, UCAN, peer-to-peer) prove identity via Ed25519 challenge-response without any provider involvement. Useful signal that the spec is already getting real protocol proposals on day five, not just typo fixes.

Substantive critique. Colvin's issue raises four concerns worth surfacing rather than burying:

  1. The spec does not address SSO sign-in providers (GitHub, Google) as identity sources.
  2. No story for multi-region services (US/EU separation, residency).
  3. Significant overlap with existing OAuth RFCs (7591 Dynamic Client Registration, 8628 Device Authorization Grant, 6749 OAuth 2.0) that the spec does not engage with.
  4. He directly asks "how many of the words in this repo were written (or even reviewed) by a human." Whatever the answer, the question is the kind a serious adopter will ask, and WorkOS's response to issue #1 will signal how this spec is going to be maintained.

None of this is fatal. Specs grow up in public, and these are the right kinds of questions to be asking on day one. But they are open, and a service team deciding whether to invest implementation effort should track the issue tracker before committing.

How this changes things for credential brokers

I run authsome, which is on the agent side of the wire. So this is the question I cannot dodge: does auth.md make agent-side credential brokers obsolete?

Short answer: no, it complements them.

auth.md tells the agent how to get a credential. A credential broker tells the agent how to use a credential without leaking it. The broker still owns:

  • Encrypted storage. The bearer token needs to live somewhere safer than the agent's environment.
  • Refresh handling. Even with no refresh tokens, an agent needs to know when to re-mint an ID-JAG and trigger a re-registration. That logic belongs in a broker, not in every agent codebase.
  • Injection at the proxy boundary. The agent process should never hold the secret. The broker injects the Authorization: Bearer … header on the outbound call so the agent only ever sees a placeholder.
  • Audit. Every credential read should be logged. auth.md says nothing about this.

If anything, auth.md gives credential brokers a cleaner story for bootstrapping. Today, getting a credential into the broker usually means a manual authsome login github that opens a browser. If services start publishing auth.md, an agent broker could discover the file, walk through the flow programmatically, and land the resulting bearer in the vault with no user interaction beyond an OTP at first claim.

That is a thing authsome does not implement today. The spec is 24 hours old. But it is the natural next flow to add alongside pkce, device_code, dcr_pkce, and api_key.

What to actually do about this, today

Three audiences, three different action items.

If you run a service: the publish-in-an-afternoon checklist

The spec itself is small. The actual work is in the scope vocabulary, not the JWT plumbing. The order I would do it in:

Pre-work

  • Skim RFC 9728 (Protected Resource Metadata).
  • Skim the IETF ID-JAG draft so the issuer / subject / audience / client_id claims are not confusing later.
  • Clone github.com/workos/auth.md and read the reference handler in the examples/ directory.

Schema decisions (this is the actual hard part)

  • Map your existing permission system onto a scope vocabulary. Aim for 5 to 10 scopes total.
  • Decide which scopes are pre-claim-safe (issuable without OTP). Read-only and narrow is the right default.
  • Pick your audience identifier. Usually the canonical API base URL, e.g. https://api.taskco.com.
  • Pick your trusted ID-JAG providers. At launch the realistic list is OpenAI, Anthropic, and Cursor; expand as more ship endpoints.

Infrastructure

  • Add the /.well-known/oauth-protected-resource endpoint with an agent_auth block.
  • Add or extend /.well-known/oauth-authorization-server if you do not have one.
  • Host /auth.md at your service root. Static file is fine.
  • Make sure every 401 from your API includes WWW-Authenticate: Bearer resource_metadata="...".

Verified flow handler

  • Fetch and cache each trusted provider's JWKS with a 1-hour TTL.
  • Validate the ID-JAG JWT: signature, aud, exp, iat, jti (replay-protect), client_id.
  • Implement user resolution in order: prior (iss, sub) delegation record → verified email match → just-in-time provision or reject.
  • Issue a credential bound to (user_id, agent_client_id, granted_scopes).

Claim flow handler

  • OTP generation and email delivery. Reuse your existing transactional email path.
  • Anonymous registration that returns a pre-claim credential at limited scopes.
  • /agent-auth/claim/complete to upgrade scopes after OTP verification.

Revocation and audit

  • Subscribe to OIDC backchannel logout from each trusted provider so user-side revocations propagate to you.
  • Dashboard UI for users to revoke individual agents.
  • Audit log: registration.created, claim.requested, otp.generated, claim.confirmed, registration.expired, registration.revoked.

About a week of work for one engineer if your auth stack is in good shape, two if it is not. The payoff is forward-compatibility: agents that do not yet know to look for auth.md keep using whatever you have today; agents that do, get a much better experience.

If you build agents

Watch which model providers ship ID-JAG endpoints. Anthropic, OpenAI, and Cursor are the most likely first movers given they are named in the WorkOS launch materials. Until at least one of them ships, the verified flow is theoretical and you are in the user-claimed (OTP) path for everything.

When a provider does ship, the agent-side integration is small (see the pseudo-code above). The bigger lift is the vault: where does the returned access_token live, and how is it injected into outbound calls without the agent process seeing it.

If you operate agents in production

Nothing changes immediately. Credential hygiene is identical regardless of how the credential was originally minted: store secrets out of the agent process, inject at the boundary, audit the reads. auth.md gives you better bootstrapping; the rest of the lifecycle is unchanged.

The shorter version

WorkOS shipped a real, well-engineered piece of plumbing. It is one of the first serious attempts to make agent self-registration a first-class flow rather than a workaround. It builds on existing OAuth standards rather than reinventing them, which is the right move. The protocol's success depends on adoption from two sides at once (services and agent providers), which is hard, and on ID-JAG getting through the IETF process, which is not guaranteed.

In the meantime, the agent-side problem of "where does the credential live and how does the agent use it safely" is unchanged. auth.md is upstream of that question, not a replacement for it.

Priyansh Khodiyar

Priyansh Khodiyar

Maintainer

Works on authsome and the agentr.dev tooling.