Router macro that mounts the authorization-server endpoints.
use AttestoPhoenix.Router makes the attesto_routes/1 macro available
inside a Phoenix.Router. Calling it inside (or alongside) a scope
declares the OAuth 2.0 / OpenID Connect server surface:
GET /.well-known/oauth-authorization-server- authorization-server metadata (RFC 8414 §3).GET /.well-known/openid-configuration- OpenID Provider configuration (OpenID Connect Discovery 1.0 §4).GET /.well-known/jwks.json- the JSON Web Key Set of the verification keys (RFC 7517 §5; the discovery document'sjwks_uriper RFC 8414 §2).GET /.well-known/oauth-protected-resource- protected-resource metadata (RFC 9728 §3), the discovery target of the §5.1WWW-Authenticatechallenge the resource-server plugs emit. For a resource identifier that carries a path, the §3.1 path-inserted form (/.well-known/oauth-protected-resource/mcp) is mounted via:protected_resource_paths- see the option below; the root document alone does not satisfy clients that derive that form.GET /oauth/authorize- the authorization endpoint (RFC 6749 §3.1; OpenID Connect Core 1.0 §3.1.2).POST /oauth/token- the token endpoint (RFC 6749 §3.2).POST /oauth/par- pushed authorization requests (RFC 9126).POST /oauth/revoke- the token revocation endpoint (RFC 7009 §2).POST /oauth/introspect- the token introspection endpoint (RFC 7662 §2), with the RFC 9701 signed-JWT response negotiated by theAcceptheader.POST /oauth/register- dynamic client registration (RFC 7591 §3.1), mounted only when registration is enabled (see:registrationbelow).DELETE /oauth/register/:client_id- dynamic client registration management cleanup (RFC 7592 §2), mounted with registration.GETandPOST /oauth/userinfo- the UserInfo endpoint (OpenID Connect Core 1.0 §5.3); a bearer-authenticated protected resource (RFC 6750 §2.1).GETandPOST /oauth/end_session- the end-session endpoint (OpenID Connect RP-Initiated Logout 1.0 §2), mounted only withlogout: true.GET /oauth/check_session- thecheck_session_iframe(OpenID Connect Session Management 1.0 §3.3), mounted only withsession_management: true.
The macro emits nothing but Phoenix.Router route entries pointing at this
library's controllers; it holds no policy of its own. Every behavioral
decision (which clients exist, which scopes are granted, whether DPoP / mTLS
binding is offered, whether registration is open) is owned by the host
through AttestoPhoenix.Config, which the controllers read at request time.
Placement and pipelines
The discovery, OpenID configuration, and JWKS documents are unauthenticated
public metadata (RFC 8414 §5; OpenID Connect Discovery 1.0 §4; RFC 8615).
The authorization endpoint does not authenticate the client (RFC 6749 §3.1):
the resource owner authenticates through the host's login/consent callbacks,
so it carries no client-authentication pipeline. The token, revocation, and
registration endpoints authenticate the client from the request itself
(RFC 6749 §2.3, RFC 7009 §2, RFC 7591 §3), and the UserInfo endpoint is
bearer-authenticated from the Authorization header (RFC 6750 §2.1) by its
controller, rather than from a caller session, so they too take no
session-bearing pipeline. Supply a :pipeline only to attach
transport-level concerns the host wants in front of every endpoint (for
example a parser that accepts application/x-www-form-urlencoded at the
token endpoint per RFC 6749 §4.4.2, or an HTTPS-enforcing plug).
scope "/" do
attesto_routes()
end
# or with a host pipeline and a mount prefix:
scope "/" do
attesto_routes(pipeline: :oauth_server, prefix: "/auth")
endOptions
:prefix- path segment prepended to the/oauth/*endpoints (the well-known documents always live at the host root per RFC 8615, so the prefix does not apply to them). Defaults to"".:pipeline- a pipeline name (atom) or list of pipeline names topipe_throughfor the mounted routes. Defaults to[](no extra pipeline; the surroundingscope'spipe_through, if any, still applies).:registration- whentrue, mountsPOST /oauth/register(RFC 7591) andDELETE /oauth/register/:client_id(RFC 7592). Defaults tofalse. The endpoints still fail closed at request time unless the host has wired the registration callbacks inAttestoPhoenix.Config; this option only controls whether the routes exist, so a deployment that never offers registration presents no registration surface at all.:device- whentrue, mounts the RFC 8628 device-authorization endpoint and verification page. Defaults tofalse.:ciba- whentrue, mountsPOST /oauth/bc-authorize, the OpenID Connect CIBA backchannel authentication endpoint. Defaults tofalse. The endpoint still fails closed at request time unless the host also enablesciba: [enabled: true]inAttestoPhoenix.Config.:logout- whentrue, mountsGET/POST /oauth/end_session(OpenID Connect RP-Initiated Logout 1.0). Defaults tofalse.:session_management- whentrue, mountsGET /oauth/check_session(OpenID Connect Session Management 1.0 §3.3). Defaults tofalse. The page answers 404 unless the host also enablessession_management: [enabled: true]inAttestoPhoenix.Config.:protected_resource_paths- additionally mounts the RFC 9728 §3.1 path-inserted protected-resource metadata URI for the given resource path. RFC 9728 §3.1 derives the well-known URI by inserting the well-known segment between the origin and the resource path: for the resource identifierhttps://host.example/mcpthe metadata lives at/.well-known/oauth-protected-resource/mcp. The root document alone is NOT RFC 9728-complete for such a resource: clients that derive the path-inserted URI from the resource URL (current MCP clients probe it first, before theWWW-Authenticateresource_metadatafallback) miss a host that serves only the root form. Accepts a single-element list (["/mcp"]; a bare"mcp"is normalized to"/mcp"). The served document must satisfy RFC 9728 §3.3 - itsresourcemember must equal the identifier the URI was derived from - so the controller fails closed at request time when the configured resource identifier's path does not match. More than one entry is a compile-time error: one controller document cannot equal two identifiers; multi-resource hosts should useattesto_mcp'sAttestoMCP.Router.attesto_mcp_protected_resource_metadata/2, which serves per-resource documents. Defaults to[](root only, today's behavior).:protected_resource_root- whenfalse, does not mount the root/.well-known/oauth-protected-resourcedocument. Use this when PRM ownership lives elsewhere: a host that mountsattesto_mcp'sattesto_mcp_protected_resource_metadata/2with its root compatibility document enabled should passprotected_resource_root: falsehere so exactly one package owns each PRM route. Defaults totrue(today's behavior).
The library never inspects :registration to make a policy decision: it is
a route-existence toggle. Authorization-server metadata advertised at the
discovery endpoint is derived from AttestoPhoenix.Config by the discovery
controller, not from these macro options.
How protected-resource discovery actually happens
A client calls the resource URL and gets a 401 whose WWW-Authenticate
challenge carries a resource_metadata pointer (RFC 9728 §5.1). Modern
clients ALSO - often first - derive the §3.1 path-inserted well-known URI
from the resource URL itself (https://host.example/mcp →
/.well-known/oauth-protected-resource/mcp) and probe it before falling
back to the challenge pointer. Both URIs must serve the same document, and
its resource member must equal the identifier the URI was derived from
(§3.3) or the client is required to reject it. A single-resource AS+RS host
covers all of this with attesto_routes(protected_resource_paths: ["/mcp"]);
a host with multiple protected resources needs per-resource documents and
should mount them with attesto_mcp's
AttestoMCP.Router.attesto_mcp_protected_resource_metadata/2 instead
(passing protected_resource_root: false here if that macro also owns the
root document, so each PRM route has exactly one owner).
Summary
Functions
Mounts the authorization-server endpoints. See the module documentation for the route table and the accepted options.