All notable changes to this project are documented here. The format is based on Keep a Changelog and this project adheres to Semantic Versioning.

[0.13.0] - 2026-06-23

Added

  • OpenID Connect Logout (RP-Initiated Logout 1.0 + Back-Channel Logout 1.0).
    • Attesto.LogoutToken mints a signed Back-Channel logout_token (§2.4): typ: "logout+jwt", the events claim {"http://schemas.openid.net/event/backchannel-logout": {}}, iss/aud/ iat/jti/short-exp, at least one of sub/sid, and never a nonce.
    • Attesto.EndSession is the conn-free RP-Initiated Logout validator: parse/2 verifies the id_token_hint, resolves the Relying Party client_id (rejecting a client_id parameter that disagrees with the hint's aud), and extracts the session sub/sid; confirm_redirect/2 honors a post_logout_redirect_uri only on an exact match against the client's registered set and appends state — an unregistered or unidentifiable return URI is refused (no open redirect).
    • Attesto.IDToken gains a sid claim (:sid mint option, OIDC Back-Channel Logout §2.1) and verify_logout_hint/2, which validates a hint's signature
      • issuer while tolerating expiry and reading the RP from aud rather than requiring it up front (RP-Initiated Logout §2).
    • Attesto.LogoutSessionStore behaviour: the OP-side (sid, client_id) -> backchannel_logout_uri delivery map, with an atomic take_targets/1 (enumerate-and-delete) so concurrent logouts cannot double-deliver.
    • Discovery (Attesto.Discovery) gains end_session_endpoint, backchannel_logout_supported, and backchannel_logout_session_supported.

[0.12.0] - 2026-06-23

Added

  • RFC 8628 Device Authorization Grant. New Attesto.DeviceCode primitive (issue + the §3.5 polling state machine: authorization_pending / slow_down / expired_token / access_denied, expiry-beats-approval, single-use consume), Attesto.DeviceCode.Grant, the Attesto.DeviceCodeStore behaviour (every transition a single atomic guarded operation), and an ETS reference store. The user_code uses an ambiguity-free base-20 alphabet and is normalized + charset-validated before any store lookup. device_authorization_endpoint is now an accepted RFC 8414 metadata field.

[0.11.0] - 2026-06-22

Added

  • RFC 9470 Step-Up Authentication Challenge. New Attesto.StepUp + Attesto.StepUp.Requirement primitive: a requirement is accepted acr_values and/or a max_age freshness bound; evaluate/3 checks a verified token's acr / auth_time claims (conjunction, fail-closed on absent/malformed) and returns the §3 challenge params.
  • Attesto.Token.mint/3 accepts optional :acr / :auth_time, written as access-token claims (the carrier a resource server enforces step-up against).
  • Attesto.RefreshToken carries the original acr / auth_time across rotation unchanged, so a refresh-minted access token reports the real authentication event (auth_time is never re-stamped).
  • Attesto.Plug.OAuthError.insufficient_user_authentication/4 (the RFC 9470 §3 401 challenge) and a :step_up option on Attesto.Plug.Authenticate that enforces a per-route requirement after token verification.
  • acr_values_supported is now an accepted protected-resource-metadata host field (RFC 9728), so a resource server can advertise the acr values it can demand.

[0.10.0] - 2026-06-22

Added

  • RFC 8707 Resource Indicators. New Attesto.ResourceIndicator primitive (validate/1 for §2.1 absolute-URI syntax over the scalar/array resource parameter; authorize/2 for §2.2 allow-listing → :invalid_target).
  • Attesto.Token.mint/3's :audience option now accepts a list of resource identifiers, written as a JWT aud array (a single resource still collapses to a string). Token.verify already checks array membership, so a resource server validates that its own identifier is in aud.
  • Attesto.AuthorizationCode / Attesto.RefreshToken bind a resource set alongside scope; refresh rotation carries it and narrows it subset-only (a requested resource outside the granted set is :invalid_target).
  • Attesto.AuthorizationRequest parses and validates the resource parameter, surfacing a malformed value as a redirectable invalid_target error.

[0.9.0] - 2026-06-21

Changed

  • Attesto.Plug.Authenticate bearer presentation methods are explicit and header-only by default. The new :bearer_methods option accepts :header / "header" and :body / "body"; it defaults to [:header]. A resource server that intentionally accepts RFC 6750 §2.2 form-body access_token credentials must opt in with bearer_methods: [:header, :body] and advertise the matching bearer_methods_supported metadata. URI-query bearer tokens remain unsupported. DPoP, mTLS, and host-provided :credential_from_conn fallback credentials are unchanged.

[0.8.1] - 2026-06-21

Changed

  • Attesto.Plug.OAuthError.insufficient_scope/4 now honors the transport hooks. The 403 scope-rejection path threads the same :send_error, :www_authenticate, and :no_store options unauthorized/4 already honored, so a resource server can override the 403 response envelope and inject a per-conn challenge (e.g. a request-derived RFC 9728 resource_metadata pointer) on the scope-rejection path, not just the authentication-rejection path. The insufficient_scope code, 403 status, and the error_description / scope challenge semantics remain owned by the renderer; the default response is byte-identical when no hooks are passed.
  • Attesto.Plug.RequireScopes now threads those transport hooks onto both the 403 insufficient_scope and the 401 invalid_token it renders, alongside the existing :resource_metadata pointer. Previously they were dropped, so a host could not override the scope-rejection envelope through this plug.

[0.8.0] - 2026-06-20

Added

  • Attesto.ProtectedResourceMetadata — renderer for the RFC 9728 OAuth 2.0 Protected Resource Metadata document (the resource-server analogue of Attesto.Discovery). metadata/2 returns the string-keyed map a resource publishes at /.well-known/oauth-protected-resource: the REQUIRED resource identifier (defaulting to config.audience, overridable via :resource) plus the nil-droppable RFC 9728 §2 host fields (authorization_servers, jwks_uri, scopes_supported, bearer_methods_supported, resource_signing_alg_values_supported, authorization_details_types_supported, the resource_name/documentation/ policy/ToS members, tls_client_certificate_bound_access_tokens, dpop_bound_access_tokens_required, dpop_signing_alg_values_supported, and signed_metadata). A present-but-malformed :resource (the REQUIRED member) fails fast with ArgumentError. Conn-free; mounting a serving endpoint is the host's concern.

  • RFC 9728 §5.1 resource_metadata challenge pointer. Attesto.Plug.OAuthError.unauthorized/4 and insufficient_scope/3,4 now append a resource_metadata="<url>" auth-param to the WWW-Authenticate challenge when a :resource_metadata opt is present, so a client refused with 401/403 can discover the resource's protected-resource metadata (and thereby its authorization server). Threaded through Attesto.Plug.Authenticate (:resource_metadata init opt) and Attesto.Plug.RequireScopes (:resource_metadata init opt). Omitted when unset.

  • Attesto.Token.mint/3 :audience option — a per-call override for the access token's aud claim, defaulting to config.audience. RFC 8707 §2: when a token request carries a resource indicator the access token's aud MUST identify that resource; the host derives the resource identifier and passes it here. The override is conn-free and does not mutate config, so one issuer can mint resource-audienced tokens for one grant without changing aud for any other. A present-but-malformed override (a nil, "", list, or other non-string) is rejected {:error, :invalid_audience} rather than minted, so a miswired resource cannot produce a malformed aud.

  • Attesto.IdentityAssertion — verification for the Identity Assertion JWT Authorization Grant (ID-JAG), the resource Authorization Server's half of draft-ietf-oauth-identity-assertion-authz-grant-04 (the grant behind MCP Enterprise-Managed Authorization). Conn-free and side-effect-free: verify/3 checks the assertion's signature against a caller-supplied trusted issuer JWKS (kid selection, RS256/PS256/ES*/EdDSA) and enforces the draft's claim rules — JOSE typ pinned to oauth-id-jag+jwt, iss matches the trusted issuer (NOT the client_id), aud is exactly this server's issuer (strict single value), the required iss/sub/aud/client_id/ jti/exp/iat claims, client_id binding, and exp/iat/nbf skew with an optional :max_lifetime_seconds bound. peek_issuer/1 reads the unverified iss so the caller can select the trusted issuer before verifying. The stateful concerns (JWKS fetch/cache, jti replay, subject resolution, error mapping to RFC 6749 invalid_grant) belong to the attesto_phoenix token layer. Distinct from private_key_jwt client auth (RFC 7523 §3) and the RFC 8693 token-exchange grant (which runs at the IdP).

[0.7.2] - 2026-06-16

Added

  • Attesto.CodeStore.get/1 (OPTIONAL callback) — read a stored authorization code WITHOUT consuming it (unlike take/1). Implemented by the bundled Attesto.CodeStore.ETS. Lets the token endpoint run read-only pre-checks (e.g. a holder-of-key requirement) without burning the single-use code.
  • Attesto.AuthorizationCode.dpop_bound?/2 — returns whether a stored code is bound to a DPoP key (RFC 9449 §10), via the store's non-consuming get/1. Used to surface a holder-of-key (invalid_request/invalid_dpop_proof) rejection ahead of the client-auth error at the token endpoint (FAPI2 ensure-holder-of-key-required), without consuming the code. Returns false when the store has no get/1, so behaviour is unchanged for stores that don't implement it.

[0.7.1] - 2026-06-14

Security

  • Refresh-rotation grace no longer replays an already-rotated successor. RefreshToken.rotate/3's within-grace idempotent-retry path returned the parent's cached successor without checking it was still the live, unconsumed leaf. After A → B → C, a replay of the captured parent A inside the grace window re-issued B (and minted a fresh access token from it) instead of detecting reuse — suppressing the OAuth 2.0 Security BCP §4.13 captured-token signal and forking a live chain. The grace retry now confirms the cached successor is still unconsumed; if it has been rotated onward, the replay is treated as reuse and the whole family is revoked.

Added

  • Attesto.AuthorizationRequest carries dpop_jkt. The validated request now exposes the RFC 9449 §10 dpop_jkt parameter, read from the EFFECTIVE (post-request-object-merge) params — so a signed request object's dpop_jkt is authoritative and an unsigned outer-query value is ignored when a request object is present. (Consumed by attesto_phoenix's authorization endpoint, which previously read it from the raw outer query.)

