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 set, 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 is the tuple
(subject, client_id, redirect_uri, scope, 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.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 the newline-joined canonical fields,
URL-base64 encoded (no 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.
Types
@type binding() :: %{ subject: String.t(), client_id: String.t(), redirect_uri: String.t(), scope: [String.t()], 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 ("client_id", "redirect_uri", "scope",
"code_challenge", and "code_challenge_method"). Missing "scope" becomes
[]; a present scope string is split on spaces; missing PKCE fields become
nil. Unknown params are ignored. 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 the newline-joined canonical fields, URL-base64 encoded without
padding. The scope set is order-normalized (sorted then space-joined) so scope
order is not significant (RFC 6749 §3.3); a missing code_challenge and
code_challenge_method each hash as the empty string. Identical on the mint
and consume sides for the same request.