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.9.5] - 2026-06-16
Fixed
- Holder-of-key (DPoP) failures are now surfaced ahead of the client-auth
error (FAPI2
ensure-holder-of-key-required). A token request redeeming a sender-constrained (DPoP-bound) authorization code WITHOUT a DPoP proof is a holder-of-key failure; FAPI2 expects it reported asinvalid_request/invalid_grant/invalid_dpop_proof. When such a request ALSO lacked client authentication, the client-auth check masked it withinvalid_client. The token endpoint now reads the code (via the store's non-consumingAttesto.CodeStore.get/1) and, when it is DPoP-bound and no proof is presented, returnsinvalid_request "DPoP proof required"— even before the client-auth failure. The code is NOT consumed, so a legitimate retry is unaffected. Only DPoP-bound codes are affected; a plain (e.g. OIDC) code still surfacesinvalid_client. Requires attesto 0.7.2.
Added
AttestoPhoenix.Store.EctoCodeStore.get/1— the non-consuming read (Attesto.CodeStore.get/1) for the Ecto-backed code store, a plain SELECT of the live (unconsumed) row.
[0.9.4] - 2026-06-14
Security
Adversarial-review hardening of the token, authorization, and revocation endpoints (all found by an internal multi-agent security review).
Public clients can no longer run confidential-only grants. The token endpoint gated grants only on the optional per-client
:client_grant_typescallback (unset ⇒ all grants allowed), so a public (none) client that proved possession of no credential could runclient_credentials(RFC 6749 §4.4) or RFC 8693 token-exchange. The resolved client-auth method is now threaded into the request, and both grants reject the:nonepath withinvalid_client, independent of any host policy.The revocation endpoint now enforces TLS.
RevocationControllernever calledcheck_https, so under the defaultrequire_https: truea plain-HTTPPOST /oauth/revokecarrying the client secret + refresh token was still processed — leaking both over cleartext. It now gates on TLS first, like every other credential-bearing endpoint.DPoP proofs are replay-protected at the token endpoint (RFC 9449 §11.1).
SenderConstraint.bind_dpopnever wired:replay_check, so a captured token-endpoint proof'sjtiwas never recorded and the proof was replayable within its acceptance window. The proof'sjtiis now recorded (via the same defaultAttesto.DPoP.ReplayCachethe PAR endpoint uses).The direct (non-PAR) authorization endpoint honors a signed
dpop_jkt. It readdpop_jktfrom the raw outer query, ignoring the signed request object — letting a front-channel attacker strip or substitute the code's DPoP key binding. It now reads the value off the verified request (Attesto.AuthorizationRequest.dpop_jkt, requires attesto 0.7.1), so a signed request object's value is authoritative. The PAR-resolved path continues to use the PAR-verified thumbprint stored at the top level (a pushed request object is re-merged at/authorize, which would otherwise drop it).The dynamic client registration endpoint now enforces TLS.
RegistrationController.create/2returns a freshly minted plaintextclient_secretanddelete/2reads a registration-access-token bearer credential; neither gated on TLS, so under the defaultrequire_https: truethey served those credentials over cleartext. Both now refuse plain HTTP first, like every other credential-bearing endpoint. (Found by adversarial verification of the revocation fix — same class, uncovered sibling.)The revocation endpoint equalizes client-auth timing. A lookup failure skipped
verify_client_secret, leaving a timing oracle for client-id enumeration. It now runs a dummy verify against an:unknown_clientsentinel so the unknown-client and wrong-secret paths match in observable timing, matching the sharedAttestoPhoenix.ClientAuthenticationcore.CIMD SSRF guard covers Teredo and ORCHIDv2. Added
2001:0000::/32(Teredo) and2001:20::/28(ORCHIDv2) to the RFC 6890 special-use IPv6 table the fetcher screens against.
[0.9.3] - 2026-06-14
Security
Token exchange can no longer broaden scope (RFC 8693 §2.1). The token-exchange grant validated the requested
scopeonly against the host's:authorize_scopepolicy, which is never handed the subject token — so the library could not, and did not, enforce that the issued token's scope stays within the subject token's. A client registered for a broad scope set could exchange a narrowly-scoped subject token for a broader one. The token endpoint now rejects (invalid_scope) any requested scope not present in the subject token's scope, before delegating to:authorize_scope— an exchange can only preserve or narrow scope. (:authorize_scopemay still narrow further.)The token endpoint now enforces
grant_types_supported. Previously the only grant gate was the optional per-client:client_grant_typescallback (unset ⇒ every grant allowed), while discovery advertised a hardcoded grant superset including token-exchange — so a host that didn't lock every client down had an advertised, working token-exchange grant it never opted into. The token endpoint now rejects (unsupported_grant_type) anygrant_typeoutside the configured set, as a global backstop independent of per-client policy.
Changed
grant_types_supportedis now driven by host config, not a hardcoded list. Both discovery documents (RFC 8414 and OpenID configuration) and the new token endpoint gate readAttestoPhoenix.Config.grant_types_supported/1, which defaults to every implemented grant (so existing deployments are unchanged) and is narrowed by configuring:grant_types_supported— dropping a grant (e.g. token-exchange) now disables it across discovery, the token endpoint, and dynamic registration at once, instead of only registration.
[0.9.2] - 2026-06-14
Fixed
- A CIMD client no longer crashes a host
:authorize_scopepolicy. A Client ID Metadata Document need not declare ascopemember, so the metadata map attesto hands the host policy callbacks carried no scope key at all. A scope policy written for a registered client (readingclient.scopes) raisedKeyErroron it, 500-ing the token endpoint for an otherwise valid CIMD authorization_code exchange (observed end-to-end against the ChatGPT MCP connector).host_client/1now exposes the document's declared scopes — or an empty set when the document omitsscope— under the atom:scopeskey, so the callback reads an empty declared set instead of a missing key. The host still owns what an empty set grants (typically the resource owner's consent).
Added
AttestoPhoenix.ClientIdMetadata.scopes/1— the public accessor for a CIMD document's declared scopes (its space-delimited RFC 7591 §2scopemember as a list;[]when absent), alongside the existingclient_id/1,redirect_uris/1, andjwks/1accessors.
[0.9.1] - 2026-06-14
Added
- Boot-time discovery-document safety guard.
AttestoPhoenix.Config.new/1now validates, at config-build time (alongside the existing required-key checks), that the discovery documents it will serve are internally consistent — so a "silent discovery mismatch" (a document that omits a required endpoint or advertises one the router does not mount, served 200 with no error) can no longer ship. It raisesArgumentErrorwith an actionable message for two classes of failure:- A required discovery endpoint that would be missing or non-absolute. The
RFC 8414 §2 / OpenID Connect Discovery §3 endpoints the library derives —
issuer,authorization_endpoint,token_endpoint, andjwks_uri— must each resolve to an absolute URL (scheme + host). The realistic trigger is an:issuerthat is not an absolute https URL (e.g."issuer.example"), whichURI.merge/2turns into host-less, unresolvable endpoint URLs; this is the same class of failure as the 0.9.1 regression where the RFC 8414 document silently omittedauthorization_endpoint. - An
:oauth_path_prefixvs explicit per-endpoint override mismatch. When a host declares a non-default:oauth_path_prefix(committing every OAuth endpoint to one mount tree) but then sets a per-endpoint override (:token_pathand friends) that escapes that prefix, discovery would advertise that endpoint at a path the router — which mounts every OAuth endpoint under one shared prefix — does not serve. That provable divergence now fails fast. A per-endpoint override on the default prefix, or one that stays under the declared prefix, remains a supported feature.
- A required discovery endpoint that would be missing or non-absolute. The
RFC 8414 §2 / OpenID Connect Discovery §3 endpoints the library derives —
Fixed
- The RFC 8414
/.well-known/oauth-authorization-serverdocument now advertisesauthorization_endpoint. It was omitted entirely:Attesto.Discoveryderives onlyissuer/token_endpoint, and the controller's host-member list never suppliedauthorization_endpoint, so the OAuth metadata silently lacked a field RFC 8414 §2 requires for the authorization-code flow. An OAuth client that reads this document rather than OpenID Discovery (e.g. the ChatGPT MCP connector) therefore concluded the server "does not implement OAuth." It is now derived viaauthorize_endpoint_url/1— the same path resolution astoken_endpoint, so the two cannot diverge. (OpenID Discovery's/.well-known/openid-configurationalready advertised it.)
[0.9.0] - 2026-06-14
Requires attesto ~> 0.7.0.
Added
Client ID Metadata Documents (CIMD,
draft-ietf-oauth-client-id-metadata-document-01) — opt-in, default off. A client can 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 and uses it as the client. Enable withclient_id_metadata: [enabled: true, ...]in the config.AttestoPhoenix.ClientIdMetadata.Fetcher(+ the default...Fetcher.Req) — the SSRF-guarded outbound GET. It resolves the host, rejects any A/AAAA address that is special-use (RFC 6890: loopback, private, link-local, CGNAT, multicast, reserved, and every IPv6 form that embeds an IPv4 — IPv4-mapped, NAT6464:ff9b::/96and local-use64:ff9b:1::/48, 6to4, IPv4-compatible — unwrapped and re-checked), pins the connection to a validated IP to close the DNS-rebinding window (TLS SNI/cert stay on the original hostname), refuses redirects, requires200+ JSON, and caps the body at 5 KB. Requires the optional:reqdependency, or a host-supplied:fetcher(e.g. a CIMD proxy service).AttestoPhoenix.ClientIdMetadata.Cache(default Ecto, cluster-coherent; ETS opt-out) — respects RFC 9111 cache headers clamped to bounds, never caches errors/invalid documents, re-checks expiry on read. New tableattesto_client_id_metadata(mix attesto_phoenix.gen.migration), swept byAttestoPhoenix.Store.Sweeper.AttestoPhoenix.ClientIdMetadata.Resolver+ integration: a CIMDclient_idURL resolves via the document and is wired through the authorization, PAR, and token endpoints as a{:cimd, metadata}client — PKCE forced, treated as a public client (orprivate_key_jwtvia the documentjwks/jwks_uri),redirect_uriexact-matched against the document'sredirect_urisand (by default) required to be same-origin with theclient_idURL. Opaqueclient_ids still resolve through:load_clientunchanged.- Discovery advertises
client_id_metadata_document_supportedwhen enabled.
New optional dependency
{:req, "~> 0.5", optional: true}for the default CIMD fetcher (a host that never enables CIMD pays nothing).
[0.8.0] - 2026-06-14
Requires attesto ~> 0.6.16.
Added
AttestoPhoenix.Store.EctoPARStore— a Postgres-backed Pushed Authorization Request store (RFC 9126), closing the last per-node gap to a fully clusterable authorization server. PAR was the only mutable OAuth store without an Ecto implementation: the defaultAttestoPhoenix.Store.PAR.ETSkeeps therequest_uri→ params mapping in per-node memory, so a reference pushed to one node could not be resolved when/authorizelanded on another — and FAPI 2.0 requires PAR. The new store persists each pushed request so any node resolves arequest_uriissued by any other, matching the code/refresh/nonce/replay Ecto stores.fetch/1is non-consuming (the authorization endpoint may re-enter after a login/consent detour);take/1is an atomic single-useDELETE … RETURNING.- New
AttestoPhoenix.Schema.PushedAuthorizationRequest(tableattesto_pushed_authorization_requests, keyed on therequest_uriprimary key,paramsasjsonb). mix attesto_phoenix.gen.migrationnow creates the fifth table, andmix attesto_phoenix.installwirespar_store: …EctoPARStoreby default, so a by-the-docs install is cluster-safe out of the box.AttestoPhoenix.Store.Sweepernow also reclaims expired PAR references.
- New
Atomic single-use of the PAR
request_uriat completion. The authorization endpoint now claims the pushed reference with the store's atomictake/1before issuing the code (it was previously consumed after issuance, with the result ignored), so two concurrent completions — on one node or across a cluster — can no longer each mint a code from one pushed request: exactly one wins the claim; the loser is redirectedinvalid_request_uriand issues nothing. Resolution still uses the non-consumingfetch/1, so a host may establish login/consent and re-enter/authorizewith the same reference.
Changed
- README documents the clustering story end-to-end and the PAR caveat; the
:par_storeconfig doc points atEctoPARStorefor clustered/FAPI deployments. The defaultpar_storeis unchanged (single-node ETS), so existing single-node hosts are unaffected.
[0.7.7] - 2026-06-13
Requires attesto ~> 0.6.16.
Fixed
- Token endpoint finalizes the authorization code only after the full
response is built. The
authorization_codegrant now callsAttesto.AuthorizationCode.finalize/3(new in attesto 0.6.16) once the access token, optional refresh token, and id_token have all been minted and recorded successfully. Previously the reuse marker was set the moment the code validated, so any later failure in the same request (a refresh-store write error, an id_token mint fault, a hostbuild_principalcallback returning the subject under the wrong key) left the code spent AND flagged as a successful redemption — turning a legitimate client retry into a false reuse attack that revoked the whole refresh-token family. A redemption that validates but fails downstream is now a cleaninvalid_granton replay.
[0.7.6] - 2026-06-12
Requires attesto ~> 0.6.13.
Fixed
- The token endpoint no longer short-circuits a missing PKCE
code_verifierasinvalid_request. PKCE enforcement is challenge-based:Token.fetch_code_verifier/3passes the verifier through toAttesto.AuthorizationCode.redeem/4, which requires a matching verifier for a challenge-bound code and collapses a missing OR mismatched verifier to a singleinvalid_grant(RFC 7636 §4.6). The authorization/PAR endpoint still requires acode_challengefor clients that must use PKCE (RequestPolicy.require_pkce?/2), so a challenge-bound code is always issued. Matches the FAPI ensure-pkce-code-verifier-required test (it expectsinvalid_grant).
[0.7.5] - 2026-06-10
Requires attesto ~> 0.6.13.
Security
- PAR
request_uriis now single-use (RFC 9126 §2.2 / FAPI 2.0). The reference is consumed once an authorization code is issued (not on the non-consumingfetchthat lets the host establish login/consent and re-enter), so a completed flow cannot be replayed within the remaining TTL. An already-consumed reference is rejected asinvalid_request_uri. (Flips the conformancePARAttemptReuseRequestUriwarning to a clean pass.) - UserInfo derives the DPoP
htuviaRequestContext.canonical_url, like every other endpoint — honouring a configured:htubut otherwise gatingX-Forwarded-*/Host on the trusted-proxy allowlist. Previously it fell back to the raw request Host when:htuwas unset (its default), the one endpoint that bypassed the host-header trust boundary.
Fixed
- Sender-constrained (DPoP/mTLS) clients now require PKCE. A FAPI 2.0 client
is sender-constrained, and FAPI 2.0 Security Profile §5.3.1.2 / RFC 9700
§2.1.1 mandate PKCE for it even though it authenticates confidentially (e.g.
private_key_jwt).RequestPolicy.require_pkce?/2now forces PKCE wheneverclient_requires_dpop?/client_requires_mtls?is true, regardless of the global:require_pkceflag, and the token endpoint enforces the matchingcode_verifierthrough that same predicate (one source of truth, so the authorization and token endpoints cannot drift). A plain confidential Basic-profile client still follows the global flag. (Flips the conformanceEnsurePKCERequiredtest to a pass.)
[0.7.4] - 2026-06-04
Requires attesto ~> 0.6.13.
Security / FAPI 2.0 conformance
Closes four conformance gaps found by auditing the OpenID FAPI 2.0 test suite source against the implementation:
- PAR
request_uriis bound to the client. The authorization endpoint now rejects a front-channelclient_idthat does not match the client therequest_uriwas issued to (RFC 9126 §2.2 /PAREnsureRequestUriIsBoundToClient) instead of silently using the stored client. - Unknown/expired PAR
request_uri→invalid_request_uri. Aurn:ietf:params:oauth:request_uri:reference not in the store now returns the correctinvalid_request_urierror rather than falling through torequest_uri_not_supported/invalid_request(RFC 9126 §2.2 /PARAttemptToUseExpiredRequestUri). External (non-PAR) references still reportrequest_uri_not_supported. - PAR rejects a
request_uriparameter. The PAR endpoint rejects a request carryingrequest_uri(RFC 9126 §2.1 step 2), checked on the raw parameters so it cannot be masked by arequestobject replacing the set. - Client-assertion audience is issuer-only.
private_key_jwtassertions at the token, PAR, and introspection endpoints must be audienced to the issuer identifier (FAPI 2.0 §5.3.2.1); the concrete endpoint URL is no longer accepted asaud, closing a confused-deputy gap (PAREndpointAsAudienceFails).
Changed
:authorization_response_issnow defaults totrue(RFC 9207 authorization-server mix-up defense, mandated by FAPI 2.0). Setfalseto opt out. Discovery advertisesauthorization_response_iss_parameter_supportedaccordingly.- Internal:
mix dialyzeris clean again.token.exresolves:principal_kindsby reading the struct field directly (its type admits a list, unlike thecallback() | nilreader), and two fail-closed grant-pipeline clauses are documented in.dialyzer_ignore.exs. No behaviour change.
[0.7.3] - 2026-06-04
The FAPI 2.0 Message Signing endpoints on the Phoenix layer: signed
authorization responses (JARM), the RFC 7662 / RFC 9701 introspection endpoint,
and PAR/JAR hardening. Requires attesto ~> 0.6.13.
Added
POST /oauth/introspect— OAuth 2.0 Token Introspection (RFC 7662) with the RFC 9701 signed-JWT response (FAPI 2.0 Message Signing §5.5). Authenticates the caller through the sharedAttestoPhoenix.ClientAuthenticationcore (client_secret_basic/client_secret_post/private_key_jwt), introspects via the conn-freeAttesto.Introspection, and negotiates byAcceptbetween the plain JSON response andapplication/token-introspection+jwt.:introspection_authorizeConfig callback(caller_client_id, response -> boolean)— authorizes the authenticated introspection caller against the token (RFC 7662 §4 / RFC 9701 §5). Consulted only for an active response; a non-truereturn (or a raise) downgrades the response to%{"active" => false}so a caller not entitled to the token learns nothing about it. Optional — when unset, every authenticated caller may introspect any token (the single-trust-domain default).- The authorization endpoint emits JARM (§5.4) responses for the JARM
response_modes (jwt/query.jwt/fragment.jwt/form_post.jwt), and the discovery documents advertise the supportedresponse_modes_supported,authorization_signing_alg_values_supported, the introspection endpoint, and its signing-algorithm metadata.
Changed
- The PAR endpoint now validates the pushed request as an authorization request
at push time (RFC 9126 §2.1 step 3): the request
redirect_urimust exactly match one of the client's registered URIs (RFC 6749 §3.1.2.3), and theresponse_type/PKCE/response_modemust be valid, so an invalid request is refused early rather than only when therequest_uriis later resolved at/authorize. The redirect-URI/PKCE/nonce policy is resolved by the new conn-freeAttestoPhoenix.AuthorizationServer.RequestPolicy, shared with the authorization endpoint so both validate identically. A host that mounts the PAR endpoint must configure:client_redirect_uris(the authorization endpoint already required it). AttestoPhoenix.ClientAuthentication.Result.client_idfalls back to the presented credential identifier so the signed-introspection audience (and the PAR/token client identity) resolves without a separate:client_idcallback.- OpenID Provider Metadata derives
request_parameter_supported(and only then advertisesrequest_object_signing_alg_values_supported) from actual request-object capability — whether the host can resolve a client's trusted JWKS (a:client_jwkscallback or an installed:client_store). An install without that capability now advertisesrequest_parameter_supported: falseinstead of a JAR support it cannot honour. - The OAuth 2.0 Authorization Server Metadata document (RFC 8414) now advertises
the signed-request-object metadata (
require_signed_request_objectandrequest_object_signing_alg_values_supported, RFC 9101 §10.5), matching the OpenID Provider Metadata document so a FAPI client reading either sees identical JAR support. Both documents derive it from the new conn-freeAttestoPhoenix.AuthorizationServer.RequestObjectMetadata(no more split, drift-prone assembly). AttestoPhoenix.Confignow rejects at boot a:request_object_policythat requires a signed request object (e.g.Policy.fapi_message_signing/0) when no:client_jwkscapability is configured. Such a config is unsatisfiable (every authorization request would be rejected) and would otherwise advertise the incoherent pairrequest_parameter_supported: false+require_signed_request_object: true. Pair the policy with:client_jwks(or an installed:client_store).
[0.7.2] - 2026-06-03
Added
:request_object_policyConfig key (anAttesto.RequestObject.Policy, default%Policy{}= generic OpenID Connect §6.1). It is enforced at BOTH the PAR endpoint and/authorize: a signed request object pushed to/paris verified there (rejected withinvalid_request_objectif it fails the policy), and re-verified at/authorize(RFC 9101). On success the PAR store holds the VERIFIED request-object parameters, never the unsigned body values beside them (RFC 9101 §6.3). A non-%Attesto.RequestObject.Policy{}value is rejected at boot. SetAttesto.RequestObject.Policy.fapi_message_signing()for the FAPI 2.0 Message Signing §5.3.1 profile (nbf/exprequired and bounded to 60 minutes,typ="oauth-authz-req+jwt"). Behaviour is unchanged unless a host opts in. Requiresattesto ~> 0.6.12.
[0.7.1] - 2026-06-03
Added
:client_auth_signing_algsConfig key — the JOSE algorithms accepted forprivate_key_jwtclient-assertion signatures, threaded intoAttesto.ClientAssertion.verify/5(via its:accepted_algsopt) and also rendered astoken_endpoint_auth_signing_alg_values_supportedin discovery. Defaults toAttesto.SigningAlg.fapi_algs/0(PS256, ES256, EdDSA), so behaviour is unchanged unless a host overrides it. Verification and the advertised metadata now read this one value and cannot drift. Requiresattesto ~> 0.6.11.
[0.7.0] - 2026-06-03
A structural refactor of the token/PAR controllers into a reusable authorization-server core, plus a behaviour-module install surface and several correctness fixes. Pre-1.0 minor bump because it carries breaking changes to the host-callback contract (see BREAKING below).
Added
- Behaviour-module install for host callbacks. The Config keys
:client_store,:principal_store,:consent_policy,:scope_policy,:event_sink,:registration, and:claims_providereach resolve their callbacks from a single installed module. Precedence is fixed: an explicit flat callback key wins; else the installed behaviour module if it exports the callback; elsenil. The required capabilities (load_client,verify_client_secret,load_principal) are validated by resolution at boot, so a behaviour-module-only install works. Boot-time conformance validation fails fast on a typo'd or partial module. AttestoPhoenix.ClaimsProviderbehaviour — the host UserInfo/ID-Token claim source (build_userinfo_claims/3,build_id_token_claims/4).AttestoPhoenix.Callback— one callback dispatcher (function /{m,f}/{m,f,extra}), replacing ~10 duplicated privateinvoke/2helpers.AttestoPhoenix.ClientAuthenticationandAttestoPhoenix.AuthorizationServer.{SenderConstraint, Token, PAR}— conn-free core modules. The token and PAR controllers are now thin adapters that lift conn facts into data, call the core, and render; the core returns data and audit events rather than writing the conn or emitting events.
Changed
- BREAKING: the ID-Token extra-claims source is now the separate
:build_id_token_claimscallback ((client, subject, granted_scopes, requested_claims -> map), and it MUST NOT carrysub). Previously the 4-arity form of:build_userinfo_claimsdoubled as the ID-Token source;:build_userinfo_claimsis now the 3-arity UserInfo source only. Hosts that wired a 4-arity:build_userinfo_claimsmust move it to:build_id_token_claims. - BREAKING:
AttestoPhoenix.ClaimsProviderno longer declaresbuild_principal/3; principal building stays solely onAttestoPhoenix.PrincipalStore. Claim sourcing and principal loading are separate concerns. - Client-assertion
audnow accepts the issuer or the concrete token/PAR endpoint URL (RFC 7523 / OIDC Core §9), widened from issuer-only. The endpoint URL is derived from trusted Config (issuer + path), never the request Host. Still FAPI 2 valid (the issuer remains accepted). - Client authentication (RFC 6749 §2.3.1): a request-body
client_idpresented alongside HTTP Basic is accepted as identification when it matches the Basic userid, and rejected asinvalid_requestwhen it conflicts. Only a second credential (bodyclient_secretorclient_assertion) is treated as a competing authentication method. The token and PAR endpoints now share one client-authentication implementation, so they no longer diverge. - PAR stores the resolved authenticated
client_id; when no:client_idcallback is configured it leaves the request's presentedclient_idintact rather than clobbering it. The opaque-structclient[:id]/client["id"]fallback is removed.
[0.6.23] - 2026-06-02
Changed
- Require the client-authentication assertion
audto be the issuer identifier at both the token and PAR endpoints (FAPI 2). The endpoint URL is no longer accepted as an audience. Requiresattesto ~> 0.6.10.
[0.6.22] - 2026-06-02
Changed
- Advertise only the FAPI 2 client-authentication signing algorithms
(
PS256,ES256,EdDSA) intoken_endpoint_auth_signing_alg_values_supported, matching the underlying enforcement in attesto 0.6.9 which rejects RS256 client assertions. Requiresattesto ~> 0.6.9.
[0.6.21] - 2026-06-02
Fixed
- Return the standard OAuth token endpoint error
invalid_requestwhen a client that requires DPoP omits the proof entirely. Presented-but-invalid proofs still returninvalid_dpop_proof; the omitted-proof case now matches FAPI's expected token endpoint error classification.
[0.6.20] - 2026-06-02
Added
- Add
:refresh_token_rotation_grace_secondstoAttestoPhoenix.Configand pass it through toAttesto.RefreshToken.rotate/3. The default is now a FAPI retry-compatible 60-second idempotency window for retrying a just-rotated refresh token when the client did not receive or persist the first rotation response; set0for strict immediate reuse revocation.
[0.6.19] - 2026-06-02
Fixed
- Bind refresh tokens to the DPoP proof key only for public clients, as required by RFC 9449. Confidential clients keep refresh tokens bound to the authenticated client, allowing a later refresh request to use a fresh DPoP proof key while still minting the returned access token as DPoP-bound to that current proof.
[0.6.18] - 2026-06-02
Added
- Add
:client_requires_dpop?as a host callback so deployments can mark a client as requiring DPoP-bound token issuance. When such a client calls the token endpoint without a DPoP proof, the controller now rejects the request withinvalid_dpop_proofrather than silently issuing an unbound Bearer token.
[0.6.17] - 2026-06-02
Fixed
- Treat a resolved PAR
request_urias the complete authorization request, so front-channel parameters outside the pushed request object do not augment the request. In particular, astatequery parameter that was not included in the pushed request is no longer echoed in the authorization response.
[0.6.16] - 2026-06-02
Fixed
- Allow PAR requests to carry an explicit
dpop_jktwithout also requiring a DPoP proof on the PAR request itself. If a PAR DPoP proof is present, an explicitdpop_jktmust still match that proof; otherwise the stored thumbprint is later enforced when the authorization code is redeemed.
[0.6.15] - 2026-06-02
Fixed
- Carry the DPoP JWK thumbprint from a pushed authorization request into the issued authorization code. A token request that redeems the code with a different DPoP proof key is now rejected instead of minting a token bound to the later key.
[0.6.14] - 2026-06-01
Fixed
- Verify DPoP proofs at the PAR endpoint and bind stored pushed
authorization requests to the verified proof key. If a PAR request includes
an explicit
dpop_jkt, it must match the verified proof JWK thumbprint; mismatches now returninvalid_dpop_proofinstead of issuing arequest_uri.
[0.6.13] - 2026-06-01
Fixed
- Accept
private_key_jwtclient assertions whoseaudis the issuer at the token endpoint and PAR endpoint, while continuing to accept endpoint-specific audiences and reject unrelated audiences. This matches FAPI conformance suite client-authentication behavior without relaxing signature,iss/sub,jti, or replay checks.
[0.6.12] - 2026-06-01
Security
- Reject replayed
private_key_jwtclient assertions at the token endpoint and PAR endpoint by recording assertionjtivalues through the configured replay check. - Enforce per-client registered grant types when a host provides
:client_grant_types, preventing a client registered for one grant from minting tokens through another. - Bind PAR
request_uriauthorization requests to the authenticated pushed request client and store that authenticated client id, rather than trusting a front-channel or body-suppliedclient_id.
Fixed
- Preserve keystore-provided per-key
algmetadata in the JWKS endpoint. This keeps FAPI deployments that sign ID tokens withPS256from advertising the same key asRS256. - Add the zero-arity
issue/0entrypoint to the Ecto DPoP nonce store so server-issued DPoP nonces work when the store is configured directly as a behaviour module. - Decode form-encoded client id and secret values in revocation endpoint Basic authentication, matching the token endpoint.
- Make the default ETS PAR store tolerate concurrent first-use table creation.
[0.6.11] - 2026-06-01
Fixed
- Resolve PAR
request_urireferences non-destructively at the authorization endpoint, so host login or consent re-entry can complete without consuming the pushed request before authorization-code issuance.
Changed
- Add a
fetchcallback toAttestoPhoenix.PARStorefor authorization-endpoint resolution. Existing custom stores that only implementtake/1still work through a compatibility fallback, but new stores should implementfetch/1.
[0.6.10] - 2026-06-01
Fixed
- Treat an explicit
nil:par_storeconfig value as unset when applying the default ETS PAR store. This prevents PAR from callingnil.put/3when hosts enable pushed authorization requests without overriding the development PAR store. - Apply the same nil-aware defaulting to authorization-endpoint PAR resolution.
[0.6.9] - 2026-06-01
Added
- Advertise FAPI-required discovery metadata when configured:
authorization_response_iss_parameter_supported: truewhen RFC 9207 authorization-responseissis enabled, andtoken_endpoint_auth_signing_alg_values_supportedfrom Attesto's asymmetric signing algorithm set forprivate_key_jwtclients.
[0.6.8] - 2026-06-01
Added
- Add host-configurable FAPI-oriented authorization-server controls:
:require_pushed_authorization_requestsrejects direct front-channel authorization requests unless they arrive through a PARrequest_uri, and:authorization_response_issincludes the RFC 9207issparameter on successful and error authorization responses. - Allow hosts to configure the advertised and accepted token endpoint client
authentication methods. The token endpoint and PAR endpoint now enforce
:token_endpoint_auth_methods_supportedwhen set, so deployments can expose stricter profiles such asprivate_key_jwtonly. - Advertise configured token endpoint authentication methods and PAR-required policy in OAuth/OIDC metadata.
[0.6.7] - 2026-06-01
Added
- Mount
POST /oauth/authorizealongsideGET /oauth/authorize, matching OpenID Connect Core's requirement that the Authorization Endpoint support both methods. - Extend the Ecto authorization-code store with successful-consumption markers
and issued-access-token tracking. When a successfully redeemed authorization
code is replayed, the token endpoint still returns
invalid_grantand now revokes the access token minted by the original code redemption when the Ecto store is configured.
[0.6.6] - 2026-06-01
Fixed
- Dynamic client registration now preserves inline
jwksmetadata (RFC 7591 §2) and hands it to the host:register_clientcallback. Hosts can then return those keys through:client_jwksfor request-object andprivate_key_jwtverification.
[0.6.5] - 2026-06-01
Fixed
- Return a clean
request_uri_not_supportedauthorization response for unsupported OIDCrequest_urireferences when no PAR store is configured, instead of calling a nil PAR store.
[0.6.4] - 2026-05-31
Changed
- Replace the direct
jasondependency with Elixir's built-inJSONmodule.
Added
- Add a test-only
req_dpopcompatibility check proving thatAttestoPhoenix.Plug.Authenticateaccepts RFC 9449 DPoP proofs generated by an external Req client plugin.req_dpopis not a runtime dependency. - Document
req_dpopas an optional Req client companion for tests and internal tooling.
[0.6.3] - 2026-05-31
Added
mix attesto_phoenix.install, an upgrade-aware Igniter installer. It is idempotent and re-runnable: it adds theAttestoPhoenix.Configconfig skeleton (issuer, keystore, repo, the Ecto-backed token stores, a chosen:oauth_path_prefix, and neutral defaults) to the host config, mountsattesto_routes/1at the chosen prefix into the host router, scaffolds host callback modules implementing the recommended behaviours (ClientStore,PrincipalStore,ScopePolicy,ConsentPolicy,RegistrationStore,EventSink) with documented stub callbacks, and points the host atmix attesto_phoenix.gen.migrationfor the Ecto tables.igniteris declared as an optional dependency, so the runtime package never forces it on consumers; the task is available to a host that opts into running it. Options:--oauth-path-prefixand--callbacks-module.Configurable OAuth endpoint paths.
AttestoPhoenix.Confignow accepts an:oauth_path_prefix(default"/oauth", reproducing the historic surface) plus explicit per-endpoint overrides (:authorize_path,:token_path,:par_path,:revocation_path,:registration_path,:userinfo_path) that win when set. Resolver helpers (token_endpoint_url/1,par_endpoint_url/1,revocation_endpoint_url/1,registration_endpoint_url/1,userinfo_endpoint_url/1,authorize_endpoint_url/1,jwks_uri/1,registration_client_uri/2, and the*_path/1helpers) build absolute URLs from the issuer and the resolved path. The discovery (RFC 8414), OpenID-configuration (OpenID Connect Discovery), and registration (RFC 7591 / RFC 7592) controllers read every advertised URL from these resolvers instead of hardcoding/oauth/*, andto_attesto_config/2passes the resolved token path to the core builder automatically so the DPoPhtufollows the mount. A host that mounts under/mcp/oauthnow advertises correct URLs.Named host-contract behaviours documenting the full callback contract with the governing RFC for each callback, as the recommended production shape:
AttestoPhoenix.ClientStore,AttestoPhoenix.PrincipalStore,AttestoPhoenix.ScopePolicy,AttestoPhoenix.ConsentPolicy,AttestoPhoenix.RegistrationStore, andAttestoPhoenix.EventSink. Wiring is unchanged: pass an anonymous function, a{module, function}pair, or a{module, function, extra_args}triple perAttestoPhoenix.Configkey.Dynamic registration metadata passthrough (RFC 7591 §2). The registration endpoint now validates and carries the known client-identity members (
client_name,client_uri,logo_uri,contacts,policy_uri,tos_uri, and related software/JWKS members) through to:register_clientso consent screens keep the client's identity. Unknown members are dropped and never promoted to trusted policy; known members are merged under the validated protocol-critical members so they cannot override them.Actionable
AttestoPhoenix.Config.new/1validation errors that name the callback/store/path to add for each enabled feature, and absolute-path validation for:oauth_path_prefixand the per-endpoint overrides.Operations guides wired into the published docs:
replay_nonce_production.md,proxy_canonical_host.md,error_envelope.md,consumer_migration.md, andexamples.md.
[0.6.2]
- Advertise
response_modes_supported: ["query"]from the RFC 8414 OAuth Authorization Server Metadata endpoint, matching the authorization-code redirect response mode already used by the Phoenix authorization endpoint.
[0.6.1]
- Emit
:token_deniedaudit/telemetry events for token endpoint failures, including OAuth error, status, client/grant/scope context when available, and sender-constraint presence. - Normalize Phoenix callback specs before handing
:cert_derto core Attesto protected-resource verification, so function captures,{Module, function}, and{Module, function, extra_args}all work consistently.
[0.6.0]
Initial release: a Phoenix/Ecto OAuth 2.0 / OIDC authorization server layer over attesto.
Added
AttestoPhoenix.Config: centralized, validated configuration with neutral host callbacks (:load_client,:verify_client_secret,:load_principal,:authorize_scope,:on_event, and others), deriving theAttesto.Configthe protocol layer consumes.AttestoPhoenix.Router: theattesto_routes/1macro mounting the token, revocation, discovery, JWKS, and optional dynamic-registration endpoints.- Controllers for the token endpoint (
authorization_code,refresh_token, andclient_credentialsgrants), revocation (RFC 7009), discovery (RFC 8414), JWKS (RFC 7517), and optional dynamic client registration (RFC 7591). AttestoPhoenix.Plug.AuthenticateandAttestoPhoenix.Plug.RequireScopesprotected-resource plugs with DPoP and mTLS sender-constraint enforcement.- Ecto-backed implementations of the attesto store behaviours: code store,
refresh store (rotation with reuse detection), DPoP nonce store, and DPoP
jtireplay check, plus an optional TTL sweeper. mix attesto_phoenix.gen.migrationto generate the operational tables.- Pushed Authorization Requests (PAR, RFC 9126),
private_key_jwtclient authentication, signed request object validation, token exchange, UserInfo, registration management cleanup, and Phoenix resource-server plugs.