Builders for OAuth metadata used by HTTP MCP authorization.
MCP HTTP servers that require authorization act as OAuth protected resources. The MCP authorization spec points clients at RFC 9728 protected-resource metadata first, then at RFC 8414 authorization-server metadata. This module builds those documents without coupling the package to any MCP server SDK.
Origin pinning behind a proxy
The resource identifier and the default authorization_servers are origin
values: a client reads them to learn where it is and where to get a token.
By default they are derived from the live request connection
(conn.scheme/conn.host/conn.port), which is correct for a simple
single-host deployment.
Behind a TLS-terminating reverse proxy that derivation is both fragile and a
spoofing vector: conn.scheme is often http, conn.host is whatever
X-Forwarded-* rewriting produced, and an attacker who can set
X-Forwarded-Host could make the metadata advertise an attacker-controlled
resource or authorization server. A FAPI-grade deployment should pin both
instead of trusting the request:
- The
resourceidentifier and the well-known metadata URL useresolve_origin/2- the resource server origin, pinned via:base_urlor:origin(aString.t(), a(Plug.Conn.t() -> String.t())callback, or a{module, fun}/{module, fun, args}tuple), falling back to the live request origin. authorization_serversdefaults to the:configissuer (or an explicit:issuerstring) when one is supplied - the authorization server the host already trusts for token verification, advertised verbatim - and otherwise to the resource origin. The issuer is deliberately NOT used as theresourceorigin: RFC 9728 keeps the protected resource and its authorization server distinct, and in a FAPI deployment they are usually different hosts.
Either can still be overridden outright with an explicit :resource /
:authorization_servers opt. See guides/proxy_origin.md for the recipe.
Summary
Functions
Append resource_metadata to a WWW-Authenticate challenge.
Build the authorization-server metadata document by delegating to Attesto.
Build an RFC 9728 protected-resource metadata document.
Build protected-resource metadata from a Plug connection and resource path.
Build the well-known metadata URL for an MCP resource path.
Build the well-known metadata URL, resolving the origin via resolve_origin/2.
Resolve the resource server origin used to build the resource identifier
and the well-known metadata URL.
Build the WWW-Authenticate auth-param value for RFC 9728 metadata discovery.
Functions
Append resource_metadata to a WWW-Authenticate challenge.
@spec authorization_server( Attesto.Config.t(), keyword() ) :: %{required(String.t()) => term()}
Build the authorization-server metadata document by delegating to Attesto.
Build an RFC 9728 protected-resource metadata document.
Required options:
:resource- the protected resource identifier, usually the canonical MCP server URI such as"https://mcp.example.com/mcp".:authorization_servers- a non-empty list of issuer identifiers.
Common options include :scopes_supported, :bearer_methods_supported,
:dpop_signing_alg_values_supported, and
:tls_client_certificate_bound_access_tokens.
@spec protected_resource(Plug.Conn.t(), String.t(), keyword()) :: %{ required(String.t()) => term() }
Build protected-resource metadata from a Plug connection and resource path.
resource_path is the path of the MCP endpoint, for example "/mcp" or
"/mcp/admin". The resource identifier is resolve_origin/2 (the resource
server origin, pinned via :base_url/:origin) joined with that path.
:authorization_servers defaults to the :config issuer (or an explicit
:issuer string) when one is given - the trusted authorization server,
advertised verbatim - and otherwise to the resource origin.
An explicit :resource or :authorization_servers opt overrides the derived
value, and short-circuits its derivation: a pinned origin / issuer callback is
not invoked (and cannot fail) for a value the host supplied outright.
@spec protected_resource_url(String.t(), String.t()) :: String.t()
@spec protected_resource_url(Plug.Conn.t(), String.t()) :: String.t()
Build the well-known metadata URL for an MCP resource path.
iex> AttestoMCP.Metadata.protected_resource_url("https://mcp.example.com", "/mcp")
"https://mcp.example.com/.well-known/oauth-protected-resource/mcp"A Plug connection can also be passed as the first argument.
@spec protected_resource_url(Plug.Conn.t(), String.t(), keyword()) :: String.t()
Build the well-known metadata URL, resolving the origin via resolve_origin/2.
Pass :base_url/:origin in opts to pin the resource origin (so the
challenge URL matches a pinned resource); with no such opt the live request
origin is used.
@spec resolve_origin( Plug.Conn.t(), keyword() ) :: String.t()
Resolve the resource server origin used to build the resource identifier
and the well-known metadata URL.
This is the protected resource's own origin (RFC 9728 resource), which is
distinct from the authorization server (authorization_servers / the
Attesto.Config issuer) - the two are often different hosts in a FAPI
deployment, so the issuer is NOT used here. Precedence:
- an explicit
:base_urlor:originopt - aString.t(), a(Plug.Conn.t() -> String.t())callback, or a{module, fun}/{module, fun, args}tuple (the conn is prepended toargs), resolved at request time; - the live request origin (
conn.scheme/conn.host/conn.port).
A blank or non-binary pin is treated as "not configured" and falls back to the request origin; a trailing slash is trimmed so the result joins cleanly with a resource path.
Build the WWW-Authenticate auth-param value for RFC 9728 metadata discovery.