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

DPoP test fixtures for host application suites.

A host that protects routes with Attesto's DPoP verification
(`Attesto.DPoP.verify_proof/2` composed with `Attesto.Token.verify/3`)
needs, in its own tests, the client half of the RFC 9449 exchange: a
DPoP-sender-constrained access token, the matching proof JWT, and the
deliberately-broken proofs that must be rejected. Hand-rolling those in
every consumer re-implements JWS signing and the `cnf.jkt` / `ath`
derivations the library already owns, and drifts from the verifier the
moment a rule changes.

This module ships under `lib/` (like `AttestoMCP.Test.DPoPReplay`) so a
consumer can call it from its `test/` tree without depending on
Attesto's own test support. It builds everything through the same
primitives the production code uses - `Attesto.Token.mint/3`,
`Attesto.DPoP.compute_jkt/1`, `Attesto.DPoP.compute_ath/1`, and
`JOSE.JWS` - so a fixture is correct by construction against the
verifier and stays in step with it.

## Proof key

Every function takes the client's DPoP key as a `%JOSE.JWK{}` (generate
one with `generate_key/1`, or supply your own EC/RSA/OKP key). The proof
embeds only the key's **public** half in its protected header, as
RFC 9449 §4.2 requires; `Attesto.DPoP.verify_proof/2` rejects any header
carrying private-key material.

## 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)

    proof =
      Attesto.Test.DPoP.proof(jwk, "GET", "https://api.example/thing",
        access_token: token
      )

    {:ok, %{jkt: jkt}} =
      Attesto.DPoP.verify_proof(proof,
        http_method: "GET",
        http_uri: "https://api.example/thing",
        access_token: token
      )

    {:ok, _claims} = Attesto.Token.verify(config, token, dpop_jkt: jkt)

# `flaw`

```elixir
@type flaw() :: :wrong_htm | :wrong_htu | :missing_ath | :expired
```

A deliberate defect to bake into a proof so a negative test can assert
the verifier rejects it:

  * `:wrong_htm` - sign a method the request will not carry.
  * `:wrong_htu` - sign a target URI the request will not carry.
  * `:missing_ath` - omit `ath` even though an access token is presented.
  * `:expired` - backdate `iat` past the acceptance window.

# `generate_key`

```elixir
@spec generate_key(term()) :: JOSE.JWK.t()
```

Generate a fresh DPoP proof key.

Defaults to an EC P-256 key (`ES256`), the smallest of the algorithms
`Attesto.DPoP` accepts. Pass a `JOSE.JWK.generate_key/1` spec to choose
another, e.g. `generate_key({:rsa, 2048})`.

# `invalid_proof`

```elixir
@spec invalid_proof(JOSE.JWK.t(), flaw(), String.t(), String.t(), keyword()) ::
  String.t()
```

Build a DPoP proof carrying a single deliberate defect, for negative
tests that assert `Attesto.DPoP.verify_proof/2` rejects it.

`flaw` is one of the `t:flaw/0` values. `htm`/`htu` are the values the
request will actually carry; the defect is applied relative to them
(e.g. `:wrong_htu` signs a different URI than `htu`). `opts` is the same
as `proof/4`; for `:missing_ath`, pass `:access_token` (the proof omits
`ath` despite the token being presented, which the verifier rejects with
`:missing_ath`).

# `mint_access_token`

```elixir
@spec mint_access_token(
  Attesto.Config.t(),
  Attesto.Token.principal(),
  JOSE.JWK.t(),
  Attesto.Token.mint_opts()
) :: {String.t(), Attesto.Token.token_response()}
```

Mint a DPoP-sender-constrained access token bound to `jwk`.

Computes the RFC 7638 thumbprint of `jwk`'s public half and mints a
token through `Attesto.Token.mint/3` with that thumbprint as the
`cnf.jkt` binding (RFC 9449 §6 / RFC 7800). `principal` and `opts` are
passed through to `mint/3` unchanged (except `:dpop_jkt`, which this
function supplies), so the caller controls subject, scope, audience,
lifetime, and clock exactly as with a direct mint.

Returns `{access_token, token_response}` where `token_response` is the
full `Attesto.Token.mint/3` map (`token_type` is `"DPoP"`). Raises if
`mint/3` returns an error, since a fixture that fails to mint is a test
bug, not a condition under test.

# `proof`

```elixir
@spec proof(JOSE.JWK.t(), String.t(), String.t(), keyword()) :: String.t()
```

Build a valid DPoP proof JWT signed with `jwk` for `(htm, htu)`.

The proof carries the protected header `%{"typ" => "dpop+jwt", "alg" =>
..., "jwk" => <public jwk>}` and the payload `%{"htm" => htm, "htu" =>
htu, "iat" => now, "jti" => <random>}` (RFC 9449 §4.2). The signing
`alg` is derived from the key shape via `Attesto.SigningAlg`, and only
the key's public half is embedded, so the result verifies under
`Attesto.DPoP.verify_proof/2`.

Options:

  * `:access_token` - when given, the proof carries `ath`
    (`base64url(SHA-256(access_token))`, RFC 9449 §4.3) so it verifies
    against the bound token on a protected-resource request. Omit it for
    a token-endpoint proof, where no access token exists yet.
  * `:nonce` - a server-issued DPoP nonce to carry in the `nonce` claim
    (RFC 9449 §8).
  * `:now` - `DateTime` or unix-seconds clock used for `iat`. Defaults
    to `DateTime.utc_now/0`.
  * `:jti` - override the random replay identifier (e.g. to drive a
    replay test that presents the same `jti` twice).

---

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