379 lines
12 KiB
PHP
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());
|
|
}
|
|
}
|