AttestoClient.AuthorizationCode (AttestoClient v2.9.0)

Copy Markdown View Source

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

Functions

callback(store, response, opts \\ [])

@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.

start(store, opts)

@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.