Skip to main content
BLOG

OAuth discovery for AI agents: RFC 9728 and RFC 8414

OAuth discovery for AI agents: a 401 points to RFC 9728 resource metadata, then RFC 8414 server metadata. 2 JSON files, 8 pitfalls, and when to skip it.

By IMozzUpdated 2026-10-26
OAuth discovery for AI agents — aiSiteReady

OAuth discovery for AI agents is a chain of two metadata documents: a protected API answers 401 and points to its RFC 9728 resource metadata, which names an authorization server, whose RFC 8414 metadata lists the endpoints the agent needs. If nothing on your site needs an access token, there is nothing to discover, and you should publish neither document.

This is the layer after making your site agent-callable. An MCP Server Card or an API catalog tells an agent where your API lives, not how to sign in. A protected API has to answer that in a form a program can read. Support is up to each client: MCP makes it mandatory for MCP clients, but another agent may use credentials a person configured instead.

Key takeaways

  • The chain: a 401 with WWW-Authenticate: Bearer resource_metadata="…", then /.well-known/oauth-protected-resource (RFC 9728, April 2025), then /.well-known/oauth-authorization-server (RFC 8414, June 2018) or OpenID Connect Discovery.
  • Each document names itself (resource, issuer). If the value isn't identical to the identifier the URL was built from, the client MUST NOT use the data.
  • MCP's authorization spec (revision 2026-07-28, current on 2 October 2026) requires servers that use authorization to implement RFC 9728 (MCP) and clients to refuse an authorization server without PKCE (security considerations).
  • Public read-only sites, first-party logins, and API-key APIs need neither document. Faking them is worse than a clean 404.
  • Our own observation from aiSiteReady's check code: both OAuth checks become not applicable when no OAuth signal is found, and a bare MCP card isn't one.

What is OAuth discovery, and why do AI agents need it?

OAuth discovery is how a client learns, from the server itself, where to get a token: RFC 9728 describes the protected resource, RFC 8414 the authorization server. An agent at a protected endpoint has no browser tab, docs page, or onboarding email, only an HTTP response, so that response must carry the next step. The resource document lists its identifier, trusted authorization servers, and scopes; the server document lists its issuer, endpoints, and capabilities (RFC 9728, RFC 8414).

RFC 9728 spells out the sequence. The client calls without a token. The resource answers with a WWW-Authenticate header carrying the metadata URL. The client validates that metadata, builds the authorization server's metadata URL from a listed issuer, fetches it, completes the flow, and retries with the token (RFC 9728, Section 5).

Sequence diagram: a 401 points the agent to RFC 9728 resource metadata, then RFC 8414 server metadata, then a PKCE S256 code flow returns a token.

Step 1 is the header on the 401. It also carries a scope parameter (RFC 6750, Section 3), which MCP servers SHOULD include (wrapped for display):

GET /mcp HTTP/1.1
Host: api.example.com

HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer resource_metadata="https://api.example.com/.well-known/oauth-protected-resource/mcp",
                  scope="scans:read"

RFC 9728 defines one new parameter, resource_metadata, and its own example is a bare Bearer resource_metadata="…" (RFC 9728, Section 5.1). MCP accepts the header, a well-known URI, or both. Clients must support both and prefer the header's URL (MCP, discovery).

What goes in the protected resource metadata (RFC 9728)?

One field is required, resource. The rest are optional or recommended in the RFC, and that is where MCP is stricter (RFC 9728, Section 2).

FieldRFC 9728 statusWhat it tells the agent
resourceREQUIREDThe resource identifier: an https URL with no fragment
authorization_serversOPTIONAL (MCP: required, at least one)Issuers that can issue tokens for this resource
scopes_supportedRECOMMENDEDScope values used to request access
bearer_methods_supportedOPTIONALHow a token may be sent: header, body, or query
resource_nameRECOMMENDEDHuman-readable name

The MCP specification says the document MUST include authorization_servers with at least one entry, and that clients send tokens in the Authorization header, never the query string (MCP, discovery, MCP, authorization). So ["header"] is the honest bearer_methods_supported for an MCP server. A minimal document for https://api.example.com/mcp:

{
  "resource": "https://api.example.com/mcp",
  "authorization_servers": ["https://auth.example.com"],
  "scopes_supported": ["scans:read", "scans:write"],
  "bearer_methods_supported": ["header"],
  "resource_name": "Example API"
}

