# `AttestoPhoenix.Router`
[🔗](https://github.com/XukuLLC/attesto_phoenix/blob/v2.0.2/lib/attesto_phoenix/router.ex#L1)

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 with
    `openid_configuration: false`.
  * `GET /.well-known/jwks.json` - the JSON Web Key Set of the verification
    keys (RFC 7517 §5; the discovery document's `jwks_uri` per RFC 8414 §2).
  * `GET /.well-known/oauth-protected-resource` - protected-resource metadata
    (RFC 9728 §3), the discovery target of the §5.1 `WWW-Authenticate`
    challenge 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 the `Accept` header.
  * `POST /oauth/register` - dynamic client registration (RFC 7591 §3.1),
    mounted only when registration is enabled (see `:registration` below).
  * `DELETE /oauth/register/:client_id` - dynamic client registration
    management cleanup (RFC 7592 §2), mounted with registration.
  * `GET` and `POST /oauth/userinfo` - the UserInfo endpoint (OpenID Connect
    Core 1.0 §5.3); a bearer-authenticated protected resource (RFC 6750
    §2.1/§2.2), omitted with `userinfo: false`.
  * `GET` and `POST /oauth/end_session` - the end-session endpoint (OpenID
    Connect RP-Initiated Logout 1.0 §2), mounted only with `logout: true`.
  * `GET /oauth/check_session` - the `check_session_iframe` (OpenID Connect
    Session Management 1.0 §3.3), mounted only with `session_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]
        ]
      )
    end

A 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 to
    `pipe_through` for the mounted routes. Defaults to `[]` (no extra
    pipeline; the surrounding `scope`'s `pipe_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 `:pipeline` for its class; classes not present keep
    the `:pipeline` default. 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
    `ArgumentError` during router compilation. When this option is absent,
    the legacy single-`:pipeline` route expansion is used unchanged. Write
    pipeline names as literal atoms/lists inside a Phoenix `scope`; module
    attributes are not available when Phoenix expands the nested route
    macro and are rejected with an actionable error.
  * `:registration` - when `true`, mounts `POST /oauth/register`
    (RFC 7591) and `DELETE /oauth/register/:client_id` (RFC 7592). Defaults
    to `false`. The endpoints still fail closed at request time unless the
    host has wired the registration callbacks in `AttestoPhoenix.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. When `false`, omits
    `GET`/`POST /oauth/userinfo` while retaining the generic OAuth
    authorization-server routes. Defaults to `true` for compatibility.
    The retained local Provider Metadata route suppresses only
    `userinfo_endpoint: :derived` at 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 `:prefix` is 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` - when `false`, omits the OpenID Provider
    configuration document while retaining RFC 8414 authorization-server
    metadata. This is also a compile-time route-mount control and defaults to
    `true` for 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` - when `true`, mounts the RFC 8628 device-authorization
    endpoint and verification page. Defaults to `false`.
  * `:ciba` - when `true`, mounts `POST /oauth/bc-authorize`, the OpenID
    Connect CIBA backchannel authentication endpoint. Defaults to `false`.
    The endpoint still fails closed at request time unless the host also
    enables `ciba: [enabled: true]` in `AttestoPhoenix.Config`.
  * `:logout` - when `true`, mounts `GET`/`POST /oauth/end_session` (OpenID
    Connect RP-Initiated Logout 1.0). Defaults to `false`.
  * `:session_management` - when `true`, mounts `GET /oauth/check_session`
    (OpenID Connect Session Management 1.0 §3.3). Defaults to `false`. The
    page answers 404 unless the host also enables
    `session_management: [enabled: true]` in `AttestoPhoenix.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 identifier `https://host.example/mcp` the 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 the `WWW-Authenticate` `resource_metadata` fallback) 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 - its `resource` member 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 use
    `attesto_mcp`'s `AttestoMCP.Router.attesto_mcp_protected_resource_metadata/2`,
    which serves per-resource documents. Defaults to `[]` (root only,
    today's behavior).
  * `:protected_resource_root` - when `false`, does not mount the root
    `/.well-known/oauth-protected-resource` document. Use this when PRM
    ownership lives elsewhere: a host that mounts `attesto_mcp`'s
    `attesto_mcp_protected_resource_metadata/2` with its root compatibility
    document enabled should pass `protected_resource_root: false` here so
    exactly one package owns each PRM route. Defaults to `true` (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).

# `attesto_routes`
*macro* 

Mounts the authorization-server endpoints. See the module documentation for
the route table and the accepted options.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
