Attesto.Test.DPoPVerifier (Attesto v0.6.3)

Copy Markdown View Source

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:

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. The authorization and dpop headers are read.
  • :access_token - the access token the proof's ath must bind to and the token to verify. Defaults to the token carried in the Authorization header. Omit it (and the header) for a proof-only / token-endpoint proof, where no access token exists yet and ath is not constrained.
  • :verify_token - when true, the access token is verified with Attesto.Token.verify/3 and :config is required. Default false.
  • :config - an Attesto.Config or a zero-arity function returning one. Required when :verify_token is true.
  • :replay_check - the RFC 9449 §11.1 (jti, ttl) -> :ok | {:error, :replay} callback, forwarded to Attesto.DPoP.verify_proof/2.

  • :nonce_check - the RFC 9449 §8 (nonce | nil) -> :ok | {:error, :use_dpop_nonce} callback, forwarded to Attesto.DPoP.verify_proof/2.

  • :nonce_issue - a zero-arity function returning a fresh nonce. When a use_dpop_nonce challenge is produced, its value is placed on the challenge's DPoP-Nonce header (mirroring a resource server's reply).
  • :now - clock override (DateTime or unix seconds), forwarded to both verifiers.
  • :max_age_seconds - proof acceptance window, forwarded to the proof verifier.
  • :expected_typ, :mtls_cert_thumbprint - forwarded to Attesto.Token.verify/3 when token verification runs.

Result

  • {:ok, verified} where verified is a map with :scheme (:dpop | :bearer), :jkt (the verified proof thumbprint, or nil), :proof (the Attesto.DPoP.verify_proof/2 result, or nil), and :claims (the verified token claims, or nil when token verification was not requested).
  • {:error, challenge} where challenge is 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 (or nil), and :headers (a list of {name, value} pairs including WWW-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

challenge()

@type challenge() :: %{
  status: pos_integer(),
  scheme: scheme(),
  error: String.t(),
  error_reason: atom(),
  error_description: String.t() | nil,
  www_authenticate: String.t(),
  dpop_nonce: String.t() | nil,
  headers: [{String.t(), String.t()}]
}

scheme()

@type scheme() :: :bearer | :dpop

verified()

@type verified() :: %{
  scheme: scheme(),
  jkt: String.t() | nil,
  proof: Attesto.DPoP.verified_proof() | nil,
  claims: Attesto.Token.claims() | nil
}

Functions

verify_request(opts)

@spec verify_request(keyword()) :: {:ok, verified()} | {:error, challenge()}

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.