From 6d75a3f6ef0d76834e7b804743ec783441e26a6d Mon Sep 17 00:00:00 2001 From: Duncan McClean Date: Wed, 23 Sep 2026 12:15:58 +0100 Subject: [PATCH 1/2] add option to disable frontend authentication --- config/users.php | 12 ++++++ routes/web.php | 3 +- src/Auth/UserTags.php | 43 +++++++++++++++++++ ...rontendAuthenticationDisabledException.php | 13 ++++++ .../Middleware/EnsureFrontendAuthEnabled.php | 21 +++++++++ src/Providers/AppServiceProvider.php | 4 ++ tests/Tags/User/DeletePasskeyFormTest.php | 8 ++++ tests/Tags/User/DisableTwoFactorFormTest.php | 8 ++++ tests/Tags/User/ElevatedSessionFormTest.php | 11 +++++ tests/Tags/User/ForgotPasswordFormTest.php | 8 ++++ tests/Tags/User/LoginFormTest.php | 18 ++++++++ tests/Tags/User/LogoutTest.php | 12 ++++++ tests/Tags/User/PasskeyFormTest.php | 9 ++++ tests/Tags/User/PasswordFormTest.php | 8 ++++ tests/Tags/User/ProfileFormTest.php | 8 ++++ tests/Tags/User/RegisterFormTest.php | 8 ++++ tests/Tags/User/ResetPasswordFormTest.php | 18 ++++++++ .../ResetTwoFactorRecoveryCodesFormTest.php | 8 ++++ .../Tags/User/TwoFactorChallengeFormTest.php | 9 ++++ tests/Tags/User/TwoFactorEnableFormTest.php | 8 ++++ .../User/TwoFactorRecoveryCodesTagTest.php | 9 ++++ tests/Tags/User/TwoFactorSetupFormTest.php | 9 ++++ tests/Tags/User/UserTagsTest.php | 35 +++++++++++++++ 23 files changed, 289 insertions(+), 1 deletion(-) create mode 100644 src/Exceptions/FrontendAuthenticationDisabledException.php create mode 100644 src/Http/Middleware/EnsureFrontendAuthEnabled.php diff --git a/config/users.php b/config/users.php index da2d293b3e2..be2169db26d 100644 --- a/config/users.php +++ b/config/users.php @@ -153,6 +153,18 @@ 'web' => 'web', ], + /* + |-------------------------------------------------------------------------- + | Front-end Authentication + |-------------------------------------------------------------------------- + | + | Determines if Statamic registers front-end registration routes to power + | {{ user:login_form }} and related tags. Doesn't affect the Control Panel. + | + */ + + 'frontend_auth_enabled' => true, + /* |-------------------------------------------------------------------------- | Impersonation diff --git a/routes/web.php b/routes/web.php index ab7347d7a0a..0d49625b4df 100755 --- a/routes/web.php +++ b/routes/web.php @@ -28,6 +28,7 @@ use Statamic\Http\Middleware\AuthGuard; use Statamic\Http\Middleware\CP\AuthGuard as CPAuthGuard; use Statamic\Http\Middleware\CP\HandleInertiaRequests; +use Statamic\Http\Middleware\EnsureFrontendAuthEnabled; use Statamic\Http\Middleware\RedirectIfTwoFactorSetupIncomplete; use Statamic\Http\Middleware\RequireElevatedSession; use Statamic\Statamic; @@ -45,7 +46,7 @@ Route::get('fieldtypes/dictionaries/{dictionary}', DictionaryFieldtypeController::class)->middleware([CPAuthGuard::class, 'throttle:statamic.dictionaries'])->name('dictionary-fieldtype'); - Route::group(['prefix' => 'auth', 'middleware' => [AuthGuard::class]], function () { + Route::group(['prefix' => 'auth', 'middleware' => [EnsureFrontendAuthEnabled::class, AuthGuard::class]], function () { Route::get('logout', [LoginController::class, 'logout'])->name('logout'); Route::group(['middleware' => [HandlePrecognitiveRequests::class, 'throttle:statamic.auth']], function () { diff --git a/src/Auth/UserTags.php b/src/Auth/UserTags.php index 4ffe3c487c2..1c1a64540fe 100644 --- a/src/Auth/UserTags.php +++ b/src/Auth/UserTags.php @@ -4,6 +4,7 @@ use Illuminate\Support\Collection; use Statamic\Contracts\Auth\Role; +use Statamic\Exceptions\FrontendAuthenticationDisabledException; use Statamic\Facades\TwoFactor; use Statamic\Facades\URL; use Statamic\Facades\User; @@ -103,6 +104,8 @@ public function profile() */ public function loginForm() { + $this->ensureFrontendAuthEnabled(); + $data = array_merge($this->getFormSession(), [ 'passkey_options_url' => route('statamic.passkeys.options'), 'passkey_verify_url' => route('statamic.passkeys.login'), @@ -150,6 +153,8 @@ public function loginForm() */ public function registerForm() { + $this->ensureFrontendAuthEnabled(); + $data = $this->getFormSession('user.register'); $data['fields'] = $this->getRegistrationFields(); @@ -196,6 +201,8 @@ public function registerForm() */ public function profileForm() { + $this->ensureFrontendAuthEnabled(); + if (session()->has('status')) { return $this->parse(['success' => true]); } @@ -248,6 +255,8 @@ public function profileForm() */ public function passwordForm() { + $this->ensureFrontendAuthEnabled(); + if (session()->has('status')) { return $this->parse(['success' => true]); } @@ -308,6 +317,8 @@ public function registrationForm() */ public function passkeyForm() { + $this->ensureFrontendAuthEnabled(); + $data = [ 'passkey_options_url' => route('statamic.passkeys.create'), 'passkey_verify_url' => route('statamic.passkeys.store'), @@ -361,6 +372,8 @@ public function passkeys() */ public function deletePasskeyForm() { + $this->ensureFrontendAuthEnabled(); + if (! $user = User::current()) { return ''; } @@ -414,6 +427,8 @@ public function deletePasskeyForm() */ public function logoutUrl() { + $this->ensureFrontendAuthEnabled(); + $queryParams = []; if ($redirect = $this->params->get('redirect')) { @@ -430,6 +445,8 @@ public function logoutUrl() */ public function logout() { + $this->ensureFrontendAuthEnabled(); + auth()->logout(); abort(redirect($this->params->get('redirect', '/'), $this->params->get('response', 302))); @@ -444,6 +461,8 @@ public function logout() */ public function forgotPasswordForm() { + $this->ensureFrontendAuthEnabled(); + $data = $this->getFormSession('user.forgot_password'); // Alias for backwards compatibility. @@ -508,6 +527,8 @@ private function getPasswordResetUrl(?string $url = null): ?string */ public function resetPasswordForm() { + $this->ensureFrontendAuthEnabled(); + if (session()->has('status')) { return $this->parse(['success' => true]); } @@ -750,6 +771,8 @@ public function notIn() */ public function elevatedSessionForm() { + $this->ensureFrontendAuthEnabled(); + if (! ($user = User::current())) { return; } @@ -817,6 +840,8 @@ public function twoFactorEnabled(): bool */ public function twoFactorChallengeForm() { + $this->ensureFrontendAuthEnabled(); + if ( ! TwoFactor::enabled() || session()->missing('login.id') @@ -868,6 +893,8 @@ public function twoFactorChallengeForm() */ public function twoFactorEnableForm() { + $this->ensureFrontendAuthEnabled(); + $user = User::current(); if ( @@ -918,6 +945,8 @@ public function twoFactorEnableForm() */ public function twoFactorSetupForm() { + $this->ensureFrontendAuthEnabled(); + $user = User::current(); if ( @@ -1000,6 +1029,8 @@ public function twoFactorRecoveryCodes() */ public function twoFactorRecoveryCodesDownloadUrl() { + $this->ensureFrontendAuthEnabled(); + $user = User::current(); if ( @@ -1021,6 +1052,8 @@ public function twoFactorRecoveryCodesDownloadUrl() */ public function resetTwoFactorRecoveryCodesForm() { + $this->ensureFrontendAuthEnabled(); + $user = User::current(); if ( @@ -1070,6 +1103,8 @@ public function resetTwoFactorRecoveryCodesForm() */ public function disableTwoFactorForm() { + $this->ensureFrontendAuthEnabled(); + $user = User::current(); if ( @@ -1284,4 +1319,12 @@ protected function getPasswordFields() ->values() ->all(); } + + private function ensureFrontendAuthEnabled(): void + { + throw_unless( + config('statamic.users.frontend_auth_enabled', true), + new FrontendAuthenticationDisabledException + ); + } } diff --git a/src/Exceptions/FrontendAuthenticationDisabledException.php b/src/Exceptions/FrontendAuthenticationDisabledException.php new file mode 100644 index 00000000000..89b0850e10e --- /dev/null +++ b/src/Exceptions/FrontendAuthenticationDisabledException.php @@ -0,0 +1,13 @@ +app[\Illuminate\Contracts\Http\Kernel::class] + ->addToMiddlewarePriorityBefore( + \Illuminate\Contracts\Auth\Middleware\AuthenticatesRequests::class, + \Statamic\Http\Middleware\EnsureFrontendAuthEnabled::class + ) ->pushMiddleware(\Statamic\Http\Middleware\PoweredByHeader::class) ->pushMiddleware(\Statamic\Http\Middleware\CheckComposerJsonScripts::class) ->pushMiddleware(\Statamic\Http\Middleware\CheckMultisite::class) diff --git a/tests/Tags/User/DeletePasskeyFormTest.php b/tests/Tags/User/DeletePasskeyFormTest.php index 74fbf0b0cf8..433a7bf2070 100644 --- a/tests/Tags/User/DeletePasskeyFormTest.php +++ b/tests/Tags/User/DeletePasskeyFormTest.php @@ -151,4 +151,12 @@ public function it_returns_403_when_deleting_nonexistent_passkey() ->deleteJson(route('statamic.passkeys.destroy', ['id' => 'nonexistent'])) ->assertStatus(403); } + + #[Test] + public function it_returns_404_when_frontend_authentication_is_disabled(): void + { + config(['statamic.users.frontend_auth_enabled' => false]); + + $this->delete('/!/auth/passkeys/id')->assertNotFound(); + } } diff --git a/tests/Tags/User/DisableTwoFactorFormTest.php b/tests/Tags/User/DisableTwoFactorFormTest.php index 571b4be679b..81b2b86a172 100644 --- a/tests/Tags/User/DisableTwoFactorFormTest.php +++ b/tests/Tags/User/DisableTwoFactorFormTest.php @@ -207,4 +207,12 @@ private function userWithTwoFactorEnabled() return $user; } + + #[Test] + public function it_returns_404_when_frontend_authentication_is_disabled(): void + { + config(['statamic.users.frontend_auth_enabled' => false]); + + $this->delete('/!/auth/two-factor/disable')->assertNotFound(); + } } diff --git a/tests/Tags/User/ElevatedSessionFormTest.php b/tests/Tags/User/ElevatedSessionFormTest.php index c1eed6de8bb..ab190065bac 100644 --- a/tests/Tags/User/ElevatedSessionFormTest.php +++ b/tests/Tags/User/ElevatedSessionFormTest.php @@ -252,4 +252,15 @@ public function it_redirects_to_intended_url_after_confirmation() ]) ->assertRedirect('/intended-destination'); } + + #[Test] + public function it_returns_404_when_frontend_authentication_is_disabled(): void + { + config(['statamic.users.frontend_auth_enabled' => false]); + + $this->get('/!/auth/confirm-password')->assertNotFound(); + $this->post('/!/auth/elevated-session')->assertNotFound(); + $this->get('/!/auth/elevated-session/passkey-options')->assertNotFound(); + $this->get('/!/auth/elevated-session/resend-code')->assertNotFound(); + } } diff --git a/tests/Tags/User/ForgotPasswordFormTest.php b/tests/Tags/User/ForgotPasswordFormTest.php index cda33cf5f97..119ddc699b8 100644 --- a/tests/Tags/User/ForgotPasswordFormTest.php +++ b/tests/Tags/User/ForgotPasswordFormTest.php @@ -315,4 +315,12 @@ public function it_fetches_form_data() $this->assertArrayHasKey('_token', $form['params']); } + + #[Test] + public function it_returns_404_when_frontend_authentication_is_disabled(): void + { + config(['statamic.users.frontend_auth_enabled' => false]); + + $this->post('/!/auth/password/email')->assertNotFound(); + } } diff --git a/tests/Tags/User/LoginFormTest.php b/tests/Tags/User/LoginFormTest.php index 1b9a0fdcead..6a2d22f4ba1 100644 --- a/tests/Tags/User/LoginFormTest.php +++ b/tests/Tags/User/LoginFormTest.php @@ -720,4 +720,22 @@ public function it_fails_passkey_login_when_validation_fails() $this->assertGuest(); } + + #[Test] + public function it_returns_404_when_frontend_authentication_is_disabled(): void + { + User::make()->email('test@example.com')->password('secret')->save(); + + config(['statamic.users.frontend_auth_enabled' => false]); + + $this->post('/!/auth/login', [ + 'email' => 'test@example.com', + 'password' => 'secret', + ])->assertNotFound(); + + $this->get('/!/auth/passkeys/options')->assertNotFound(); + $this->post('/!/auth/passkeys/auth')->assertNotFound(); + + $this->assertGuest(); + } } diff --git a/tests/Tags/User/LogoutTest.php b/tests/Tags/User/LogoutTest.php index 2150dc432bc..fb959af2431 100644 --- a/tests/Tags/User/LogoutTest.php +++ b/tests/Tags/User/LogoutTest.php @@ -48,4 +48,16 @@ private function createUser() { return tap(User::make()->id('test-user')->email('test@example.com')->password('secret'))->save(); } + + #[Test] + public function disabled_frontend_authentication_does_not_log_out_authenticated_users(): void + { + $user = User::make()->email('test@example.com')->save(); + + config(['statamic.users.frontend_auth_enabled' => false]); + + $this->actingAs($user)->get('/!/auth/logout')->assertNotFound(); + + $this->assertAuthenticatedAs($user); + } } diff --git a/tests/Tags/User/PasskeyFormTest.php b/tests/Tags/User/PasskeyFormTest.php index 54877b97394..ffb57b96adb 100644 --- a/tests/Tags/User/PasskeyFormTest.php +++ b/tests/Tags/User/PasskeyFormTest.php @@ -152,4 +152,13 @@ public function it_fails_storing_when_validation_throws_exception() ]) ->assertStatus(500); } + + #[Test] + public function it_returns_404_when_frontend_authentication_is_disabled(): void + { + config(['statamic.users.frontend_auth_enabled' => false]); + + $this->get('/!/auth/passkeys/create')->assertNotFound(); + $this->post('/!/auth/passkeys')->assertNotFound(); + } } diff --git a/tests/Tags/User/PasswordFormTest.php b/tests/Tags/User/PasswordFormTest.php index 34701c8f518..926bf4ca0fb 100644 --- a/tests/Tags/User/PasswordFormTest.php +++ b/tests/Tags/User/PasswordFormTest.php @@ -351,4 +351,12 @@ public function it_will_delete_any_password_reset_tokens_when_updating_password( $this->assertFalse(Password::tokenExists($user, $token)); } + + #[Test] + public function it_returns_404_when_frontend_authentication_is_disabled(): void + { + config(['statamic.users.frontend_auth_enabled' => false]); + + $this->post('/!/auth/password')->assertNotFound(); + } } diff --git a/tests/Tags/User/ProfileFormTest.php b/tests/Tags/User/ProfileFormTest.php index 77ee951d77a..53143d77337 100644 --- a/tests/Tags/User/ProfileFormTest.php +++ b/tests/Tags/User/ProfileFormTest.php @@ -471,4 +471,12 @@ private function regex(): string { return '/(?:.+<\/h[23]>|