ReqDPoP

Hex.pm Hexdocs.pm License: MIT

ReqDPoP is a small Req plugin for OAuth 2.0 DPoP proof generation as defined by RFC 9449.

It is not an OAuth provider, token store, or full OAuth client. It does not fetch tokens for you. It only signs DPoP proofs for outgoing Req requests and, when configured, attaches a DPoP-bound access token.

Installation

def deps do
  [
    {:req_dpop, "~> 0.6"}
  ]
end

Resource Request

key = ReqDPoP.Key.generate(:es256)

client =
  Req.new(base_url: "https://api.example.com")
  |> ReqDPoP.attach(key: key, access_token: access_token)

Req.get!(client, url: "/resource")

The plugin adds:

htu and htm are derived from the final Req request URL and method after Req has applied base URL and path options. Per RFC 9449, htu excludes the request URL's query string and fragment.

Token Endpoint Proof

Use proof-only mode by omitting :access_token:

Req.new()
|> ReqDPoP.attach(key: key)
|> Req.post!(url: "https://auth.example.com/oauth/token", form: params)

Nonce Retry

By default, ReqDPoP retries once when a response carries a single DPoP-Nonce header with an RFC 9449 nonce value and is a nonce challenge in either form:

The retry proof includes the server nonce. Nonces and the retry budget are tracked per origin. Configure this behavior with :retry_on_nonce and :max_nonce_retries.

A compressed challenge body is decoded only when the request was made with Req's compressed: true, the same opt-in Req applies to every response body.

Redirects and Retries

Each hop of a request gets its own proof for the hop's method and URL with a fresh jti, including hops made by Req's retry and redirect steps.

On an untrusted cross-origin redirect, Req removes the Authorization header. ReqDPoP then removes the DPoP header as well, in proof-only mode too, so the redirect target receives neither the token nor a proof. Set Req's redirect_trusted: true to send both to the new origin.

Key Persistence

Generated keys are process-local values. Production clients should persist the DPoP key if they need stable sender binding across restarts:

key = ReqDPoP.Key.generate(:es256)
jwk = ReqDPoP.Key.export(key)
key = ReqDPoP.Key.load!(jwk)

Security Notes

Development

The test suite includes server-side interop checks against Attesto's DPoP verifier as a test-only dependency; Attesto is not required at runtime.

mix format --check-formatted
mix compile --warnings-as-errors
mix credo --strict
mix test
mix docs
mix hex.build

License

MIT. See LICENSE.