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 with ATTESTO_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 jti cap, valid scope and resource, exactly one supported cnf.jkt member, and rejection of unsupported authorization_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 only age_over_21 and watch name and birthdate stay in the wallet. Runs entirely on the published attesto + attesto_client libraries.

[2.3.0] - 2026-08-03

Added

  • Add the OID4VCI/OID4VP wallet-holder role. AttestoClient.Wallet drives 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 plural proofs, 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 :dpop option with a use_dpop_nonce single retry; a DPoP-bound access token is presented with the DPoP authentication scheme.
  • Add AttestoClient.WalletAttestation (OAuth Attestation-Based Client Authentication — Client Attestation JWT + per-request PoP) and a {:client_attestation, ...} client-auth method, and AttestoClient.KeyAttestation (OID4VCI Key Attestation) carried in the holder proof's key_attestation header. 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 a direct_post vp_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_trusted before 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; refuse direct_post.jwt in submit/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 Ed25519 and Ed448 identifiers across issuer-signed JWT verification and client artifact builders while retaining legacy EdDSA.
  • Add independent Node crypto parity for Ed25519 and Ed448 signatures and OIDC detached hash claims.

Security

  • Bind ID Token at_hash, c_hash, and s_hash calculation to the verified issuer JWK. Legacy EdDSA now 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, and key_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 explicit accepted_algs list remains a deliberate non-FAPI override unless enforce_fapi_alg_policy: true is 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 nil as 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-kid rotation refresh, response-size limits, and refresh- storm throttling/backoff.
  • Add AttestoClient.ResourceServer.Plug for 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-free 503 rather than an invalid_token challenge.
  • Add bidirectional PyJWT parity coverage for RFC 9068 signing and verification, including an independently signed unknown-kid key-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 optional nbf, and uses constant-time nonce/subject comparisons. A missing kid remains 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.Store and a bounded, single-node ETS implementation with atomic insert/consume and monotonic expiry.
  • AttestoClient.Token refresh and RFC 7009 revocation operations, AttestoClient.RefreshCoordinator, AttestoClient.TokenSet, and verified refresh ID Token results.
  • AttestoClient.Logout.url/1 for 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_age is retained in the transaction and enforced against the callback ID Token's auth_time.

Changed

  • Supplying :access_token to AttestoClient.IDToken.verify/2 validates at_hash when present but no longer requires the claim for a token-endpoint ID Token, where OIDC Core permits omission. Pass require_at_hash: true for 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 attesto dependency both depend on Elixir's built-in JSON module.

Migration

  • AttestoClient.AuthorizationCode.start/2 and callback/3 now 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 kid values, 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/2 now compares the document's issuer exactly 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's https://.../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/2 accepts allow_unsigned: true, an explicit opt-in for the OIDC Core §3.1.3.7 case: a client that registered id_token_signed_response_alg none and 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.Discovery hardens its discovery/JWKS fetches against SSRF: redirects are no longer followed (a 3xx surfaces as {:http_status, _} rather than being chased to its Location), and a URL whose host resolves to a loopback, private, link-local, or unique-local address is rejected with :blocked_host — so an attacker-influenced issuer/jwks_uri cannot 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 detached at_hash / c_hash / s_hash validation. Interop-tested against Attesto.IDToken.mint/4.
  • AttestoClient.IdentityAssertion - build Identity Assertion JWT Authorization Grant assertions (ID-JAG / EMA) with the oauth-id-jag+jwt header and the required iss/sub/aud/client_id/jti/iat/exp claims. Interop-tested against Attesto.IdentityAssertion.verify/3.
  • AttestoClient.PKCE - generate S256 PKCE verifier/challenge pairs, delegating challenge computation to Attesto.PKCE.challenge/1 so generated pairs verify under Attesto.PKCE.verify/3.
  • AttestoClient.SignedIntrospection - verify RFC 9701 signed token introspection responses against authorization-server JWKS/discovery. Interop-tested against Attesto.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 - build private_key_jwt client 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 against Attesto.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 the oauth-authz-req+jwt typ, 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 against Attesto.RequestObject.verify/3 under 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, none rejected, kid selection), plus iss/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 by Attesto.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) over Req, with https and RFC 8414 §3.3 issuer-match validation. Verified in-family against Attesto.OpenIDDiscovery.metadata/2 output.

Changed

  • Require attesto ~> 0.9 so the client mirror can use the current ID Token, ID-JAG, PKCE, signed introspection, signing-algorithm, and hash primitives.