Skip to content

2.4.0-rc.7

Pre-release
Pre-release

Choose a tag to compare

@lakhansamani lakhansamani released this 22 Jul 17:02
ca628ce

Security

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_reset MFA-session purpose (alongside the existing verified/challenge), minted only by ForgotPassword's mobile OTP leg.
  • VerifyOTP no longer accepts a password_reset session — closes a path where a forgot-password OTP session could be redeemed for a full access token instead of only a password change.
  • ResetPassword now requires the password_reset purpose specifically — a verified/challenge session from an unrelated login/signup flow can no longer complete a password change.
  • EmailOTPMFASetup/SMSOTPMFASetup and WebauthnLoginOptions now require a verified (or, for the webauthn login-alternative flow, verified/challenge) session — a password_reset session can no longer be used to enroll a brand-new MFA factor or enumerate an account's passkey credential IDs.
  • refresh_token requests now verify a presented client secret when one is sent (previously ignored for this grant), rejecting a wrong secret instead of authenticating on client_id alone.
  • signup and the new-user branch of magic_link_login now explicitly reject a client-requested protected role, instead of relying on Roles/ProtectedRoles staying disjoint by operator convention (oauth_callback already 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' GetClientByClientID now return (nil, nil) for a genuinely absent client, never a wrapped driver error — codified as an explicit contract on storage.Provider.
  • A real storage error now surfaces as a new 503 temporarily_unavailable response instead of 400 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.