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.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.SigningAlg exposes the permitted set via fapi_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 htu URI 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 iat values up to 60 seconds ahead of the server clock, matching Attesto's JWT verifier clock-skew policy. Proofs remain short-lived through max_age_seconds, and replay-cache TTLs now cover the full acceptance window.

[0.6.6] - 2026-06-01

Fixed

  • Sign PS256 JWTs 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 request JWT is present, unsigned query parameters no longer supplement missing signed parameters such as PKCE inputs.
  • Require signed request objects to carry iss, matching client_id, and a configured aud, 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 typ header is intentionally disabled.

[0.6.5] - 2026-06-01

Fixed

  • Allow an authorization code that was not pre-bound with dpop_jkt to be redeemed at the token endpoint with a DPoP proof. Codes explicitly bound with dpop_jkt still 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, and signing_alg/0. Cold modules now advertise and use their configured per-key algorithms deterministically instead of briefly falling back to inferred RSA RS256 metadata.

[0.6.3] - 2026-06-01

Added

  • Allow OAuth authorization-server metadata (RFC 8414) hosts to advertise authorization_response_iss_parameter_supported and token_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 (request JWTs with alg: "none") are now rejected with the redirectable request_not_supported error instead of invalid_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 to Attesto.Test.DPoP. From a plain request description (method, url, headers) it verifies the presented DPoP proof and, when verify_token: true, the access token, returning {:ok, verified} or an {:error, challenge} map carrying the HTTP status, the WWW-Authenticate challenge, and an optional DPoP-Nonce. It does not reimplement RFC 9449: it delegates every decision to the production verifiers Attesto.DPoP.verify_proof/2 and Attesto.Token.verify/3, and mirrors the resource server's scheme handling (a DPoP-bound token presented as Bearer surfaces a DPoP challenge, RFC 9449 §7.1; a missing required nonce surfaces use_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 under lib/ so a consumer can call it from its test/ tree without depending on Attesto's own test support. generate_key/1 mints a proof key (EC P-256 / ES256 by default); mint_access_token/4 mints a DPoP-sender-constrained access token bound to that key via cnf.jkt (RFC 7800); proof/4 builds a valid proof JWT for a (htm, htu) pair, optionally carrying ath (RFC 9449 §4.3) and a server nonce (§8); invalid_proof/5 builds 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 against Attesto.DPoP.verify_proof/2 and stays in step with it.

[0.6.0]

Added

  • Attesto.IDToken.mint/3 rounds out the OpenID Connect Core §2 ID Token claim set: auth_time (REQUIRED when the request asked for it or carried max_age), acr, amr, and azp are accepted as optional inputs and omitted when absent. Arbitrary additional claims requested through the OIDC Core §5.5 claims parameter 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_nonce option (default false). When true, a request with no nonce is rejected with a redirectable invalid_request error (OIDC Core §3.1.2.1); when false, nonce stays OPTIONAL and is carried through unenforced (RFC 6749 keeps the code flow 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/3 accepts an optional :family_id that links a code to the refresh-token family it spawns; it rides onto the redeemed Attesto.AuthorizationCode.Grant (new :family_id field). Attesto.CodeStore gains an OPTIONAL reuse-tracking pair: a mark_consumed/2 callback and a third take/1 return value {:error, :consumed, meta}. When a store implements them, redeem/4 records the spent code's family_id/subject and 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} | :error take/1 contract and a re-presented code stays {:error, :invalid_grant}, with single-use atomicity unchanged.
  • Refresh-token rotation grace for honest retries. Attesto.RefreshToken.rotate/3 now 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 (default 10). Outside that window, or on any mismatch, reuse still revokes the whole family. Attesto.RefreshStore entries now carry :consumed_at and :successor, and stores may implement remember_successor/3 to support the idempotent retry path.
  • Attesto.Plug.Authenticate accepts a :credential_from_conn fallback hook for host-owned credential channels such as first-party cookies. The Authorization header remains authoritative when present; the callback is consulted only when no usable header credential exists.
  • Attesto.Plug.OAuthError supports 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 - prompt tokens are now validated against the fixed OIDC set {none, login, consent, select_account}; an unknown token is a redirectable invalid_request error (OIDC Core §3.1.2.1). The parsed list is still exposed for the controller, which enforces semantics such as prompt=none (the OP MUST NOT show UI).
  • Attesto.RefreshStore.consume/2 receives 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/3 and Attesto.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 through Base.url_decode64/2 and Base.url_encode64/2 byte-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 an alg:none token 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), including at_hash/c_hash generation, nonce, and the client-id audience and generic JWT typ that distinguish an ID Token from an RFC 9068 access token. Shares the keystore/kid/RS256 path with Attesto.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-match redirect_uri, scope/openid detection, 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 of Attesto.Discovery.
  • mix check alias running formatting, --warnings-as-errors compile, 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/2 performed a non-atomic lookup-then-insert, so at the exact TTL boundary two concurrent callers could both re-admit a just-expired jti and a proof could be replayed more than once. Re-admission is now a single atomic compare-and-delete (:ets.select_delete/2 guarded on expiry) followed by insert_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/3 and Attesto.IDToken.verify/3 reject 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.CodeStore take/1 callback with the correct callback reference, clearing a docs-build warning.