It lives at https://api.example.com/.well-known/oauth-protected-resource/mcp. The suffix goes between host and path, after removing any terminating slash that follows the host. A path-less identifier uses the bare /.well-known/oauth-protected-resource.

What goes in the authorization server metadata (RFC 8414)?

RFC 8414 requires issuer and response_types_supported. The endpoints are required unless no supported grant uses them (RFC 8414, Section 2).

FieldRFC 8414 statusWhy an agent cares
issuerREQUIREDhttps URL, no query or fragment; the identity the document must match
authorization_endpointREQUIRED unless no grant uses itWhere the user approves access
token_endpointREQUIRED unless only implicitWhere the code becomes a token
response_types_supportedREQUIRED["code"] for the authorization code flow
grant_types_supportedOPTIONALOmitted means ["authorization_code", "implicit"]
scopes_supportedRECOMMENDEDScopes the agent may request
registration_endpointOPTIONALDynamic Client Registration (RFC 7591)
code_challenge_methods_supportedOPTIONALOmitted means no PKCE support
{
  "issuer": "https://auth.example.com",
  "authorization_endpoint": "https://auth.example.com/authorize",
  "token_endpoint": "https://auth.example.com/token",
  "registration_endpoint": "https://auth.example.com/register",
  "client_id_metadata_document_supported": true,
  "scopes_supported": ["scans:read", "scans:write"],
  "response_types_supported": ["code"],
  "grant_types_supported": ["authorization_code", "refresh_token"],
  "code_challenge_methods_supported": ["S256"],
  "token_endpoint_auth_methods_supported": ["none"]
}

Two details matter. code_challenge_methods_supported is the only way a client learns about PKCE support. And agents are usually public clients: none is the RFC 7591 value for that, while an omitted token_endpoint_auth_methods_supported defaults to client_secret_basic. Keep registration_endpoint only if you still run Dynamic Client Registration.

Where does the document live when the issuer has a path?

For an issuer of https://auth.example.com/tenant1, the document lives at /.well-known/oauth-authorization-server/tenant1: RFC 8414 inserts the suffix between the host and the issuer's path. OpenID Connect Discovery appends it instead. RFC 8414 Section 5 documents the difference and allows publishing at both locations during a transition (RFC 8414). MCP clients try, in order: /.well-known/oauth-authorization-server/tenant1, /.well-known/openid-configuration/tenant1, /tenant1/.well-known/openid-configuration (MCP, discovery).

How the well-known URL is built: the suffix goes between host and path; for an issuer with a path, MCP clients try three URLs in a fixed order.

Serve both documents as 200 OK with Content-Type: application/json; each RFC requires it. RFC 9728 adds that normal HTTP caching applies and suggests Cache-Control with max-age (RFC 9728, Section 7.10). Neither RFC mentions CORS, but a browser-based client reading the documents cross-origin needs Access-Control-Allow-Origin (MDN). For public metadata without secrets, * is a reasonable choice. That is my recommendation, not a requirement. On nginx:

location = /.well-known/oauth-protected-resource/mcp {
    default_type application/json;
    add_header Cache-Control "public, max-age=3600" always;
    add_header Access-Control-Allow-Origin "*" always;
    alias /srv/metadata/oauth-protected-resource.json;
}

How does MCP authorization use these documents?

MCP requires this chain from every HTTP server that uses authorization: the server publishes RFC 9728 metadata, its authorization server publishes RFC 8414 or OpenID Connect metadata, and clients follow both. Authorization itself stays optional. Revision 2026-07-28 is listed as current on the versioning page as of 2 October 2026 and sets these rules (MCP, authorization).

RoleRequirement in revision 2026-07-28
MCP serverMUST implement RFC 9728, with authorization_servers listing at least one server
MCP serverMUST advertise it through resource_metadata in a 401, or at a well-known URI
Authorization serverMUST provide RFC 8414 metadata or OpenID Connect Discovery
MCP clientMUST support both mechanisms; header URL first, else well-known URIs
MCP clientMUST reject metadata whose issuer differs from the identifier used to build the URL
MCP clientMUST verify PKCE from code_challenge_methods_supported and refuse if absent; MUST use S256 when capable

Sources: discovery and security considerations. The PKCE row is why code_challenge_methods_supported isn't decoration: omit it and a compliant client stops before the user sees a consent screen.

