SSO Update :)
This commit is contained in:
141
middleware/ApiKeyAuth.php
Normal file
141
middleware/ApiKeyAuth.php
Normal file
@@ -0,0 +1,141 @@
|
||||
<?php
|
||||
/**
|
||||
* ApiKeyAuth - Handles API key authentication for external services
|
||||
*/
|
||||
require_once dirname(__DIR__) . '/models/ApiKeyModel.php';
|
||||
require_once dirname(__DIR__) . '/models/UserModel.php';
|
||||
|
||||
class ApiKeyAuth {
|
||||
private $apiKeyModel;
|
||||
private $userModel;
|
||||
private $conn;
|
||||
|
||||
public function __construct($conn) {
|
||||
$this->conn = $conn;
|
||||
$this->apiKeyModel = new ApiKeyModel($conn);
|
||||
$this->userModel = new UserModel($conn);
|
||||
}
|
||||
|
||||
/**
|
||||
* Authenticate using API key from Authorization header
|
||||
*
|
||||
* @return array User data for system user
|
||||
* @throws Exception if authentication fails
|
||||
*/
|
||||
public function authenticate() {
|
||||
// Get Authorization header
|
||||
$authHeader = $this->getAuthorizationHeader();
|
||||
|
||||
if (empty($authHeader)) {
|
||||
$this->sendUnauthorized('Missing Authorization header');
|
||||
exit;
|
||||
}
|
||||
|
||||
// Check if it's a Bearer token
|
||||
if (!preg_match('/^Bearer\s+(.+)$/i', $authHeader, $matches)) {
|
||||
$this->sendUnauthorized('Invalid Authorization header format. Expected: Bearer <api_key>');
|
||||
exit;
|
||||
}
|
||||
|
||||
$apiKey = $matches[1];
|
||||
|
||||
// Validate API key
|
||||
$keyData = $this->apiKeyModel->validateKey($apiKey);
|
||||
|
||||
if (!$keyData) {
|
||||
$this->sendUnauthorized('Invalid or expired API key');
|
||||
exit;
|
||||
}
|
||||
|
||||
// Get system user (or the user who created the key)
|
||||
$user = $this->userModel->getSystemUser();
|
||||
|
||||
if (!$user) {
|
||||
$this->sendUnauthorized('System user not found');
|
||||
exit;
|
||||
}
|
||||
|
||||
// Add API key info to user data for logging
|
||||
$user['api_key_id'] = $keyData['api_key_id'];
|
||||
$user['api_key_name'] = $keyData['key_name'];
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Authorization header from various sources
|
||||
*
|
||||
* @return string|null Authorization header value
|
||||
*/
|
||||
private function getAuthorizationHeader() {
|
||||
// Try different header formats
|
||||
if (isset($_SERVER['HTTP_AUTHORIZATION'])) {
|
||||
return $_SERVER['HTTP_AUTHORIZATION'];
|
||||
}
|
||||
|
||||
if (isset($_SERVER['REDIRECT_HTTP_AUTHORIZATION'])) {
|
||||
return $_SERVER['REDIRECT_HTTP_AUTHORIZATION'];
|
||||
}
|
||||
|
||||
// Check for Authorization in getallheaders if available
|
||||
if (function_exists('getallheaders')) {
|
||||
$headers = getallheaders();
|
||||
if (isset($headers['Authorization'])) {
|
||||
return $headers['Authorization'];
|
||||
}
|
||||
if (isset($headers['authorization'])) {
|
||||
return $headers['authorization'];
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send 401 Unauthorized response
|
||||
*
|
||||
* @param string $message Error message
|
||||
*/
|
||||
private function sendUnauthorized($message) {
|
||||
header('HTTP/1.1 401 Unauthorized');
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'error' => 'Unauthorized',
|
||||
'message' => $message
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify API key without throwing errors (for optional auth)
|
||||
*
|
||||
* @return array|null User data or null if not authenticated
|
||||
*/
|
||||
public function verifyOptional() {
|
||||
$authHeader = $this->getAuthorizationHeader();
|
||||
|
||||
if (empty($authHeader)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!preg_match('/^Bearer\s+(.+)$/i', $authHeader, $matches)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$apiKey = $matches[1];
|
||||
$keyData = $this->apiKeyModel->validateKey($apiKey);
|
||||
|
||||
if (!$keyData) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$user = $this->userModel->getSystemUser();
|
||||
|
||||
if ($user) {
|
||||
$user['api_key_id'] = $keyData['api_key_id'];
|
||||
$user['api_key_name'] = $keyData['key_name'];
|
||||
}
|
||||
|
||||
return $user;
|
||||
}
|
||||
}
|
||||
256
middleware/AuthMiddleware.php
Normal file
256
middleware/AuthMiddleware.php
Normal file
@@ -0,0 +1,256 @@
|
||||
<?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() {
|
||||
// Start session if not already started
|
||||
if (session_status() === PHP_SESSION_NONE) {
|
||||
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");
|
||||
}
|
||||
|
||||
// Store user in session
|
||||
$_SESSION['user'] = $user;
|
||||
$_SESSION['last_activity'] = time();
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user