Agent discovery
An agent that hits a 401 on a protected resource can work out how to authenticate on its own. Two
hops of standard metadata get it to the endpoints; a Markdown document at /auth.md tells it, in
prose it can act on, which of those endpoints apply to it and in what order.
The same file is a short integration note for a human. That is the point of publishing the recipe as Markdown rather than as another JSON document: one artifact, two readers.
The chain
Section titled “The chain”HTTP/1.1 401 UnauthorizedWWW-Authenticate: Bearer resource_metadata="https://acme.oauth.work/.well-known/oauth-protected-resource", error="invalid_token"- Fetch the
resource_metadataURL — protected resource metadata (RFC 9728). It names the resource and its authorization servers. - Fetch the authorization server’s own metadata at
/.well-known/oauth-authorization-server(RFC 8414). - Read the
agent_authblock in that document, and fetch theskillURL it names.
Steps 1 and 2 are what any OAuth client does; see MCP authorization for the same walk from a resource server’s perspective. Step 3 is what makes the rest of it legible to an agent with no prior knowledge of this platform.
The agent_auth block
Section titled “The agent_auth block”{ "issuer": "https://acme.oauth.work", "agent_auth": { "skill": "https://acme.oauth.work/auth.md", "identity_types_supported": [] }}skill is the URL of the skill document on this issuer. Each tenant subdomain is its own issuer with
its own keys and its own copy, so the URL always points back at the host whose metadata you just
read — never the apex. See multi-tenancy.
identity_types_supported lists the identity types a self-registration endpoint would accept. It
is empty, deliberately. This platform has no agent identity endpoint: an agent authenticates as a
client, and it gets that client identity one of the three ways below. An agent that finds its
assertion type absent from this list is meant to stop looking for a registration endpoint and read
skill instead.
No identity_endpoint, claim_endpoint, or events_endpoint key appears in the block, because
there are no such endpoints. Advertising a URL an agent cannot use is worse than advertising nothing:
the agent follows it, gets a 404 mid-flow, and has no human in the loop to notice. If those keys
ever appear here, they will appear together with the endpoints that answer them.
The skill document
Section titled “The skill document”GET /auth.mdContent-Type: text/markdown; charset=utf-8Cache-Control: public, max-age=300, stale-while-revalidate=3600Access-Control-Allow-Origin: *Served at the root of every issuer, unauthenticated, and readable cross-origin — a browser-hosted
agent reaches it by following skill out of metadata it just fetched, so a document only some
clients can read would be no use. Every URL in it is absolute and already resolved for the host you
fetched it from:
curl -s https://acme.oauth.work/auth.md | head -20It contains a capability table (which RFCs are implemented), an explicit list of what is not implemented, and six numbered steps: discover, get a client identity, get an access token, call the API, refresh, revoke.
What it tells an agent to do
Section titled “What it tells an agent to do”The steps map onto the guides here:
| Step | Where it goes |
|---|---|
| Get a client identity | Dynamic registration, URL client IDs, or a provisioned agent |
| Get a token | Client credentials, code + PKCE, or token exchange |
| Call the API | Audience binding and per-tool scopes |
| Refresh and revoke | Token lifecycle |
| Read an error | Errors and rate limits |
Only a provisioned agent can hold tool scopes or be bound to an
organization, and the document says so — an agent that self-registers and then requests tool:search
would otherwise be left guessing why the scope came back empty.
What it says is unavailable
Section titled “What it says is unavailable”Three surfaces an agent might reasonably probe for are named as absent, so it stops instead:
- Self-registration by identity assertion. An assertion cannot be exchanged for a client identity, so it does not replace getting a client. It can be exchanged for an access token — see identity chaining — but only by a client already registered here, and only from an issuer the organization has explicitly trusted.
- A user-code claim ceremony, or device authorization. There is no user code and no polling
grant. A human joins the flow at
/authorize, where they sign in and consent directly. Freshness is controlled there too, withprompt=loginormax_age— see authorization code + PKCE. - Anonymous credentials. Every token is bound to a client identity.
Stating an absence is load-bearing for a reader with no human to escalate to. Without it, an agent
probes, reads the 404s as a transport fault, and retries.
Keeping it accurate
Section titled “Keeping it accurate”Where the document and the metadata disagree, the metadata wins — it is generated from the running configuration, and the document says as much. A test asserts that every grant type and every endpoint URL the metadata advertises also appears in the document, so the two cannot drift apart without the suite failing.