OAuth and OpenID Connect Authorization Code flows with S256 PKCE.
start/2 validates discovery metadata, creates high-entropy state, nonce, and
PKCE values, stores the transaction with a finite lifetime, and returns the
authorization URL. callback/3 atomically consumes state, exchanges the code
exactly once, and verifies the ID Token against the stored issuer, client,
nonce, configured algorithm, and returned access token.
Transactions are also bound to an opaque application-supplied browser-session value, preventing a response initiated in one user agent from being used in another. The store protects protocol correlation only. Applications remain responsible for deciding whether verified claims authorize a user, creating or retaining a session, and persisting rotated tokens.
protocol: :oauth supports plain OAuth authorization without OIDC metadata
or an ID Token. PAR and DPoP are supported in either protocol mode.
Summary
Functions
Consume an authorization response and complete the code exchange.
Begin an authorization transaction.
Types
@type store() :: AttestoClient.AuthorizationTransaction.Store.store()
Functions
@spec callback(store(), map() | String.t(), keyword()) :: {:ok, %{tokens: AttestoClient.TokenSet.t(), id_token_claims: map() | nil}} | {:error, term()}
Consume an authorization response and complete the code exchange.
The second argument may be either the original callback URI (or raw
application/x-www-form-urlencoded response body) or a string-keyed callback
parameter map. Prefer the encoded form: it is inspected before conversion to
a map and rejects repeated parameter names, including percent-encoded aliases
such as co%64e plus code. A framework-produced map cannot reveal names
that its parser already collapsed; reject duplicates at that parser or pass
the original URI/body here.
State is consumed before any token request, so replay and concurrent duplicate
callbacks fail. A malformed or ambiguous encoded response is rejected before
state is consumed.
:browser_binding is required and must equal the opaque value supplied to
start/2; a mismatch consumes state and fails before token exchange. The
client authentication option is forwarded as :client_auth; supported forms
are :none, {:client_secret_basic, secret},
{:client_secret_post, secret}, and {:private_key_jwt, jwk}.
The three-element form {:private_key_jwt, jwk, assertion_opts} accepts
:alg, :kid, :audience, :lifetime, :now, and :jti for registrations
whose client assertion differs from the defaults; :client_id is always
pinned to the stored transaction.
A timeout leaves the remote outcome unknown and the transaction consumed; do not retry an authorization code.
@spec start( store(), keyword() ) :: {:ok, %{url: String.t(), state: String.t(), expires_in: pos_integer()}} | {:error, term()}
Begin an authorization transaction.
Required options are :issuer, :client_id, :redirect_uri, and an opaque
:browser_binding retained in the initiating user agent's secure, HttpOnly
application session. The same binding is mandatory at callback. The value is
protocol correlation only; authorization and session policy remain
application-owned.
Discovery is fetched unless :metadata is supplied. :scopes defaults to
["openid"] and must include openid. :id_token_alg defaults to "RS256"
and must match the client's registration and the provider metadata.
Additional request values may be supplied in :authorization_params, but
protocol-bound parameters and client-authentication fields cannot be
overridden, including bracket aliases decoded by web frameworks.
Use protocol: :oauth for a plain OAuth flow, including OID4VCI issuance.
It does not require the openid scope, OIDC metadata, or an ID Token; the
callback returns id_token_claims: nil. The protocol is pinned in the
transaction and cannot be changed at callback.
par: true submits the request to the advertised Pushed Authorization
Request endpoint with :client_auth and :dpop. The default :auto uses
PAR when the server or selected profile requires it. par: false cannot
override a required PAR policy. DPoP keys supplied at start are bound to the transaction and
must also be supplied at callback; their thumbprint is sent as dpop_jkt.
:alg, :kid, and :now configure the DPoP proofs and must remain
compatible with the retained key and selected profile.
Set exactly one of haip: true or fapi?: true to select the stored profile.
Both require authenticated PAR, the authorization response's iss, and a
retained DPoP private key even when metadata omits the corresponding flags.
This client supports the DPoP sender constraint; it does not bind mTLS
certificate identities. FAPI requires private_key_jwt with a compatible
signing algorithm. HAIP supports the existing authenticated methods, including
client attestation according to the issuer's ecosystem policy.
Retain the same client-authentication method and signing key at callback.
Client attestation retains the client subject and instance key, so renewed
attestations and refreshed challenges are allowed. HAIP's Appendix E format
requires a bounded x5c certificate chain. The authorization server verifies
its signature, certificate trust and validity; an optional attester iss
claim is not part of the local instance binding.
The transaction records public key identity or a secret digest, never private
signing material. Callback options cannot weaken the selected profile.
Generic flows preserve existing authentication choices, require iss when
metadata advertises support, and always reject a returned issuer mismatch.