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, and PKCE challenge — and
nothing else. This module builds that binding from an
%Attesto.AuthorizationRequest{} (the validated front-channel request) plus
the authenticated subject, and hashes it canonically so the grant and consume
sides agree on one digest.
Canonical binding
The binding is the tuple (subject, client_id, redirect_uri, scope, code_challenge):
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.
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 is nil when the request carries no PKCE
challenge.
Functions
Builds the consent binding for request consented to by 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 }
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 is 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, and code_challenge 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.
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 hashes as
the empty string. Identical on the mint and consume sides for the same request.