[0.7.0] - 2026-06-14

Added

  • Attesto.ClientIdMetadata — the pure core of Client ID Metadata Documents (CIMD, draft-ietf-oauth-client-id-metadata-document-01). CIMD lets a client identify itself with no prior registration by using an HTTPS URL as its client_id; the authorization server dereferences that URL to a JSON client metadata document. This module is the conn-free, HTTP-free half: client_id_url?/1 and validate_client_id/1 enforce the draft §2 URL grammar (https, path required, no fragment/userinfo/dot-segments); validate_document/2 validates a fetched document (the client_id must equal the URL, no shared symmetric secret / client_secret_* auth method, a non-empty redirect_uris) and normalizes it into the same client shape the RFC 7591 registration path produces. The network half (SSRF-guarded fetch, caching) lives in attesto_phoenix; this module touches no socket and adds no dependency.
  • Attesto.Discovery advertises the client_id_metadata_document_supported authorization-server metadata field when the host enables it.

[0.6.16] - 2026-06-13

Fixed

  • Authorization-code redemption is now atomic. Attesto.AuthorizationCode.redeem/4 no longer records the reuse marker (consumed_success) itself; that moved to the new finalize/3, which the caller runs ONLY after the full token response is successfully built. So a code whose redemption validated but whose downstream issuance then failed (a mint or refresh-store fault, a host build_principal callback returning the subject under the wrong key) is left single-use-spent but NOT reuse-flagged: a replay is a clean invalid_grant instead of a false reuse attack that revokes the family, and a legitimate retry of a transient failure is not mistaken for an attack. Previously any post-validation failure permanently bricked the code AND marked it a successful redemption.

    Caller change: after a successful token response, call AuthorizationCode.finalize/3 to record the reuse marker. The bundled attesto_phoenix token endpoint (>= 0.7.7) does this. Stores that do not implement the optional mark_consumed/2 are unaffected.

