OpenFeed.Dpop (OpenFeed v0.1.0)

Copy Markdown View Source

DPoP proof JWTs (RFC 9449) for OpenFeed resource-server requests.

Every call to the sharing API carries two things: the access token in an Authorization: DPoP <token> header, and a single-use proof in a DPoP header, signed by the key OpenFeed has on file for your app. That is what makes the token sender-constrained: stealing it is not enough, because the thief cannot sign proofs.

Proofs are single-use. Never replay one — retrying a request means building a fresh proof, which is why OpenFeed.Client handles its own retries instead of delegating to Req's.

Claims

ClaimMeaning
jtiUnique per proof. The server rejects reuse.
htmHTTP method, uppercased.
htuTarget URI, normalised, with query and fragment removed.
iatIssued at.
expShort expiry (30s by default).
nbfNot before, back-dated by :clock_skew if set.
athSHA-256 of the access token. Present on resource calls.
nonceOnly when the server has demanded one via DPoP-Nonce.

exp and nbf are not required by RFC 9449, but oidcc's own proofs include them and they narrow the replay window, so we do too.

The algorithm is always PS256: OpenFeed's auth server hard-codes dPoPSigningAlgValues = ['PS256'] and rejects ES256 and EdDSA outright, so there is nothing to negotiate.

Summary

Functions

The ath claim value for an access token: base64url of its SHA-256, unpadded.

The htu claim value: the target URI with query and fragment removed.

Functions

ath(access_token)

@spec ath(String.t() | nil) :: String.t() | nil

The ath claim value for an access token: base64url of its SHA-256, unpadded.

Returns nil for nil, so it can be threaded through unconditionally.

htu(url)

@spec htu(String.t()) :: String.t()

The htu claim value: the target URI with query and fragment removed.

Also normalises away a default port (:443 for https, :80 for http), because the server reconstructs the URI from its own request and compares. A mismatch on https://host:443/x versus https://host/x would otherwise fail the proof for no good reason.

Examples

iex> OpenFeed.Dpop.htu("https://api.openfeed.au/v1/banking/accounts?limit=10#frag")
"https://api.openfeed.au/v1/banking/accounts"

iex> OpenFeed.Dpop.htu("https://api.openfeed.au:443/v1/app")
"https://api.openfeed.au/v1/app"

proof(config, method, url, opts \\ [])

@spec proof(OpenFeed.Config.t(), atom() | String.t(), String.t(), keyword()) ::
  {:ok, String.t()} | {:error, term()}

Build a DPoP proof.

Options

  • :access_token — include the ath claim. Required by the resource server; omit only for proofs sent to the authorization server before a token exists.
  • :nonce — the value from a DPoP-Nonce response header, when retrying after a nonce challenge.
  • :lifetime — seconds until exp. Defaults to 30.
  • :clock_skew — seconds to back-date nbf by. Defaults to 0.

Examples

{:ok, proof} = OpenFeed.Dpop.proof(config, :get, url, access_token: token)