Postgres-backed Attesto.DPoP.NonceStore for clustered deployments
(RFC 9449 §8).
A server may require a server-issued nonce in every DPoP proof, binding
each proof to a short-lived, server-chosen value. This defeats proof
pre-generation and narrows the replay window beyond what the jti cache
(RFC 9449 §11.1) alone provides. An in-memory store cannot share a nonce
across nodes: a nonce issued on one node would be unknown to another. This
implementation persists each nonce so any node honours a nonce issued by any
other.
Behaviour callbacks
issue/1mints, persists, and returns an opaque nonce for theDPoP-Nonceresponse header (RFC 9449 §8.1). The TTL is recorded on the row as a concreteexpires_atso a later check needs no TTL argument.valid?/1reports whether a nonce was issued by this store, has not expired, and has not yet been consumed. It is read-only, so it is the shapeAttesto.DPoP.verify_proof/2expects for:nonce_checkwhen the caller does not need single-use consumption.
Single-use consume
A read-only check cannot make a nonce single-use under concurrency: two
requests could both observe the same live nonce. accept/2 is the atomic
consume primitive that delivers the single-use guarantee (RFC 9449 §8). It
marks the nonce used and returns :ok to exactly one caller, or
{:error, :used | :expired | :unknown} to every other, via one conditional
statement:
UPDATE dpop_nonces
SET used_at = $now
WHERE nonce = $nonce AND used_at IS NULL AND issued_at >= $cutoffPostgres serialises concurrent updates to the same row, so exactly one
caller observes an affected-row count of 1 (the winner) and the rest
observe 0. No read-modify-write race exists.
TTL
issue/1 records issuance and the derived expiry; accept/2 also takes the
TTL so the caller's policy, not this store, fixes the freshness window. A
nonce whose issued_at is older than now - ttl is rejected with
{:error, :expired} (RFC 9449 §8).
The repository is read from the supplied AttestoPhoenix.Config.
Summary
Functions
Atomically consumes nonce under a freshness window of ttl seconds.
Like accept/2, using an explicit AttestoPhoenix.Config.
Mints and persists a fresh nonce valid for ttl_seconds, returning the
opaque value to put in a DPoP-Nonce header. Behaviour entrypoint; resolves
the repo from the application-wide configured AttestoPhoenix.Config.
Like issue/1, using an explicit AttestoPhoenix.Config.
Returns true iff nonce was issued by this store, has not expired, and has
not been consumed. Behaviour entrypoint; resolves the repo from the
application-wide configured AttestoPhoenix.Config.
Like valid?/1, using an explicit AttestoPhoenix.Config.
Functions
@spec accept(String.t(), pos_integer()) :: :ok | {:error, :used | :expired | :unknown}
Atomically consumes nonce under a freshness window of ttl seconds.
Returns :ok to the single caller that wins the consume, and a precise
reason to every other so the server can answer with the correct DPoP error
(RFC 9449 §8) rather than silently rejecting (fail-closed):
{:error, :used}- the nonce was already consumed.{:error, :expired}- the nonce is past the freshness window.{:error, :unknown}- the nonce was never issued by this store.
Behaviour-style entrypoint; resolves the repo from the application-wide
configured AttestoPhoenix.Config.
@spec accept(AttestoPhoenix.Config.t(), String.t(), pos_integer()) :: :ok | {:error, :used | :expired | :unknown}
Like accept/2, using an explicit AttestoPhoenix.Config.
@spec issue(pos_integer()) :: String.t()
Mints and persists a fresh nonce valid for ttl_seconds, returning the
opaque value to put in a DPoP-Nonce header. Behaviour entrypoint; resolves
the repo from the application-wide configured AttestoPhoenix.Config.
@spec issue(AttestoPhoenix.Config.t(), pos_integer()) :: String.t()
Like issue/1, using an explicit AttestoPhoenix.Config.
Returns true iff nonce was issued by this store, has not expired, and has
not been consumed. Behaviour entrypoint; resolves the repo from the
application-wide configured AttestoPhoenix.Config.
@spec valid?(AttestoPhoenix.Config.t(), String.t()) :: boolean()
Like valid?/1, using an explicit AttestoPhoenix.Config.