All notable changes to this project are documented here. The format is based on Keep a Changelog and this project adheres to Semantic Versioning.
[Unreleased]
[0.6.1] - 2026-06-14
Fixed
AttestoMCP.Plug.ProtectResourcecan again be used as a compile-time router pipeline plug (plug_init_mode: :compile). 0.6.0 madeinit/1bake the generatedresource_metadataWWW-Authenticateclosure into theRequireScopestransport. Under:compilemode (the Phoenix router / production default) a plug'sinit/1result is embedded viaMacro.escape, which rejects anonymous functions, so a router carrying the plug failed to compile. The generated challenge is now built at call time from an escape-safe spec (strings /{m, f}tuples), soinit/1returns no closures and the plug compiles in a:compile-mode pipeline. Behavior is unchanged: an insufficient-scope 403 still carries theresource_metadatapointer, and a host-supplied:www_authenticatestill wins. (Callbacks the host passes to the plug must be remote captures or MFA tuples, not anonymousfn, to be embedded under:compilemode — the same constraint Plug imposes on every plug.)
[0.6.0] - 2026-06-14
Added
- Origin pinning for protected-resource metadata behind a proxy. A host can
now pin the advertised
resource/challenge origin andauthorization_serversinstead of always deriving them from the live request connection, which behind a TLS-terminating proxy is both fragile (http/internal host) and anX-Forwarded-Hostspoofing vector into the metadata document a client trusts to find its authorization server.AttestoMCP.Metadata.resolve_origin/2resolves the resource server origin: an explicit:base_url/:origin(aString.t()or(conn -> url)) wins over the request connection. It drives theresourceidentifier and theprotected_resource_url/3challenge URL.AttestoMCP.Metadata.protected_resource/3now defaultsauthorization_serversto the:configissuer (or an explicit:issuerstring) when one is given, rather than the resource origin — the issuer is the authorization server the host already trusts. Theresourceidentifier is not derived from the issuer (RFC 9728 keeps the two distinct).- The router macro
attesto_mcp_protected_resource_metadata,AttestoMCP.MetadataController,AttestoMCP.Plug.Authenticate, andAttestoMCP.Plug.ProtectResourceall accept and thread:base_url/:origin(and:issuer/:config), so the served metadata and theWWW-Authenticateresource_metadatachallenge stay aligned when pinned.:base_url/:originaccept a string, a(conn -> url)callback, or a{module, fun}/{module, fun, args}tuple (so a dynamic origin works in a compiled router macro, where an anonymous fn cannot). - The generated
resource_metadatachallenge is now emitted consistently on every rejection the MCP plugs render - token failure, principal-callback rejection (a 401 fromAttestoMCP.Plug.Authenticate), and insufficient-scope rejection (a 403 fromAttestoMCP.Plug.RequireScopesviaProtectResource)- so a client is pointed at metadata on all of them, not just token failures.
- New
guides/proxy_origin.mddocuments the pinned-origin recipe so consumers do not reinvent a canonical-host guard plug.
Fixed
authorization_serversadvertises the issuer verbatim: it is no longer trailing-slash-trimmed, since an issuer identifier is compared by exact string match (a trimmed path-based issuer would break discovery). The resource origin is still trimmed (it is joined with a path). A blank explicit:issueris ignored rather than publishing[""]or overriding a configured issuer.- A blank, relative, or non-binary
:base_url/:originpin ("","/","/prefix","mcp.example.com", …) is treated as "not configured" and falls back (:base_url→:origin→ request origin) instead of producing relative or empty security metadata (no moreresource: "/mcp"orauthorization_servers: [""]). A pin must be an absolutescheme://…origin. - An explicit
:resource/:authorization_serversnow short-circuits the derivation, so a lower-precedence origin/issuer callback is never invoked (and cannot fail) for a value the host supplied outright. The resource origin is also resolved at most once per document and shared with theauthorization_serversfallback, so a stateful origin callback cannot make theresourceandauthorization_serversdisagree.
Changed
AttestoMCP.Metadata.protected_resource/3now setsresourcewithput_new(wasput), so an explicit:resourceopt overrides the derived identifier — matching how:authorization_serversalready behaved.- With no pinning options supplied, behavior is unchanged: both origins derive from the request connection.
[0.5.2] - 2026-05-31
Fixed
- Correct the README installation snippet now that the package is published on Hex.
[0.5.1] - 2026-05-31
Changed
- Reuse
Attesto.Test.DPoPfor MCP DPoP proof fixtures so downstream MCP tests stay aligned with Attesto's published DPoP helper API.
[0.5.0] - 2026-05-31
Added
AttestoMCP.Plug.ProtectResource: a single plug composingAttestoMCP.Plug.AuthenticatethenAttestoMCP.Plug.RequireScopesinto a correctly ordered, halt-respecting pipeline, with the RFC 9728resource_metadataWWW-Authenticatechallenge auto-wired from the resource path.AttestoMCP.Routerwith theattesto_mcp_protected_resource_metadata/2Phoenix router macro, andAttestoMCP.MetadataController, serving per-resource/.well-known/oauth-protected-resource/<path>metadata plus a backwards-compatible root/.well-known/oauth-protected-resourceroute. The servedresourceidentifier matches theProtectResourcechallenge.AttestoMCP.Test.DPoPAssertions: shipped ExUnit assertions for host apps proving a DPoP-bound token presented as a plain Bearer is rejected and is accepted with a valid DPoP proof.guides/mcp_wiring.md: copy-pasteable end-to-end wiring guide.phoenixas an optional dependency (only needed byAttestoMCP.RouterandAttestoMCP.MetadataController).- Initial Plug/Phoenix authentication wrapper for protecting HTTP MCP endpoints with Attesto access-token verification, DPoP proof checks, and mTLS certificate-bound token checks.
- MCP scope convention helpers.
- OAuth protected-resource metadata builder and authorization-server metadata delegation.
- Focused tests for Bearer, DPoP, mTLS, scope enforcement, principal mapping, custom error rendering, and public assign names.