2.4.0-rc.7
Pre-releaseSecurity
MFA session-purpose scoping. Several MFA-adjacent flows shared one bare "does an MFA session exist for this user" check without distinguishing why that session was minted, letting a session obtained for one purpose be redeemed for a stronger one:
- Added a distinct
password_resetMFA-session purpose (alongside the existingverified/challenge), minted only byForgotPassword's mobile OTP leg. VerifyOTPno longer accepts apassword_resetsession — closes a path where a forgot-password OTP session could be redeemed for a full access token instead of only a password change.ResetPasswordnow requires thepassword_resetpurpose specifically — averified/challengesession from an unrelated login/signup flow can no longer complete a password change.EmailOTPMFASetup/SMSOTPMFASetupandWebauthnLoginOptionsnow require averified(or, for the webauthn login-alternative flow,verified/challenge) session — apassword_resetsession can no longer be used to enroll a brand-new MFA factor or enumerate an account's passkey credential IDs.refresh_tokenrequests now verify a presented client secret when one is sent (previously ignored for this grant), rejecting a wrong secret instead of authenticating onclient_idalone.signupand the new-user branch ofmagic_link_loginnow explicitly reject a client-requested protected role, instead of relying onRoles/ProtectedRolesstaying disjoint by operator convention (oauth_callbackalready defended this way).
OAuth token endpoint: storage errors no longer misreported as invalid credentials. A transient storage failure during client lookup (e.g. SQLITE_BUSY under concurrent load, a network blip to an external DB) was previously indistinguishable from "no such client," causing /oauth/token to intermittently return 400 invalid_client for a real, correctly-credentialed client under load.
- All 6 storage backends'
GetClientByClientIDnow return(nil, nil)for a genuinely absent client, never a wrapped driver error — codified as an explicit contract onstorage.Provider. - A real storage error now surfaces as a new
503 temporarily_unavailableresponse instead of400 invalid_client— correctly signaling a retryable failure instead of permanently-wrong credentials, and no longer counted as a security event. - Verified live: reproduced real SQLite contention under concurrent load and confirmed zero misleading
400s post-fix.
Admin _update_user: MFA flag stuck at nil on first explicit disable. Fixed a "did the flag change" check that compared false != false when a user's IsMultiFactorAuthEnabled had never been set, silently skipping the write and leaving the flag nil instead of the requested false.
Verified via make test (SQLite), make test-all-db (all 7 backing databases), make smoke (GraphQL + REST + gRPC + MCP end-to-end), and make lint — all clean.
This is a release candidate (pre-release). Run make test-all-db cross-DB validation before promoting to a stable release.