Skip to content
Merged
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
6 changes: 5 additions & 1 deletion src/Http/Middleware/EnsureTwoFactorEnabled.php
Original file line number Diff line number Diff line change
Expand Up @@ -87,13 +87,17 @@ protected function shouldPassThrough(Request $request): bool
return true;
}

// trim() empties a root Nova path ("/"), so build the prefix with a
// trailing slash only when there is one β€” otherwise the patterns would
// gain a leading slash and never match Request::is()'s slash-less path.
$novaPath = trim(Nova::path(), '/');
$prefix = $novaPath === '' ? '' : "{$novaPath}/";

// Anti-lockout: never block Nova's own assets, logout, or the enrolment
// page, or an admin could brick themselves / break the SPA. Not
// configurable β€” a consumer editing except.* can never strip these.
if ($request->routeIs('nova.asset.*', 'nova.logout')
|| $request->is("{$novaPath}/user-security", "{$novaPath}/user-security/*")
|| $request->is("{$prefix}user-security", "{$prefix}user-security/*")
) {
return true;
}
Expand Down
29 changes: 29 additions & 0 deletions tests/Feature/MiddlewareTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,35 @@ function loginAdmin(bool $enrolled = false): User
get('/nova/user-security/confirm')->assertOk()->assertSee('confirm');
});

it('allow-lists the user-security subtree when the Nova path is root ("/")', function (): void {
// A root-mounted Nova (config nova.path = "/") makes Nova::path() return "/";
// the enrolment subtree must still be reachable so an un-enrolled admin can set 2FA up.
config(['nova.path' => '/']);

Route::middleware(['web', EnsureTwoFactorEnabled::class])->group(function (): void {
Route::get('/user-security', fn (): string => 'security')->name('nova.pages.user-security');
Route::get('/user-security/two-factor-authentication', fn (): string => 'enable');
});

loginAdmin(); // un-enrolled

get('/user-security', ['Accept' => 'text/html'])->assertOk()->assertSee('security');
get('/user-security/two-factor-authentication', ['Accept' => 'text/html'])->assertOk()->assertSee('enable');
});

it('still redirects an un-enrolled admin to user security when the Nova path is root ("/")', function (): void {
config(['nova.path' => '/']);

Route::middleware(['web', EnsureTwoFactorEnabled::class])->group(function (): void {
Route::get('/dashboard', fn (): string => 'dash');
Route::get('/user-security', fn (): string => 'security')->name('nova.pages.user-security');
});

loginAdmin();

get('/dashboard', ['Accept' => 'text/html'])->assertRedirect(route('nova.pages.user-security'));
});

it('never blocks the Fortify 2FA enrolment endpoints under user-security', function (string $path): void {
Route::middleware(['web', EnsureTwoFactorEnabled::class])->get($path, fn (): string => 'ok');
loginAdmin(); // un-enrolled β€” must still reach every enrolment endpoint
Expand Down
Loading