All notable changes to this project are documented here. The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
[2.5.0] - 2026-08-31
Changed
- Support both Attesto 1.x and 2.x. Attesto 2.0 changes authorization-server storage contracts that AttestoClient does not implement; the protocol and verification APIs used by this package remain compatible.
- The published package dependency is
>= 1.13.0 and < 3.0.0, so ordinary Hex resolution can select either supported Attesto major line. No AttestoClient application-code migration is required; source checkouts may opt into the coordinated Attesto 2 path withATTESTO_PATH=1.
[2.4.1] - 2026-08-27
Security
- Carry the screened IP address through every OAuth, wallet, discovery, and
JWKS request while retaining the external hostname for TLS verification,
SNI, HTTP authority, DPoP
htu, and client-assertion audiences. DPoP nonce retries reuse the same screened address. This closes the remaining DNS- rebinding interval in the shared OAuth HTTP path. - Isolate protected requests from Req application defaults and caller options that can replace routing, credentials, protocol bodies, redirects, TLS peer identity, or the screened origin. Unsafe proxies and incompatible Finch pools now fail closed; URL-keyed Req caching is disabled because different HTTPS authorities can share a pinned IP and path.
- Keep metadata and JWKS retrieval unauthenticated, force discovery requests to use GET, remove hop-by-hop request headers, and include DNS resolution and request preparation within the caller's overall deadline. OAuth POST response bodies are now bounded before decoding as well as by-reference GET bodies.
- Refresh the IPv6 special-purpose address policy from IANA's registry, rejecting the dummy, deprecated protocol-assignment, and documentation ranges and all reserved space outside conventional global unicast, while retaining ordinary globally reachable allocations.
Upgrade notes
- Protected network requests now reject caller-owned proxies, Finch pools,
adapters, and Unix sockets because they can bypass the screened destination
or its TLS identity. Applications that previously supplied one of those
routing overrides will receive
{:error, :unsafe_transport_options}and should move controlled egress routing outside the request process.
[2.4.0] - 2026-08-11
Security
- Pin discovery and JWKS connections to an address that was screened against
special-use networks, while retaining TLS SNI, certificate verification, and
the HTTP Host authority on the original hostname. This closes the DNS-
rebinding interval between the prior pre-flight lookup and the socket connect.
A host that resolves to no address is now rejected (
:blocked_host) rather than handed to the transport — the screening path is fail-closed. - Bound discovery and JWKS response bodies before JSON decoding and disable transparent response decompression on those trust-root fetches.
- Reject unsafe RSA modulus/exponent parameters from a remote JWKS before JOSE constructs bignums or attempts signature verification, preventing scheduler- pinning verification work.
- Require
attesto >= 1.13.0, carrying the matching raw-key bounds and current parser, wallet, status-list, and ID-JAG hardening into every client installation. - Make the ID-JAG builder enforce the server's signed-claim constraints before
signing: a 256-byte
jticap, validscopeandresource, exactly one supportedcnf.jktmember, and rejection of unsupportedauthorization_details.
[2.3.1] - 2026-08-03
Added
- Add an end-to-end digital wallet Livebook (
guides/digital_wallet.livemd), now rendered in the docs. Issue an SD-JWT VC PID credential, hold it, and present it to a verifier with an interactive selective-disclosure form — request onlyage_over_21and watch name and birthdate stay in the wallet. Runs entirely on the publishedattesto+attesto_clientlibraries.
[2.3.0] - 2026-08-03
Added
- Add the OID4VCI/OID4VP wallet-holder role.
AttestoClient.Walletdrives pre-authorized_code issuance end to end — credential offer, token exchange,c_nonce, holder key proof, Credential Request, and verification of each returned credential (SD-JWT VC,jwt_vc_json, or mdoc) — with support for OID4VCI 1.0 final's pluralproofs, batch issuance (a list of holder keys → one credential per key), and the §10 Notification Endpoint. - Add
AttestoClient.DPoP(RFC 9449 proof generation), threaded through the token and credential requests via a:dpopoption with ause_dpop_noncesingle retry; a DPoP-bound access token is presented with theDPoPauthentication scheme. - Add
AttestoClient.WalletAttestation(OAuth Attestation-Based Client Authentication — Client Attestation JWT + per-request PoP) and a{:client_attestation, ...}client-auth method, andAttestoClient.KeyAttestation(OID4VCI Key Attestation) carried in the holder proof'skey_attestationheader. These are the client-side mirrors of the matching attesto verifiers. - Add OID4VP presentation (
AttestoClient.Wallet.Presentation/PresentationRequest): verify a signed Authorization Request, select held credentials by DCQL, and build adirect_postvp_token(SD-JWT VC with a holder Key Binding JWT, or an ISO 18013-5 mdoc DeviceResponse).
Security
- Bind each issued credential to the holder key whose proof requested it
(thumbprint membership, one credential per proof), and fail closed with
:missing_trustedbefore any network call when no issuer trust anchor is supplied. - Scope SD-JWT claim minimisation to top-level Disclosures (digest present in
the issuer payload's
_sd), so a nested claim sharing a requested top-level name is never disclosed; refusedirect_post.jwtinsubmit/3; check the signing key against the credential's holder binding before disclosing any contents; and validate the DCQL query shape so a signed-but-malformed request cannot crash selection. - Refresh client-auth
jtis on a DPoP-nonce retry (no replay), send a single Authorization header, require an explicit audience for the client-attestation PoP, and bound by-reference fetch bodies (with decompression disabled).
[2.2.0] - 2026-07-25
Added
- Support RFC 9864
Ed25519andEd448identifiers across issuer-signed JWT verification and client artifact builders while retaining legacyEdDSA. - Add independent Node crypto parity for Ed25519 and Ed448 signatures and OIDC detached hash claims.
Security
- Bind ID Token
at_hash,c_hash, ands_hashcalculation to the verified issuer JWK. LegacyEdDSAnow selects SHA-512 with a 32-byte left half for Ed25519 and SHAKE256 with 114 bytes of output and a 57-byte left half for Ed448. - Move JARM onto the shared hardened verifier: require a unique eligible key,
honor JWK
alg,use, andkey_ops, reject RSA keys below 2048 bits, bind every algorithm to its key type and curve, and apply FAPI's Ed25519-only Edwards policy by default. An explicitaccepted_algslist remains a deliberate non-FAPI override unlessenforce_fapi_alg_policy: trueis set. - Validate every explicit outbound signing algorithm against its private key before signing, including exact Edwards curve binding.
Changed
- Require Attesto 1.3 or later for key-aware OIDC hashing and trusted-key algorithm validation.
- Keep the JOSE security floor at 1.11.12 while widening the upper-compatible range to all 1.x releases, so a future 1.x release with native OTP SHA-3 and Ed448 detection is reachable without another AttestoClient release.
[2.1.1] - 2026-07-17
Security
- Restrict the optional Plug integration to the security-patched releases on every supported minor line: Plug 1.16.6, 1.17.4, 1.18.5, 1.19.5, and 1.20.3 and later 1.x releases.
- Raise the JOSE floor to 1.11.12, excluding releases affected by
GHSA-9mg4-v392-8j68 / CVE-2023-50966, the 1.11.7-1.11.8 packages that
included development-only tooling as runtime dependencies, and the
1.11.9-1.11.10 releases with broken EC key conversion on OTP 28. JOSE
1.11.11 fixed that conversion but could encode Elixir
nilas the JSON string"nil"with OTP's built-in JSON module; 1.11.12 fixes that too. - Refresh the development lock to Mint 1.9.3, resolving EEF-CVE-2026-59249 / CVE-2026-59249.
- Extend discovery and JWKS SSRF checks across non-global IPv4, translated IPv6, deprecated site-local, and local-use NAT64 destinations while retaining globally reachable anycast and public NAT64 targets.
- Require a replay callback for every DPoP request; the former test-only acknowledgement flag can no longer disable replay protection.
- Keep internal token, DPoP, certificate, and nonce rejection reasons out of OAuth error responses.
[2.1.0] - 2026-07-16
Added
- Add
AttestoClient.ResourceServer, a supervised remote-issuer RFC 9068 JWT access-token verifier. It performs exact issuer and audience checks, strict algorithm/key selection, required claim and time validation, exact scope enforcement, optional subject/client and token-age/lifetime policy, explicit warming/readiness, and fail-closed DPoP/mTLS confirmation binding. - Add coordinated discovery/JWKS caching with bounded key count, configurable
fresh and stale intervals, transient-error-only stale-key use, single-flight
refresh, unknown-
kidrotation refresh, response-size limits, and refresh- storm throttling/backoff. - Add
AttestoClient.ResourceServer.Plugfor Bearer, DPoP, and mTLS protected resources, including fail-closed DPoP replay wiring and RFC 6750 error and insufficient-scope responses. Upstream key availability failures produce a detail-free503rather than aninvalid_tokenchallenge. - Add bidirectional PyJWT parity coverage for RFC 9068 signing and verification,
including an independently signed unknown-
kidkey-rotation flow.
Security
- Remote discovery and JWKS refreshes retain HTTPS, exact-issuer, SSRF, and redirect protections; disable Req retries and bound each coordinated refresh with an application-configured deadline.
[2.0.0] - 2026-07-16
Security
- Add a complete OpenID Connect Authorization Code flow that always uses S256 PKCE, binds callbacks to high-entropy state, nonce, and a mandatory opaque application browser-session value, validates the response issuer when supplied, atomically consumes expiring transaction state, pins the registered ID Token algorithm, and gives the code exchange a bounded deadline with retries disabled.
- ID Token verification now rejects ambiguous eligible JWKS keys and RSA keys
below 2048 bits, honors JWK
use/key_ops, validates optionalnbf, and uses constant-time nonce/subject comparisons. A missingkidremains valid only when exactly one eligible verification key exists. - Add bounded single-flight refresh-token rotation. Concurrent callers for one application key share one request and one result; timeout and worker failure wake all waiters and clear the flight.
- Require HTTPS redirect URIs except for native-client loopback HTTP redirects.
- Raise the Req dependency floor to 0.6.1, the first release patched for EEF-CVE-2026-49755 decompression-bomb denial of service.
Added
AttestoClient.AuthorizationTransaction.Storeand a bounded, single-node ETS implementation with atomic insert/consume and monotonic expiry.AttestoClient.Tokenrefresh and RFC 7009 revocation operations,AttestoClient.RefreshCoordinator,AttestoClient.TokenSet, and verified refresh ID Token results.AttestoClient.Logout.url/1for OpenID Connect RP-Initiated Logout.- Structural discovery validation for required OIDC endpoints and capability fields before an authorization flow uses them.
- Adversarial coverage for replay and concurrent state consumption, expiry, issuer/audience/nonce confusion, ambiguous or ineligible keys, weak RSA, malformed discovery, refresh races, independent refreshes, and deadlines.
- Authorization-request
max_ageis retained in the transaction and enforced against the callback ID Token'sauth_time.
Changed
- Supplying
:access_tokentoAttestoClient.IDToken.verify/2validatesat_hashwhen present but no longer requires the claim for a token-endpoint ID Token, where OIDC Core permits omission. Passrequire_at_hash: truefor front-channel or profile rules that require it. - Authorization decisions, durable token persistence, refresh-result compare-and-swap, and session-retention/termination remain application-owned.
- The Elixir floor remains 1.18: this package and its required
attestodependency both depend on Elixir's built-inJSONmodule.
Migration
AttestoClient.AuthorizationCode.start/2andcallback/3now require the same opaque:browser_binding. Generate or retain it in the initiating user agent's secure application session; callbacks with a missing or different binding consume state and fail before token exchange.- The key-selection and minimum-RSA checks intentionally reject tokens that 1.x
could accept. Applications with duplicate
kidvalues, multiple eligible kid-less keys, encryption-only verification keys, or RSA keys below 2048 bits must correct their JWKS before upgrading. - Because those security checks tighten existing public verification APIs, this release is a 2.0.0 major release.
[1.1.0] - 2026-07-07
Changed
AttestoClient.Discovery.fetch/2now compares the document'sissuerexactly against the supplied issuer (RFC 8414 §3.3 / OpenID Connect Discovery 1.0 §4.3) instead of normalising a trailing slash away. A slash-terminated path issuer (e.g. a multi-tenant issuer, or the OpenID conformance suite'shttps://.../test/a/<alias>/) was previously rejected with:issuer_mismatch; conversely, two identifiers that differ only by a trailing slash no longer match. The trailing slash is still removed when constructing the well-known request URL, as both specs require.
Added
AttestoClient.IDToken.verify/2acceptsallow_unsigned: true, an explicit opt-in for the OIDC Core §3.1.3.7 case: a client that registeredid_token_signed_response_algnoneand received the ID Token directly from the token endpoint over TLS may accept an unsigned (alg: "none") token. All claim checks still run; the signature part must be empty; signed tokens are unaffected; the default remains to reject unsigned tokens.
[1.0.0] - 2026-07-04
First stable release; the public API is now under semantic versioning. No
functional change from 0.6.0. Requires attesto ~> 1.0.
[0.6.0] - 2026-06-21
Security
AttestoClient.Discoveryhardens its discovery/JWKS fetches against SSRF: redirects are no longer followed (a 3xx surfaces as{:http_status, _}rather than being chased to itsLocation), and a URL whose host resolves to a loopback, private, link-local, or unique-local address is rejected with:blocked_host— so an attacker-influencedissuer/jwks_uricannot point a server-side fetch at an internal service or the cloud metadata endpoint. An unresolvable host is left to the transport.
Added
AttestoClient.IDToken- verify OpenID Connect ID Tokens against authorization server JWKS/discovery, including issuer, audience,azp, expiration, issued-at, nonce,max_age/auth_time, and detachedat_hash/c_hash/s_hashvalidation. Interop-tested againstAttesto.IDToken.mint/4.AttestoClient.IdentityAssertion- build Identity Assertion JWT Authorization Grant assertions (ID-JAG / EMA) with theoauth-id-jag+jwtheader and the requirediss/sub/aud/client_id/jti/iat/expclaims. Interop-tested againstAttesto.IdentityAssertion.verify/3.AttestoClient.PKCE- generate S256 PKCE verifier/challenge pairs, delegating challenge computation toAttesto.PKCE.challenge/1so generated pairs verify underAttesto.PKCE.verify/3.AttestoClient.SignedIntrospection- verify RFC 9701 signed token introspection responses against authorization-server JWKS/discovery. Interop-tested againstAttesto.SignedIntrospection.response_jwt/4.AttestoClient.UserInfo- verify signed OpenID Connect UserInfo JWT responses, including issuer/audience/subject checks and optional binding to a previously verified ID Token subject.- Internal AttestoClient.Verifier shared by the AS-signed JWT verifiers (not public API; hidden from docs).
AttestoClient.ClientAssertion- buildprivate_key_jwtclient authentication assertions (RFC 7523 / OpenID Connect Core §9), signed with the client's own key. Carries a cross-language parity test against an independent PyJWT reference verifier, plus in-family interop againstAttesto.ClientAssertion.verify/5.AttestoClient.RequestObject- build signed authorization request objects (JAR, RFC 9101 / FAPI 2.0 Message Signing §5.3.1): the caller's authorization parameters wrapped with the iss/aud/iat/nbf/exp/jti envelope and theoauth-authz-req+jwttyp, signed with the client's key. The lifetime is bounded to the FAPI 60-minute window. Parity-tested against an independent PyJWT reference and in-family againstAttesto.RequestObject.verify/3under the FAPI Message Signing policy.- Internal AttestoClient.Builder shared by the builders (not public API; hidden from docs).
AttestoClient.JARM- verify a signed authorization response (JARM, FAPI 2.0 Message Signing §5.4): JWS signature against the authorization server's JWKS (FAPI algorithm allow-list,nonerejected, kid selection), plusiss/aud/iat/exp, returning the response parameters. Parity-tested by verifying a JARM token signed by an independent PyJWT signer (the flipped external direction) and one signed byAttesto.JARM.response_jwt/4(in-family).AttestoClient.Discovery- fetch and read OAuth 2.0 / OpenID Connect authorization-server metadata and JWKS (RFC 8414 / OpenID Connect Discovery 1.0) overReq, withhttpsand RFC 8414 §3.3 issuer-match validation. Verified in-family againstAttesto.OpenIDDiscovery.metadata/2output.
Changed
- Require
attesto ~> 0.9so the client mirror can use the current ID Token, ID-JAG, PKCE, signed introspection, signing-algorithm, and hash primitives.