[6.x] Frontend Two-Factor Authentication#14525
Open
duncanmcclean wants to merge 17 commits into6.xfrom
Open
Conversation
Replaces the two_factor_challenge_url/two_factor_setup_url tag params and hidden-field plumbing with statamic.users.two_factor_challenge_url and two_factor_setup_url config keys. Eliminates the encrypt/decrypt dance and the session-based URL storage. CP flows are unaffected. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Frontend form submissions without a _redirect param now redirect back with a flash message instead of returning raw JSON. CP axios calls keep their JSON response because they set the appropriate Accept header. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pulls the value instead of getting it, and caches the result in __invoke so the early-return and the Inertia prop share the same resolved URL. Prevents a stale redirect from lingering in the session after the setup flow completes. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
login.id and login.remember were migrated through session regenerate and never consumed. Cleaning them up prevents the challenge form tag (which gates on login.id presence) from rendering for a user who has already authenticated. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds middleware tests for the configured two_factor_setup_url (redirect target and loop bypass) and login-form tests covering when login.redirect is or isn't stashed in the session. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The enable and confirm routes had no auth enforcement; disable and the recovery-codes routes relied on RequireElevatedSession which is a no-op when elevated sessions are disabled. Wraps the whole action-routes group in auth middleware so guests are redirected regardless of the elevated-sessions config. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Move the enable endpoint from GET to POST so it no longer mutates state
on a safe verb. Calling enable when a pending secret already exists is
now an idempotent no-op. Adds a {{ user:two_factor_enable_form }} tag
for frontend opt-in; the setup_form tag no longer auto-generates the
secret on render and only appears once setup is pending.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Each 2FA form tag now reads its success/error session under a named bag (user.two_factor_setup, user.two_factor_disable, etc.) and the matching controllers flash to the same names. Previously every tag pulled from the unscoped default bag, so a success flash from confirming setup would also render inside disable and reset-recovery-codes forms on the redirect page. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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.
This pull request introduces Antlers tags for building frontend two-factor authentication flows, allowing users to set up, challenge, and manage 2FA entirely outside the Control Panel.
Login Form
The existing
{{ user:login_form }}tag now acceptstwo_factor_challenge_urlandtwo_factor_setup_urlparameters. These let you control where users are redirected when they need to verify a 2FA code or set up 2FA for the first time.{{ user:login_form redirect="/dashboard" two_factor_challenge_url="/auth/two-factor-challenge" two_factor_setup_url="/auth/setup-2fa" }} <input type="email" name="email" value="{{ old:email }}" /> <input type="password" name="password" /> <button type="submit">Log in</button> {{ /user:login_form }}The
two_factor_setup_urlvalue is encrypted to prevent tampering, since the user is only partially authenticated at that point.2FA Challenge
After logging in, users with 2FA enabled are redirected to the challenge URL. The
{{ user:two_factor_challenge_form }}tag renders a form for entering a TOTP code or recovery code.{{ user:two_factor_challenge_form redirect="/dashboard" }} {{ if errors }} <div class="text-red-500">{{ errors }}{{ value }}{{ /errors }}</div> {{ /if }} <input type="text" name="code" inputmode="numeric" autocomplete="one-time-code" /> <button type="submit">Verify</button> {{ /user:two_factor_challenge_form }}If no
redirectis specified, the redirect from the original login form carries through.2FA Setup
The
{{ user:two_factor_setup_form }}tag renders a QR code and secret key for users to scan with their authenticator app, along with a confirmation code input.{{ user:two_factor_setup_form redirect="/account/recovery-codes" }} {{ if errors }} <div class="text-red-500">{{ errors }}{{ value }}{{ /errors }}</div> {{ /if }} <div>{{ qr_code }}</div> <p>Or enter manually: <code>{{ secret_key }}</code></p> <input type="text" name="code" inputmode="numeric" maxlength="6" /> <button type="submit">Enable 2FA</button> {{ /user:two_factor_setup_form }}Recovery Codes & Disable 2FA
{{ user:two_factor_recovery_codes }}— loops through the user's recovery codes ({{ code }}){{ user:two_factor_recovery_codes_download_url }}— outputs a URL to download codes as a text file{{ user:reset_two_factor_recovery_codes_form }}— renders a form to regenerate recovery codes{{ user:disable_two_factor_form }}— renders a form to disable 2FA, with asetup_urlparameter for users whose roles enforce 2FAThe reset recovery codes and disable forms require an elevated session.
Docs PR: statamic/docs#1891
Closes statamic/ideas#1398