[0.6.15] - 2026-06-12

Fixed

  • Attesto.RequestObject compares the JOSE typ header CASE-INSENSITIVELY (RFC 7515 §4.1.9 typ is a media type; RFC 2045 §5.1 media types are case-insensitive). The FAPI 2.0 Message Signing conformance suite signs request objects with a randomly-cased typ (e.g. OautH-auThZ-REQ+jWt) to exercise this; the previous exact-match rejected them as invalid_typ, failing the Message-Signing happy-flow / user-rejects tests at the PAR endpoint. A wrong type is still rejected; an absent typ is still governed by accepted_typ.

[0.6.14] - 2026-06-12

Fixed

  • Attesto.RequestObject.Policy.fapi_message_signing/0 no longer requires the JOSE typ header on signed request objects - it now accepts an absent typ (accepted_typ: ["oauth-authz-req+jwt", nil]) while still rejecting a wrong one. FAPI 2.0 Message Signing §5.3.1 ("shall accept that typ") and RFC 9101 §4 make typ RECOMMENDED, not mandatory, and the OpenID FAPI conformance suite signs its request objects with no typ header - so the previous strict pinning rejected every conformant pushed request object and failed the FAPI2 Message Signing certification at the PAR endpoint. typ is still validated for the RFC 9101 §10.8 explicit-typing defence when a client does send it.

