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