Registration changed between revisions. In 2025-11-25, Client ID Metadata Documents were a SHOULD and Dynamic Client Registration (RFC 7591) a MAY kept for compatibility (2025-11-25). Revision 2026-07-28 deprecates Dynamic Client Registration and points new implementations at Client ID Metadata Documents, advertised with client_id_metadata_document_supported: true. Servers that still run Dynamic Client Registration keep advertising it through registration_endpoint (MCP, client registration).

OAuth 2.1 and Client ID Metadata Documents were still Internet-Drafts on 2 October 2026 (revisions 16 and 02; MCP cites mostly 13 and 00), so this part may change again.

The Server Card guide covers where the MCP endpoint is and which transport it speaks. The OAuth documents say how to get a token for it. Publish the card either way, and the OAuth documents only if the server is protected.

What mistakes break OAuth discovery?

Most failures are an identifier that doesn't match or a response that looks like success but isn't.

MistakeWhat goes wrongFix
issuer differs from the issuer behind the URL (trailing slash, http, a proxy rewriting the host)Clients must not use it (RFC 8414 Section 3.3); aiSiteReady reports issuer_mismatch, scored PARTIALBuild issuer and the URL from one configured value
resource missing, or not the URL the client asked forRequired (RFC 9728 Section 2); a mismatch must not be used (Section 3.3)Emit the canonical resource URL
Endpoints that 404 or redirect to a login pageDiscovery works, then the flow dies at authorization_endpoint or token_endpointcurl every URL you list; drop registration_endpoint if you don't run it
200 with an HTML page for a missing well-known URL (single-page-app fallback)A soft 404: it looks like success, then fails JSON parsing. The scanner records an unsupported media type, never metadataServe the JSON before the fallback route; return a real 404 when there is nothing to publish
Wrong Content-Type such as text/plainBoth RFCs say application/json; the scanner flags unexpected_content_type on resource metadataSet the type explicitly, as in the nginx block
code_challenge_methods_supported absentMeans no PKCE; MCP clients refuse to proceedAdvertise ["S256"], but only if the server enforces PKCE
code_challenge_methods_supported without S256 (only plain, say)MCP clients must use S256 when they can; aiSiteReady scores the server PARTIALSupport and advertise S256
Suffix appended for oauth-authorization-serverNot found: RFC 8414 inserts, only OpenID Connect Discovery appendsFollow the lookup order above

Exact means exact. RFC 8414 compares the two strings code point by code point, with no normalization, so https://auth.example.com and https://auth.example.com/ are different values (RFC 8414, Section 4). aiSiteReady currently expects a root issuer without a trailing slash, so a provider that uses one (Auth0 publishes https://<tenant>.auth0.com/) is reported as issuer_mismatch, even though a client that built the URL from that exact string accepts it.

How do you verify your OAuth discovery?

Verify from outside, the way an agent does: one request for the challenge, one for each document, against the public URLs.

# 1. The challenge: does the protected endpoint point at its metadata?
curl -sS -D - -o /dev/null https://api.example.com/mcp | grep -i -E '^(HTTP|www-authenticate)'
# 2. The resource metadata: status, content type, body
curl -sS -D - https://api.example.com/.well-known/oauth-protected-resource/mcp
# 3. The authorization server named in authorization_servers
curl -sS -D - https://auth.example.com/.well-known/oauth-authorization-server

The first command should print something like this (wrapped for display):

HTTP/2 401
www-authenticate: Bearer resource_metadata="https://api.example.com/.well-known/oauth-protected-resource/mcp",
                  scope="scans:read"

The other two should return 200 with application/json.

Then check the identities, where most real breakage hides. This CI smoke test starts from the resource URL, not the 401, then follows the chain: it applies the path-insertion rule, tries the OpenID Connect locations if the RFC 8414 one fails, and compares resource and issuer exactly:

#!/usr/bin/env bash
set -euo pipefail
RESOURCE="${1:?usage: check-oauth.sh https://api.example.com/mcp}"
origin()  { sed -E 's#^(https://[^/]+).*#\1#' <<<"$1"; }
# urlpath drops a trailing slash, as the aiSiteReady scanner does; RFC 9728 keeps it
urlpath() { local p; p="$(sed -E 's#^https://[^/]+##' <<<"$1")"; echo "${p%/}"; }

