Skip to content

Authorization and client identity

Development snapshot — not a release

Core 0.1-dev · Source revision and matching artifacts.

How an application or an agent gets a token a pod will accept, and what a pod may conclude from it. What that token then permits is grants.md; this chapter stops at the point where a caller is identified.

Status: this text decides, and can still change. See ../../GOVERNANCE.md.

Profiles: OAuth 2.1 and RFC 6749, RFC 7636 (PKCE), RFC 7591 (Dynamic Client Registration), RFC 8252 §7.3 (native app redirect URIs), RFC 10017 (BCP 212, browser-based applications), RFC 9728 (Protected Resource Metadata), RFC 8414 (Authorization Server Metadata), OIDC Core 1.0 §3.1.2.1 (prompt). Error codes are index.md §5.

1. Flows

SPS-AUTH-001 — The only user-facing flow an implementation MUST support is OAuth 2.1 Authorization Code. An implementation MUST NOT accept the implicit flow or the resource owner password credentials grant.

SPS-AUTH-002 — An implementation MUST support the Client Credentials grant for service clients (§4), and MUST NOT offer it to any other class of client.

2. Client identity has three shapes

Prescribing the wrong one produces a client that cannot connect, so the distinction is a question about the client's role, not about its label.

Shape Who Registration
did:web:* an application with a stable HTTPS origin none — the identity is the origin
dyn:* an end-user client with no origin of its own RFC 7591 dynamic registration
service client a backend acting inside its own sandbox implementation-defined

did:web:* — origin-bound

SPS-AUTH-003 — A did:web: client identifier MUST be parsed locally and structurally. An implementation MUST NOT dereference it, MUST NOT fetch a DID document, and MUST NOT fetch a client-metadata document.

This is the strongest MUST NOT in the chapter, and the one an implementer is most likely to "correct". Fetching buys no security: the identifier is an assertion either way, and the only thing that makes it meaningful is that the authorization code goes nowhere but that origin — which is a local check. What fetching costs is an SSRF surface in the login path, a cache to keep coherent, and a third party whose downtime becomes a sign-in outage.

SPS-AUTH-004 — A did:web: client's redirect_uri MUST match the identifier's host and port.

SPS-AUTH-005 — A path-scoped identifier MUST be matched per path segment. did:web:example.org:mcp covers /mcp and /mcp/cb; it MUST NOT cover /mcp-other/cb.

A raw startsWith is wrong here for the same reason it is wrong for #manage (SPS-GRANT-007), and the consequence is worse: two services sharing a host would receive each other's authorization codes.

SPS-AUTH-006 — An identifier naming a loopback origin MUST be refused outside development, and the refusal MUST be evaluated before the host-and-port match that would otherwise accept it.

A did:web: is asserted, not issued. Without this, anyone may claim did:web:localhost%3A5173 and route a code to whatever happens to be listening on that port on the user's machine.

SPS-AUTH-007 — An implementation MUST NOT require a did:web: client to register.

dyn:* — dynamically registered

SPS-AUTH-008 — An implementation MUST offer RFC 7591 dynamic client registration at POST {pod}/_system/auth/register. A public client registered with token_endpoint_auth_method=none MUST receive an identifier prefixed dyn:.

SPS-AUTH-009 — A dyn: client registers with token_endpoint_auth_method=none. PKCE is therefore REQUIRED for it, and an implementation MUST reject an authorization request from a dyn: client that carries no code_challenge.

SPS-AUTH-010 — An implementation MUST render the consent screen for every interactive dyn: authorization request, regardless of existing grants and regardless of any prompt value other than none. Existing grants SHOULD arrive pre-selected, so that the common path is a single confirmation.

prompt=none is the exception, and it has to be: it means show nothing (SPS-AUTH-039), so "always show the screen" and "show no screen" cannot both hold. The resolution is already in SPS-AUTH-040 — a dyn: client never auto-grants, so prompt=none from one is consent_required, which is the non-interactive way of saying what the screen would have asked.

SPS-AUTH-011 — An implementation MUST NOT issue a service token to a dyn: client. A registration response with a dyn: identifier MUST NOT advertise client_credentials.

The asymmetry with did:web: is deliberate. A dynamic client reaches /authorize only because the person just triggered the flow, so a confirmation is what they expect; an origin-bound client reaches it from background-facing UI, where an unavoidable dialog is disruptive.

