Initial commit
This commit is contained in:
@@ -0,0 +1,378 @@
|
||||
<?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());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user