Security

  • Attesto.DPoP now applies the strict canonical-base64url check to the proof's JOSE header (no padding, no non-significant trailing bits) that the Token/IDToken/ClientAssertion/RequestObject verifiers already apply, so a DPoP proof header cannot be presented in a non-canonical/aliased encoding. Defense-in-depth (the signature is verified over the real bytes regardless).

[0.6.13] - 2026-06-04

The FAPI 2.0 Message Signing surface: signed request objects (JAR, §5.3), signed authorization responses (JARM, §5.4), and token introspection with signed responses (§5.5). All additions are backward-compatible; behaviour is unchanged unless a caller opts into the new policy/options.

Added

  • Attesto.JARM — JWT Secured Authorization Response Mode (§5.4). Signs an authorization response (success: code/state; error: error/error_description/state) into a JWT carrying iss/aud/exp/ iat, using the keystore signing key (algorithm pinned, never none).
  • Attesto.Introspection — OAuth 2.0 Token Introspection (RFC 7662). Access tokens are introspected statelessly with the full Attesto.Token verifier except the sender-binding proof match (the cnf is echoed for the resource server); refresh tokens are checked against an Attesto.RefreshStore (active only while unconsumed and unexpired). Never an error — an invalid, expired, revoked, or unknown token is reported inactive (no existence oracle).
  • Attesto.SignedIntrospection — the RFC 9701 signed introspection response (a JWT with iss/aud/iat and a token_introspection claim, JOSE header typ = "token-introspection+jwt").
  • Attesto.RequestObject.Policy gains require_request_object (false in generic/0, true in fapi_message_signing/0) and require_request_object?/1. Attesto.AuthorizationRequest.validate/2 rejects a request that carries no signed request object when the policy requires one (redirectable invalid_request; non-redirectable when the client is untrusted, OIDC Core §3.1.2.6).
  • Attesto.AuthorizationRequest parses and validates response_mode (the RFC 6749 query plus the JARM modes jwt/query.jwt/fragment.jwt/ form_post.jwt); supported_response_modes/0 exposes the accepted set. Trusted redirectable errors carry the requested response_mode and the client_id so the transport can return the error as a JARM JWT.
  • Attesto.Discovery allowlists the RFC 9101 §10.5 metadata members require_signed_request_object and request_object_signing_alg_values_supported.
  • Attesto.SigningAlg.keystore_algs/1 — the unique signing algorithms across a keystore's verification keys (shared by the ID Token / JARM / introspection signing-algorithm metadata).
  • Attesto.Token.verify/3 accepts require_confirmation_binding: false to verify a token's signature/claims while skipping only the sender-binding proof match (used by introspection); the cnf shape is still validated.
  • Attesto.Introspection.introspect/3 accepts an :authorize predicate (response -> boolean) consulted with the active response before it is returned (RFC 7662 §4 / RFC 9701 §5: the AS MAY restrict which tokens a caller may introspect). A non-true return — or a raise — downgrades the response to %{"active" => false} so a caller not authorized for the token learns nothing about it. When omitted, every authenticated caller may introspect any token (the single-trust-domain default).
  • Attesto.Introspection surfaces the RFC 7662 sub/scope/client_id/cnf members for an active refresh token from the stored record's own data contract (Attesto.RefreshToken build context), when present, so a resource server — and an :authorize policy — can decide per refresh token rather than allow/deny every refresh token wholesale. A store that does not populate them yields the minimal active+exp response.