Service clients

How a service is registered and who assigns its grants are implementation choices. For example, a service can register without grants and receive access later. Its current grants determine access on each request (SPS-GRANT-002).

SPS-AUTH-013 — A service client's grants MUST consist only of per-context grants. An implementation MUST NOT accept public-read or an OIDC scope for one.

SPS-AUTH-014 — An implementation MUST refuse a service client's #manage root that sits at or above the pod's context namespace.

<pod>#manage and <pod>/_system#manage are both ancestors of every context on the pod, so the slash-delimited rule would make either of them match everything. Refusing the position rather than the two spellings somebody happened to think of is what makes this hold for the third spelling.

SPS-AUTH-015 — A service client's secret MUST be stored only as a slow one-way hash, and MUST be returned exactly once, at the moment it is minted.

SPS-AUTH-016 — An implementation MUST NOT let response timing reveal whether a client identifier is registered. A request naming an unknown client MUST perform equivalent work to one naming a known client.

SPS-AUTH-017 — A service token's sub MUST be the client identifier, and the token MUST be marked as belonging to the service class. A service token MUST NOT express a person.

A backend acting outside its own sandbox, or an operation whose attribution to a person other parties must trust, is user-delegated work and belongs in the Authorization Code flow. That is a consequence of SPS-AUTH-017 rather than a separate rule: there is no person in the token to attribute it to.

3. Redirect URIs

SPS-AUTH-018 — A redirect URI MUST be absolute, MUST NOT carry a fragment, MUST use https on any host, and MAY use http only on a loopback address.

Absolute and fragment-free are RFC 6749 §3.1.2 unchanged. The scheme half is not: §3.1.2.1 only says the redirection endpoint SHOULD require TLS, and this requirement raises that to a MUST with OAuth 2.1, then re-opens plain http for the loopback case RFC 8252 §7.3 describes. A reader distinguishing inherited behaviour from this specification's own should read the first two clauses as the profile and the last two as the deviation.

SPS-AUTH-056 — A redirect URI MUST NOT carry code, response or state as a query parameter.

RFC 6749 §3.1.2 permits a query component, and SPS-AUTH-055 requires an implementation to preserve it. This is where that permission stops, and the reason is the fragment's: those three names belong to the authorization response. An implementation that builds a response either overwrites what the address already carried — breaking the client that registered it — or leaves it in place, and then hands that client a value it cannot distinguish from the response's own — the code this server issues, or the state the client itself chose and RFC 6749 §4.1.2 obliges this server to return unchanged. A redirect URI registered as …/cb?state=x receives its own x back as the state of a request that carried none; one registered as …/cb?code=old can, depending on how the response is assembled, arrive carrying two codes. Which of the two such a client redeems is a property of its parser rather than of this protocol, and that is the situation the prohibition removes.

error and error_description are deliberately not prohibited. A client that registers them can only confuse itself, and no other client's response is reachable through them.

SPS-AUTH-057 — A query parameter counts as one of SPS-AUTH-056's prohibited names when its name, percent-decoded and matched case-sensitively, equals that name — whether or not the parameter carries a value.

One recognition rule, and three spellings decide whether an implementation has it right. ?CODE= is a different parameter and stays acceptable, because it is not the code any client reads; ?%63ode= reaches the client as code, so it is one; and ?code with no = is still that name. Decoding is per parameter name rather than over the whole query — decoding the query first would split a value such as ?next=a%26state%3Dx into a parameter that was never there.

SPS-AUTH-019 — An implementation MUST apply SPS-AUTH-018 and SPS-AUTH-056 at registration as well as at authorization, so that an address a login could never honour is refused when it is first offered rather than at first use.

SPS-AUTH-020 — A loopback redirect URI MUST be matched with its port ignored (RFC 8252 §7.3). A non-loopback redirect URI MUST be matched with its port significant.

4. Authorization request

SPS-AUTH-021 — An implementation MUST serve GET {pod}/_system/auth/authorize accepting response_type=code, client_id, redirect_uri, state, code_challenge, code_challenge_method, and OPTIONAL scope and prompt.

SPS-AUTH-022 — Where a code_challenge is present, S256 MUST be the only accepted method, and the method name MUST be compared case-sensitively (RFC 7636 §4.3). plain, s256 and an absent method MUST all be refused.

