The request binding a single-use consent grant is tied to, and the canonical hash over it (RFC 6749 §4.1.1).
A consent grant must approve exactly the authorization request the resource
owner saw — the same client, redirect URI, scope and resource sets, requested
claims, DPoP key, PKCE challenge, and PKCE method — and nothing else. This
module builds that binding from either
raw authorization params (binding_from_params/2, used by the consent-screen
mint action) or from a validated %Attesto.AuthorizationRequest{} (binding/2,
used by the live /authorize consume side). Both builders feed the same
canonical field list and therefore yield the same binding_hash/1 for the
equivalent request.
Canonical binding
The binding contains (subject, client_id, redirect_uri, scope, resource, claims, dpop_jkt, code_challenge, code_challenge_method).
subject- the OIDCsubof the resource owner who consented. Binding to the subject stops one user's consent token from approving another's request.client_id/redirect_uri- the requesting client and the exact redirect URI the code will be returned to (RFC 6749 §3.1.2). Binding both stops a consent shown for one client/redirect from authorizing a different one.scope- the requested scope set. Order is not significant (RFC 6749 §3.3), so the set is sorted before hashing: a request withscope=openid profileand one withscope=profile openidhash identically, while adding or dropping a scope changes the hash.resource- the RFC 8707 resource set. Order and duplicates are insignificant, while changing the authorized resource changes the hash.claims- the validated OIDC claims request. JSON objects are encoded recursively with sorted keys and explicit type/length framing.dpop_jkt- the RFC 9449 authorization-code key binding, ornil.code_challenge- the PKCE challenge (RFC 7636 §4.3), or the empty string when the request carries none. Binding it stops a consent from being replayed against a request that swapped in a different PKCE challenge.code_challenge_method- the PKCE method (S256, RFC 7636 §4.3), or the empty string when the request carries no PKCE challenge. Binding it stops a consent granted for anS256request from being reused for aplainrequest with the same challenge value.
binding_hash/1 is SHA-256 over a type-tagged, length-framed canonical
encoding, URL-base64 encoded without padding. It is stable across the mint
and consume sides because both derive it from this one function.
Summary
Types
The fields a consent grant is bound to. subject, client_id, and
redirect_uri are required; scope is a (possibly empty) list whose order is
normalized away; code_challenge and code_challenge_method are nil when
the request carries no PKCE challenge.
Functions
Builds the consent binding for request consented to by subject.
Builds the consent binding from raw OAuth authorization params and subject.
The canonical binding hash for binding.
Safely builds a consent binding from untrusted raw parameters.
Types
@type binding() :: %{ subject: String.t(), client_id: String.t(), redirect_uri: String.t(), scope: [String.t()], resource: [String.t()], claims: map(), dpop_jkt: String.t() | nil, code_challenge: String.t() | nil, code_challenge_method: String.t() | nil }
The fields a consent grant is bound to. subject, client_id, and
redirect_uri are required; scope is a (possibly empty) list whose order is
normalized away; code_challenge and code_challenge_method are nil when
the request carries no PKCE challenge.
Functions
@spec binding(Attesto.AuthorizationRequest.t(), String.t()) :: binding()
Builds the consent binding for request consented to by subject.
request is the validated %Attesto.AuthorizationRequest{} (the front-channel
request whose client_id, redirect_uri, scope, code_challenge, and
code_challenge_method the user saw); subject is the authenticated resource
owner's OIDC sub. The returned map feeds binding_hash/1 and the store's
mint/2 / consume/2.
Builds the consent binding from raw OAuth authorization params and subject.
Use this on the consent-screen mint side, where the host has the raw
string-keyed params that reached /authorize / the consent action rather than
the validated %Attesto.AuthorizationRequest{}. The consume side should keep
using binding/2.
params is read by string keys. Scope and resource sets are normalized;
claims may be the original JSON object string or an already-decoded map.
Unknown params are ignored. Malformed values raise ArgumentError; callers
handling untrusted data without an existing validation boundary should use
safe_binding_from_params/2. For the equivalent validated request,
binding_hash(binding_from_params(params, subject)) == binding_hash(binding(request, subject)).
The canonical binding hash for binding.
SHA-256 over a type-tagged, length-framed encoding, URL-base64 encoded without padding. Scope and resource are sorted unique sets; nested claims maps are ordered by key. Identical on the mint and consume sides for the same request.
Safely builds a consent binding from untrusted raw parameters.
Returns {:error, :invalid_binding} for an invalid required scalar, set,
claims object, DPoP thumbprint value, or nested JSON value.