2026-01-01 15:40:32 -05:00
|
|
|
<?php
|
|
|
|
|
/**
|
|
|
|
|
* AuthMiddleware - Handles authentication via Authelia forward auth headers
|
|
|
|
|
*/
|
|
|
|
|
require_once dirname(__DIR__) . '/models/UserModel.php';
|
|
|
|
|
|
|
|
|
|
class AuthMiddleware {
|
|
|
|
|
private $userModel;
|
|
|
|
|
private $conn;
|
|
|
|
|
|
|
|
|
|
public function __construct($conn) {
|
|
|
|
|
$this->conn = $conn;
|
|
|
|
|
$this->userModel = new UserModel($conn);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Authenticate user from Authelia forward auth headers
|
|
|
|
|
*
|
|
|
|
|
* @return array User data array
|
|
|
|
|
* @throws Exception if authentication fails
|
|
|
|
|
*/
|
|
|
|
|
public function authenticate() {
|
2026-01-09 16:23:09 -05:00
|
|
|
// Start session if not already started with secure settings
|
2026-01-01 15:40:32 -05:00
|
|
|
if (session_status() === PHP_SESSION_NONE) {
|
2026-01-09 16:23:09 -05:00
|
|
|
// Configure secure session settings
|
|
|
|
|
ini_set('session.cookie_httponly', 1);
|
|
|
|
|
ini_set('session.cookie_secure', 1); // Requires HTTPS
|
2026-01-23 21:16:29 -05:00
|
|
|
ini_set('session.cookie_samesite', 'Lax'); // Lax allows redirects from Authelia
|
2026-01-09 16:23:09 -05:00
|
|
|
ini_set('session.use_strict_mode', 1);
|
2026-01-23 21:16:29 -05:00
|
|
|
ini_set('session.gc_maxlifetime', 18000); // 5 hours
|
|
|
|
|
ini_set('session.cookie_lifetime', 0); // Until browser closes
|
2026-01-09 16:23:09 -05:00
|
|
|
|
2026-01-01 15:40:32 -05:00
|
|
|
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)) {
|
|
|
|
|
// 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");
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-09 16:23:09 -05:00
|
|
|
// Regenerate session ID to prevent session fixation attacks
|
|
|
|
|
session_regenerate_id(true);
|
|
|
|
|
|
2026-01-01 15:40:32 -05:00
|
|
|
// Store user in session
|
|
|
|
|
$_SESSION['user'] = $user;
|
|
|
|
|
$_SESSION['last_activity'] = time();
|
|
|
|
|
|
2026-01-09 16:23:09 -05:00
|
|
|
// Generate new CSRF token on login
|
|
|
|
|
require_once __DIR__ . '/CsrfMiddleware.php';
|
|
|
|
|
CsrfMiddleware::generateToken();
|
|
|
|
|
|
2026-01-01 15:40:32 -05:00
|
|
|
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() {
|
|
|
|
|
// Redirect to the auth endpoint (Authelia will handle the redirect back)
|
|
|
|
|
header('HTTP/1.1 401 Unauthorized');
|
|
|
|
|
echo '<!DOCTYPE html>
|
|
|
|
|
<html>
|
|
|
|
|
<head>
|
|
|
|
|
<title>Authentication Required</title>
|
|
|
|
|
<style>
|
|
|
|
|
body {
|
|
|
|
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
|
|
|
|
display: flex;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
align-items: center;
|
|
|
|
|
min-height: 100vh;
|
|
|
|
|
margin: 0;
|
|
|
|
|
background: #f5f5f5;
|
|
|
|
|
}
|
|
|
|
|
.auth-container {
|
|
|
|
|
background: white;
|
|
|
|
|
padding: 2rem;
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
|
|
|
|
text-align: center;
|
|
|
|
|
max-width: 400px;
|
|
|
|
|
}
|
|
|
|
|
.auth-container h1 {
|
|
|
|
|
color: #333;
|
|
|
|
|
margin-bottom: 1rem;
|
|
|
|
|
}
|
|
|
|
|
.auth-container p {
|
|
|
|
|
color: #666;
|
|
|
|
|
margin-bottom: 1.5rem;
|
|
|
|
|
}
|
|
|
|
|
.auth-container a {
|
|
|
|
|
display: inline-block;
|
|
|
|
|
background: #4285f4;
|
|
|
|
|
color: white;
|
|
|
|
|
padding: 0.75rem 2rem;
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
text-decoration: none;
|
|
|
|
|
transition: background 0.2s;
|
|
|
|
|
}
|
|
|
|
|
.auth-container a:hover {
|
|
|
|
|
background: #357ae8;
|
|
|
|
|
}
|
|
|
|
|
</style>
|
|
|
|
|
</head>
|
|
|
|
|
<body>
|
|
|
|
|
<div class="auth-container">
|
|
|
|
|
<h1>Authentication Required</h1>
|
|
|
|
|
<p>You need to be logged in to access Tinker Tickets.</p>
|
|
|
|
|
<a href="/">Continue to Login</a>
|
|
|
|
|
</div>
|
|
|
|
|
</body>
|
|
|
|
|
</html>';
|
|
|
|
|
exit;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Show access denied page
|
|
|
|
|
*
|
|
|
|
|
* @param string $username Username
|
|
|
|
|
* @param string $groups User groups
|
|
|
|
|
*/
|
|
|
|
|
private function showAccessDenied($username, $groups) {
|
|
|
|
|
header('HTTP/1.1 403 Forbidden');
|
|
|
|
|
echo '<!DOCTYPE html>
|
|
|
|
|
<html>
|
|
|
|
|
<head>
|
|
|
|
|
<title>Access Denied</title>
|
|
|
|
|
<style>
|
|
|
|
|
body {
|
|
|
|
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
|
|
|
|
display: flex;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
align-items: center;
|
|
|
|
|
min-height: 100vh;
|
|
|
|
|
margin: 0;
|
|
|
|
|
background: #f5f5f5;
|
|
|
|
|
}
|
|
|
|
|
.denied-container {
|
|
|
|
|
background: white;
|
|
|
|
|
padding: 2rem;
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
|
|
|
|
text-align: center;
|
|
|
|
|
max-width: 500px;
|
|
|
|
|
}
|
|
|
|
|
.denied-container h1 {
|
|
|
|
|
color: #d32f2f;
|
|
|
|
|
margin-bottom: 1rem;
|
|
|
|
|
}
|
|
|
|
|
.denied-container p {
|
|
|
|
|
color: #666;
|
|
|
|
|
margin-bottom: 0.5rem;
|
|
|
|
|
}
|
|
|
|
|
.denied-container .user-info {
|
|
|
|
|
background: #f5f5f5;
|
|
|
|
|
padding: 1rem;
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
margin: 1rem 0;
|
|
|
|
|
font-family: monospace;
|
|
|
|
|
font-size: 0.9rem;
|
|
|
|
|
}
|
|
|
|
|
</style>
|
|
|
|
|
</head>
|
|
|
|
|
<body>
|
|
|
|
|
<div class="denied-container">
|
|
|
|
|
<h1>Access Denied</h1>
|
|
|
|
|
<p>You do not have permission to access Tinker Tickets.</p>
|
|
|
|
|
<p>Required groups: <strong>admin</strong> or <strong>employee</strong></p>
|
|
|
|
|
<div class="user-info">
|
|
|
|
|
<div>Username: ' . htmlspecialchars($username) . '</div>
|
|
|
|
|
<div>Groups: ' . htmlspecialchars($groups ?: 'none') . '</div>
|
|
|
|
|
</div>
|
|
|
|
|
<p>Please contact your administrator if you believe this is an error.</p>
|
|
|
|
|
</div>
|
|
|
|
|
</body>
|
|
|
|
|
</html>';
|
|
|
|
|
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();
|
|
|
|
|
}
|
|
|
|
|
}
|