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'); } }