Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions config/users.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion routes/web.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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 () {
Expand Down
43 changes: 43 additions & 0 deletions src/Auth/UserTags.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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'),
Expand Down Expand Up @@ -150,6 +153,8 @@ public function loginForm()
*/
public function registerForm()
{
$this->ensureFrontendAuthEnabled();

$data = $this->getFormSession('user.register');

$data['fields'] = $this->getRegistrationFields();
Expand Down Expand Up @@ -196,6 +201,8 @@ public function registerForm()
*/
public function profileForm()
{
$this->ensureFrontendAuthEnabled();

if (session()->has('status')) {
return $this->parse(['success' => true]);
}
Expand Down Expand Up @@ -248,6 +255,8 @@ public function profileForm()
*/
public function passwordForm()
{
$this->ensureFrontendAuthEnabled();

if (session()->has('status')) {
return $this->parse(['success' => true]);
}
Expand Down Expand Up @@ -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'),
Expand Down Expand Up @@ -361,6 +372,8 @@ public function passkeys()
*/
public function deletePasskeyForm()
{
$this->ensureFrontendAuthEnabled();

if (! $user = User::current()) {
return '';
}
Expand Down Expand Up @@ -414,6 +427,8 @@ public function deletePasskeyForm()
*/
public function logoutUrl()
{
$this->ensureFrontendAuthEnabled();

$queryParams = [];

if ($redirect = $this->params->get('redirect')) {
Expand All @@ -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)));
Expand All @@ -444,6 +461,8 @@ public function logout()
*/
public function forgotPasswordForm()
{
$this->ensureFrontendAuthEnabled();

$data = $this->getFormSession('user.forgot_password');

// Alias for backwards compatibility.
Expand Down Expand Up @@ -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]);
}
Expand Down Expand Up @@ -750,6 +771,8 @@ public function notIn()
*/
public function elevatedSessionForm()
{
$this->ensureFrontendAuthEnabled();

if (! ($user = User::current())) {
return;
}
Expand Down Expand Up @@ -817,6 +840,8 @@ public function twoFactorEnabled(): bool
*/
public function twoFactorChallengeForm()
{
$this->ensureFrontendAuthEnabled();

if (
! TwoFactor::enabled()
|| session()->missing('login.id')
Expand Down Expand Up @@ -868,6 +893,8 @@ public function twoFactorChallengeForm()
*/
public function twoFactorEnableForm()
{
$this->ensureFrontendAuthEnabled();

$user = User::current();

if (
Expand Down Expand Up @@ -918,6 +945,8 @@ public function twoFactorEnableForm()
*/
public function twoFactorSetupForm()
{
$this->ensureFrontendAuthEnabled();

$user = User::current();

if (
Expand Down Expand Up @@ -1000,6 +1029,8 @@ public function twoFactorRecoveryCodes()
*/
public function twoFactorRecoveryCodesDownloadUrl()
{
$this->ensureFrontendAuthEnabled();

$user = User::current();

if (
Expand All @@ -1021,6 +1052,8 @@ public function twoFactorRecoveryCodesDownloadUrl()
*/
public function resetTwoFactorRecoveryCodesForm()
{
$this->ensureFrontendAuthEnabled();

$user = User::current();

if (
Expand Down Expand Up @@ -1070,6 +1103,8 @@ public function resetTwoFactorRecoveryCodesForm()
*/
public function disableTwoFactorForm()
{
$this->ensureFrontendAuthEnabled();

$user = User::current();

if (
Expand Down Expand Up @@ -1284,4 +1319,12 @@ protected function getPasswordFields()
->values()
->all();
}

private function ensureFrontendAuthEnabled(): void
{
throw_unless(
config('statamic.users.frontend_auth_enabled', true),
new FrontendAuthenticationDisabledException
);
}
}
12 changes: 12 additions & 0 deletions src/Console/Commands/stubs/config/users.php.stub
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,18 @@ return [
'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
Expand Down
13 changes: 13 additions & 0 deletions src/Exceptions/FrontendAuthenticationDisabledException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php

namespace Statamic\Exceptions;

use Exception;

class FrontendAuthenticationDisabledException extends Exception
{
public function __construct()
{
parent::__construct('Front-end authentication is disabled. Enable it using the statamic.users.frontend_auth_enabled config option.');
}
}
21 changes: 21 additions & 0 deletions src/Http/Middleware/EnsureFrontendAuthEnabled.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php

namespace Statamic\Http\Middleware;

use Closure;
use Illuminate\Http\Request;
use Statamic\Exceptions\NotFoundHttpException;
use Symfony\Component\HttpFoundation\Response;

class EnsureFrontendAuthEnabled
{
public function handle(Request $request, Closure $next): Response
{
throw_unless(
config('statamic.users.frontend_auth_enabled', true),
new NotFoundHttpException
);

return $next($request);
}
}
4 changes: 4 additions & 0 deletions src/Providers/AppServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,10 @@ public function boot()
}

$this->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)
Expand Down
8 changes: 8 additions & 0 deletions tests/Tags/User/DeletePasskeyFormTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
}
8 changes: 8 additions & 0 deletions tests/Tags/User/DisableTwoFactorFormTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
}
11 changes: 11 additions & 0 deletions tests/Tags/User/ElevatedSessionFormTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
}
8 changes: 8 additions & 0 deletions tests/Tags/User/ForgotPasswordFormTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
}
18 changes: 18 additions & 0 deletions tests/Tags/User/LoginFormTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
}
12 changes: 12 additions & 0 deletions tests/Tags/User/LogoutTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
9 changes: 9 additions & 0 deletions tests/Tags/User/PasskeyFormTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
}
8 changes: 8 additions & 0 deletions tests/Tags/User/PasswordFormTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
}
Loading
Loading