SPS-AUTH-023 — PKCE is RECOMMENDED but NOT REQUIRED for a did:web: client. An implementation MUST NOT reject a did:web: authorization request solely because it carries no code_challenge.

SPS-AUTH-024 — The standard delegation flow MUST NOT require a scope parameter naming contexts. The authorizing person selects contexts at consent time.

SPS-AUTH-025 — The implementation MUST handle authorization success and error responses per RFC 6749 §§4.1.2 and 4.1.2.1, including validation of the client's redirect_uri and, in responses sent to that client, echoing state unchanged only when supplied in the authorization request.

RFC 6749 §4.1.1 recommends state; it is not a required request parameter. Success carries code; a redirectable failure carries error, with optional error_description and error_uri. Both responses echo a supplied state; without it, neither adds one. For a missing or invalid client identifier, or a missing, invalid or mismatching redirect URI, the RFC prohibits automatically redirecting to the invalid client URI. How the server informs the resource owner, including a server-owned error page, its status and representation, is implementation-defined. Optional state does not remove the client's CSRF protection obligations under RFC 6749 §10.12.

Validation uses the did:web: origin/path rules (SPS-AUTH-004, SPS-AUTH-005) or the dyn: client's registered address. A did:web: client needs no registration (SPS-AUTH-007).

SPS-AUTH-055 — The response parameters MUST be added to the redirect URI's query component per RFC 6749 §4.1.2, preserving any component it already carries. An implementation MUST NOT construct the redirect by appending ? to the registered value.

A registered address may legitimately carry a query — https://app.example/cb?tenant=a is an ordinary redirect URI. Appending ?code=… to it produces a second ?, which puts the authorization response inside the value of tenant and loses the code.

Preserving a component the client chose is safe only because SPS-AUTH-056 keeps the response's own names out of it. Without that prohibition this requirement is what makes a registered code reach the client alongside the issued one.

SPS-AUTH-026 — Submitting a consent decision MUST require two independent things: a session established on the pod's own origin, and a token minted for that one consent screen and accepted only once.

Neither alone is enough, and the reason is not defence in depth for its own sake. A screen token lifted out of a page cannot be spent without the session; the session alone does not imply consent to anything. The single-use half is what stops a replay: a submission writes the selection as the grant set, so a replayable form could restore a selection the person has since narrowed.

6. Token endpoint

SPS-AUTH-027 — An implementation MUST serve POST {pod}/_system/auth/token accepting application/x-www-form-urlencoded, supporting authorization_code, refresh_token and client_credentials.

SPS-AUTH-028 — An access token MUST be a signed JWT whose issuer is the pod's base URL, and MUST carry the authorizing subject, the client identifier, and its scope set.

SPS-AUTH-029 — The scope claim MUST carry feature scopes only. An implementation MUST NOT place per-context grants in it — see SPS-GRANT-001.

SPS-AUTH-030 — An implementation MUST publish its signing keys at GET {pod}/_system/auth/jwks.json.

SPS-AUTH-031 — A scope parameter on a refresh exchange MAY narrow the token's feature scopes and MUST NOT widen them.

SPS-AUTH-032 — An implementation MUST reject a scope parameter on a client_credentials exchange with invalid_scope.

A service token cannot select a subset of the client's grants. Each request uses the client's current server-side grants (SPS-GRANT-002).

SPS-AUTH-062 — An implementation MUST refuse an authorization code whose consent decision has changed since the code was issued, counting an answer given under any URI equivalent to the one the code carries (SPS-AUTH-052) as such a change.

A code is a request, not an authority. It stays redeemable for minutes, and the client holds its verifier, so the window belongs to whoever answered first. Two cases show what the refusal is for. Somebody authorizes an application, then disconnects it before the code is exchanged: without this the code still mints the family the earlier answer allowed, for an application that now holds nothing. Somebody authorizes an application, then consents again on narrower terms while the first code is still unspent: a withdrawal reaches what the application holds, which at that moment is nothing, and the older code then buys exactly what the newer answer withheld. The second consent may run under another of the person's URIs, which is why the comparison is over the person and not over the subject the code names.

Refusing only a code whose application now holds nothing is too weak for the second case — after the reconnect it holds something again. What the code has to carry is which answer produced it.

Refresh tokens

SPS-AUTH-033 — Refresh tokens MUST be rotated. A refresh token belongs to a family seeded at code exchange, and on detected reuse of an already-rotated token the implementation MUST revoke the whole family.

