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.1.0"}
]
endResource 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:
DPoP: <proof-jwt>Authorization: DPoP <access_token>athin the proof when an access token is present
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 is a DPoP nonce challenge:
- status
400or401 - a
DPoP-Nonceresponse header - a
WWW-Authenticateheader containingDPoPanduse_dpop_nonce
The retry proof includes the server nonce. Configure this behavior with
:retry_on_nonce and :max_nonce_retries.
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
- Do not log access tokens or private JWKs.
- Do not store tokens globally.
athis SHA-256 over the ASCII access token, encoded as unpadded base64url.- URL query strings and fragments are excluded from
htu. - Supported signing algorithms are
:es256and:rs256.
Development
mix format --check-formatted
mix compile --warnings-as-errors
mix credo --strict
mix test
mix docs
mix hex.build
License
MIT. See LICENSE.