PRM="$(curl -sSf -H 'Accept: application/json' \
  "$(origin "$RESOURCE")/.well-known/oauth-protected-resource$(urlpath "$RESOURCE")")"
jq -e --arg r "$RESOURCE" '.resource == $r' <<<"$PRM" >/dev/null \
  || { echo 'FAIL: resource does not match the URL'; exit 1; }

ISSUER="$(jq -er '.authorization_servers[0]' <<<"$PRM")"
AS="$(curl -sf -H 'Accept: application/json' \
    "$(origin "$ISSUER")/.well-known/oauth-authorization-server$(urlpath "$ISSUER")" \
  || curl -sf -H 'Accept: application/json' \
    "$(origin "$ISSUER")/.well-known/openid-configuration$(urlpath "$ISSUER")" \
  || curl -sSf -H 'Accept: application/json' \
    "$(origin "$ISSUER")$(urlpath "$ISSUER")/.well-known/openid-configuration")"
jq -e --arg i "$ISSUER" '.issuer == $i' <<<"$AS" >/dev/null \
  || { echo 'FAIL: issuer mismatch'; exit 1; }
jq -e '.code_challenge_methods_supported | index("S256") != null' <<<"$AS" >/dev/null \
  || { echo 'FAIL: no S256'; exit 1; }
echo OK

Finally, the negative test, for a site with nothing to publish. A missing well-known URL should be a real 404, not a friendly page:

curl -sS -o /dev/null -w '%{http_code} %{content_type}\n' \
  https://example.com/.well-known/oauth-authorization-server
# expected: 404 ...   suspicious: 200 text/html (your app's fallback page)

Do you need OAuth discovery at all?

Often not. These documents describe an OAuth deployment, so without one there is nothing true to publish. Walk the tree before you write any JSON.

Decision tree: publish OAuth discovery metadata only if agents get Bearer tokens through OAuth; public sites and API-key APIs publish nothing.

Your situationRFC 9728 documentRFC 8414 document
Public, read-only content siteDon't publishDon't publish
Public API or MCP server with no sign-inDon't publishDon't publish
API keys or static tokens onlyDon't publish; document the keysDon't publish
First-party login for your own app onlyDon't publishDon't publish
Protected API, tokens from a hosted providerPublish, naming the providerThe provider serves it; check its URL
Protected API, you run the authorization serverPublishPublish

Don't publish placeholder metadata to turn a check green. Both RFCs exist so a client can discard a document that doesn't match its identifiers. A made-up authorization_servers entry sends an agent to a server that will never issue it a token, and a 404 is more honest and cheaper to debug.

The opposite trap exists too: aiSiteReady reads a bare WWW-Authenticate: Bearer on the scanned URL as an OAuth signal. If you send that header, add the resource_metadata pointer. If you have no OAuth, don't send it.

How does aiSiteReady check OAuth discovery?

Two checks in aiSiteReady's Protocol/API category cover this article: OAuth protected resource (oauth_protected_resource) and OAuth authorization server (oauth_authorization_server). Both belong to the oauth_discovery scan option. The category is off for the Content site profile (content_site) and carries 40% of the score under the API/Application profile (api_application) and 20% under the default All checks profile. The exact checks and weights live on the methodology page.

For the authorization server, the scanner tries /.well-known/oauth-authorization-server and /.well-known/openid-configuration, adds the insertion and appending variants when the scanned URL has a path, and follows Link headers on the scanned URL with the relations oauth-authorization-server or openid-configuration. Neither RFC defines those relations; the scanner accepts them as a hint.

For the resource, it tries the path-inserted and the bare well-known URL, then makes one anonymous request and reads the WWW-Authenticate challenge. As an SSRF guard it follows a resource_metadata URL only if it is HTTPS, on the same host and port, and under /.well-known/oauth-protected-resource. Then it fetches each issuer in authorization_servers, up to five, at the RFC 8414 and OpenID inserted locations (not yet the appended one), through the SSRF-safe fetcher. No remote JavaScript runs.

How does the scanner score each document?

A missing or invalid authorization_servers makes the resource document invalid, which is stricter than RFC 9728 and matches MCP. A wrong Content-Type is recorded as evidence and doesn't change the status; a missing scopes_supported keeps the document usable for the issuer chain but caps the result at PARTIAL. An agent-ready server is RFC 8414-valid, supports the authorization code flow, and advertises S256 and scopes_supported.