Security

  • Attesto.AuthorizationRequest.validate/2 now judges the OIDC openid-scope gate for the require_nonce policy on the EFFECTIVE (post-merge) request, so a direct JAR carrying scope=openid only inside the signed request object can no longer bypass the host's nonce requirement. A plain OAuth request (no openid scope) remains un-nonce-constrained.
  • Attesto.RequestObject.verify/3 rejects a signed request object whose aud is an array containing any non-string member (RFC 7519 §4.1.3), rather than accepting it on a single matching member — matching the hardened Token/IDToken/JARM audience handling.
  • Attesto.RequestObject.verify/3 rejects a request object that itself carries a request or request_uri claim (RFC 9101 §4 forbids them) instead of silently dropping them, so a nested-request smuggle fails closed at the verifier.

[0.6.12] - 2026-06-03

Added

  • Attesto.RequestObject.Policy — a data-only JAR verification policy for signed authorization request objects (RFC 9101). generic/0 is the OpenID Connect §6.1 baseline (the default: nbf/exp/typ not required); fapi_message_signing/0 is the FAPI 2.0 Message Signing §5.3.1 profile (nbf required ≤60 min past, exp required ≤60 min after nbf, JOSE header typ = "oauth-authz-req+jwt"). Attesto.AuthorizationRequest.validate/2 accepts a :request_object_policy option (default %Policy{}, generic) and threads it into Attesto.RequestObject.verify/3. An aud that is an array containing the issuer is already accepted. Behaviour is unchanged unless a caller opts into the FAPI profile.

[0.6.11] - 2026-06-03

Added

  • :accepted_algs option on Attesto.ClientAssertion.verify/5 and Attesto.RequestObject.verify/3 (default Attesto.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/0 as a named helper for the default client-presented signature verification policy.
  • Strict JAR policy options on Attesto.RequestObject.verify/3 for 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_exp demand a non-negative integer NumericDate (a missing or malformed value fails); :max_lifetime_seconds requires both nbf and exp anchors. These default to the prior lenient behaviour, so callers opt into strictness with explicit policy.

Fixed

  • Attesto.RequestObject.verify/3 now honours nbf as a not-before claim (RFC 7519 §4.1.5): a request object with nbf in the future is rejected as :not_yet_valid even in lenient mode (clock skew tolerated).

[0.6.10] - 2026-06-02

Changed

  • Require a single-valued string aud in client-authentication assertions (FAPI 2). An array aud is 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.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.