Files
timebank-cc-public/tests/Feature/Security/Authentication/MultiGuardAuthenticationTest.php
Ronald Huynen 2547717edb Initial commit
2026-03-23 21:37:59 +01:00

379 lines
12 KiB
PHP

<?php
namespace Tests\Feature\Security\Authentication;
use App\Models\Admin;
use App\Models\Bank;
use App\Models\Organization;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use Tests\TestCase;
/**
* Multi-Guard Authentication Security Tests
*
* Tests the 4-guard authentication system:
* - web: Individual user accounts
* - organization: Non-profit organization profiles
* - bank: Timebank operator profiles
* - admin: Administrative profiles
*
* @group security
* @group critical
* @group authentication
*/
class MultiGuardAuthenticationTest extends TestCase
{
use RefreshDatabase;
// ==========================================
// BASIC GUARD AUTHENTICATION TESTS
// ==========================================
/**
* Test that a user can authenticate on the web guard
*/
public function test_user_can_authenticate_on_web_guard()
{
$user = User::factory()->create([
'email' => 'user@test.com',
'password' => Hash::make('password123'),
]);
$this->withoutMiddleware(\App\Http\Middleware\VerifyCsrfToken::class);
$response = $this->post(route('login'), [
'name' => $user->email,
'password' => 'password123',
]);
// Assert authenticated on web guard
$this->assertTrue(Auth::guard('web')->check());
$this->assertEquals($user->id, Auth::guard('web')->id());
// Assert not authenticated on other guards
$this->assertFalse(Auth::guard('organization')->check());
$this->assertFalse(Auth::guard('bank')->check());
$this->assertFalse(Auth::guard('admin')->check());
}
/**
* Test that authentication fails with invalid credentials
*/
public function test_cannot_authenticate_with_invalid_credentials()
{
$user = User::factory()->create([
'email' => 'user@test.com',
'password' => Hash::make('correct-password'),
]);
$this->withoutMiddleware(\App\Http\Middleware\VerifyCsrfToken::class);
$this->post(route('login'), [
'name' => $user->email,
'password' => 'wrong-password',
]);
// Assert not authenticated on any guard
$this->assertFalse(Auth::guard('web')->check());
$this->assertFalse(Auth::guard('organization')->check());
$this->assertFalse(Auth::guard('bank')->check());
$this->assertFalse(Auth::guard('admin')->check());
}
/**
* Test that a user cannot authenticate on wrong guard
*/
public function test_cannot_authenticate_user_on_organization_guard()
{
$user = User::factory()->create([
'password' => Hash::make('password123'),
]);
// Attempt to manually authenticate user on organization guard (should not work)
$result = Auth::guard('organization')->attempt([
'email' => $user->email,
'password' => 'password123',
]);
$this->assertFalse($result);
$this->assertFalse(Auth::guard('organization')->check());
}
// ==========================================
// GUARD ISOLATION TESTS
// ==========================================
/**
* Test that web guard remains active when elevated guard is active
*/
public function test_web_guard_remains_active_with_elevated_guard()
{
$user = User::factory()->create();
$organization = Organization::factory()->create([
'password' => Hash::make('org-password'),
]);
// Link user to organization
$user->organizations()->attach($organization->id);
// Authenticate on web guard
Auth::guard('web')->login($user);
$this->assertTrue(Auth::guard('web')->check());
// Now authenticate on organization guard
Auth::guard('organization')->login($organization);
session(['active_guard' => 'organization']);
// Both guards should be active
$this->assertTrue(Auth::guard('web')->check(), 'Web guard should remain active');
$this->assertTrue(Auth::guard('organization')->check(), 'Organization guard should be active');
$this->assertEquals('organization', session('active_guard'));
}
/**
* Test that only one elevated guard can be active at a time
*/
public function test_only_one_elevated_guard_active_at_time()
{
$user = User::factory()->create();
$organization = Organization::factory()->create([
'password' => Hash::make('password'),
]);
$bank = Bank::factory()->create([
'password' => Hash::make('password'),
]);
// Link user to both profiles
$user->organizations()->attach($organization->id);
$user->banksManaged()->attach($bank->id);
// Authenticate on web guard first
Auth::guard('web')->login($user);
// Authenticate on organization guard
Auth::guard('organization')->login($organization);
session(['active_guard' => 'organization']);
$this->assertTrue(Auth::guard('organization')->check());
// Now use SwitchGuardTrait to switch to bank (should logout organization)
$controller = new class {
use \App\Traits\SwitchGuardTrait;
};
$controller->switchGuard('bank', $bank);
// Only bank guard should be active among elevated guards
$this->assertFalse(Auth::guard('organization')->check(), 'Organization guard should be logged out');
$this->assertTrue(Auth::guard('bank')->check(), 'Bank guard should be active');
$this->assertTrue(Auth::guard('web')->check(), 'Web guard should remain active');
$this->assertEquals('bank', session('active_guard'));
}
/**
* Test switching guard logs out other elevated guards
*/
public function test_switching_guard_logs_out_other_elevated_guards()
{
$user = User::factory()->create();
$organization = Organization::factory()->create(['password' => Hash::make('password')]);
$bank = Bank::factory()->create(['password' => Hash::make('password')]);
$admin = Admin::factory()->create(['password' => Hash::make('password')]);
// Link user to all profiles
$user->organizations()->attach($organization->id);
$user->banksManaged()->attach($bank->id);
$user->admins()->attach($admin->id);
Auth::guard('web')->login($user);
// Use SwitchGuardTrait
$controller = new class {
use \App\Traits\SwitchGuardTrait;
};
// Switch to organization
$controller->switchGuard('organization', $organization);
$this->assertTrue(Auth::guard('organization')->check());
$this->assertFalse(Auth::guard('bank')->check());
$this->assertFalse(Auth::guard('admin')->check());
// Switch to bank
$controller->switchGuard('bank', $bank);
$this->assertFalse(Auth::guard('organization')->check(), 'Organization should be logged out');
$this->assertTrue(Auth::guard('bank')->check());
$this->assertFalse(Auth::guard('admin')->check());
// Switch to admin
$controller->switchGuard('admin', $admin);
$this->assertFalse(Auth::guard('organization')->check());
$this->assertFalse(Auth::guard('bank')->check(), 'Bank should be logged out');
$this->assertTrue(Auth::guard('admin')->check());
}
// ==========================================
// SESSION STATE MANAGEMENT TESTS
// ==========================================
/**
* Test that active guard is stored in session
*/
public function test_active_guard_stored_in_session()
{
$user = User::factory()->create();
$organization = Organization::factory()->create(['password' => Hash::make('password')]);
$user->organizations()->attach($organization->id);
Auth::guard('web')->login($user);
$controller = new class {
use \App\Traits\SwitchGuardTrait;
};
$controller->switchGuard('organization', $organization);
$this->assertEquals('organization', session('active_guard'));
}
/**
* Test that logging out non-web guards sets active guard to web
*/
public function test_logout_non_web_guards_sets_active_guard_to_web()
{
$user = User::factory()->create();
$organization = Organization::factory()->create(['password' => Hash::make('password')]);
$user->organizations()->attach($organization->id);
Auth::guard('web')->login($user);
$controller = new class {
use \App\Traits\SwitchGuardTrait;
};
$controller->switchGuard('organization', $organization);
$this->assertEquals('organization', session('active_guard'));
// Logout from non-web guards
$controller->logoutNonWebGuards();
$this->assertEquals('web', session('active_guard'));
$this->assertFalse(Auth::guard('organization')->check());
$this->assertTrue(Auth::guard('web')->check());
}
// ==========================================
// AUTHENTICATION EDGE CASES
// ==========================================
/**
* Test that guest cannot access authenticated routes
*/
public function test_guest_cannot_access_authenticated_routes()
{
$response = $this->get(route('main'));
$response->assertRedirect();
}
/**
* Test that authenticated user can access web guard routes
*/
public function test_authenticated_user_can_access_web_guard_routes()
{
$user = User::factory()->create();
$this->actingAs($user, 'web');
$response = $this->get(route('main'));
$response->assertStatus(200);
}
/**
* Test authentication persists across requests
*/
public function test_authentication_persists_across_requests()
{
$user = User::factory()->create([
'email' => 'user@test.com',
'password' => Hash::make('password123'),
]);
$this->withoutMiddleware(\App\Http\Middleware\VerifyCsrfToken::class);
// First request - login
$this->post(route('login'), [
'name' => $user->email,
'password' => 'password123',
]);
$this->assertTrue(Auth::guard('web')->check());
// Second request - should still be authenticated
$response = $this->get(route('main'));
$this->assertTrue(Auth::guard('web')->check());
$response->assertStatus(200);
}
/**
* Test that logging out clears authentication
*/
public function test_logging_out_clears_authentication()
{
$user = User::factory()->create();
$this->actingAs($user, 'web');
$this->assertTrue(Auth::guard('web')->check());
Auth::guard('web')->logout();
$this->assertFalse(Auth::guard('web')->check());
}
// ==========================================
// CYCLOS LEGACY PASSWORD MIGRATION TESTS
// ==========================================
/**
* Test that legacy Cyclos passwords are migrated on successful login
*/
public function test_cyclos_password_migrated_on_successful_organization_login()
{
$user = User::factory()->create();
$organization = Organization::factory()->create([
'cyclos_salt' => 'legacy_salt',
'password' => strtolower(hash('sha256', 'legacy_salt' . 'old-password')),
]);
$user->organizations()->attach($organization->id);
// Store intent in session
session([
'intended_profile_switch_type' => 'Organization',
'intended_profile_switch_id' => $organization->id,
]);
$this->actingAs($user, 'web');
$this->withoutMiddleware(\App\Http\Middleware\VerifyCsrfToken::class);
// Attempt login with old password
$response = $this->post(route('organization.login.post'), [
'password' => 'old-password',
]);
// Refresh organization from database
$organization->refresh();
// Assert password was rehashed and salt removed
$this->assertNull($organization->cyclos_salt, 'Cyclos salt should be removed');
$this->assertTrue(Hash::check('old-password', $organization->password), 'Password should be rehashed with Laravel Hash');
// Assert authentication succeeded
$this->assertTrue(Auth::guard('organization')->check());
}
}