RFC 10017 §6.3.2.3 leaves the choice between rotation and a sender-constrained token; this specification takes rotation.

SPS-AUTH-034 — A refresh token MUST NOT be stored in a form from which the presented value can be recovered.

SPS-AUTH-059 — An implementation MUST NOT make offline_access in the authorization request a condition of issuing a refresh token.

An MCP client cannot send a scope its authorization server never advertised, and refusing it a refresh token on that ground makes the credential the client's to ask for rather than the person's to grant. A client that does send the scope has said what it wants, not what it gets: what the parameter buys is a preselected control, where an implementation offers one.

offline_access is a sempods extension that borrows an OpenID Connect name and is requested bare. Pairing it with openid asks for something the pod authorization surface does not define: it has no openid scope or id_token, and the oidc module is an identity bridge that adds neither.

SPS-AUTH-063 — A code exchange that seeds a refresh-token family, and anything that revokes one, MUST each write before they check, so that whichever lands second sees the first: the exchange seeds and then re-reads, giving up the family where the consent behind the code no longer stands or a revocation has landed since; the revoking side persists and then sweeps.

Checking first leaves both sides able to miss each other. The exchange reads a decision the newer one is already replacing; the revocation sweeps a family that has not been seeded yet; and the application ends with the connection the person just declined, having satisfied SPS-AUTH-062 along the way. A forced reauthorization (SPS-MCP-012) loses the same race one turn later: recording its challenge changes no consent decision, so an exchange that re-read only the decision would seed after the sweep and keep the connection the review was there to end. This is SPS-GRANT-018's race, one step further down.

Abuse

SPS-AUTH-037 — An implementation SHOULD rate-limit the token endpoint, and where it does, it MUST key the first tier on the caller's network address rather than on a client identifier.

The order is the whole point. A client identifier arrives as a form parameter, so a caller can vary it, and counting it first hands out a fresh budget per invented name. Which name identifies the caller is also decided by the grant and not by what is present: client_credentials authenticates the Basic credential and never reads the form field; the other grants read the form field and ignore the header. Accepting whichever is present gives a caller two key spaces to choose from.

SPS-AUTH-038 — Where a request is refused for rate, the response MUST be 429 with Retry-After and the OAuth error slow_down.

7. prompt

SPS-AUTH-039 — An implementation MUST interpret prompt per OIDC Core 1.0 §3.1.2.1. Where the parameter is absent it MAY auto-grant on the strength of grants that already stand; where it carries consent it MUST NOT.

Auto-granting is what §3.1.2.1 leaves open, and the one way a pod could answer prompt=consent without showing anything.

SPS-AUTH-040 — prompt=none MUST succeed only where all three hold: the pod itself remembers the person, the client is not a dyn: client, and grants for that client survive. Otherwise the implementation MUST answer login_required or consent_required.

SPS-AUTH-041 — prompt=login MUST NOT be satisfied by an existing session. The person asked to prove themselves again, and the session is exactly what they asked to bypass.

A client SHOULD treat login_required and consent_required identically: fall back to a full interactive authorization. The distinction is diagnostic, not actionable.

8. public-read

SPS-AUTH-042 — scope=public-read MUST be accepted at the authorization endpoint, with or without an established identity. Without one, the issued token's subject MUST be a synthetic, opaque, per-request anonymous identifier.

SPS-AUTH-043 — Where an identity assertion is present but invalid or expired, the implementation MUST fail with access_denied. It MUST NOT silently downgrade the request to anonymous.

The precedence is: invalid assertion beats invalid scope beats absent assertion. A manipulated credential is a hard signal, and quietly treating its bearer as an anonymous visitor is how a tampered token becomes a successful request.

SPS-AUTH-044 — Where the pod has no public context, the implementation MUST answer consent_required rather than issue a token that can read nothing.

9. Identity

What a pod stores about a person, as opposed to how they signed in. How they signed in is the oidc module, and it is optional; this is not.

SPS-AUTH-049 — A pod MUST know a person only as a WebID URI. An implementation MUST NOT store a local user record as the identity a grant names, and MUST NOT accept a consumer's internal user identifier in place of one.

SPS-AUTH-050 — Where a person is introduced by an email address, the implementation MUST derive a deterministic identity URI from it and store that. The address itself MUST NOT become the identity.

