Server-side DPoP verification harness for host application test suites.
Where Attesto.Test.DPoP builds the client half of an RFC 9449 exchange -
a sender-constrained access token, the matching proof, and deliberately
broken proofs - this module exercises the server half from a plain request
description (method, URL, headers) and returns a test-friendly result.
It does NOT reimplement RFC 9449. It is a thin adapter that delegates every security decision to Attesto's production verifiers:
- the DPoP proof is checked by
Attesto.DPoP.verify_proof/2; - when token verification is requested, the access token is checked by
Attesto.Token.verify/3, including the RFC 7800cnf.jktsender constraint that binds the token to the proof key.
Because it calls the same functions the resource server runs in production, a passing assertion here means the proof or token would pass the real resource server, and it tracks the verifier automatically when a rule changes.
It depends on neither Plug, Phoenix, nor any HTTP client: a request is an
ordinary keyword list and a failure is an ordinary map, so it runs from any
ExUnit suite. The failure map mirrors the wire response a DPoP-aware resource
server owes the client (RFC 6750 §3.1 / RFC 9449 §7.1, §8) - the HTTP status,
the WWW-Authenticate challenge, and an optional DPoP-Nonce - so a test can
assert on the protocol-visible challenge without standing up a connection.
Request options
:method(or:http_method) - the HTTP method, e.g."GET". Required.:url(or:http_uri) - the request target URI, scheme and host included; query/fragment are normalized away by the verifier. Required.:headers- a list of{name, value}pairs or a map; names are matched case-insensitively. Theauthorizationanddpopheaders are read.:access_token- the access token the proof'sathmust bind to and the token to verify. Defaults to the token carried in theAuthorizationheader. Omit it (and the header) for a proof-only / token-endpoint proof, where no access token exists yet andathis not constrained.:verify_token- whentrue, the access token is verified withAttesto.Token.verify/3and:configis required. Defaultfalse.:config- anAttesto.Configor a zero-arity function returning one. Required when:verify_tokenistrue.:replay_check- the RFC 9449 §11.1(jti, ttl) -> :ok | {:error, :replay}callback, forwarded toAttesto.DPoP.verify_proof/2.:nonce_check- the RFC 9449 §8(nonce | nil) -> :ok | {:error, :use_dpop_nonce}callback, forwarded toAttesto.DPoP.verify_proof/2.:nonce_issue- a zero-arity function returning a fresh nonce. When ause_dpop_noncechallenge is produced, its value is placed on the challenge'sDPoP-Nonceheader (mirroring a resource server's reply).:now- clock override (DateTimeor unix seconds), forwarded to both verifiers.:max_age_seconds- proof acceptance window, forwarded to the proof verifier.:expected_typ,:mtls_cert_thumbprint- forwarded toAttesto.Token.verify/3when token verification runs.
Result
{:ok, verified}whereverifiedis a map with:scheme(:dpop | :bearer),:jkt(the verified proof thumbprint, ornil),:proof(theAttesto.DPoP.verify_proof/2result, ornil), and:claims(the verified token claims, ornilwhen token verification was not requested).{:error, challenge}wherechallengeis a map with:status,:error(the OAuth error code string),:error_reason(the underlying verifier atom),:error_description,:scheme,:www_authenticate(the challenge string),:dpop_nonce(ornil), and:headers(a list of{name, value}pairs includingWWW-Authenticate).
Example
jwk = Attesto.Test.DPoP.generate_key()
{token, _resp} =
Attesto.Test.DPoP.mint_access_token(config, %{
kind: "client",
sub: "oc_acme",
scopes: ["read"],
claims: %{"client_id" => "acme"}
}, jwk)
url = "https://api.example.test/resource"
proof = Attesto.Test.DPoP.proof(jwk, "GET", url, access_token: token)
{:ok, verified} =
Attesto.Test.DPoPVerifier.verify_request(
config: config,
method: "GET",
url: url,
headers: [
{"authorization", "DPoP " <> token},
{"dpop", proof}
],
verify_token: true
)
verified.claims["sub"]
# => "oc_acme"
Summary
Functions
Verify a protected-resource (or token-endpoint) request described by opts.
Types
@type scheme() :: :bearer | :dpop
@type verified() :: %{ scheme: scheme(), jkt: String.t() | nil, proof: Attesto.DPoP.verified_proof() | nil, claims: Attesto.Token.claims() | nil }
Functions
Verify a protected-resource (or token-endpoint) request described by opts.
See the module documentation for the accepted options and the shape of the
{:ok, verified} / {:error, challenge} result. Raises ArgumentError when
:method/:url are missing, or when :verify_token is true without a
:config.