AttestoPhoenix.Router (AttestoPhoenix v2.0.2)

Copy Markdown View Source

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).

Summary

Functions

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

Functions

attesto_routes(opts \\ [])

(macro)

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