Shipped ExUnit assertions for the DPoP sender-constraint contract.
RFC 9449 binds an access token to the client key that signed a DPoP proof
via the token's cnf.jkt claim (RFC 7800). Two properties of that contract
are worth a host MCP server proving on its own pipeline, because getting
either wrong silently downgrades sender-constrained tokens to bearer tokens:
- A DPoP-bound token presented as a plain
Bearertoken (no proof) MUST be rejected. Otherwise a captured token is usable without the key. - A DPoP-bound token presented as
DPoPwith a valid proof for the live request MUST be accepted.
These helpers drive a host's already-wired authentication plug against both
cases so the host's exact :config, :replay_check, :htu, and error
rendering are exercised. They build the token and proof with
AttestoMCP.Test.Factory and use only Attesto's published DPoP API
(Attesto.DPoP.compute_ath/1 via the factory), so they do not depend on any
Attesto-internal test scaffolding.
The module compiles only when ExUnit is loaded, so it adds nothing to a
host's production build.
Usage
defmodule MyApp.MCPAuthTest do
use ExUnit.Case
import AttestoMCP.Test.DPoPAssertions
setup do
%{config: AttestoMCP.Test.Factory.config()}
end
test "MCP endpoint enforces DPoP binding", %{config: config} do
plug = fn conn ->
MyAppWeb.MCPAuth.call(conn, MyAppWeb.MCPAuth.init([]))
end
assert_dpop_bound_bearer_rejected(plug, config)
assert_dpop_proof_accepted(plug, config)
end
endplug_fun is a one-arity function that runs the host's authentication
pipeline on a Plug.Conn and returns the resulting conn. A host that
requires :replay_check for DPoP requests must wire it inside plug_fun.
Summary
Functions
Assert that a DPoP-bound token presented as a plain Bearer token (with no
proof) is rejected by the host pipeline.
Assert that a DPoP-bound token presented as DPoP with a valid proof for
the live request is accepted by the host pipeline.
Types
@type plug_fun() :: (Plug.Conn.t() -> Plug.Conn.t())
Functions
@spec assert_dpop_bound_bearer_rejected(plug_fun(), Attesto.Config.t(), keyword()) :: Plug.Conn.t()
Assert that a DPoP-bound token presented as a plain Bearer token (with no
proof) is rejected by the host pipeline.
Options:
:path- request path (default"/mcp").:method- request method (default:post).:scopes- scopes minted into the token (default the factory default).
Returns the resulting halted Plug.Conn for further assertions.
@spec assert_dpop_proof_accepted(plug_fun(), Attesto.Config.t(), keyword()) :: Plug.Conn.t()
Assert that a DPoP-bound token presented as DPoP with a valid proof for
the live request is accepted by the host pipeline.
Options:
:path- request path (default"/mcp").:method- request method (default:post).:scopes- scopes minted into the token (default the factory default).:htu- thehtuclaim of the proof (default the factory audience). It must match the host's computed request URI.
Returns the resulting non-halted Plug.Conn for further assertions.