A deterministic derivation is what lets a pod address a person before any identity service exists, and lets the same person keep their grants when one appears.

SPS-AUTH-051 — Pod ownership MUST be decided by comparing the request's subject against the pod's recorded owner, not by a grant and not by a scope.

This is what lets a pod with no contexts acquire its first one. An owner whose authority came from a grant would need a grant on a context that does not exist yet.

SPS-AUTH-052 — Where a person is known by several equivalent identity URIs, an implementation MUST apply the equivalence when a grant is written, and MUST NOT apply it when one is read.

A request carries one identity URI. Resolving equivalences on the read path would put an identity join on every authenticated request and make the answer depend on state that changed since the grant was made.

SPS-AUTH-061 — An implementation that revokes a person's refresh-token families MUST reach every equivalent identity URI it holds for them, not only the URI the request carries.

Revoking a grant is a write, so SPS-AUTH-052 already covers it. A refresh-token family is not a grant, and without this one issued under a person's other URI survives their withdrawal — the survivor being the one nobody was looking at. The read path is untouched: this is what a revocation covers, not how a request is answered.

SPS-AUTH-053 — An identity assertion MUST NOT be usable as a pod credential. An implementation MUST consume it once, at the login callback, and MUST NOT accept it as a bearer token afterwards.

SPS-AUTH-054 — A pod MUST accept a browser session credential only where it issued that credential for itself.

Stated as what the pod accepts rather than as where the browser sends it, because the browser is the wrong place to put the boundary. A cookie is not scoped by port and only coarsely by path, so a deployment cannot confine one to a pod's base URL, and a requirement that asked it to would forbid ordinary cookie sessions while buying nothing: the credential still arrives, and what matters is that the pod refuses it. Narrowing the cookie is worth doing and is an implementation's business.

A sign-in at one pod is therefore not a sign-in at another, including where both are served from the same host. That follows from what each pod accepts, which is where it can be tested.

10. Discovery

A client starts with the pod's canonical base URL, for example https://example.org/alice. The pod is both the protected resource and the token issuer (SPS-AUTH-028). The MCP module instead identifies its endpoint as a separate protected resource and supplies endpoint-only discovery. Its issuer is still the pod. The pod-local profile below continues to serve the other pod surfaces.

This section profiles RFC 9728 §§2–7 for resource metadata and Bearer discovery, and RFC 8414 §§2–4 and 6 for authorization server metadata. SPS-AUTH-067/068 define the pod-local address and validation deviations. Optional metadata fields retain their standard meanings; they add no required OAuth flow.

Resource metadata and challenges

Bearer challenges follow RFC 9110 §11.6.1 and RFC 6750 §3, with the missing/rejected credential rule in SPS-CORE-015. RFC 6750 leaves realm optional; SPS-MCP-009 retains the MCP module's requirement to include it.

SPS-AUTH-045 — An implementation MUST serve RFC 9728 Protected Resource Metadata at GET {pod}/.well-known/oauth-protected-resource, without authentication, carrying at least resource, authorization_servers and bearer_methods_supported.

SPS-AUTH-064 — Every Bearer challenge from a pod's protected-resource endpoint, in core or a module other than MCP, MUST carry resource_metadata equal to the absolute URL {pod}/.well-known/oauth-protected-resource. MCP challenges MUST use the endpoint resource's metadata URL specified by SPS-MCP-009.

This covers missing credentials on protected operations and rejected credentials on public reads. It does not turn a successful public request into a 401 (SPS-CORE-016), or change OAuth token endpoint errors into resource challenges. For example, a write without a bearer token returns:

HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer resource_metadata="https://example.org/alice/.well-known/oauth-protected-resource"

With a rejected token, the challenge includes invalid_token:

HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer error="invalid_token", resource_metadata="https://example.org/alice/.well-known/oauth-protected-resource"

SPS-AUTH-065 — The pod's resource metadata MUST set resource to its canonical base URL, set authorization_servers to an array containing only that URL, and include header in bearer_methods_supported.

Fetching the example's metadata URL without credentials gives 200 application/json:

{
  "resource": "https://example.org/alice",
  "authorization_servers": ["https://example.org/alice"],
  "bearer_methods_supported": ["header"]
}

SPS-AUTH-046 — Protected Resource Metadata MUST NOT enumerate the pod's public context IRIs. A count MAY be advertised.

