AttestoMCP.Metadata (AttestoMCP v0.6.0)

Copy Markdown View Source

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 resource identifier and the well-known metadata URL use resolve_origin/2 - the resource server origin, pinned via :base_url or :origin (a String.t(), a (Plug.Conn.t() -> String.t()) callback, or a {module, fun} / {module, fun, args} tuple), falling back to the live request origin.
  • authorization_servers defaults to the :config issuer (or an explicit :issuer string) 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 the resource origin: 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(challenge, url)

@spec append_resource_metadata(String.t(), String.t()) :: String.t()

Append resource_metadata to a WWW-Authenticate challenge.

authorization_server(config, opts \\ [])

@spec authorization_server(
  Attesto.Config.t(),
  keyword()
) :: %{required(String.t()) => term()}

Build the authorization-server metadata document by delegating to Attesto.

protected_resource(opts)

@spec protected_resource(keyword()) :: %{required(String.t()) => term()}

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.

protected_resource(conn, resource_path, opts \\ [])

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

protected_resource_url(base_url, resource_path)

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

protected_resource_url(conn, resource_path, opts)

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

resolve_origin(conn, opts)

@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:

  1. an explicit :base_url or :origin opt - a String.t(), a (Plug.Conn.t() -> String.t()) callback, or a {module, fun} / {module, fun, args} tuple (the conn is prepended to args), resolved at request time;
  2. 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.

resource_metadata_param(url)

@spec resource_metadata_param(String.t()) :: String.t()

Build the WWW-Authenticate auth-param value for RFC 9728 metadata discovery.