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. The two discovery
routes listed below are the registered forms for an origin-only issuer. A
path-bearing issuer has different standards-derived well-known locations;
the host must mount those explicitly rather than use these convenience
routes unchanged.
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), omitted withopenid_configuration: false.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/§2.2), omitted withuserinfo: false.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, optional 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 its configured RFC 6750 credential channels by its
controller, rather than from a caller session, so they too take no
session-bearing pipeline. Supply a :pipeline to attach transport-level
concerns the host wants in front of every endpoint (for example an
HTTPS-enforcing plug), and use :route_pipelines when the interactive,
metadata, and non-browser protocol surfaces need different host pipelines.
scope "/" do
attesto_routes()
end
# or with a host pipeline and a mount prefix:
scope "/" do
attesto_routes(pipeline: :oauth_server, prefix: "/auth")
end
# or classify browser-facing routes separately while retaining shared
# transport policy on every class:
scope "/" do
attesto_routes(
pipeline: :oauth_common,
route_pipelines: [
interactive: [:oauth_interactive, :oauth_common]
]
)
endA route-class override is the complete ordered pipeline list for that class;
Attesto does not append or prepend the :pipeline default. The host remains
responsible for the policy inside those pipelines. In particular, externally
submitted OAuth POST requests must not accidentally inherit a generic browser
pipeline that rejects them through CSRF protection or browser-only Accept
negotiation. The :interactive name means that those endpoints participate
in resource-owner/browser interactions; it does not mean Attesto silently
applies the host's ordinary browser pipeline.
Options
:prefix- path segment prepended to the/oauth/*endpoints. It does not apply to this macro's root well-known routes. Defaults to"". Those fixed routes are standards-correct for an origin-only issuer; a path-bearing issuer requires the host to mount the distinct OIDC Discovery and RFC 8414 derived locations explicitly.: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).:route_pipelines- optional route-class overrides. Accepts a keyword list whose keys are:metadata,:interactive, or:protocol, and whose values are a pipeline atom or an ordered list of pipeline atoms. An override replaces:pipelinefor its class; classes not present keep the:pipelinedefault. The classes are::metadata- authorization-server discovery, OpenID configuration, JWKS, and protected-resource metadata routes owned by this macro.:interactive- authorization, device verification, end-session, and check-session routes.:protocol- token, PAR, revocation, introspection, registration management, UserInfo, device authorization, and CIBA backchannel authentication routes.
Unknown or duplicate class keys and malformed values raise
ArgumentErrorduring router compilation. When this option is absent, the legacy single-:pipelineroute expansion is used unchanged. Write pipeline names as literal atoms/lists inside a Phoenixscope; module attributes are not available when Phoenix expands the nested route macro and are rejected with an actionable error.: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.:userinfo- a compile-time route-mount control. Whenfalse, omitsGET/POST /oauth/userinfowhile retaining the generic OAuth authorization-server routes. Defaults totruefor compatibility. The retained local Provider Metadata route suppresses onlyuserinfo_endpoint: :derivedat the removed route-equivalent path. A configured URL remains authoritative, so a host can replace the bundled controller at the same path. Dynamic surrounding Phoenix scopes are supported. A dynamic macro:prefixis rejected for this option combination because that prefix does not apply to the root metadata request; move it to a surrounding scope instead.:openid_configuration- whenfalse, omits the OpenID Provider configuration document while retaining RFC 8414 authorization-server metadata. This is also a compile-time route-mount control and defaults totruefor compatibility. An OAuth-only host that is not an OpenID Provider normally disables this and:userinfo. OIDC conformance for features such as CIBA, logout, and session management relies on Provider Metadata, so deployments using those features must keep it enabled unless equivalent metadata is served separately.: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. Metadata is otherwise derived from
AttestoPhoenix.Config at request time rather than from macro options. The
bounded exception is userinfo: false: the retained local Provider Metadata
route records the removed route so only userinfo_endpoint: :derived can be
suppressed when it resolves there. A configured HTTPS URL is authoritative,
including at the same path, which lets the host replace the bundled
controller without losing discovery. For a dynamically discovered and
dynamically registered OpenID Provider that issues access tokens, the OIDC
requirements for Discovery and UserInfo still apply; the opt-outs are not a
claim that every combination is an OIDC-conformant deployment.
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.