conn = $conn; $this->userModel = new UserModel($conn); } /** * Log security event for authentication failures * * @param string $event Event type (e.g., 'auth_required', 'access_denied', 'session_expired') * @param array $context Additional context data */ private function logSecurityEvent(string $event, array $context = []): void { $logData = [ 'event' => $event, 'ip' => $_SERVER['REMOTE_ADDR'] ?? 'unknown', 'forwarded_for' => $_SERVER['HTTP_X_FORWARDED_FOR'] ?? null, 'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? 'unknown', 'request_uri' => $_SERVER['REQUEST_URI'] ?? 'unknown', 'request_method' => $_SERVER['REQUEST_METHOD'] ?? 'unknown', 'timestamp' => date('c') ]; // Merge additional context $logData = array_merge($logData, $context); // Remove null values for cleaner logs $logData = array_filter($logData, fn($v) => $v !== null); // Format log message $message = sprintf( "[SECURITY] %s: %s", strtoupper($event), json_encode($logData, JSON_UNESCAPED_SLASHES) ); error_log($message); } /** * Authenticate user from Authelia forward auth headers * * @return array User data array * @throws Exception if authentication fails */ public function authenticate() { // Start session if not already started with secure settings if (session_status() === PHP_SESSION_NONE) { // Configure secure session settings ini_set('session.cookie_httponly', 1); ini_set('session.cookie_secure', 1); // Requires HTTPS ini_set('session.cookie_samesite', 'Lax'); // Lax allows redirects from Authelia ini_set('session.use_strict_mode', 1); ini_set('session.gc_maxlifetime', 18000); // 5 hours ini_set('session.cookie_lifetime', 0); // Until browser closes session_start(); } // Check if user is already authenticated in session if (isset($_SESSION['user']) && isset($_SESSION['user']['user_id'])) { // Verify session hasn't expired (5 hour timeout) if (isset($_SESSION['last_activity']) && (time() - $_SESSION['last_activity'] > 18000)) { // Log session expiration $this->logSecurityEvent('session_expired', [ 'username' => $_SESSION['user']['username'] ?? 'unknown', 'user_id' => $_SESSION['user']['user_id'] ?? null, 'session_age_seconds' => time() - $_SESSION['last_activity'] ]); // Session expired, clear it session_unset(); session_destroy(); session_start(); } else { // Update last activity time $_SESSION['last_activity'] = time(); return $_SESSION['user']; } } // Read Authelia forward auth headers $username = $this->getHeader('HTTP_REMOTE_USER'); $displayName = $this->getHeader('HTTP_REMOTE_NAME'); $email = $this->getHeader('HTTP_REMOTE_EMAIL'); $groups = $this->getHeader('HTTP_REMOTE_GROUPS'); // Check if authentication headers are present if (empty($username)) { // No auth headers - user not authenticated $this->redirectToAuth(); exit; } // Check if user has required group membership if (!$this->checkGroupAccess($groups)) { $this->showAccessDenied($username, $groups); exit; } // Sync user to database (create or update) $user = $this->userModel->syncUserFromAuthelia($username, $displayName, $email, $groups); if (!$user) { throw new Exception("Failed to sync user from Authelia"); } // Regenerate session ID to prevent session fixation attacks session_regenerate_id(true); // Store user in session $_SESSION['user'] = $user; $_SESSION['last_activity'] = time(); // Generate new CSRF token on login require_once __DIR__ . '/CsrfMiddleware.php'; CsrfMiddleware::generateToken(); return $user; } /** * Get header value from server variables * * @param string $header Header name * @return string|null Header value or null if not set */ private function getHeader($header) { if (isset($_SERVER[$header])) { return $_SERVER[$header]; } return null; } /** * Check if user has required group membership * * @param string $groups Comma-separated group names * @return bool True if user has access */ private function checkGroupAccess($groups) { if (empty($groups)) { return false; } // Check for admin or employee group membership $userGroups = array_map('trim', explode(',', strtolower($groups))); $requiredGroups = ['admin', 'employee']; return !empty(array_intersect($userGroups, $requiredGroups)); } /** * Redirect to Authelia login */ private function redirectToAuth() { // Log unauthenticated access attempt $this->logSecurityEvent('auth_required', [ 'reason' => 'no_auth_headers' ]); // Redirect to the auth endpoint (Authelia will handle the redirect back) header('HTTP/1.1 401 Unauthorized'); echo ' Authentication Required

Authentication Required

You need to be logged in to access Tinker Tickets.

Continue to Login
'; exit; } /** * Show access denied page * * @param string $username Username * @param string $groups User groups */ private function showAccessDenied($username, $groups) { // Log access denied event with user details $this->logSecurityEvent('access_denied', [ 'username' => $username, 'groups' => $groups ?: 'none', 'required_groups' => 'admin,employee', 'reason' => 'insufficient_group_membership' ]); header('HTTP/1.1 403 Forbidden'); echo ' Access Denied

Access Denied

You do not have permission to access Tinker Tickets.

Required groups: admin or employee

Username: ' . htmlspecialchars($username) . '
Groups: ' . htmlspecialchars($groups ?: 'none') . '

Please contact your administrator if you believe this is an error.

'; exit; } /** * Get current authenticated user from session * * @return array|null User data or null if not authenticated */ public static function getCurrentUser() { if (session_status() === PHP_SESSION_NONE) { session_start(); } return $_SESSION['user'] ?? null; } /** * Logout current user */ public static function logout() { if (session_status() === PHP_SESSION_NONE) { session_start(); } session_unset(); session_destroy(); } }