Passkeys and two-factor login come to Statamic's front end
Passkeys, two-factor authentication, and password confirmation used to stop at the control panel. In April they reached the front end, along with rate limiting that penetration tests had found missing.
· Updated · 9 min read · Thomas Sausen
Two kinds of users
Statamic has always had two kinds of users, even though both have the same kind of account. Editors log into the control panel. Members log into the site itself, through a login form built with Antlers tags, to reach a customer portal, a member area, or a course behind a paywall.
Statamic 6 gave the first group a lot. Two-factor authentication and elevated sessions were part of the very first alpha in August 2025, and passkeys followed in October with a pull request by Ryan Mitchell. An elevated session means asking for the password again, or a code, before someone changes something sensitive. The second group got none of it. If a member area needed two-factor login, you built it yourself or found an addon.
In April, Duncan McClean closed that gap in three pull requests, totaling more than 4,000 lines. Passkeys came first, in 6.13. Elevated sessions and two-factor authentication followed together in 6.15, along with a change that matters for almost every site, members or not: rate limits on logins and forms.
Passkeys
Frontend Passkeys extends the existing {{ user:login_form }} tag and adds new ones for everything around it: user:passkey_form to register a passkey, user:passkeys to list them with their last use, and user:delete_passkey_form to remove one. The browser side lives in Statamic's front-end helpers script, which now exposes Statamic.$passkeys. It has to load before your own script, which is easy to miss when you copy only the form:
<script src="/vendor/statamic/frontend/js/helpers.js"></script>
{{ user:login_form }}
<input type="email" name="email" autocomplete="username webauthn">
<button type="button" id="passkey-login">Log in with a passkey</button>
<script>
Statamic.$passkeys.configure({
optionsUrl: '{{ passkey_options_url }}',
verifyUrl: '{{ passkey_verify_url }}',
onSuccess: (data) => window.location = data.redirect || '/',
});
document.getElementById('passkey-login')
.addEventListener('click', () => Statamic.$passkeys.authenticate());
Statamic.$passkeys.initAutofill();
</script>
{{ /user:login_form }}The autocomplete="username webauthn" on the email field is what lets the browser offer a saved passkey as soon as someone clicks into it. Without it, autofill doesn't work.
Statamic doesn't hand you a finished login page here. You get the tags, the URLs, and a small JavaScript API, and the markup is yours. That's consistent with how its other user tags work, and it means a passkey login can look like the rest of the site.
During review, Jason Varga added rate limiting to the passkey routes that don't require a logged-in user. It wouldn't be the last time rate limits came up that month.
Elevated sessions
The second pull request, Frontend Elevated Sessions, looks unspectacular on its own. Duncan wrote that he hadn't attached it to any front-end route yet and was planning to use it in another pull request. That one turned out to be two-factor authentication, where disabling 2FA or generating new recovery codes should require confirming who you are.
Out of the box, a member who needs to confirm lands on a page that looks like Statamic's own. Set elevated_sessions_url in config/statamic/users.php and you can build your own page with {{ user:elevated_session_form }}. The tag tells you how the member should confirm: with a password, with a code sent by email if the account has no password, or with a passkey.
Jason's review found the month's most important issue here. The endpoint that checks the password had no rate limiting, so anyone with a valid session could try passwords without limit. Duncan pointed out that the same gap already existed in the control panel, and the front end had simply inherited it. Jason added rate limiting for both before merging.
A small companion change came from Steven Grant: elevated sessions can now be switched off entirely, with elevated_sessions_enabled in the same config file.
Two-factor authentication
Frontend Two-Factor Authentication is the largest of the three, at over 2,000 lines, and it closes a community idea: Frontend Forms support for 2FA. After login, the user:two_factor_challenge_form tag asks for a code from an authenticator app or a recovery code. The user:two_factor_setup_form tag shows the QR code and the secret key, and asks for a first code to confirm. Further tags list the recovery codes, offer them as a download, generate new ones, or turn 2FA off again. The last two require an elevated session, which is where the previous pull request comes in.
Where members go for the challenge and the setup is set in config/statamic/users.php, with two_factor_challenge_url and two_factor_setup_url. Roles listed in two_factor_enforced_roles can't skip setup, on the front end just as in the control panel.
That config wasn't in Duncan's first version. He had put both URLs on the login form tag as parameters, and encrypted the setup URL because the member is only half logged in at that point. Jason moved them to the config, so the login, the middleware, and everything else read from one place. His second change is easy to miss and more important. In the first version, opening the setup page turned 2FA on. Jason found it awkward for a GET request to change an account like that. There is now a separate user:two_factor_enable_form with a button that sends a POST, and only after that does the member see the QR code. He pointed to Laravel Fortify, which handles it the same way.
Rate limiting
The change with the widest reach in April came from Ryan Mitchell. He opened Improve rate limiting because, as he wrote, penetration tests on his team's sites had flagged missing rate limits on front-end login and form routes a few times. Until then, the workaround was to grab Statamic's routes in a service provider after boot and attach Laravel's throttle middleware by hand.
His first version added two config options for middleware on the auth and form routes. Jason argued against it in review. A throttle on the whole route group would override the more generous limits on the passkey routes, and it would also count harmless GET requests, so someone clicking around the login pages could hit the limit. And if rate limiting was the actual goal, it should be solved directly. Ryan rebuilt it around named rate limiters, which Jason then extended to the control panel and the passkey routes.
The defaults are 4 login attempts per minute and 10 form submissions per minute, per IP address. Ryan raised the form limit because some forms use precognitive requests for live validation. You can override each limiter in a service provider:
use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\RateLimiter;
public function boot(): void
{
RateLimiter::for('statamic.auth', fn (Request $request) => Limit::perMinute(10)->by($request->ip()));
RateLimiter::for('statamic.forms', fn (Request $request) => Limit::perMinute(20)->by($request->ip()));
}There are separate limiters for the control panel login, statamic.cp.auth, and for passkeys, statamic.passkeys and statamic.cp.passkeys. This is the part of 6.15 that affects every site, not just those with members. Any site with a contact form now has a limit on how often it can be submitted.
The smaller fixes around it
A few changes elsewhere in April fit the same picture. Since 6.11, resetting a password no longer logs the user in automatically. They're sent to the login form instead, so the normal login runs afterward, including any two-factor challenge. The forgot password form now always reports success, whether the email address belongs to an account. A comment in the code explains why: the earlier error messages, including the one for throttled requests, revealed which addresses were registered. That fix also went into Statamic 5.
In the control panel, assigning groups and roles now requires an elevated session, which makes sense given what a role can grant. And Ryan contributed a second change: control over who can be impersonated, through the UserPolicy.
What it means for client sites
If you build member areas, the security side is now largely taken care of. Two-factor login, passkeys, and password confirmation used to mean custom code or a paid addon. Now they're tags in core, maintained with every release. What's left is the markup, the part your client actually sees.
The rate limits deserve a look after updating to 6.15, even on sites without a single member. Ten form submissions per minute from one IP address is plenty for a contact form. It may not be for a form that many people submit from one shared network, such as an office or an event, or for a form that validates as you type. Both limits can be raised in a service provider.
Moving features exposes assumptions
The most telling moment of the month was a review comment. Bringing elevated sessions to the front end meant someone looked at the password check again, and found it had never been rate limited, not even in the control panel. Features built for a small, trusted group of editors carry assumptions about who's on the other side. Opening them up to every site visitor is when those assumptions get tested. This time the review caught it before the front end shipped with the same gap.
Sources
- Frontend Passkeys, statamic/cms on GitHub, April 9, 2026
- Frontend Elevated Sessions, statamic/cms on GitHub, April 20, 2026
- Frontend Two-Factor Authentication, statamic/cms on GitHub, April 23, 2026
- Frontend Forms support for 2FA, statamic/ideas on GitHub
- Improve rate limiting, statamic/cms on GitHub, April 20, 2026
- Add config for disabling Elevated Sessions, statamic/cms on GitHub, April 21, 2026
- Stop auto-logging in users after password reset, statamic/cms on GitHub, April 7, 2026
- Always show success when using forgot password form, statamic/cms on GitHub, April 23, 2026
- Add elevated session guards to AssignGroups and AssignRoles actions, statamic/cms on GitHub, April 7, 2026
- Allow control over who can be impersonated in UserPolicy, statamic/cms on GitHub, April 7, 2026
- Default statamic.forms rate limiter conflicts with Precognition live validation, statamic/cms on GitHub
- Give precognitive form requests a separate rate limit bucket, statamic/cms on GitHub, June 11, 2026
- Release Notes, Statamic
Thomas Sausen is a freelance web developer and the editor of Flatbard. He builds with Statamic and writes about the surrounding ecosystem.