# `Attesto.Test.DPoPVerifier`
[🔗](https://github.com/XukuLLC/attesto/blob/v0.13.0/lib/attesto/test/dpop_verifier.ex#L1)

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 7800 `cnf.jkt` sender
    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. 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"

# `challenge`

```elixir
@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`

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

# `verified`

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

# `verify_request`

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

---

*Consult [api-reference.md](api-reference.md) for complete listing*
