feat(2fa): onboarding no longer forces 2FA (only password rotation)

feat/v1-foundation
boban 2026-06-14 20:44:12 +02:00
parent 9a01bfedf7
commit 82d5644767
3 changed files with 53 additions and 14 deletions

View File

@ -7,8 +7,8 @@ use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
/**
* After login, force the security onboarding before the panel is reachable:
* 1) rotate the seeded password, then 2) enrol 2FA.
* After login, force the seeded-password rotation before the panel is reachable.
* 2FA is optional (offered in Settings), so it is NOT forced here.
*/
class EnsureSecurityOnboarded
{
@ -16,16 +16,8 @@ class EnsureSecurityOnboarded
{
$user = $request->user();
if ($user) {
$allowed = $request->routeIs('password.change', 'two-factor.setup', 'logout');
if ($user->must_change_password && ! $request->routeIs('password.change', 'logout')) {
return redirect()->route('password.change');
}
if (! $user->must_change_password && ! $user->hasTwoFactorEnabled() && ! $allowed) {
return redirect()->route('two-factor.setup');
}
if ($user && $user->must_change_password && ! $request->routeIs('password.change', 'logout')) {
return redirect()->route('password.change');
}
return $next($request);

View File

@ -32,8 +32,8 @@ class PasswordChange extends Component
'must_change_password' => false,
])->save();
// onboarding continues: the middleware sends an un-enrolled user to 2FA setup.
return $this->redirect(route('two-factor.setup'), navigate: true);
// 2FA is optional now — go straight to the panel; 2FA is offered in Settings.
return $this->redirect(route('dashboard'), navigate: true);
}
public function render()

View File

@ -0,0 +1,47 @@
<?php
namespace Tests\Feature;
use App\Livewire\Auth\PasswordChange;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Livewire\Livewire;
use Tests\TestCase;
class OptionalOnboardingTest extends TestCase
{
use RefreshDatabase;
public function test_rotated_user_without_2fa_reaches_the_dashboard(): void
{
$user = User::factory()->create(['must_change_password' => false]);
$this->actingAs($user)->get('/')->assertOk();
}
public function test_user_that_must_change_password_is_still_redirected(): void
{
$user = User::factory()->create(['must_change_password' => true]);
$this->actingAs($user)->get('/')->assertRedirect(route('password.change'));
}
public function test_password_change_lands_on_dashboard_not_2fa_setup(): void
{
$user = User::factory()->create(['must_change_password' => true, 'password' => bcrypt('old-Password-1')]);
Livewire::actingAs($user)->test(PasswordChange::class)
->set('current', 'old-Password-1')
->set('password', 'new-Password-123')
->set('password_confirmation', 'new-Password-123')
->call('update')
->assertRedirect(route('dashboard'));
}
public function test_broadcast_gate_passes_on_password_rotation_alone(): void
{
$user = User::factory()->create(['must_change_password' => false]);
$this->assertTrue($user->securityOnboarded());
}
}