feat: Optional 2FA - #8285
Open
distantnative wants to merge 3 commits into
Open
Conversation
distantnative
commented
Jul 10, 2026
distantnative
force-pushed
the
v6/feat/auth-2fa-optional
branch
2 times, most recently
from
July 10, 2026 17:53
634a083 to
e470aee
Compare
distantnative
marked this pull request as ready for review
July 10, 2026 18:30
distantnative
force-pushed
the
v6/feat/auth-2fa-optional
branch
from
July 10, 2026 19:40
e470aee to
28183ca
Compare
distantnative
commented
Jul 19, 2026
distantnative
force-pushed
the
v6/feat/auth-2fa-optional
branch
4 times, most recently
from
July 23, 2026 15:18
70cd6c9 to
ee1e676
Compare
distantnative
force-pushed
the
v6/feat/auth-2fa-optional
branch
from
July 23, 2026 15:45
ee1e676 to
3042961
Compare
distantnative
force-pushed
the
v6/feat/auth-2fa-optional
branch
from
July 23, 2026 15:52
3042961 to
7b75e86
Compare
lukasbestle
reviewed
Jul 24, 2026
distantnative
force-pushed
the
v6/feat/auth-2fa-optional
branch
2 times, most recently
from
August 3, 2026 15:28
a961ea1 to
0e2bd42
Compare
distantnative
force-pushed
the
v6/feat/auth-2fa-optional
branch
from
August 6, 2026 09:13
0e2bd42 to
b245ca0
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Review
Timing: No rush, but ideally before the v6-beta.4
Description
Adds optional, per-user two-factor authentication and makes it the default behaviour.
Previously, 2FA was all-or-nothing per install (
2fa => true). This branch lets individual users opt into a second factor (passkey, TOTP or email code) without an admin enforcing it for everyone.It also adds a new sub drawer in the security drawer for the "Code via email" second factor that users can activate themselves.
PasswordMethod::has2FA()is now public and resolves per user:2fa => true: enforced for every user (unchanged)Challenges::hasAvailable($user, '2fa'))EmailChallengebecomes opt-in as a second factor, so simply having an email address no longer forces it. It stays available for every other mode (password reset,codelogin method), and when 2FA is enforced it stays the baseline factor for users who haven't set up anything else, so nobody is locked out. Users activate it from their security settings (having to enter the emailed code once when activating it doubles as a deliverability check).HTTP Basic auth can't run a challenge, so
BasicAuthMethod::authenticate()now defers toPasswordMethod::has2FA()and rejects any user who would be challenged, while letting factor-less accounts (e.g. dedicated API users) keep working.Also in this branch:
UserCredentialDrawerController::authorizeCurrentUser(): the pending challenge stored in the session now expires with the challenge timeout, failed attempts count against the shared auth rate limit, single-use challenges (WebAuthn) are invalidated after a failed attempt, and the pending is consumed on success so a code or nonce can't be replayed for a second change. Reusable codes deliberately survive a typo, so the account owner isn't locked out by one wrong digit.Deliberately out of scope:
Changelog
🎉 Features
auth.methods.password.2fais now opt-in by default: leave it unset and only users who set up a second factor are challenged. Set it totrueto keep requiring a second factor from every user.☠️ Deprecated
Kirby\Cms\System::is2FA(): use$kirby->auth()->methods()->hasAnyRequiring2FA()instead.Kirby\Cms\System::is2FAWithTOTP(): use$kirby->auth()->methods()->hasAnyRequiring2FA()together with$kirby->auth()->enabledChallenges()instead.Docs
On most sites, forcing 2FA on every editor isn't realistic, but the admins still want it for themselves (and/or anyone that is willing to use it). Until now that wasn't possible:
2fa => truehit everyone or nobody. Now every user decides for their own account, and admins can still enforce it site-wide when they need to.Update the 2FA / login methods page:
How a user enables a second factor: Account → Security → pick "Passkey", "Authenticator app" or "Code via email".
For "Code via email": press Activate, Kirby sends a one-time code to the account's email address, entering it activates the challenge. That step is intentional: it proves the address is actually reachable before it becomes a login requirement. Disabling works the same way. An admin disabling it for someone else confirms with their own admin password instead.
Worth documenting as a gotcha: HTTP Basic auth (
api.basicAuth) can't run a challenge, so a user with a second factor can no longer authenticate with it. API accounts should stay without a second factor (and2fa => truedisables basic auth entirely, as before).For review team