Skip to content

Authorization and client identity

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: descriptive. 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 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 out-of-band, by the host operator

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, and MUST issue identifiers 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. Dynamic registration responses 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

SPS-AUTH-012 — A service client MUST be registered out of band, through host-level operator authority. An implementation MUST NOT allow a service client to be created through dynamic registration or through any pod-scoped token.

SPS-AUTH-013 — A service client's grants MUST be fixed at registration and 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.

SPS-AUTH-019 — An implementation MUST apply SPS-AUTH-018 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 — On success the implementation MUST redirect to the request's validated redirect_uri carrying code and state; on failure, carrying error and error_description, and OPTIONALLY error_uri. A client MUST treat error_uri as optional.

"Validated" rather than "registered", because a did:web: client registers nothing (SPS-AUTH-007) — its address is validated against the origin its identifier names (SPS-AUTH-004, SPS-AUTH-005). A dyn: client's address is validated against what it registered. Both are validated; only one is registered.

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.

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 carries no per-token state that could express a subset; it grants the client's registered set or nothing.

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.

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

SPS-AUTH-035 — A token issued for public-read MUST NOT carry a refresh token. The client re-authorizes when it expires.

SPS-AUTH-036 — A service token MUST NOT carry a refresh token.

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: space-separated and multi-valued. Absent, it MAY auto-grant where grants exist; consent MUST show the consent screen; none MUST show no interactive screen; login MUST force fresh authentication.

SPS-AUTH-040prompt=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-041prompt=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-042scope=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-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 browser session established at a pod MUST be scoped to that pod. A sign-in at one pod MUST NOT be a sign-in at another, including where both are served from the same host.

Pods are isolated tenants that may share a host on a path-scoped deployment. A session that spanned them would make the isolation depend on the deployment layout.

10. Discovery

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-056 — An implementation MUST also serve the metadata at the host-rooted form RFC 9728 §3.1 constructs, by inserting /.well-known/oauth-protected-resource between the authority and the pod's path: {origin}/.well-known/oauth-protected-resource/{pod}. Both addresses MUST return the same document.

Serving only the append form is the failure this requirement exists for, and it is invisible until a client that follows the RFC arrives. A generic OAuth client — anything driving RFC 9728 discovery rather than following a WWW-Authenticate hint — constructs the host-rooted address and finds nothing there. The append form is what a resource_metadata hint points at and is equally necessary; neither replaces the other.

SPS-AUTH-057 — Where an implementation advertises RFC 8414 Authorization Server Metadata, it MUST serve it at the host-rooted form for its issuer as well as at the append form, and both MUST return the same document.

The same split, one layer up: a client that reads authorization_servers[0] out of the resource metadata and then probes the host-rooted path for it gets nothing from an implementation that serves only the append form.

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

The URIs would be a topology leak on an unauthenticated route — the same rule as SPS-CORE-017, reached from the other direction.

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

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.