Skip to content

[6.x] Frontend Elevated Sessions#14424

Merged
jasonvarga merged 18 commits into6.xfrom
frontend-elevated-sessions
Apr 20, 2026
Merged

[6.x] Frontend Elevated Sessions#14424
jasonvarga merged 18 commits into6.xfrom
frontend-elevated-sessions

Conversation

@duncanmcclean
Copy link
Copy Markdown
Member

@duncanmcclean duncanmcclean commented Apr 2, 2026

We introduced the concept of "Elevated Sessions" in v6 (#11688), allowing you to prompt users for their password or a verification code before taking sensitive actions.

Until now, elevated sessions have only been available in the Control Panel. This PR brings them to the frontend, ahead of another PR I'm working on.

Note

I haven't added the RequireElevatedSession middleware to any routes yet but I'm planning to use it on another PR I'm working on. For testing purposes, I've added it to a random page in my sandbox app.

Elevated Session page

Before you can take certain actions, you'll be redirected to a Statamic-looking authentication page where you'll be asked to confirm your identity.

If necessary, you may provide a custom page by specifying the URL in the users.php config file and using the {{ user:elevated_session_form }} tag.

// config/statamic/users.php

'elevated_sessions_url' => '/auth/confirm-password',

// (Note - this PR uses elevated_session_url (singular) but a follow-up PR changes it to elevated_sessions_url)
{{ user:elevated_session_form }}

    {{ if errors }}
        <div class="bg-red-300 text-white p-2">
            {{ errors }}
                {{ value }}<br>
            {{ /errors }}
        </div>
    {{ /if }}

    {{ if method == "password_confirmation" }}
        <label>Password</label>
        <input type="password" name="password" />
    {{ /if }}

    {{ if method == "verification_code" }}
        <p>A verification code has been sent to your email.</p>
        <label>Verification Code</label>
        <input type="text" name="verification_code" />
        <a href="{{ resend_code_url }}">Resend code</a>
    {{ /if }}

    {{ if method !== "passkey" }}
        <button type="submit">Confirm</button>
    {{ /if }}

    {{ if allow_passkey }}
        <button type="button" id="passkey-confirm">Confirm with Passkey</button>

        <script src="/vendor/statamic/frontend/js/helpers.js"></script>
        <script>
            Statamic.$passkeys.configure({
                optionsUrl: '{{ passkey_options_url }}',
                verifyUrl: '{{ submit_url }}',
                onSuccess: (data) => window.location = data.redirect || '/',
                onError: (error) => alert(error.message)
            });

            document.getElementById('passkey-confirm').addEventListener('click', () => {
                Statamic.$passkeys.authenticate();
            });
        </script>
    {{ /if }}

{{ /user:elevated_session_form }}

The method variable indicates how the user should confirm their identity:

  • password_confirmation - User should enter their password.
  • verification_code - User doesn't have a password, so they should enter the verification code sent to their email.
  • passkey - User requires a passkey to login.

Users will be redirected back to the original page once they've confirmed their identity.


Docs PR: statamic/docs#1878

@duncanmcclean duncanmcclean marked this pull request as ready for review April 10, 2026 10:01
Copy link
Copy Markdown
Member

@jasonvarga jasonvarga left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed the new frontend elevated-session flow, the moved macros/controller logic, and the tag. Overall the refactor is clean (CP controller inherits from the shared base; macros moved to AppServiceProvider). Findings below, ordered by severity.

Warnings

1. No rate limiting on POST /!/auth/elevated-session (password brute-force risk)
routes/web.php:59 registers the confirm endpoint with only auth middleware. An attacker with a valid session cookie can attempt unlimited passwords/verification codes. The resend endpoint is throttled (throttle:send-elevated-session-code, line 61), but the actual password-check isn't. Compare: Laravel's default auth:confirm-password uses throttling. The CP has the same gap pre-existing — but frontend exposure is broader. Consider adding a throttle.

2. No rate limiting on GET /!/auth/elevated-session/passkey-options
routes/web.php:60 — the analogous passkey-login options route at line 66 uses ThrottleRequests::class.':30,1'. For consistency, throttle this one too (calls WebAuthn::prepareAssertion(), which is non-trivial).

3. Un-localized error message
src/Http/Controllers/Auth/ElevatedSessionController.php:87'Resend code is only available for verification code method' is English only. All user-facing strings should be localized (same issue existed in the old CP controller; now inherited by both surfaces).

Notes

4. Variable shadowing in UserTags::elevatedSessionForm()
src/Auth/UserTags.php:754 sets \$method = \$user->getElevatedSessionMethod() (e.g. password_confirmation), then line 770 reassigns \$method = 'POST' (HTTP verb). It works because \$data is already built, but rename the second to \$httpMethod for clarity.

5. CP resendCode flash key changed from successstatus
By inheriting the new base controller, the CP's resend now flashes status (tests updated at tests/Auth/ElevatedSessionTest.php:381,402,405). This is intentional (and actually makes the CP's <Description v-if=\"status\"> render, fixing a latent bug where the flash was invisible), but worth calling out as a CP behavior change.

6. Resend-code is a GET that sends an email + writes session
Docs show <a href=\"{{ resend_code_url }}\">. A link prefetcher, email scanner, or crawler following the link could trigger a resend. Pre-existing pattern from the CP; safe-ish because throttled, but a POST form would be more correct.

7. User::current()->getElevatedSessionMethod() called before custom-URL early-return
src/Http/Controllers/Auth/ElevatedSessionController.php:19-21 — unused work when elevated_session_page is configured. Minor.

Missing tests

8. Middleware→exception→custom-URL flowfrontend_elevated_session_redirects_to_custom_url_when_configured only covers a direct GET to /!/auth/confirm-password. No test asserts the full flow where the RequireElevatedSession middleware denies a request and the user ultimately lands on elevated_session_page.

9. confirm with multiple credentials present — if a request includes both password and verification_code, all three validators run; no test documents which combinations short-circuit vs. require all-valid.

10. Rate-limit coverage for new frontend resend routeresending_code_is_rate_limited only exercises the CP route; the frontend route shares the rate limiter in routes/web.php:61, but there's no frontend equivalent test.

No critical or security-blocking issues. The warnings around rate limiting are the main items worth addressing before merge.

@duncanmcclean
Copy link
Copy Markdown
Member Author

1. No rate limiting on POST confirm:
2. No rate limiting on passkey-options:
Good catch. This is a pre-existing gap from the CP controller though — the frontend inherits the same behaviour. Do you want me to add it here, or wait until #14475 is merged and adopt what it does?

4. Variable shadowing:
This is consistent with the other user form tags (loginForm, registerForm, etc.) which all re-use the same $method variable.

5. CP resendCode flash key:
Yes, this is intentional. It actually fixes the CP's <Description v-if="status"> which wasn't rendering before.

6. Resend-code is GET:
Pre-existing pattern from the CP. Changing it would require a dedicated form, which isn't worth the trade-off for a throttled endpoint.

8. Missing middleware→custom-URL flow test:
The middleware, exception, and custom URL redirect are each tested individually. The glue between them is standard Laravel behavior (exception rendering → redirect). An end-to-end test here would mostly be testing the framework.

9. Multiple credentials test:
Each validator returns early if its field isn't filled ($request->filled()), so they're independent by design. Documenting every combination would be low value.

10. Frontend resend rate-limit test:
Added an equivalent test for the frontend. 👍

Copy link
Copy Markdown
Member

@jasonvarga jasonvarga left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

.

@jasonvarga jasonvarga dismissed their stale review April 20, 2026 17:30

Changes made.

jasonvarga and others added 6 commits April 20, 2026 14:28
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Inertia requests now use the default error bag so the Vue Form's
errors slot picks them up. Antlers submissions still use the
user.elevated_session bag for the tag's {{ errors }} loop.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@jasonvarga jasonvarga merged commit a206f4c into 6.x Apr 20, 2026
18 checks passed
@jasonvarga jasonvarga deleted the frontend-elevated-sessions branch April 20, 2026 21:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants