feat: enterprise MFA with TOTP, email codes, backup codes, and trusted devices
Three verification methods (TOTP authenticator, email code, backup codes), trusted device management with 30-day expiry, role-based enforcement for super_admin and org_admin, admin reset capability, and full test coverage (46 tests). Modifies login flow to support MFA challenge/response with temporary session tokens stored in cache. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
93
api/tests/Feature/Auth/TrustedDeviceControllerTest.php
Normal file
93
api/tests/Feature/Auth/TrustedDeviceControllerTest.php
Normal file
@@ -0,0 +1,93 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Feature\Auth;
|
||||
|
||||
use App\Models\TrustedDevice;
|
||||
use App\Models\User;
|
||||
use App\Services\MfaService;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
class TrustedDeviceControllerTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_index_returns_active_devices(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
$mfaService = app(MfaService::class);
|
||||
|
||||
$mfaService->trustDevice($user, 'fp-1', '192.168.1.1', 'Chrome on macOS');
|
||||
$mfaService->trustDevice($user, 'fp-2', '192.168.1.2', 'Firefox on Windows');
|
||||
|
||||
$response = $this->actingAs($user)->getJson('/api/v1/auth/trusted-devices');
|
||||
|
||||
$response->assertOk();
|
||||
$this->assertCount(2, $response->json('data'));
|
||||
}
|
||||
|
||||
public function test_index_excludes_expired_devices(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
$mfaService = app(MfaService::class);
|
||||
|
||||
$device = $mfaService->trustDevice($user, 'fp-1', '192.168.1.1', 'Old Device');
|
||||
$device->update(['trusted_until' => now()->subDay()]);
|
||||
|
||||
$mfaService->trustDevice($user, 'fp-2', '192.168.1.2', 'Active Device');
|
||||
|
||||
$response = $this->actingAs($user)->getJson('/api/v1/auth/trusted-devices');
|
||||
|
||||
$response->assertOk();
|
||||
$this->assertCount(1, $response->json('data'));
|
||||
}
|
||||
|
||||
public function test_destroy_revokes_device(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
$device = app(MfaService::class)->trustDevice($user, 'fp-1', '192.168.1.1', 'Chrome');
|
||||
|
||||
$response = $this->actingAs($user)->deleteJson("/api/v1/auth/trusted-devices/{$device->id}");
|
||||
|
||||
$response->assertNoContent();
|
||||
$this->assertDatabaseMissing('trusted_devices', ['id' => $device->id]);
|
||||
}
|
||||
|
||||
public function test_destroy_all_revokes_all_devices(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
$mfaService = app(MfaService::class);
|
||||
|
||||
$mfaService->trustDevice($user, 'fp-1', '192.168.1.1', 'Device 1');
|
||||
$mfaService->trustDevice($user, 'fp-2', '192.168.1.2', 'Device 2');
|
||||
|
||||
$response = $this->actingAs($user)->deleteJson('/api/v1/auth/trusted-devices');
|
||||
|
||||
$response->assertNoContent();
|
||||
$this->assertDatabaseCount('trusted_devices', 0);
|
||||
}
|
||||
|
||||
public function test_cannot_manage_other_users_devices(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
$otherUser = User::factory()->create();
|
||||
|
||||
$device = app(MfaService::class)->trustDevice($otherUser, 'fp-1', '192.168.1.1', 'Other Device');
|
||||
|
||||
// Try to delete other user's device — should succeed silently (no record found for this user)
|
||||
$response = $this->actingAs($user)->deleteJson("/api/v1/auth/trusted-devices/{$device->id}");
|
||||
|
||||
$response->assertNoContent();
|
||||
|
||||
// Other user's device should still exist
|
||||
$this->assertDatabaseHas('trusted_devices', ['id' => $device->id]);
|
||||
}
|
||||
|
||||
public function test_unauthenticated_cannot_access_devices(): void
|
||||
{
|
||||
$this->getJson('/api/v1/auth/trusted-devices')
|
||||
->assertUnauthorized();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user