All notable changes to this project are documented here. The format is based on Keep a Changelog and this project adheres to Semantic Versioning.
[Unreleased]
[0.6.11] - 2026-06-03
Added
:accepted_algsoption onAttesto.ClientAssertion.verify/5andAttesto.RequestObject.verify/3(defaultAttesto.SigningAlg.fapi_algs/0), so the accepted client-authentication / request-object signature algorithms are caller-supplied policy rather than a hardcoded constant. The default preserves current behaviour.Attesto.SigningAlg.default_client_algs/0as a named helper for the default client-presented signature verification policy.- Strict JAR policy options on
Attesto.RequestObject.verify/3for the FAPI Message Signing 2.0 (§5.3.1) / RFC 9101 profile::require_nbf,:max_nbf_age_seconds,:require_exp,:max_lifetime_seconds, and:accepted_typ(e.g."oauth-authz-req+jwt").:require_nbf/:require_expdemand a non-negative integer NumericDate (a missing or malformed value fails);:max_lifetime_secondsrequires bothnbfandexpanchors. These default to the prior lenient behaviour, so callers opt into strictness with explicit policy.
Fixed
Attesto.RequestObject.verify/3now honoursnbfas a not-before claim (RFC 7519 §4.1.5): a request object withnbfin the future is rejected as:not_yet_valideven in lenient mode (clock skew tolerated).
[0.6.10] - 2026-06-02
Changed
- Require a single-valued string
audin client-authentication assertions (FAPI 2). An arrayaudis now rejected even when it contains an accepted value, and the string must match an expected audience exactly.
[0.6.9] - 2026-06-02
Changed
- Restrict client-authentication assertions (
private_key_jwt) and request objects to the FAPI 2 signing algorithms PS256, ES256, and EdDSA. Assertions or request objects signed with RS256 are now rejected.Attesto.SigningAlgexposes the permitted set viafapi_algs/0. The provider's own token signing (allowed/0) is unaffected and still admits RS256.
[0.6.8] - 2026-06-02
Fixed
- Canonicalize DPoP
htuURI comparison by ignoring query/fragment, normalizing scheme and host case, and treating an explicit HTTPS default port as equivalent to an omitted port. Non-HTTPS URIs, host/path mismatches, and non-default port mismatches remain rejected.
[0.6.7] - 2026-06-01
Fixed
- Accept DPoP proof
iatvalues up to 60 seconds ahead of the server clock, matching Attesto's JWT verifier clock-skew policy. Proofs remain short-lived throughmax_age_seconds, and replay-cache TTLs now cover the full acceptance window.
[0.6.6] - 2026-06-01
Fixed
- Sign
PS256JWTs with the RFC 7518 salt length (32 bytes for SHA-256) instead of JOSE/OpenSSL's maximum salt length. This makes PS256 access tokens and ID Tokens verifiable by strict FAPI/OIDF validators while keeping Attesto's key-derived algorithm policy unchanged. - Treat signed authorization request object parameters as authoritative
(RFC 9101 §6.3). When a
requestJWT is present, unsigned query parameters no longer supplement missing signed parameters such as PKCE inputs. - Require signed request objects to carry
iss, matchingclient_id, and a configuredaud, preventing cross-client or cross-issuer replay of otherwise valid request objects. - Reject access-token-shaped payloads during ID Token verification even when the
access token JOSE
typheader is intentionally disabled.
[0.6.5] - 2026-06-01
Fixed
- Allow an authorization code that was not pre-bound with
dpop_jktto be redeemed at the token endpoint with a DPoP proof. Codes explicitly bound withdpop_jktstill require the exact same proof key at redemption. This matches FAPI-style DPoP flows where the authorization request does not pre-bind the code, but the token endpoint proof sender-constrains the access token being minted.
[0.6.4] - 2026-06-01
Fixed
- Load keystore modules before checking optional callbacks such as
verification_pems/0,key_algs/0, andsigning_alg/0. Cold modules now advertise and use their configured per-key algorithms deterministically instead of briefly falling back to inferred RSARS256metadata.
[0.6.3] - 2026-06-01
Added
- Allow OAuth authorization-server metadata (RFC 8414) hosts to advertise
authorization_response_iss_parameter_supportedandtoken_endpoint_auth_signing_alg_values_supported. These are host capability declarations; Attesto still drops nil values and ignores unlisted metadata keys.
[0.6.2] - 2026-06-01
Fixed
- Unsigned OpenID Connect request objects (
requestJWTs withalg: "none") are now rejected with the redirectablerequest_not_supportederror instead ofinvalid_request_object. Attesto still deliberately does not accept unsigned request objects; this change makes the unsupported-feature signal match OIDC Core §3.1.2.6 and the OpenID conformance suite.
[0.6.1] - 2026-05-31
Added
Attesto.Test.DPoPVerifier- a server-side DPoP verification harness for host application suites, the counterpart toAttesto.Test.DPoP. From a plain request description (method,url,headers) it verifies the presented DPoP proof and, whenverify_token: true, the access token, returning{:ok, verified}or an{:error, challenge}map carrying the HTTP status, theWWW-Authenticatechallenge, and an optionalDPoP-Nonce. It does not reimplement RFC 9449: it delegates every decision to the production verifiersAttesto.DPoP.verify_proof/2andAttesto.Token.verify/3, and mirrors the resource server's scheme handling (a DPoP-bound token presented as Bearer surfaces aDPoPchallenge, RFC 9449 §7.1; a missing required nonce surfacesuse_dpop_nonce, §8). It depends on neither Plug, Phoenix, nor any HTTP client, so it runs from any ExUnit suite.Attesto.Test.DPoP- DPoP test fixtures for host application suites (RFC 9449). Ships underlib/so a consumer can call it from itstest/tree without depending on Attesto's own test support.generate_key/1mints a proof key (EC P-256 /ES256by default);mint_access_token/4mints a DPoP-sender-constrained access token bound to that key viacnf.jkt(RFC 7800);proof/4builds a valid proof JWT for a(htm, htu)pair, optionally carryingath(RFC 9449 §4.3) and a servernonce(§8);invalid_proof/5builds a proof with a single deliberate defect (:wrong_htm,:wrong_htu,:missing_ath,:expired) for negative tests. Every fixture is built through the same primitives the production code uses (Attesto.Token.mint/3,Attesto.DPoP.compute_jkt/1,Attesto.DPoP.compute_ath/1,Attesto.SigningAlg.infer/1,JOSE.JWS), and embeds only the proof key's public half (RFC 9449 §4.2), so a fixture is correct by construction againstAttesto.DPoP.verify_proof/2and stays in step with it.
[0.6.0]
Added
Attesto.IDToken.mint/3rounds out the OpenID Connect Core §2 ID Token claim set:auth_time(REQUIRED when the request asked for it or carriedmax_age),acr,amr, andazpare accepted as optional inputs and omitted when absent. Arbitrary additional claims requested through the OIDC Core §5.5claimsparameter or a host userinfo mapping are supplied via:extra_claims, a string-keyed map merged after the protocol claims. The merge is non-overriding: a key colliding with a reserved protocol claim (iss,sub,aud,exp,iat,nonce,azp,auth_time,acr,amr,at_hash,c_hash) is rejected with:reserved_claim_conflict, and a non-map or non-string-keyed value with:invalid_extra_claims.at_hash/c_hash(OIDC Core §3.1.3.6, §3.3.2.11) were already present.Attesto.AuthorizationRequest.validate/2-:require_nonceoption (defaultfalse). Whentrue, a request with nononceis rejected with a redirectableinvalid_requesterror (OIDC Core §3.1.2.1); whenfalse,noncestays OPTIONAL and is carried through unenforced (RFC 6749 keeps thecodeflow at SHOULD). The OP policy is the host's, signalled per call.- Authorization-code reuse detection (OAuth 2.0 Security BCP §4.13 /
RFC 6749 §4.1.2).
Attesto.AuthorizationCode.issue/3accepts an optional:family_idthat links a code to the refresh-token family it spawns; it rides onto the redeemedAttesto.AuthorizationCode.Grant(new:family_idfield).Attesto.CodeStoregains an OPTIONAL reuse-tracking pair: amark_consumed/2callback and a thirdtake/1return value{:error, :consumed, meta}. When a store implements them,redeem/4records the spent code'sfamily_id/subjectand surfaces a later replay of that code as{:error, {:reuse, meta}}so the caller can revoke the descendant family. The addition is purely additive and fail-safe: a store that does not implement the pair keeps the{:ok, entry} | :errortake/1contract and a re-presented code stays{:error, :invalid_grant}, with single-use atomicity unchanged. - Refresh-token rotation grace for honest retries.
Attesto.RefreshToken.rotate/3now returns the same successor when the just-consumed parent is immediately retried by the same client, DPoP binding, and narrowed scope within:rotation_grace_seconds(default10). Outside that window, or on any mismatch, reuse still revokes the whole family.Attesto.RefreshStoreentries now carry:consumed_atand:successor, and stores may implementremember_successor/3to support the idempotent retry path. Attesto.Plug.Authenticateaccepts a:credential_from_connfallback hook for host-owned credential channels such as first-party cookies. TheAuthorizationheader remains authoritative when present; the callback is consulted only when no usable header credential exists.Attesto.Plug.OAuthErrorsupports transport hooks (:send_error,:www_authenticate,:no_store) so hosts can preserve their API error envelope while Attesto owns the OAuth status/challenge semantics.
Changed
Attesto.AuthorizationRequest.validate/2-prompttokens are now validated against the fixed OIDC set{none, login, consent, select_account}; an unknown token is a redirectableinvalid_requesterror (OIDC Core §3.1.2.1). The parsed list is still exposed for the controller, which enforces semantics such asprompt=none(the OP MUST NOT show UI).Attesto.RefreshStore.consume/2receives rotation options such as the claim timestamp and returns consumed records with enough metadata for retry/reuse decisions. This is the intentional 0.6 store-contract change.
Security
- Closed a JWS signature-malleability gap in the compact-form boundary of
both
Attesto.Token.verify/3andAttesto.IDToken.verify/3. The boundary previously checked each segment against the base64url alphabet only (RFC 4648 §5), which accepts a non-canonical final character: the 342-byte RS256 signature segment is a partial quantum (342 rem 4 == 2) whose last character carries four unused low-order bits, so several distinct characters decode to the same signature bytes (RFC 4648 §3.5). JOSE's liberal decoder normalises such a variant and verifies it, so a tampered serialization that is not byte-identical to the issuer's token was accepted. The boundary now requires each segment to round-trip throughBase.url_decode64/2andBase.url_encode64/2byte-identically, rejecting padding, non-alphabet bytes, and non-zero unused trailing bits in one check, before the token reaches JOSE. Canonical unpadded base64url tokens are unaffected; the empty signature segment of analg:nonetoken still round-trips and is classified:invalid_signature.
[0.5.1]
Added
Attesto.IDToken- mint and verify OpenID Connect ID Tokens (OIDC Core 1.0 §2), includingat_hash/c_hashgeneration,nonce, and the client-id audience and genericJWTtypthat distinguish an ID Token from an RFC 9068 access token. Shares the keystore/kid/RS256 path withAttesto.Token.Attesto.AuthorizationRequest- protocol-shape validation for the authorization endpoint (RFC 6749 §4.1.1, OIDC Core §3.1.2.1, PKCE §4.3):response_type,client_id, exact-matchredirect_uri, scope/openiddetection, and the PKCE parameters.Attesto.OpenIDDiscovery- the OpenID Provider Metadata document (OIDC Discovery 1.0 §3) served from/.well-known/openid-configuration, built on top ofAttesto.Discovery.mix checkalias running formatting,--warnings-as-errorscompile, property tests, and Credo strict in one command.
Security
- DPoP replay cache: closed a race in the expired-entry re-admission path.
Attesto.DPoP.ReplayCache.check_and_record/2performed a non-atomic lookup-then-insert, so at the exact TTL boundary two concurrent callers could both re-admit a just-expiredjtiand a proof could be replayed more than once. Re-admission is now a single atomic compare-and-delete (:ets.select_delete/2guarded on expiry) followed byinsert_new/2, so exactly one caller wins and the losers see:replay. - Token verification now enforces canonical compact-JWS form at its own
boundary.
Attesto.Token.verify/3andAttesto.IDToken.verify/3reject any=padding or non-base64url byte in a compact segment before the token reaches JOSE, refusing to verify a serialization the issuer never emitted (JOSE's decoder would otherwise tolerantly normalize trailing padding). Unpadded base64url tokens are unaffected.
Fixed
- Documentation: the authorization-code single-use note now links the
Attesto.CodeStoretake/1callback with the correct callback reference, clearing a docs-build warning.