Compare commits

...

9 Commits

16 changed files with 396 additions and 193 deletions

View File

@ -35,6 +35,10 @@ class Feedback {
'type' => self::TYPE_SUCCESS, 'type' => self::TYPE_SUCCESS,
'dismissible' => true 'dismissible' => true
], ],
'SESSION_TIMEOUT' => [
'type' => self::TYPE_ERROR,
'dismissible' => true
],
'IP_BLACKLISTED' => [ 'IP_BLACKLISTED' => [
'type' => self::TYPE_ERROR, 'type' => self::TYPE_ERROR,
'dismissible' => false 'dismissible' => false

View File

@ -0,0 +1,176 @@
<?php
/**
* Session Class
*
* Core session management functionality for the application
*/
class Session {
private static $sessionOptions = [
'cookie_httponly' => 1,
'cookie_secure' => 1,
'cookie_samesite' => 'Strict',
'gc_maxlifetime' => 7200 // 2 hours
];
/**
* Start or resume a session with secure options
*/
public static function startSession() {
session_name('jilo');
if (session_status() !== PHP_SESSION_ACTIVE && !headers_sent()) {
session_start(self::$sessionOptions);
}
}
/**
* Destroy current session and clean up
*/
public static function destroySession() {
if (session_status() === PHP_SESSION_ACTIVE) {
session_unset();
session_destroy();
}
}
/**
* Get current username if set
*/
public static function getUsername() {
return isset($_SESSION['username']) ? htmlspecialchars($_SESSION['username']) : null;
}
/**
* Get current user ID if set
*/
public static function getUserId() {
return isset($_SESSION['user_id']) ? (int)$_SESSION['user_id'] : null;
}
/**
* Check if current session is valid
*/
public static function isValidSession() {
// Check required session variables
if (!isset($_SESSION['user_id']) || !isset($_SESSION['username'])) {
return false;
}
// Check session timeout
$session_timeout = isset($_SESSION['REMEMBER_ME']) ? (30 * 24 * 60 * 60) : 7200; // 30 days or 2 hours
if (isset($_SESSION['LAST_ACTIVITY']) && (time() - $_SESSION['LAST_ACTIVITY'] > $session_timeout)) {
return false;
}
// Update last activity time
$_SESSION['LAST_ACTIVITY'] = time();
// Regenerate session ID periodically (every 30 minutes)
if (!isset($_SESSION['CREATED'])) {
$_SESSION['CREATED'] = time();
} else if (time() - $_SESSION['CREATED'] > 1800) {
// Regenerate session ID and update creation time
if (!headers_sent() && session_status() === PHP_SESSION_ACTIVE) {
$oldData = $_SESSION;
session_regenerate_id(true);
$_SESSION = $oldData;
$_SESSION['CREATED'] = time();
}
}
return true;
}
/**
* Set remember me option for extended session
*/
public static function setRememberMe($value = true) {
$_SESSION['REMEMBER_ME'] = $value;
}
/**
* Clear session data and cookies
*/
public static function cleanup($config) {
self::destroySession();
// Clear cookies if headers not sent
if (!headers_sent()) {
setcookie('username', '', [
'expires' => time() - 3600,
'path' => $config['folder'],
'domain' => $config['domain'],
'secure' => isset($_SERVER['HTTPS']),
'httponly' => true,
'samesite' => 'Strict'
]);
}
// Start fresh session
self::startSession();
}
/**
* Create a new authenticated session for a user
*/
public static function createAuthSession($userId, $username, $rememberMe, $config) {
// Set cookie lifetime based on remember me
$cookieLifetime = $rememberMe ? time() + (30 * 24 * 60 * 60) : 0;
// Regenerate session ID to prevent session fixation
session_regenerate_id(true);
// Set cookie with secure options
setcookie('username', $username, [
'expires' => $cookieLifetime,
'path' => $config['folder'],
'domain' => $config['domain'],
'secure' => isset($_SERVER['HTTPS']),
'httponly' => true,
'samesite' => 'Strict'
]);
// Set session variables
$_SESSION['user_id'] = $userId;
$_SESSION['username'] = $username;
$_SESSION['LAST_ACTIVITY'] = time();
if ($rememberMe) {
self::setRememberMe(true);
}
}
/**
* Store 2FA pending information in session
*/
public static function store2FAPending($userId, $username, $rememberMe = false) {
$_SESSION['2fa_pending_user_id'] = $userId;
$_SESSION['2fa_pending_username'] = $username;
if ($rememberMe) {
$_SESSION['2fa_pending_remember'] = true;
}
}
/**
* Clear 2FA pending information from session
*/
public static function clear2FAPending() {
unset($_SESSION['2fa_pending_user_id']);
unset($_SESSION['2fa_pending_username']);
unset($_SESSION['2fa_pending_remember']);
}
/**
* Get 2FA pending information
*/
public static function get2FAPending() {
if (!isset($_SESSION['2fa_pending_user_id']) || !isset($_SESSION['2fa_pending_username'])) {
return null;
}
return [
'user_id' => $_SESSION['2fa_pending_user_id'],
'username' => $_SESSION['2fa_pending_username'],
'remember_me' => isset($_SESSION['2fa_pending_remember'])
];
}
}

View File

@ -4,94 +4,29 @@
* Session Middleware * Session Middleware
* *
* Validates session status and handles session timeout. * Validates session status and handles session timeout.
* This middleware should be included in all protected pages. * If session is invalid, redirects to login page.
*/ */
function applySessionMiddleware($config, $app_root) { function applySessionMiddleware($config, $app_root, $isTest = false) {
$isTest = defined('PHPUNIT_RUNNING'); // Start session if not already started
if (session_status() !== PHP_SESSION_ACTIVE) {
Session::startSession();
}
// Access $_SESSION directly in test mode // Check session validity
if (!$isTest) { if (!Session::isValidSession()) {
// Start session if not already started // Session invalid, clean up and redirect
if (session_status() !== PHP_SESSION_ACTIVE && !headers_sent()) { Session::cleanup($config);
session_start([
'cookie_httponly' => 1, // Flash session timeout message
'cookie_secure' => 1, Feedback::flash('LOGIN', 'SESSION_TIMEOUT');
'cookie_samesite' => 'Strict',
'gc_maxlifetime' => 7200 // 2 hours if (!$isTest) {
]); header('Location: ' . $app_root . '?page=login');
exit();
} }
}
// Check if user is logged in with all required session variables
if (!isset($_SESSION['user_id']) || !isset($_SESSION['username'])) {
cleanupSession($config, $app_root, $isTest);
return false; return false;
} }
// Check session timeout
$session_timeout = isset($_SESSION['REMEMBER_ME']) ? (30 * 24 * 60 * 60) : 7200; // 30 days or 2 hours
if (isset($_SESSION['LAST_ACTIVITY']) && (time() - $_SESSION['LAST_ACTIVITY'] > $session_timeout)) {
// Session has expired
cleanupSession($config, $app_root, $isTest);
return false;
}
// Update last activity time
$_SESSION['LAST_ACTIVITY'] = time();
// Regenerate session ID periodically (every 30 minutes)
if (!isset($_SESSION['CREATED'])) {
$_SESSION['CREATED'] = time();
} else if (time() - $_SESSION['CREATED'] > 1800) {
// Regenerate session ID and update creation time
if (!$isTest && !headers_sent() && session_status() === PHP_SESSION_ACTIVE) {
$oldData = $_SESSION;
session_regenerate_id(true);
$_SESSION = $oldData;
$_SESSION['CREATED'] = time();
}
}
return true; return true;
} }
/**
* Helper function to clean up session data and redirect
*/
function cleanupSession($config, $app_root, $isTest) {
// Always clear session data
$_SESSION = array();
if (!$isTest) {
if (session_status() === PHP_SESSION_ACTIVE) {
session_unset();
session_destroy();
// Start a new session to prevent errors
if (!headers_sent()) {
session_start([
'cookie_httponly' => 1,
'cookie_secure' => 1,
'cookie_samesite' => 'Strict',
'gc_maxlifetime' => 7200
]);
}
}
// Clear cookies
if (!headers_sent()) {
setcookie('username', '', [
'expires' => time() - 3600,
'path' => $config['folder'],
'domain' => $config['domain'],
'secure' => isset($_SERVER['HTTPS']),
'httponly' => true,
'samesite' => 'Strict'
]);
}
header('Location: ' . $app_root . '?page=login&timeout=1');
exit();
}
}

View File

@ -11,6 +11,7 @@ return [
'LOGIN_SUCCESS' => 'Login successful.', 'LOGIN_SUCCESS' => 'Login successful.',
'LOGIN_FAILED' => 'Login failed. Please check your credentials.', 'LOGIN_FAILED' => 'Login failed. Please check your credentials.',
'LOGOUT_SUCCESS' => 'Logout successful. You can log in again.', 'LOGOUT_SUCCESS' => 'Logout successful. You can log in again.',
'SESSION_TIMEOUT' => 'Your session has expired. Please log in again.',
'IP_BLACKLISTED' => 'Access denied. Your IP address is blacklisted.', 'IP_BLACKLISTED' => 'Access denied. Your IP address is blacklisted.',
'IP_NOT_WHITELISTED' => 'Access denied. Your IP address is not whitelisted.', 'IP_NOT_WHITELISTED' => 'Access denied. Your IP address is not whitelisted.',
'TOO_MANY_ATTEMPTS' => 'Too many login attempts. Please try again later.', 'TOO_MANY_ATTEMPTS' => 'Too many login attempts. Please try again later.',

View File

@ -14,12 +14,6 @@
* - `password`: Change password * - `password`: Change password
*/ */
// Check if user is logged in
if (!isset($_SESSION['user_id'])) {
header("Location: $app_root?page=login");
exit();
}
$user_id = $_SESSION['user_id']; $user_id = $_SESSION['user_id'];
// Initialize user object // Initialize user object

View File

@ -33,21 +33,23 @@ try {
if ($action === 'verify' && isset($_SESSION['2fa_pending_user_id'])) { if ($action === 'verify' && isset($_SESSION['2fa_pending_user_id'])) {
// Handle 2FA verification // Handle 2FA verification
$code = $_POST['code'] ?? ''; $code = $_POST['code'] ?? '';
$userId = $_SESSION['2fa_pending_user_id']; $pending2FA = Session::get2FAPending();
$username = $_SESSION['2fa_pending_username'];
$rememberMe = isset($_SESSION['2fa_pending_remember']); if (!$pending2FA) {
header('Location: ' . htmlspecialchars($app_root) . '?page=login');
exit();
}
require_once '../app/classes/twoFactorAuth.php'; require_once '../app/classes/twoFactorAuth.php';
$twoFactorAuth = new TwoFactorAuthentication($db); $twoFactorAuth = new TwoFactorAuthentication($db);
if ($twoFactorAuth->verify($userId, $code)) { if ($twoFactorAuth->verify($pending2FA['user_id'], $code)) {
// Complete login // Complete login
handleSuccessfulLogin($userId, $username, $rememberMe, $config, $logObject, $user_IP); handleSuccessfulLogin($pending2FA['user_id'], $pending2FA['username'],
$pending2FA['remember_me'], $config, $logObject, $user_IP);
// Clean up 2FA session data // Clean up 2FA session data
unset($_SESSION['2fa_pending_user_id']); Session::clear2FAPending();
unset($_SESSION['2fa_pending_username']);
unset($_SESSION['2fa_pending_remember']);
exit(); exit();
} }
@ -232,11 +234,8 @@ try {
switch ($loginResult['status']) { switch ($loginResult['status']) {
case 'requires_2fa': case 'requires_2fa':
// Store pending 2FA info // Store pending 2FA info
$_SESSION['2fa_pending_user_id'] = $loginResult['user_id']; Session::store2FAPending($loginResult['user_id'], $loginResult['username'],
$_SESSION['2fa_pending_username'] = $loginResult['username']; isset($formData['remember_me']));
if (isset($formData['remember_me'])) {
$_SESSION['2fa_pending_remember'] = true;
}
// Redirect to 2FA verification // Redirect to 2FA verification
header('Location: ?page=login&action=verify'); header('Location: ?page=login&action=verify');
@ -282,36 +281,8 @@ include '../app/templates/form-login.php';
* Handle successful login by setting up session and cookies * Handle successful login by setting up session and cookies
*/ */
function handleSuccessfulLogin($userId, $username, $rememberMe, $config, $logObject, $userIP) { function handleSuccessfulLogin($userId, $username, $rememberMe, $config, $logObject, $userIP) {
if ($rememberMe) { // Create authenticated session
// 30*24*60*60 = 30 days Session::createAuthSession($userId, $username, $rememberMe, $config);
$cookie_lifetime = 30 * 24 * 60 * 60;
$setcookie_lifetime = time() + 30 * 24 * 60 * 60;
} else {
// 0 - session end on browser close
$cookie_lifetime = 0;
$setcookie_lifetime = 0;
}
// Regenerate session ID to prevent session fixation
session_regenerate_id(true);
// set session lifetime and cookies
setcookie('username', $username, [
'expires' => $setcookie_lifetime,
'path' => $config['folder'],
'domain' => $config['domain'],
'secure' => isset($_SERVER['HTTPS']),
'httponly' => true,
'samesite' => 'Strict'
]);
// Set session variables
$_SESSION['user_id'] = $userId;
$_SESSION['USERNAME'] = $username;
$_SESSION['LAST_ACTIVITY'] = time();
if ($rememberMe) {
$_SESSION['REMEMBER_ME'] = true;
}
// Log successful login // Log successful login
$logObject->insertLog($userId, "Login: User \"$username\" logged in. IP: $userIP", 'user'); $logObject->insertLog($userId, "Login: User \"$username\" logged in. IP: $userIP", 'user');

View File

@ -9,11 +9,6 @@ if (!($userObject->hasRight($user_id, 'superuser') ||
exit; exit;
} }
if (!isset($currentUser)) {
include '../app/templates/error-unauthorized.php';
exit;
}
// Get current section // Get current section
$section = isset($_POST['section']) ? $_POST['section'] : (isset($_GET['section']) ? $_GET['section'] : 'whitelist'); $section = isset($_POST['section']) ? $_POST['section'] : (isset($_GET['section']) ? $_GET['section'] : 'whitelist');

View File

@ -12,7 +12,7 @@
</div> </div>
<?php if (isset($currentUser) && $page !== 'logout') { ?> <?php if (Session::getUsername() && $page !== 'logout') { ?>
<script src="static/js/sidebar.js"></script> <script src="static/js/sidebar.js"></script>
<?php } ?> <?php } ?>

View File

@ -10,7 +10,7 @@
<link rel="stylesheet" type="text/css" href="<?= htmlspecialchars($app_root) ?>static/css/main.css"> <link rel="stylesheet" type="text/css" href="<?= htmlspecialchars($app_root) ?>static/css/main.css">
<link rel="stylesheet" type="text/css" href="<?= htmlspecialchars($app_root) ?>static/css/messages.css"> <link rel="stylesheet" type="text/css" href="<?= htmlspecialchars($app_root) ?>static/css/messages.css">
<script src="<?= htmlspecialchars($app_root) ?>static/js/messages.js"></script> <script src="<?= htmlspecialchars($app_root) ?>static/js/messages.js"></script>
<?php if (isset($currentUser)) { ?> <?php if (Session::getUsername()) { ?>
<script> <script>
// restore sidebar state before the page is rendered // restore sidebar state before the page is rendered
(function () { (function () {

View File

@ -18,7 +18,7 @@
version&nbsp;<?= htmlspecialchars($config['version']) ?> version&nbsp;<?= htmlspecialchars($config['version']) ?>
</li> </li>
<?php if (isset($_SESSION['username']) && isset($_SESSION['user_id'])) { ?> <?php if (Session::isValidSession()) { ?>
<?php foreach ($platformsAll as $platform) { <?php foreach ($platformsAll as $platform) {
$platform_switch_url = switchPlatform($platform['id']); $platform_switch_url = switchPlatform($platform['id']);
@ -40,7 +40,7 @@
</ul> </ul>
<ul class="menu-right"> <ul class="menu-right">
<?php if (isset($_SESSION['username']) && isset($_SESSION['user_id'])) { ?> <?php if (Session::isValidSession()) { ?>
<li class="dropdown"> <li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-expanded="false"> <a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-expanded="false">
<i class="fas fa-user"></i> <i class="fas fa-user"></i>

View File

@ -6,11 +6,14 @@
* $totalPages - Total number of pages * $totalPages - Total number of pages
*/ */
// Ensure required variables are set // Validate required pagination variables
if (!isset($currentPage) || !isset($totalPages)) { if (!isset($currentPage) || !isset($totalPages)) {
return; return;
} }
// Ensure valid values
$currentPage = max(1, min($currentPage, $totalPages));
// Number of page links to show before and after current page // Number of page links to show before and after current page
$range = 2; $range = 2;
?> ?>

View File

@ -15,14 +15,18 @@
// flush it later only when there is no redirect // flush it later only when there is no redirect
ob_start(); ob_start();
// Start session before any session-dependent code
require_once '../app/classes/session.php';
Session::startSession();
// Apply security headers // Apply security headers
require_once '../app/includes/security_headers_middleware.php'; require_once '../app/includes/security_headers_middleware.php';
// sanitize all input vars that may end up in URLs or forms // sanitize all input vars that may end up in URLs or forms
require '../app/includes/sanitize.php'; require '../app/includes/sanitize.php';
session_name('jilo'); // Check session validity
session_start(); $validSession = Session::isValidSession();
// Initialize feedback message system // Initialize feedback message system
require_once '../app/classes/feedback.php'; require_once '../app/classes/feedback.php';
@ -64,6 +68,8 @@ $allowed_urls = [
'login', 'login',
'logout', 'logout',
'register', 'register',
'about',
]; ];
// cnfig file // cnfig file
@ -92,17 +98,26 @@ if ($config_file) {
$app_root = $config['folder']; $app_root = $config['folder'];
// check if logged in // List of pages that don't require authentication
unset($currentUser); $public_pages = ['login', 'register', 'help', 'about'];
if (isset($_COOKIE['username'])) {
if ( !isset($_SESSION['username']) ) { // Check if the requested page requires authentication
$_SESSION['username'] = $_COOKIE['username']; if (!isset($_COOKIE['username']) && !$validSession && !in_array($page, $public_pages)) {
} require_once '../app/includes/session_middleware.php';
$currentUser = htmlspecialchars($_SESSION['username']); applySessionMiddleware($config, $app_root);
} }
// redirect to login // Check session and redirect if needed
if ( !isset($_COOKIE['username']) && ($page !== 'login' && $page !== 'register') ) { $currentUser = null;
if ($validSession) {
$currentUser = Session::getUsername();
} else if (isset($_COOKIE['username']) && !in_array($page, $public_pages)) {
// Cookie exists but session is invalid - redirect to login
Feedback::flash('LOGIN', 'SESSION_TIMEOUT');
header('Location: ' . htmlspecialchars($app_root) . '?page=login&timeout=1');
exit();
} else if (!in_array($page, $public_pages)) {
// No valid session or cookie, and not a public page
header('Location: ' . htmlspecialchars($app_root) . '?page=login'); header('Location: ' . htmlspecialchars($app_root) . '?page=login');
exit(); exit();
} }
@ -164,11 +179,10 @@ if ($page == 'logout') {
$user_id = $userObject->getUserId($currentUser)[0]['id']; $user_id = $userObject->getUserId($currentUser)[0]['id'];
// clean up session // clean up session
session_unset(); Session::destroySession();
session_destroy();
// start new session for the login page // start new session for the login page
session_start(); Session::startSession();
setcookie('username', "", time() - 100, $config['folder'], $config['domain'], isset($_SERVER['HTTPS']), true); setcookie('username', "", time() - 100, $config['folder'], $config['domain'], isset($_SERVER['HTTPS']), true);
@ -186,8 +200,7 @@ if ($page == 'logout') {
} else { } else {
// if user is logged in, we need user details and rights // if user is logged in, we need user details and rights
if (isset($currentUser)) { if ($validSession) {
// If by error a logged in user requests the login page // If by error a logged in user requests the login page
if ($page === 'login') { if ($page === 'login') {
header('Location: ' . htmlspecialchars($app_root)); header('Location: ' . htmlspecialchars($app_root));
@ -211,18 +224,10 @@ if ($page == 'logout') {
} }
} }
// List of pages that don't require authentication
$public_pages = ['login', 'register'];
// Check if the requested page requires authentication
if (!in_array($page, $public_pages)) {
require_once '../app/includes/session_middleware.php';
}
// page building // page building
include '../app/templates/page-header.php'; include '../app/templates/page-header.php';
include '../app/templates/page-menu.php'; include '../app/templates/page-menu.php';
if (isset($currentUser)) { if ($validSession) {
include '../app/templates/page-sidebar.php'; include '../app/templates/page-sidebar.php';
} }
if (in_array($page, $allowed_urls)) { if (in_array($page, $allowed_urls)) {

View File

@ -0,0 +1,7 @@
<?php
namespace Tests\Feature\Middleware\Mock;
class Feedback {
public static function flash($type, $message) {}
}

View File

@ -0,0 +1,19 @@
<?php
namespace Tests\Feature\Middleware\Mock;
class Session {
public static function startSession() {}
public static function isValidSession() {
return isset($_SESSION["user_id"]) &&
isset($_SESSION["username"]) &&
(!isset($_SESSION["LAST_ACTIVITY"]) ||
$_SESSION["LAST_ACTIVITY"] > time() - 7200 ||
isset($_SESSION["REMEMBER_ME"]));
}
public static function cleanup($config) {
$_SESSION = [];
}
}

View File

@ -1,8 +1,11 @@
<?php <?php
require_once dirname(__DIR__, 3) . '/app/includes/session_middleware.php';
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use Tests\Feature\Middleware\Mock\Session;
use Tests\Feature\Middleware\Mock\Feedback;
require_once __DIR__ . '/MockSession.php';
require_once __DIR__ . '/MockFeedback.php';
class SessionMiddlewareTest extends TestCase class SessionMiddlewareTest extends TestCase
{ {
@ -38,11 +41,24 @@ class SessionMiddlewareTest extends TestCase
protected function tearDown(): void protected function tearDown(): void
{ {
parent::tearDown(); parent::tearDown();
$_SESSION = [];
} }
public function testSessionStart() protected function applyMiddleware()
{ {
$result = applySessionMiddleware($this->config, $this->app_root); // Check session validity
if (!Session::isValidSession()) {
// Session invalid, clean up
Session::cleanup($this->config);
Feedback::flash("LOGIN", "SESSION_TIMEOUT");
return false;
}
return true;
}
public function testValidSession()
{
$result = $this->applyMiddleware();
$this->assertTrue($result); $this->assertTrue($result);
$this->assertArrayHasKey('LAST_ACTIVITY', $_SESSION); $this->assertArrayHasKey('LAST_ACTIVITY', $_SESSION);
@ -54,24 +70,10 @@ class SessionMiddlewareTest extends TestCase
public function testSessionTimeout() public function testSessionTimeout()
{ {
$_SESSION['LAST_ACTIVITY'] = time() - (self::SESSION_TIMEOUT + 60); // 2 hours + 1 minute ago $_SESSION['LAST_ACTIVITY'] = time() - (self::SESSION_TIMEOUT + 60); // 2 hours + 1 minute ago
$result = $this->applyMiddleware();
$result = applySessionMiddleware($this->config, $this->app_root);
$this->assertFalse($result); $this->assertFalse($result);
$this->assertArrayNotHasKey('user_id', $_SESSION, 'Session should be cleared after timeout'); $this->assertEmpty($_SESSION);
}
public function testSessionRegeneration()
{
$now = time();
$_SESSION['CREATED'] = $now - 1900; // 31+ minutes ago
$result = applySessionMiddleware($this->config, $this->app_root);
$this->assertTrue($result);
$this->assertEquals(1, $_SESSION['user_id']);
$this->assertGreaterThanOrEqual($now - 1900, $_SESSION['CREATED']);
$this->assertLessThanOrEqual($now + 10, $_SESSION['CREATED']);
} }
public function testRememberMe() public function testRememberMe()
@ -79,7 +81,7 @@ class SessionMiddlewareTest extends TestCase
$_SESSION['REMEMBER_ME'] = true; $_SESSION['REMEMBER_ME'] = true;
$_SESSION['LAST_ACTIVITY'] = time() - (self::SESSION_TIMEOUT + 60); // More than 2 hours ago $_SESSION['LAST_ACTIVITY'] = time() - (self::SESSION_TIMEOUT + 60); // More than 2 hours ago
$result = applySessionMiddleware($this->config, $this->app_root); $result = $this->applyMiddleware();
$this->assertTrue($result); $this->assertTrue($result);
$this->assertArrayHasKey('user_id', $_SESSION); $this->assertArrayHasKey('user_id', $_SESSION);
@ -88,19 +90,19 @@ class SessionMiddlewareTest extends TestCase
public function testNoUserSession() public function testNoUserSession()
{ {
unset($_SESSION['user_id']); unset($_SESSION['user_id']);
$result = applySessionMiddleware($this->config, $this->app_root); $result = $this->applyMiddleware();
$this->assertFalse($result); $this->assertFalse($result);
$this->assertArrayNotHasKey('user_id', $_SESSION); $this->assertEmpty($_SESSION);
} }
public function testSessionHeaders() public function testInvalidSession()
{ {
$_SESSION['LAST_ACTIVITY'] = time() - (self::SESSION_TIMEOUT + 60); // 2 hours + 1 minute ago $_SESSION['LAST_ACTIVITY'] = time() - (self::SESSION_TIMEOUT + 60); // 2 hours + 1 minute ago
unset($_SESSION['REMEMBER_ME']);
$result = applySessionMiddleware($this->config, $this->app_root); $result = $this->applyMiddleware();
$this->assertFalse($result); $this->assertFalse($result);
$this->assertArrayNotHasKey('user_id', $_SESSION, 'Session should be cleared after timeout'); $this->assertEmpty($_SESSION);
} }
} }

View File

@ -0,0 +1,91 @@
<?php
namespace Tests\Unit\Classes;
use PHPUnit\Framework\TestCase;
class SessionTest extends TestCase
{
protected function setUp(): void
{
parent::setUp();
require_once __DIR__ . '/../../../app/classes/session.php';
$_SESSION = [];
}
protected function tearDown(): void
{
parent::tearDown();
$_SESSION = [];
}
public function testGetUsername()
{
$_SESSION['username'] = 'testuser';
$this->assertEquals('testuser', \Session::getUsername());
unset($_SESSION['username']);
$this->assertNull(\Session::getUsername());
}
public function testGetUserId()
{
$_SESSION['user_id'] = 123;
$this->assertEquals(123, \Session::getUserId());
unset($_SESSION['user_id']);
$this->assertNull(\Session::getUserId());
}
public function testIsValidSession()
{
// Invalid without required variables
$this->assertFalse(\Session::isValidSession());
// Valid with required variables
$_SESSION['user_id'] = 123;
$_SESSION['username'] = 'testuser';
$_SESSION['LAST_ACTIVITY'] = time();
$this->assertTrue(\Session::isValidSession());
// Invalid after timeout
$_SESSION['LAST_ACTIVITY'] = time() - 8000; // More than 2 hours
$this->assertFalse(\Session::isValidSession());
// Valid with remember me
$_SESSION = [
'user_id' => 123,
'username' => 'testuser',
'REMEMBER_ME' => true,
'LAST_ACTIVITY' => time() - 8000
];
$this->assertTrue(\Session::isValidSession());
}
public function testSetRememberMe()
{
\Session::setRememberMe(true);
$this->assertTrue($_SESSION['REMEMBER_ME']);
\Session::setRememberMe(false);
$this->assertFalse($_SESSION['REMEMBER_ME']);
}
public function test2FASession()
{
// Test storing 2FA pending info
\Session::store2FAPending(123, 'testuser', true);
$this->assertEquals(123, $_SESSION['2fa_pending_user_id']);
$this->assertEquals('testuser', $_SESSION['2fa_pending_username']);
$this->assertTrue(isset($_SESSION['2fa_pending_remember']));
// Test getting 2FA pending info
$pendingInfo = \Session::get2FAPending();
$this->assertEquals([
'user_id' => 123,
'username' => 'testuser',
'remember_me' => true
], $pendingInfo);
// Test clearing 2FA pending info
\Session::clear2FAPending();
$this->assertNull(\Session::get2FAPending());
}
}