diff --git a/resources/js/pages/auth/TwoFactorChallenge.vue b/resources/js/pages/auth/TwoFactorChallenge.vue
new file mode 100644
index 00000000..5e8ca2cb
--- /dev/null
+++ b/resources/js/pages/auth/TwoFactorChallenge.vue
@@ -0,0 +1,97 @@
+
+
+
+
+
+
+
+
+
diff --git a/resources/js/pages/settings/TwoFactor.vue b/resources/js/pages/settings/TwoFactor.vue
new file mode 100644
index 00000000..099fd8d5
--- /dev/null
+++ b/resources/js/pages/settings/TwoFactor.vue
@@ -0,0 +1,92 @@
+
+
+
+
+
+
+
+
+
+
+
Disabled
+
+
+ When you enable two-factor authentication, you will be prompted for a secure pin during login. This pin can be retrieved from
+ a TOTP-supported application on your phone.
+
+
+
+
+
+
+
+
+
+
Enabled
+
+
+ With two-factor authentication enabled, you will be prompted for a secure, random pin during login, which you can retrieve
+ from the TOTP-supported application on your phone.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/routes/auth.php b/routes/auth.php
index 168a9f4b..191847eb 100644
--- a/routes/auth.php
+++ b/routes/auth.php
@@ -1,7 +1,6 @@
middleware('throttle:6,1')
->name('verification.send');
- Route::get('confirm-password', [ConfirmablePasswordController::class, 'show'])
- ->name('password.confirm');
-
- Route::post('confirm-password', [ConfirmablePasswordController::class, 'store'])
- ->middleware('throttle:6,1')
- ->name('password.confirm.store');
-
Route::post('logout', [AuthenticatedSessionController::class, 'destroy'])
->name('logout');
});
diff --git a/routes/settings.php b/routes/settings.php
index e4a3f794..3ab23c35 100644
--- a/routes/settings.php
+++ b/routes/settings.php
@@ -2,6 +2,7 @@
use App\Http\Controllers\Settings\PasswordController;
use App\Http\Controllers\Settings\ProfileController;
+use App\Http\Controllers\Settings\TwoFactorAuthenticationController;
use Illuminate\Support\Facades\Route;
use Inertia\Inertia;
@@ -21,4 +22,7 @@
Route::get('settings/appearance', function () {
return Inertia::render('settings/Appearance');
})->name('appearance');
+
+ Route::get('settings/two-factor', [TwoFactorAuthenticationController::class, 'show'])
+ ->name('two-factor.show');
});
diff --git a/tests/Feature/Auth/AuthenticationTest.php b/tests/Feature/Auth/AuthenticationTest.php
index 3f0c38f7..7c2a1236 100644
--- a/tests/Feature/Auth/AuthenticationTest.php
+++ b/tests/Feature/Auth/AuthenticationTest.php
@@ -5,6 +5,7 @@
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\RateLimiter;
+use Laravel\Fortify\Features;
use Tests\TestCase;
class AuthenticationTest extends TestCase
@@ -31,6 +32,35 @@ public function test_users_can_authenticate_using_the_login_screen()
$response->assertRedirect(route('dashboard', absolute: false));
}
+ public function test_users_with_two_factor_enabled_are_redirected_to_two_factor_challenge()
+ {
+ if (! Features::canManageTwoFactorAuthentication()) {
+ $this->markTestSkipped('Two-factor authentication is not enabled.');
+ }
+
+ Features::twoFactorAuthentication([
+ 'confirm' => true,
+ 'confirmPassword' => true,
+ ]);
+
+ $user = User::factory()->create();
+
+ $user->forceFill([
+ 'two_factor_secret' => encrypt('test-secret'),
+ 'two_factor_recovery_codes' => encrypt(json_encode(['code1', 'code2'])),
+ 'two_factor_confirmed_at' => now(),
+ ])->save();
+
+ $response = $this->post(route('login'), [
+ 'email' => $user->email,
+ 'password' => 'password',
+ ]);
+
+ $response->assertRedirect(route('two-factor.login'));
+ $response->assertSessionHas('login.id', $user->id);
+ $this->assertGuest();
+ }
+
public function test_users_can_not_authenticate_with_invalid_password()
{
$user = User::factory()->create();
diff --git a/tests/Feature/Auth/PasswordConfirmationTest.php b/tests/Feature/Auth/PasswordConfirmationTest.php
index 9ff9f005..ebc18d57 100644
--- a/tests/Feature/Auth/PasswordConfirmationTest.php
+++ b/tests/Feature/Auth/PasswordConfirmationTest.php
@@ -4,6 +4,7 @@
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
+use Inertia\Testing\AssertableInertia as Assert;
use Tests\TestCase;
class PasswordConfirmationTest extends TestCase
@@ -17,28 +18,16 @@ public function test_confirm_password_screen_can_be_rendered()
$response = $this->actingAs($user)->get(route('password.confirm'));
$response->assertStatus(200);
- }
-
- public function test_password_can_be_confirmed()
- {
- $user = User::factory()->create();
- $response = $this->actingAs($user)->post(route('password.confirm.store'), [
- 'password' => 'password',
- ]);
-
- $response->assertRedirect();
- $response->assertSessionHasNoErrors();
+ $response->assertInertia(fn (Assert $page) => $page
+ ->component('auth/ConfirmPassword')
+ );
}
- public function test_password_is_not_confirmed_with_invalid_password()
+ public function test_password_confirmation_requires_authentication()
{
- $user = User::factory()->create();
-
- $response = $this->actingAs($user)->post(route('password.confirm.store'), [
- 'password' => 'wrong-password',
- ]);
+ $response = $this->get(route('password.confirm'));
- $response->assertSessionHasErrors();
+ $response->assertRedirect(route('login'));
}
}
diff --git a/tests/Feature/Auth/TwoFactorChallengeTest.php b/tests/Feature/Auth/TwoFactorChallengeTest.php
new file mode 100644
index 00000000..2088f7b3
--- /dev/null
+++ b/tests/Feature/Auth/TwoFactorChallengeTest.php
@@ -0,0 +1,56 @@
+markTestSkipped('Two-factor authentication is not enabled.');
+ }
+
+ $response = $this->get(route('two-factor.login'));
+
+ $response->assertRedirect(route('login'));
+ }
+
+ public function test_two_factor_challenge_can_be_rendered(): void
+ {
+ if (! Features::canManageTwoFactorAuthentication()) {
+ $this->markTestSkipped('Two-factor authentication is not enabled.');
+ }
+
+ Features::twoFactorAuthentication([
+ 'confirm' => true,
+ 'confirmPassword' => true,
+ ]);
+
+ $user = User::factory()->create();
+
+ $user->forceFill([
+ 'two_factor_secret' => encrypt('test-secret'),
+ 'two_factor_recovery_codes' => encrypt(json_encode(['code1', 'code2'])),
+ 'two_factor_confirmed_at' => now(),
+ ])->save();
+
+ $this->post(route('login'), [
+ 'email' => $user->email,
+ 'password' => 'password',
+ ]);
+
+ $this->get(route('two-factor.login'))
+ ->assertOk()
+ ->assertInertia(fn (Assert $page) => $page
+ ->component('auth/TwoFactorChallenge')
+ );
+ }
+}
diff --git a/tests/Feature/Settings/TwoFactorAuthenticationTest.php b/tests/Feature/Settings/TwoFactorAuthenticationTest.php
new file mode 100644
index 00000000..7a5a6520
--- /dev/null
+++ b/tests/Feature/Settings/TwoFactorAuthenticationTest.php
@@ -0,0 +1,92 @@
+markTestSkipped('Two-factor authentication is not enabled.');
+ }
+
+ Features::twoFactorAuthentication([
+ 'confirm' => true,
+ 'confirmPassword' => true,
+ ]);
+
+ $user = User::factory()->create();
+
+ $this->actingAs($user)
+ ->withSession(['auth.password_confirmed_at' => time()])
+ ->get(route('two-factor.show'))
+ ->assertInertia(fn (Assert $page) => $page
+ ->component('settings/TwoFactor')
+ ->where('twoFactorEnabled', false)
+ );
+ }
+
+ public function test_two_factor_settings_page_requires_password_confirmation_when_enabled()
+ {
+ if (! Features::canManageTwoFactorAuthentication()) {
+ $this->markTestSkipped('Two-factor authentication is not enabled.');
+ }
+
+ $user = User::factory()->create();
+
+ Features::twoFactorAuthentication([
+ 'confirm' => true,
+ 'confirmPassword' => true,
+ ]);
+
+ $response = $this->actingAs($user)
+ ->get(route('two-factor.show'));
+
+ $response->assertRedirect(route('password.confirm'));
+ }
+
+ public function test_two_factor_settings_page_does_not_requires_password_confirmation_when_disabled()
+ {
+ if (! Features::canManageTwoFactorAuthentication()) {
+ $this->markTestSkipped('Two-factor authentication is not enabled.');
+ }
+
+ $user = User::factory()->create();
+
+ Features::twoFactorAuthentication([
+ 'confirm' => true,
+ 'confirmPassword' => false,
+ ]);
+
+ $this->actingAs($user)
+ ->get(route('two-factor.show'))
+ ->assertOk()
+ ->assertInertia(fn (Assert $page) => $page
+ ->component('settings/TwoFactor')
+ );
+ }
+
+ public function test_two_factor_settings_page_returns_forbidden_response_when_two_factor_is_disabled()
+ {
+ if (! Features::canManageTwoFactorAuthentication()) {
+ $this->markTestSkipped('Two-factor authentication is not enabled.');
+ }
+
+ config(['fortify.features' => []]);
+
+ $user = User::factory()->create();
+
+ $this->actingAs($user)
+ ->withSession(['auth.password_confirmed_at' => time()])
+ ->get(route('two-factor.show'))
+ ->assertForbidden();
+ }
+}