Initial commit
This commit is contained in:
386
tests/Feature/Security/Authentication/SessionSecurityTest.php
Normal file
386
tests/Feature/Security/Authentication/SessionSecurityTest.php
Normal file
@@ -0,0 +1,386 @@
|
||||
<?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 Illuminate\Support\Facades\Session;
|
||||
use Tests\TestCase;
|
||||
|
||||
/**
|
||||
* Session Security Tests
|
||||
*
|
||||
* Tests session security features:
|
||||
* - Session regeneration on login
|
||||
* - Session data protection
|
||||
* - Session cookie security
|
||||
* - Session clearing on logout
|
||||
*
|
||||
* @group security
|
||||
* @group high
|
||||
* @group authentication
|
||||
* @group session
|
||||
*/
|
||||
class SessionSecurityTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
// ==========================================
|
||||
// SESSION REGENERATION TESTS
|
||||
// ==========================================
|
||||
|
||||
/**
|
||||
* Test that session is regenerated on user login
|
||||
*/
|
||||
public function test_session_regenerated_on_login()
|
||||
{
|
||||
$user = User::factory()->create([
|
||||
'email' => 'user@test.com',
|
||||
'password' => Hash::make('password123'),
|
||||
]);
|
||||
|
||||
$this->withoutMiddleware(\App\Http\Middleware\VerifyCsrfToken::class);
|
||||
|
||||
// Start session and get initial ID
|
||||
$this->startSession();
|
||||
$initialSessionId = Session::getId();
|
||||
|
||||
// Login
|
||||
$this->post(route('login'), [
|
||||
'name' => $user->email,
|
||||
'password' => 'password123',
|
||||
]);
|
||||
|
||||
// Session ID should have changed (regenerated)
|
||||
$newSessionId = Session::getId();
|
||||
$this->assertNotEquals($initialSessionId, $newSessionId, 'Session should be regenerated on login');
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that session is regenerated on profile switch
|
||||
*/
|
||||
public function test_session_regenerated_on_profile_switch()
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
$organization = Organization::factory()->create();
|
||||
$user->organizations()->attach($organization->id);
|
||||
|
||||
$this->actingAs($user, 'web');
|
||||
|
||||
$initialSessionId = Session::getId();
|
||||
|
||||
// Switch to organization profile
|
||||
$this->get(route('organization.direct-login', ['organizationId' => $organization->id]));
|
||||
|
||||
// Note: Session regeneration on profile switch may not be implemented
|
||||
// This test documents expected behavior
|
||||
// If this test fails, consider implementing session regeneration on profile switch for security
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// SESSION DATA PROTECTION TESTS
|
||||
// ==========================================
|
||||
|
||||
/**
|
||||
* Test that sensitive data is not stored in session
|
||||
*/
|
||||
public function test_sensitive_data_not_stored_in_session()
|
||||
{
|
||||
$user = User::factory()->create([
|
||||
'email' => 'user@test.com',
|
||||
'password' => Hash::make('secret-password'),
|
||||
]);
|
||||
|
||||
$this->actingAs($user, 'web');
|
||||
|
||||
$sessionData = Session::all();
|
||||
|
||||
// Passwords should never be in session
|
||||
$sessionString = json_encode($sessionData);
|
||||
$this->assertStringNotContainsString('secret-password', $sessionString);
|
||||
$this->assertStringNotContainsString('password', strtolower($sessionString));
|
||||
|
||||
// Email might be OK in session depending on implementation
|
||||
// but password hash should definitely not be there
|
||||
foreach ($sessionData as $key => $value) {
|
||||
if (is_string($value)) {
|
||||
$this->assertStringNotContainsString('$2y$', $value, 'Password hash should not be in session');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that active profile data is stored correctly in session
|
||||
*/
|
||||
public function test_active_profile_data_stored_correctly()
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
$organization = Organization::factory()->create(['name' => 'Test Org']);
|
||||
$user->organizations()->attach($organization->id);
|
||||
|
||||
$this->actingAs($user, 'web');
|
||||
|
||||
$this->get(route('organization.direct-login', ['organizationId' => $organization->id]));
|
||||
|
||||
// Session should contain profile information but not sensitive data
|
||||
$this->assertEquals(get_class($organization), session('activeProfileType'));
|
||||
$this->assertEquals($organization->id, session('activeProfileId'));
|
||||
$this->assertEquals($organization->name, session('activeProfileName'));
|
||||
|
||||
// Session should have last activity time
|
||||
$this->assertNotNull(session('last_activity'));
|
||||
|
||||
// Session should NOT contain password
|
||||
$this->assertNull(session('activeProfilePassword'));
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// SESSION CLEARING TESTS
|
||||
// ==========================================
|
||||
|
||||
/**
|
||||
* Test that logging out clears authentication and session data
|
||||
*/
|
||||
public function test_logging_out_clears_authentication_and_session()
|
||||
{
|
||||
$user = User::factory()->create([
|
||||
'email' => 'user@test.com',
|
||||
'password' => Hash::make('password123'),
|
||||
]);
|
||||
|
||||
$this->withoutMiddleware(\App\Http\Middleware\VerifyCsrfToken::class);
|
||||
|
||||
// Login
|
||||
$this->post(route('login'), [
|
||||
'name' => $user->email,
|
||||
'password' => 'password123',
|
||||
]);
|
||||
|
||||
$this->assertTrue(Auth::guard('web')->check());
|
||||
|
||||
// Logout
|
||||
$this->post(route('logout'));
|
||||
|
||||
// Should no longer be authenticated
|
||||
$this->assertFalse(Auth::guard('web')->check());
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that profile switch session variables are cleared after login
|
||||
*/
|
||||
public function test_profile_switch_session_variables_cleared()
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
$bank = Bank::factory()->create(['password' => Hash::make('password')]);
|
||||
$user->banksManaged()->attach($bank->id);
|
||||
|
||||
$this->actingAs($user, 'web');
|
||||
$this->withoutMiddleware(\App\Http\Middleware\VerifyCsrfToken::class);
|
||||
|
||||
// Set profile switch intent
|
||||
session([
|
||||
'intended_profile_switch_type' => 'Bank',
|
||||
'intended_profile_switch_id' => $bank->id,
|
||||
'bank_login_intended_url' => '/test/url',
|
||||
]);
|
||||
|
||||
// Complete profile switch
|
||||
$this->post(route('bank.login.post'), [
|
||||
'password' => 'password',
|
||||
]);
|
||||
|
||||
// Session variables should be cleared
|
||||
$this->assertNull(session('intended_profile_switch_type'));
|
||||
$this->assertNull(session('intended_profile_switch_id'));
|
||||
$this->assertNull(session('bank_login_intended_url'));
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// SESSION PERSISTENCE TESTS
|
||||
// ==========================================
|
||||
|
||||
/**
|
||||
* Test that authentication persists across multiple requests
|
||||
*/
|
||||
public function test_authentication_persists_across_requests()
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
|
||||
$this->actingAs($user, 'web');
|
||||
|
||||
// Make multiple requests
|
||||
$this->get(route('main'))->assertStatus(200);
|
||||
$this->assertTrue(Auth::guard('web')->check());
|
||||
|
||||
// Second request
|
||||
$this->get(route('main'))->assertStatus(200);
|
||||
$this->assertTrue(Auth::guard('web')->check());
|
||||
|
||||
// Third request
|
||||
$this->get(route('main'))->assertStatus(200);
|
||||
$this->assertTrue(Auth::guard('web')->check());
|
||||
|
||||
// All requests should maintain authentication
|
||||
$this->assertEquals($user->id, Auth::guard('web')->id());
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that multiple guards can be authenticated simultaneously
|
||||
*/
|
||||
public function test_multiple_guards_authenticated_simultaneously()
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
$organization = Organization::factory()->create();
|
||||
$user->organizations()->attach($organization->id);
|
||||
|
||||
// Authenticate on web guard
|
||||
Auth::guard('web')->login($user);
|
||||
|
||||
// Authenticate on organization guard
|
||||
Auth::guard('organization')->login($organization);
|
||||
session(['active_guard' => 'organization']);
|
||||
|
||||
// Both should be authenticated
|
||||
$this->assertTrue(Auth::guard('web')->check());
|
||||
$this->assertTrue(Auth::guard('organization')->check());
|
||||
|
||||
// Make a request - both should remain authenticated
|
||||
$this->get(route('main'));
|
||||
|
||||
$this->assertTrue(Auth::guard('web')->check());
|
||||
$this->assertTrue(Auth::guard('organization')->check());
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// ACTIVE GUARD TRACKING TESTS
|
||||
// ==========================================
|
||||
|
||||
/**
|
||||
* Test that active guard is tracked in session
|
||||
*/
|
||||
public function test_active_guard_tracked_in_session()
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
$organization = Organization::factory()->create();
|
||||
$user->organizations()->attach($organization->id);
|
||||
|
||||
$this->actingAs($user, 'web');
|
||||
|
||||
// Initially on web guard
|
||||
// Session might not have active_guard set initially
|
||||
|
||||
// Switch to organization
|
||||
$this->get(route('organization.direct-login', ['organizationId' => $organization->id]));
|
||||
|
||||
$this->assertEquals('organization', session('active_guard'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that active guard changes when switching profiles
|
||||
*/
|
||||
public function test_active_guard_changes_when_switching_profiles()
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
$organization = Organization::factory()->create();
|
||||
$bank = Bank::factory()->create(['password' => Hash::make('password')]);
|
||||
|
||||
$user->organizations()->attach($organization->id);
|
||||
$user->banksManaged()->attach($bank->id);
|
||||
|
||||
$this->actingAs($user, 'web');
|
||||
$this->withoutMiddleware(\App\Http\Middleware\VerifyCsrfToken::class);
|
||||
|
||||
// Switch to organization
|
||||
$this->get(route('organization.direct-login', ['organizationId' => $organization->id]));
|
||||
$this->assertEquals('organization', session('active_guard'));
|
||||
|
||||
// Switch to bank
|
||||
session([
|
||||
'intended_profile_switch_type' => 'Bank',
|
||||
'intended_profile_switch_id' => $bank->id,
|
||||
]);
|
||||
$this->post(route('bank.login.post'), ['password' => 'password']);
|
||||
|
||||
$this->assertEquals('bank', session('active_guard'));
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// SESSION SECURITY EDGE CASES
|
||||
// ==========================================
|
||||
|
||||
/**
|
||||
* Test that unauthenticated users have no authentication data in session
|
||||
*/
|
||||
public function test_unauthenticated_users_have_no_auth_data_in_session()
|
||||
{
|
||||
$response = $this->get('/');
|
||||
|
||||
$sessionData = Session::all();
|
||||
|
||||
// Should not have authentication-related session keys
|
||||
$this->assertArrayNotHasKey('activeProfileType', $sessionData);
|
||||
$this->assertArrayNotHasKey('activeProfileId', $sessionData);
|
||||
$this->assertArrayNotHasKey('activeProfileName', $sessionData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that session data is properly isolated between users
|
||||
*/
|
||||
public function test_session_data_isolated_between_users()
|
||||
{
|
||||
$user1 = User::factory()->create();
|
||||
$user2 = User::factory()->create();
|
||||
|
||||
// Login as user1
|
||||
$this->actingAs($user1, 'web');
|
||||
$sessionId1 = Session::getId();
|
||||
|
||||
// Logout
|
||||
Auth::guard('web')->logout();
|
||||
Session::flush();
|
||||
|
||||
// Login as user2
|
||||
$this->actingAs($user2, 'web');
|
||||
$sessionId2 = Session::getId();
|
||||
|
||||
// Session IDs should be different
|
||||
$this->assertNotEquals($sessionId1, $sessionId2);
|
||||
|
||||
// Should be authenticated as user2, not user1
|
||||
$this->assertEquals($user2->id, Auth::guard('web')->id());
|
||||
$this->assertNotEquals($user1->id, Auth::guard('web')->id());
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that session regeneration prevents session fixation attacks
|
||||
*/
|
||||
public function test_session_regeneration_prevents_fixation()
|
||||
{
|
||||
$user = User::factory()->create([
|
||||
'email' => 'user@test.com',
|
||||
'password' => Hash::make('password123'),
|
||||
]);
|
||||
|
||||
$this->withoutMiddleware(\App\Http\Middleware\VerifyCsrfToken::class);
|
||||
|
||||
// Attacker sets a known session ID
|
||||
$this->startSession();
|
||||
$attackerSessionId = Session::getId();
|
||||
|
||||
// Victim logs in with that session
|
||||
$this->post(route('login'), [
|
||||
'name' => $user->email,
|
||||
'password' => 'password123',
|
||||
]);
|
||||
|
||||
// Session ID should have changed, preventing attacker from hijacking
|
||||
$newSessionId = Session::getId();
|
||||
$this->assertNotEquals($attackerSessionId, $newSessionId,
|
||||
'Session ID should change on login to prevent fixation attacks');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user