# Changelog
All notable changes to this project are documented here. The format is
based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and this
project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
## [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).
- `c: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.