All notable changes to this project are documented here. The format is based on Keep a Changelog and this project adheres to Semantic Versioning.
[1.2.2] - 2026-07-16
Fixed
- Token verification, RFC 7662 introspection, and protected-resource Plug authentication can now validate access tokens against an explicit, trusted set of RFC 8707 resource audiences without disabling audience checks. The Plug policy is configured per protected resource. Scalar audiences must occur in the configured set; every member of an array audience must be trusted. The default verifier behavior remains unchanged when no trusted set is supplied, and malformed policy fails closed.
Security
- Raise the optional Plug dependency floor to 1.19.5, excluding releases with published multipart temp-file exhaustion, nested-parameter quadratic-time denial-of-service, and cookie attribute-injection advisories.
[1.2.1] - 2026-07-08
Fixed
Attesto.Keystore.Staticnow labels the signing key's ownkidwith the configured:signing_alg, so a keystore that signsPS256(or any alg other than the one inferred from the key type) with a single key no longer needs a redundant:key_algsentry to verify its own tokens. Previously, settingsigning_alg: "PS256"on an RSA key without a matching:key_algsmap made verification inferRS256from the key and reject the server's own tokens as:invalid_signature. An explicit:key_algsentry for thatkidstill wins.
[1.2.0] - 2026-07-08
Changed
Attesto.Confignow raises a guiding error when the:issueris not anhttpsURL (RFC 8414 §2). Instead of a bare rejection, the message points developers at mkcert to serve a locally-trusted certificate so the issuer stayshttps— local development never needs to downgrade to plain http. There is deliberately no https-disable switch in the library.
[1.1.0] - 2026-07-07
Added
- OpenID Connect Client-Initiated Backchannel Authentication (CIBA Core
1.0). New
Attesto.CIBAprimitive - the device grant's async sibling:Attesto.CIBA.Request.validate/3runs the §7.1 backchannel authentication request rules (scope-with-openid, exactly-one-hint, ping/pushclient_notification_tokenentropy/length/charset,binding_message/user_code/requested_expiryshape) and verifies §7.1.1 signed authentication requests against the client's registered JWKS (all six REQUIRED claims enforced; FAPI-CIBA's 60-minute lifetime bound by default);issue/4mints a 256-bitauth_req_id(only its hash is stored) with the §7.3 acknowledgement fields;approve/4/deny/3record the user's decision atomically and return the ping-mode §10.2 notification data;redeem/4runs the token-endpoint state machine with the exact §11 vocabulary (authorization_pending/slow_down/expired_token/access_denied/invalid_grant), single-use, expiry-beats-approval, client- and DPoP-binding checked before consume. PlusAttesto.CIBA.Grant, theAttesto.CIBAStorebehaviour (every transition a single atomic guarded operation; the poll interval is frozen into the record at issue time), and an ETS reference store. Attesto.RequestObject.verify/3gains:require_iat,:require_jti, and:require_client_id_claimoptions for the CIBA signed-request profile (defaults preserve the RFC 9101 behaviour).- Discovery (
Attesto.Discovery) accepts the CIBA Core §4 metadata:backchannel_authentication_endpoint,backchannel_token_delivery_modes_supported,backchannel_authentication_request_signing_alg_values_supported, andbackchannel_user_code_parameter_supported. - OpenID Connect Front-Channel Logout 1.0 (OP side).
Attesto.FrontChannelLogout.logout_uri/3builds the exact URI an OP logout page loads in an iframe for one Relying Party: the registeredfrontchannel_logout_uriwithiss+sidquery parameters appended whenever the session'ssidis known (both together or neither, §2).Attesto.LogoutSessionStorerecords both logout channels: an entry now carriesfrontchannel_logout_uri/frontchannel_session_requiredalongside the back-channel fields, andbackchannel_logout_uriis optional — a row exists for any RP that registered at least one logout URI.Attesto.Discoveryaccepts thefrontchannel_logout_supportedandfrontchannel_logout_session_supportedhost members (§3).
- OpenID Connect Session Management 1.0 (OP side).
Attesto.SessionStatecomputes the §3.2session_statevalue (lowercase-hex SHA-256 overclient_id <> " " <> origin <> " " <> op_browser_state <> " " <> salt, dot, salt) plus the browser-formorigin/1of aredirect_uriand the salt / OP-browser-state generators — the pure computation the OP's authorization response and thecheck_session_iframe's JavaScript recomputation must agree on.Attesto.Discoveryaccepts thecheck_session_iframehost member (§3.3).
[1.0.0] - 2026-07-04
First stable release; the public API is now under semantic versioning. No functional change from 0.13.0.
The authorization-server core (tokens, ID tokens, PKCE, DPoP, PAR, discovery, RP-Initiated and Back-Channel Logout) backs an OpenID Provider that passes the OpenID Foundation conformance suite for OpenID Connect Core (Basic), FAPI 2.0 Security Profile Final, FAPI 2.0 Message Signing Final, RP-Initiated Logout, and Back-Channel Logout.
[0.13.0] - 2026-06-23
Added
- OpenID Connect Logout (RP-Initiated Logout 1.0 + Back-Channel Logout 1.0).
Attesto.LogoutTokenmints a signed Back-Channellogout_token(§2.4):typ: "logout+jwt", theeventsclaim{"http://schemas.openid.net/event/backchannel-logout": {}},iss/aud/iat/jti/short-exp, at least one ofsub/sid, and never anonce.Attesto.EndSessionis the conn-free RP-Initiated Logout validator:parse/2verifies theid_token_hint, resolves the Relying Partyclient_id(rejecting aclient_idparameter that disagrees with the hint'saud), and extracts the sessionsub/sid;confirm_redirect/2honors apost_logout_redirect_urionly on an exact match against the client's registered set and appendsstate— an unregistered or unidentifiable return URI is refused (no open redirect).Attesto.IDTokengains asidclaim (:sidmint option, OIDC Back-Channel Logout §2.1) andverify_logout_hint/2, which validates a hint's signature- issuer while tolerating expiry and reading the RP from
audrather than requiring it up front (RP-Initiated Logout §2).
- issuer while tolerating expiry and reading the RP from
Attesto.LogoutSessionStorebehaviour: the OP-side(sid, client_id) -> backchannel_logout_uridelivery map, with an atomictake_targets/1(enumerate-and-delete) so concurrent logouts cannot double-deliver.- Discovery (
Attesto.Discovery) gainsend_session_endpoint,backchannel_logout_supported, andbackchannel_logout_session_supported.
[0.12.0] - 2026-06-23
Added
- RFC 8628 Device Authorization Grant. New
Attesto.DeviceCodeprimitive (issue + the §3.5 polling state machine:authorization_pending/slow_down/expired_token/access_denied, expiry-beats-approval, single-use consume),Attesto.DeviceCode.Grant, theAttesto.DeviceCodeStorebehaviour (every transition a single atomic guarded operation), and an ETS reference store. Theuser_codeuses an ambiguity-free base-20 alphabet and is normalized + charset-validated before any store lookup.device_authorization_endpointis 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.Requirementprimitive: a requirement is acceptedacr_valuesand/or amax_agefreshness bound;evaluate/3checks a verified token'sacr/auth_timeclaims (conjunction, fail-closed on absent/malformed) and returns the §3 challenge params. Attesto.Token.mint/3accepts optional:acr/:auth_time, written as access-token claims (the carrier a resource server enforces step-up against).Attesto.RefreshTokencarries the originalacr/auth_timeacross rotation unchanged, so a refresh-minted access token reports the real authentication event (auth_timeis never re-stamped).Attesto.Plug.OAuthError.insufficient_user_authentication/4(the RFC 9470 §3 401 challenge) and a:step_upoption onAttesto.Plug.Authenticatethat enforces a per-route requirement after token verification.acr_values_supportedis now an accepted protected-resource-metadata host field (RFC 9728), so a resource server can advertise theacrvalues it can demand.
[0.10.0] - 2026-06-22
Added
- RFC 8707 Resource Indicators. New
Attesto.ResourceIndicatorprimitive (validate/1for §2.1 absolute-URI syntax over the scalar/arrayresourceparameter;authorize/2for §2.2 allow-listing →:invalid_target). Attesto.Token.mint/3's:audienceoption now accepts a list of resource identifiers, written as a JWTaudarray (a single resource still collapses to a string).Token.verifyalready checks array membership, so a resource server validates that its own identifier is inaud.Attesto.AuthorizationCode/Attesto.RefreshTokenbind aresourceset alongside scope; refresh rotation carries it and narrows it subset-only (a requested resource outside the granted set is:invalid_target).Attesto.AuthorizationRequestparses and validates theresourceparameter, surfacing a malformed value as a redirectableinvalid_targeterror.
[0.9.0] - 2026-06-21
Changed
Attesto.Plug.Authenticatebearer presentation methods are explicit and header-only by default. The new:bearer_methodsoption accepts:header/"header"and:body/"body"; it defaults to[:header]. A resource server that intentionally accepts RFC 6750 §2.2 form-bodyaccess_tokencredentials must opt in withbearer_methods: [:header, :body]and advertise the matchingbearer_methods_supportedmetadata. URI-query bearer tokens remain unsupported. DPoP, mTLS, and host-provided:credential_from_connfallback credentials are unchanged.
[0.8.1] - 2026-06-21
Changed
Attesto.Plug.OAuthError.insufficient_scope/4now honors the transport hooks. The 403 scope-rejection path threads the same:send_error,:www_authenticate, and:no_storeoptionsunauthorized/4already honored, so a resource server can override the 403 response envelope and inject a per-conn challenge (e.g. a request-derived RFC 9728resource_metadatapointer) on the scope-rejection path, not just the authentication-rejection path. Theinsufficient_scopecode, 403 status, and theerror_description/scopechallenge semantics remain owned by the renderer; the default response is byte-identical when no hooks are passed.Attesto.Plug.RequireScopesnow threads those transport hooks onto both the 403insufficient_scopeand the 401invalid_tokenit renders, alongside the existing:resource_metadatapointer. 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 ofAttesto.Discovery).metadata/2returns the string-keyed map a resource publishes at/.well-known/oauth-protected-resource: the REQUIREDresourceidentifier (defaulting toconfig.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, theresource_name/documentation/ policy/ToS members,tls_client_certificate_bound_access_tokens,dpop_bound_access_tokens_required,dpop_signing_alg_values_supported, andsigned_metadata). A present-but-malformed:resource(the REQUIRED member) fails fast withArgumentError. Conn-free; mounting a serving endpoint is the host's concern.RFC 9728 §5.1
resource_metadatachallenge pointer.Attesto.Plug.OAuthError.unauthorized/4andinsufficient_scope/3,4now append aresource_metadata="<url>"auth-param to theWWW-Authenticatechallenge when a:resource_metadataopt is present, so a client refused with 401/403 can discover the resource's protected-resource metadata (and thereby its authorization server). Threaded throughAttesto.Plug.Authenticate(:resource_metadatainit opt) andAttesto.Plug.RequireScopes(:resource_metadatainit opt). Omitted when unset.Attesto.Token.mint/3:audienceoption — a per-call override for the access token'saudclaim, defaulting toconfig.audience. RFC 8707 §2: when a token request carries aresourceindicator the access token'saudMUST identify that resource; the host derives the resource identifier and passes it here. The override is conn-free and does not mutateconfig, so one issuer can mint resource-audienced tokens for one grant without changingaudfor any other. A present-but-malformed override (anil,"", list, or other non-string) is rejected{:error, :invalid_audience}rather than minted, so a miswiredresourcecannot produce a malformedaud.Attesto.IdentityAssertion— verification for the Identity Assertion JWT Authorization Grant (ID-JAG), the resource Authorization Server's half ofdraft-ietf-oauth-identity-assertion-authz-grant-04(the grant behind MCP Enterprise-Managed Authorization). Conn-free and side-effect-free:verify/3checks the assertion's signature against a caller-supplied trusted issuer JWKS (kid selection,RS256/PS256/ES*/EdDSA) and enforces the draft's claim rules — JOSEtyppinned tooauth-id-jag+jwt,issmatches the trusted issuer (NOT theclient_id),audis exactly this server's issuer (strict single value), the requirediss/sub/aud/client_id/jti/exp/iatclaims,client_idbinding, andexp/iat/nbfskew with an optional:max_lifetime_secondsbound.peek_issuer/1reads the unverifiedissso the caller can select the trusted issuer before verifying. The stateful concerns (JWKS fetch/cache,jtireplay, subject resolution, error mapping to RFC 6749invalid_grant) belong to theattesto_phoenixtoken layer. Distinct fromprivate_key_jwtclient 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 (unliketake/1). Implemented by the bundledAttesto.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-consumingget/1. Used to surface a holder-of-key (invalid_request/invalid_dpop_proof) rejection ahead of the client-auth error at the token endpoint (FAPI2ensure-holder-of-key-required), without consuming the code. Returnsfalsewhen the store has noget/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. AfterA → B → C, a replay of the captured parentAinside the grace window re-issuedB(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.AuthorizationRequestcarriesdpop_jkt. The validated request now exposes the RFC 9449 §10dpop_jktparameter, read from the EFFECTIVE (post-request-object-merge) params — so a signed request object'sdpop_jktis authoritative and an unsigned outer-query value is ignored when a request object is present. (Consumed byattesto_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 itsclient_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?/1andvalidate_client_id/1enforce the draft §2 URL grammar (https, path required, no fragment/userinfo/dot-segments);validate_document/2validates a fetched document (theclient_idmust equal the URL, no shared symmetric secret /client_secret_*auth method, a non-emptyredirect_uris) and normalizes it into the same client shape the RFC 7591 registration path produces. The network half (SSRF-guarded fetch, caching) lives inattesto_phoenix; this module touches no socket and adds no dependency.Attesto.Discoveryadvertises theclient_id_metadata_document_supportedauthorization-server metadata field when the host enables it.
[0.6.16] - 2026-06-13
Fixed
Authorization-code redemption is now atomic.
Attesto.AuthorizationCode.redeem/4no longer records the reuse marker (consumed_success) itself; that moved to the newfinalize/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 hostbuild_principalcallback returning the subject under the wrong key) is left single-use-spent but NOT reuse-flagged: a replay is a cleaninvalid_grantinstead 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/3to record the reuse marker. The bundledattesto_phoenixtoken endpoint (>= 0.7.7) does this. Stores that do not implement the optionalmark_consumed/2are unaffected.
[0.6.15] - 2026-06-12
Fixed
Attesto.RequestObjectcompares the JOSEtypheader CASE-INSENSITIVELY (RFC 7515 §4.1.9typis 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 asinvalid_typ, failing the Message-Signing happy-flow / user-rejects tests at the PAR endpoint. A wrong type is still rejected; an absenttypis still governed byaccepted_typ.
[0.6.14] - 2026-06-12
Fixed
Attesto.RequestObject.Policy.fapi_message_signing/0no longer requires the JOSEtypheader on signed request objects - it now accepts an absenttyp(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 maketypRECOMMENDED, not mandatory, and the OpenID FAPI conformance suite signs its request objects with notypheader - so the previous strict pinning rejected every conformant pushed request object and failed the FAPI2 Message Signing certification at the PAR endpoint.typis still validated for the RFC 9101 §10.8 explicit-typing defence when a client does send it.
Security
Attesto.DPoPnow 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 carryingiss/aud/exp/iat, using the keystore signing key (algorithm pinned, nevernone).Attesto.Introspection— OAuth 2.0 Token Introspection (RFC 7662). Access tokens are introspected statelessly with the fullAttesto.Tokenverifier except the sender-binding proof match (thecnfis echoed for the resource server); refresh tokens are checked against anAttesto.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 withiss/aud/iatand atoken_introspectionclaim, JOSE headertyp="token-introspection+jwt").Attesto.RequestObject.Policygainsrequire_request_object(false ingeneric/0, true infapi_message_signing/0) andrequire_request_object?/1.Attesto.AuthorizationRequest.validate/2rejects a request that carries no signed request object when the policy requires one (redirectableinvalid_request; non-redirectable when the client is untrusted, OIDC Core §3.1.2.6).Attesto.AuthorizationRequestparses and validatesresponse_mode(the RFC 6749queryplus the JARM modesjwt/query.jwt/fragment.jwt/form_post.jwt);supported_response_modes/0exposes the accepted set. Trusted redirectable errors carry the requestedresponse_modeand theclient_idso the transport can return the error as a JARM JWT.Attesto.Discoveryallowlists the RFC 9101 §10.5 metadata membersrequire_signed_request_objectandrequest_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/3acceptsrequire_confirmation_binding: falseto verify a token's signature/claims while skipping only the sender-binding proof match (used by introspection); thecnfshape is still validated.Attesto.Introspection.introspect/3accepts an:authorizepredicate(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-truereturn — 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.Introspectionsurfaces the RFC 7662sub/scope/client_id/cnfmembers for an active refresh token from the stored record's own data contract (Attesto.RefreshTokenbuild context), when present, so a resource server — and an:authorizepolicy — can decide per refresh token rather than allow/deny every refresh token wholesale. A store that does not populate them yields the minimalactive+expresponse.
Security
Attesto.AuthorizationRequest.validate/2now judges the OIDCopenid-scope gate for therequire_noncepolicy on the EFFECTIVE (post-merge) request, so a direct JAR carryingscope=openidonly inside the signed request object can no longer bypass the host's nonce requirement. A plain OAuth request (noopenidscope) remains un-nonce-constrained.Attesto.RequestObject.verify/3rejects a signed request object whoseaudis 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/3rejects a request object that itself carries arequestorrequest_uriclaim (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/0is the OpenID Connect §6.1 baseline (the default:nbf/exp/typnot required);fapi_message_signing/0is the FAPI 2.0 Message Signing §5.3.1 profile (nbfrequired ≤60 min past,exprequired ≤60 min afternbf, JOSE headertyp="oauth-authz-req+jwt").Attesto.AuthorizationRequest.validate/2accepts a:request_object_policyoption (default%Policy{}, generic) and threads it intoAttesto.RequestObject.verify/3. Anaudthat 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_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.