StatusResource checkAuthorization server check
PASSValid document, no resource mismatch, at least one agent-ready server, scopes_supported presentAt least one agent-ready server
PARTIALFound but: resource mismatch, no scopes, server incomplete or unvalidated, or invalidValid but no S256 or no scopes; issuer mismatch; or invalid
FAILNothing found, and an OAuth signal is presentSame
N/ANothing found, and no OAuth signalSame
UNKNOWNFetch failed or was bot-blocked (timeout, 403, 429)Same

When does a missing document count as not applicable?

When I wired this into the scan pipeline, the reasoning was simple. A confident "publish RFC 8414 metadata" failure on a site with no OAuth is a false positive that costs score for something the site should not do. So one step runs after every check and rewrites only the "nothing found" FAIL, and only when nothing signals OAuth. N/A then drops out of the score's denominator.

The signals are the API/Application profile, a WWW-Authenticate: Bearer header on the scanned URL, a Link header there with oauth-authorization-server or openid-configuration, and protected resource metadata or any Bearer challenge found by the resource check.

One consequence: the API/Application profile itself counts as a signal, so under it a missing document stays FAIL even on an API-key API. If you don't run OAuth, scan with the default profile or leave the OAuth discovery option out of a custom scope.

A bare MCP card, A2A card, or API catalog is deliberately not a signal, because an agent surface can run on API keys. PASS, PARTIAL, and UNKNOWN are never rewritten, so a real deployment still scores for what it publishes. Scan the resource's own URL: because resource must equal the scanned URL, a document for https://api.example.com/mcp found while scanning https://example.com scores PARTIAL with resource_mismatch, by design.

aiSiteReady publishes neither document. Its well-known routes serve the A2A card, the API catalog, the Agent Skills index, and the MCP Server Card, and nothing for OAuth. The public MCP server is documented with Auth: none and the REST API's rate limits. The optional accounts feature, off by default, is a first-party login with short-lived access tokens, not an authorization server third-party agents can register with.

On 3 October 2026 all four OAuth well-known URLs on www.ai-siteready.com returned 404 and the homepage sent no Bearer challenge, so neither check finds an OAuth signal: under the default profile both read N/A.

Run a free scan on your API or MCP URL to see which outcome each document gets, with the HTTP evidence behind it, in English, Ukrainian, or Russian. This guide is the sign-in half of making a site agent-callable; the API catalog guide covers how agents find the API first, and the 30-minute audit shows where this fits in a full pass. For the bigger picture, start with what AI agent readiness means.

The short version: if your API is protected, publish a 401 that points to RFC 9728 metadata, and an authorization server whose RFC 8414 metadata matches its issuer and advertises S256. If it isn't, publish nothing and let the 404 speak for you.

IMozz has 20 years in software development, with the past year spent building with LLMs. He builds aiSiteReady, a read-only scanner that checks whether AI agents can read and call a site. It server-renders its own content as a working example.

Frequently asked questions

Do I need OAuth discovery metadata if my site is public and read-only?
No. These documents describe an OAuth-protected resource and its authorization server. A public, read-only site has neither, so there is nothing true to publish. Don't add placeholder files to turn a check green: clients are told to discard metadata that doesn't match, and an invented authorization server sends agents somewhere that never issues tokens. A plain 404 is the honest answer.
What is the difference between RFC 9728 and RFC 8414 metadata?
RFC 9728 describes the protected resource, meaning your API or MCP server: its resource identifier, the authorization servers it trusts, and the scopes it understands. RFC 8414 describes the authorization server: its issuer, authorization and token endpoints, and capabilities such as PKCE. An agent reads the first to learn whom to trust, then the second to learn where to send the user.
Does MCP require both documents?
For protected servers, effectively yes. In MCP revision 2026-07-28, servers must implement RFC 9728 and authorization servers must offer RFC 8414 or OpenID Connect Discovery. But authorization itself is optional in MCP, so a public MCP server with no sign-in publishes neither. The requirement applies once you decide to protect the server with OAuth.
Where do I publish metadata when my issuer has a path?
RFC 8414 inserts the well-known suffix between host and path: an issuer of https://auth.example.com/tenant1 is described at /.well-known/oauth-authorization-server/tenant1. OpenID Connect Discovery appends it instead. MCP clients try the OAuth insertion URL, then the OpenID insertion URL, then the OpenID appended URL, so if you control the server, publish at the first location.