SPS-AUTH-047 — A consumer MUST tolerate members of the metadata document it does not recognise.

Authorization server metadata

SPS-AUTH-066 — An implementation MUST serve Authorization Server Metadata at GET {pod}/.well-known/oauth-authorization-server, without authentication. The document MUST set issuer to the canonical pod base URL and advertise the chapter's authorization, token, registration and signing-key endpoints in authorization_endpoint, token_endpoint, registration_endpoint and jwks_uri. It MUST explicitly list the supported response types, grant types, token-endpoint authentication methods and PKCE methods in their RFC 8414 fields.

For Alice, the metadata URL is https://example.org/alice/.well-known/oauth-authorization-server. Its issuer is https://example.org/alice; its token_endpoint is https://example.org/alice/_system/auth/token. The issuer identifies the pod, not its auth route. The metadata describes the flows already required by this chapter; SPS-AUTH-048 still limits service-client advertisement.

SPS-AUTH-048 — Where an implementation advertises RFC 8414 Authorization Server Metadata, its grant_types_supported MUST list client_credentials only if the implementation registers service clients.

Addresses and validation

SPS-AUTH-067 — For pod-local discovery, servers and clients MUST append /.well-known/oauth-protected-resource or /.well-known/oauth-authorization-server to the canonical pod base URL. This replaces the host-rooted construction in RFC 9728 §§3/3.1 and RFC 8414 §§3/3.1, including the address mapping used by their §3.3 validation. A deployment MAY also serve the corresponding host-rooted addresses; each MUST return the same pod's metadata.

Pod / issuer Required authorization metadata URL RFC 8414 address (required with MCP)
https://alice.example https://alice.example/.well-known/oauth-authorization-server Same address
https://example.org/alice https://example.org/alice/.well-known/oauth-authorization-server https://example.org/.well-known/oauth-authorization-server/alice
https://example.org/bob https://example.org/bob/.well-known/oauth-authorization-server https://example.org/.well-known/oauth-authorization-server/bob

The resource metadata addresses use oauth-protected-resource in the same positions. A path-scoped pod can serve the required routes itself; host-rooted routes need the host's cooperation. SPS-MCP-034 makes the RFC 8414 address required for a pod advertising MCP. Its endpoint-specific resource metadata is a separate document, defined by SPS-MCP-033.

SPS-AUTH-068 — A client using this pod-local profile for a surface other than MCP MUST validate discovery against the canonical pod base URL it intended to access, not a base inferred from returned metadata. It MUST reject a challenge whose resource_metadata differs from that pod's append-form URL, resource metadata whose resource or sole authorization_servers entry differs from that pod URL, or authorization server metadata whose issuer differs from it. In RFC 9728 §3.3 validation, clients MUST use this pod URL as the expected resource for those endpoints. This replaces endpoint-URL comparison. A client MUST NOT use rejected metadata to start authorization or send credentials; the standards' exact string comparison and TLS checks still apply.

The known pod identity supplies the boundary: Alice's challenge cannot redirect a client to Bob's issuer. MCP discovery uses SPS-MCP-035 instead; it does not require the client to know or infer the pod base.

Cases to check

These are contract examples, not results from a running implementation.

Case Expected result
Protected write without a token, on a pod without MCP 401 with the pod metadata hint; the two public metadata GETs identify its authorization endpoints.
Public read without a token Normal public response; discovery does not force an authentication challenge.
Public read with a rejected bearer 401 with the same pod metadata hint; no anonymous fallback.
Metadata GET without credentials 200 application/json; no context IRIs, including public ones. Unknown fields remain tolerable.
Alice and Bob share an origin Each has its own resource, issuer and metadata URLs as above.
A challenge from Alice names Bob's metadata, or Alice's document names Bob's resource/issuer Reject discovery; do not authorize against Bob.
Metadata for Alice names https://example.org/alice/_system/auth as issuer Reject: the issuer is the pod base, even though the auth routes live below it.
Host-rooted routes are unavailable for a path-scoped pod The known-pod client uses the required pod-local addresses. This deployment cannot advertise MCP without arranging the module's host-rooted routes.
MCP challenge Endpoint-specific resource metadata and standard issuer discovery apply; see the MCP cases.
Required metadata is unavailable or fails validation Discovery has not completed; the failure supplies no authority to use another pod's metadata.