Files
easystream-main/f_core/f_classes/class.auth.php
SamiAhmed7777 f0f346deb9
Some checks failed
EasyStream Test Suite / test (pull_request) Has been cancelled
EasyStream Test Suite / code-quality (pull_request) Has been cancelled
EasyStream Test Suite / integration-test (pull_request) Has been cancelled
Sync current dev state
2025-12-15 17:28:21 -08:00

960 lines
36 KiB
PHP

<?php
/*******************************************************************************************************************
| Software Name : EasyStream
| Software Description : High End YouTube Clone Script with Videos, Shorts, Streams, Images, Audio, Documents, Blogs
| Software Author : (c) Sami Ahmed
|*******************************************************************************************************************
|
|*******************************************************************************************************************
| This source file is subject to the EasyStream Proprietary License Agreement.
|
| By using this software, you acknowledge having read this Agreement and agree to be bound thereby.
|*******************************************************************************************************************
| Copyright (c) 2025 Sami Ahmed. All rights reserved.
|*******************************************************************************************************************/
defined('_ISVALID') or header('Location: /error');
/**
* Enhanced Authentication Class with Secure Session Management
*/
class VAuth
{
private static $instance = null;
private $db;
private $logger;
private $security;
private $redis;
// Session configuration
private $sessionTimeout = 3600; // 1 hour default
private $rememberMeTimeout = 2592000; // 30 days
private $maxLoginAttempts = 5;
private $lockoutDuration = 900; // 15 minutes
public static function getInstance()
{
if (self::$instance === null) {
self::$instance = new self();
}
return self::$instance;
}
private function __construct()
{
global $class_database;
$this->db = $class_database ?: new VDatabase();
$this->logger = VLogger::getInstance();
$this->security = VSecurity::getInstance();
$this->redis = $this->getRedisConnection();
// Configure secure session settings
$this->configureSession();
}
/**
* Configure secure session settings
*/
private function configureSession()
{
if (session_status() === PHP_SESSION_NONE) {
// Secure session configuration
ini_set('session.cookie_httponly', 1);
ini_set('session.cookie_secure', isset($_SERVER['HTTPS']) ? 1 : 0);
ini_set('session.cookie_samesite', 'Strict');
ini_set('session.use_strict_mode', 1);
ini_set('session.cookie_lifetime', 0); // Session cookies only
// Use Redis for session storage if available
if ($this->redis) {
ini_set('session.save_handler', 'redis');
$redisHost = getenv('REDIS_HOST') ?: 'redis';
$redisPort = getenv('REDIS_PORT') ?: 6379;
$redisDb = getenv('REDIS_DB') ?: 0;
ini_set('session.save_path', "tcp://{$redisHost}:{$redisPort}?database={$redisDb}");
}
session_start();
}
}
/**
* Get Redis connection
* @return Redis|null
*/
private function getRedisConnection()
{
try {
if (!class_exists('Redis')) {
return null;
}
$redis = new Redis();
$host = getenv('REDIS_HOST') ?: 'redis';
$port = (int)(getenv('REDIS_PORT') ?: 6379);
$db = (int)(getenv('REDIS_DB') ?: 0);
if (!$redis->connect($host, $port, 2)) {
return null;
}
if ($db > 0) {
$redis->select($db);
}
return $redis;
} catch (Exception $e) {
$this->logger->warning('Redis connection failed for auth', ['error' => $e->getMessage()]);
return null;
}
}
/**
* Register a new user with email verification
* @param array $userData User registration data
* @return array Result with success status and message
*/
public function register($userData)
{
try {
// Validate required fields
$requiredFields = ['username', 'email', 'password'];
foreach ($requiredFields as $field) {
if (empty($userData[$field])) {
return ['success' => false, 'message' => "Field '{$field}' is required"];
}
}
// Sanitize and validate input
$username = VSecurity::validateInput($userData['username'], 'alphanum', null, ['min_length' => 3, 'max_length' => 50]);
$email = VSecurity::validateInput($userData['email'], 'email');
$password = $userData['password']; // Don't sanitize password
if (!$username) {
return ['success' => false, 'message' => 'Invalid username. Use 3-50 alphanumeric characters only.'];
}
if (!$email) {
return ['success' => false, 'message' => 'Invalid email address'];
}
if (strlen($password) < 8) {
return ['success' => false, 'message' => 'Password must be at least 8 characters long'];
}
// Check password strength
if (!$this->isPasswordStrong($password)) {
return ['success' => false, 'message' => 'Password must contain at least one uppercase letter, one lowercase letter, one number, and one special character'];
}
// Check if username or email already exists
if ($this->userExists($username, $email)) {
$this->logger->logSecurityEvent('Registration attempt with existing credentials', [
'username' => $username,
'email' => $email
]);
return ['success' => false, 'message' => 'Username or email already exists'];
}
// Generate verification token
$verificationToken = bin2hex(random_bytes(32));
// Hash password
$passwordHash = password_hash($password, PASSWORD_DEFAULT);
// Insert user into database
$sql = "INSERT INTO `db_users` (`username`, `email`, `password_hash`, `role`, `status`, `email_verified`, `verification_token`, `created_at`)
VALUES (?, ?, ?, 'member', 'active', 0, ?, NOW())";
$result = $this->db->dbConnection()->Execute($sql, [$username, $email, $passwordHash, $verificationToken]);
if (!$result) {
$this->logger->error('User registration failed', [
'username' => $username,
'email' => $email,
'error' => $this->db->dbConnection()->ErrorMsg()
]);
return ['success' => false, 'message' => 'Registration failed. Please try again.'];
}
$userId = $this->db->dbConnection()->Insert_ID();
// Send verification email
$this->sendVerificationEmail($email, $username, $verificationToken);
$this->logger->info('User registered successfully', [
'user_id' => $userId,
'username' => $username,
'email' => $email
]);
return [
'success' => true,
'message' => 'Registration successful! Please check your email to verify your account.',
'user_id' => $userId
];
} catch (Exception $e) {
$this->logger->error('Registration error', [
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString()
]);
return ['success' => false, 'message' => 'An error occurred during registration'];
}
}
/**
* Verify email address
* @param string $token Verification token
* @return array Result with success status and message
*/
public function verifyEmail($token)
{
try {
$token = VSecurity::validateInput($token, 'alphanum');
if (!$token) {
return ['success' => false, 'message' => 'Invalid verification token'];
}
$sql = "SELECT `user_id`, `username`, `email` FROM `db_users` WHERE `verification_token` = ? AND `email_verified` = 0";
$result = $this->db->dbConnection()->Execute($sql, [$token]);
if (!$result || $result->EOF) {
$this->logger->logSecurityEvent('Invalid email verification attempt', ['token' => $token]);
return ['success' => false, 'message' => 'Invalid or expired verification token'];
}
$userData = $result->fields;
// Update user as verified
$updateSql = "UPDATE `db_users` SET `email_verified` = 1, `verification_token` = NULL, `updated_at` = NOW() WHERE `user_id` = ?";
$updateResult = $this->db->dbConnection()->Execute($updateSql, [$userData['user_id']]);
if (!$updateResult) {
return ['success' => false, 'message' => 'Verification failed. Please try again.'];
}
$this->logger->info('Email verified successfully', [
'user_id' => $userData['user_id'],
'username' => $userData['username'],
'email' => $userData['email']
]);
return [
'success' => true,
'message' => 'Email verified successfully! You can now log in.',
'user_id' => $userData['user_id']
];
} catch (Exception $e) {
$this->logger->error('Email verification error', [
'error' => $e->getMessage(),
'token' => $token ?? 'unknown'
]);
return ['success' => false, 'message' => 'An error occurred during verification'];
}
}
/**
* Authenticate user login
* @param string $identifier Username or email
* @param string $password Password
* @param bool $rememberMe Remember me option
* @return array Result with success status and user data
*/
public function login($identifier, $password, $rememberMe = false)
{
try {
// Rate limiting for login attempts
$clientIP = $this->getClientIP();
$rateLimitKey = "login_attempts_{$clientIP}";
if (!VSecurity::checkRateLimit($rateLimitKey, $this->maxLoginAttempts, $this->lockoutDuration, 'login')) {
$this->logger->logSecurityEvent('Login rate limit exceeded', [
'ip' => $clientIP,
'identifier' => $identifier
]);
return ['success' => false, 'message' => 'Too many login attempts. Please try again later.'];
}
// Validate input
$identifier = VSecurity::validateInput($identifier, 'string', null, ['max_length' => 255]);
if (!$identifier || !$password) {
return ['success' => false, 'message' => 'Username/email and password are required'];
}
// Find user by username or email
$user = $this->findUserByIdentifier($identifier);
if (!$user) {
$this->logger->logSecurityEvent('Login attempt with non-existent user', [
'identifier' => $identifier,
'ip' => $clientIP
]);
return ['success' => false, 'message' => 'Invalid credentials'];
}
// Check if account is active
if ($user['status'] !== 'active') {
$this->logger->logSecurityEvent('Login attempt with inactive account', [
'user_id' => $user['user_id'],
'username' => $user['username'],
'status' => $user['status']
]);
return ['success' => false, 'message' => 'Account is not active'];
}
// Verify password
if (!password_verify($password, $user['password_hash'])) {
$this->logger->logSecurityEvent('Failed login attempt', [
'user_id' => $user['user_id'],
'username' => $user['username'],
'ip' => $clientIP
]);
return ['success' => false, 'message' => 'Invalid credentials'];
}
// Check if email is verified (optional based on configuration)
global $cfg;
$requireEmailVerification = $cfg['require_email_verification'] ?? true;
if ($requireEmailVerification && !$user['email_verified']) {
return ['success' => false, 'message' => 'Please verify your email address before logging in'];
}
// Create session
$sessionResult = $this->createSession($user, $rememberMe);
if (!$sessionResult['success']) {
return $sessionResult;
}
// Update last login
$this->updateLastLogin($user['user_id']);
$this->logger->info('User logged in successfully', [
'user_id' => $user['user_id'],
'username' => $user['username'],
'ip' => $clientIP,
'remember_me' => $rememberMe
]);
return [
'success' => true,
'message' => 'Login successful',
'user' => [
'user_id' => $user['user_id'],
'username' => $user['username'],
'email' => $user['email'],
'role' => $user['role'],
'email_verified' => $user['email_verified']
]
];
} catch (Exception $e) {
$this->logger->error('Login error', [
'error' => $e->getMessage(),
'identifier' => $identifier ?? 'unknown'
]);
return ['success' => false, 'message' => 'An error occurred during login'];
}
}
/**
* Create secure session
* @param array $user User data
* @param bool $rememberMe Remember me option
* @return array Result with success status
*/
private function createSession($user, $rememberMe = false)
{
try {
// Regenerate session ID to prevent session fixation
session_regenerate_id(true);
// Set session data
$_SESSION['USER_ID'] = $user['user_id'];
$_SESSION['USERNAME'] = $user['username'];
$_SESSION['EMAIL'] = $user['email'];
$_SESSION['ROLE'] = $user['role'];
$_SESSION['LOGIN_TIME'] = time();
$_SESSION['LAST_ACTIVITY'] = time();
$_SESSION['IP_ADDRESS'] = $this->getClientIP();
$_SESSION['USER_AGENT'] = $_SERVER['HTTP_USER_AGENT'] ?? '';
$_SESSION['SESSION_TOKEN'] = bin2hex(random_bytes(32));
// Store session in database
$sessionId = session_id();
$expiresAt = date('Y-m-d H:i:s', time() + ($rememberMe ? $this->rememberMeTimeout : $this->sessionTimeout));
$sql = "INSERT INTO `db_sessions` (`session_id`, `user_id`, `ip_address`, `user_agent`, `created_at`, `expires_at`, `remember_me`)
VALUES (?, ?, ?, ?, NOW(), ?, ?)
ON DUPLICATE KEY UPDATE
`ip_address` = VALUES(`ip_address`),
`user_agent` = VALUES(`user_agent`),
`expires_at` = VALUES(`expires_at`),
`remember_me` = VALUES(`remember_me`)";
$result = $this->db->dbConnection()->Execute($sql, [
$sessionId,
$user['user_id'],
$_SESSION['IP_ADDRESS'],
$_SESSION['USER_AGENT'],
$expiresAt,
$rememberMe ? 1 : 0
]);
if (!$result) {
$this->logger->error('Session creation failed', [
'user_id' => $user['user_id'],
'error' => $this->db->dbConnection()->ErrorMsg()
]);
return ['success' => false, 'message' => 'Session creation failed'];
}
// Set remember me cookie if requested
if ($rememberMe) {
$rememberToken = bin2hex(random_bytes(32));
$cookieExpiry = time() + $this->rememberMeTimeout;
setcookie('remember_token', $rememberToken, $cookieExpiry, '/', '', isset($_SERVER['HTTPS']), true);
// Store remember token in database
$tokenSql = "UPDATE `db_users` SET `remember_token` = ?, `remember_expires` = ? WHERE `user_id` = ?";
$this->db->dbConnection()->Execute($tokenSql, [
password_hash($rememberToken, PASSWORD_DEFAULT),
date('Y-m-d H:i:s', $cookieExpiry),
$user['user_id']
]);
}
return ['success' => true];
} catch (Exception $e) {
$this->logger->error('Session creation error', [
'error' => $e->getMessage(),
'user_id' => $user['user_id'] ?? 'unknown'
]);
return ['success' => false, 'message' => 'Session creation failed'];
}
}
/**
* Logout user and destroy session
* @return array Result with success status
*/
public function logout()
{
try {
$userId = $_SESSION['USER_ID'] ?? null;
$sessionId = session_id();
// Remove session from database
if ($sessionId) {
$sql = "DELETE FROM `db_sessions` WHERE `session_id` = ?";
$this->db->dbConnection()->Execute($sql, [$sessionId]);
}
// Clear remember me cookie and token
if (isset($_COOKIE['remember_token'])) {
setcookie('remember_token', '', time() - 3600, '/', '', isset($_SERVER['HTTPS']), true);
if ($userId) {
$tokenSql = "UPDATE `db_users` SET `remember_token` = NULL, `remember_expires` = NULL WHERE `user_id` = ?";
$this->db->dbConnection()->Execute($tokenSql, [$userId]);
}
}
// Destroy session
session_unset();
session_destroy();
$this->logger->info('User logged out successfully', [
'user_id' => $userId,
'session_id' => $sessionId
]);
return ['success' => true, 'message' => 'Logged out successfully'];
} catch (Exception $e) {
$this->logger->error('Logout error', [
'error' => $e->getMessage(),
'user_id' => $_SESSION['USER_ID'] ?? 'unknown'
]);
return ['success' => false, 'message' => 'An error occurred during logout'];
}
}
/**
* Check if user is authenticated
* @return bool True if authenticated
*/
public function isAuthenticated()
{
if (!isset($_SESSION['USER_ID']) || !isset($_SESSION['SESSION_TOKEN'])) {
return false;
}
// Check session timeout
$lastActivity = $_SESSION['LAST_ACTIVITY'] ?? 0;
if (time() - $lastActivity > $this->sessionTimeout) {
$this->logout();
return false;
}
// Update last activity
$_SESSION['LAST_ACTIVITY'] = time();
// Verify session in database
$sessionId = session_id();
$sql = "SELECT `user_id` FROM `db_sessions` WHERE `session_id` = ? AND `expires_at` > NOW()";
$result = $this->db->dbConnection()->Execute($sql, [$sessionId]);
if (!$result || $result->EOF) {
$this->logout();
return false;
}
return true;
}
/**
* Get current authenticated user
* @return array|null User data or null if not authenticated
*/
public function getCurrentUser()
{
if (!$this->isAuthenticated()) {
return null;
}
return [
'user_id' => $_SESSION['USER_ID'],
'username' => $_SESSION['USERNAME'],
'email' => $_SESSION['EMAIL'],
'role' => $_SESSION['ROLE']
];
}
/**
* Request password reset
* @param string $email Email address
* @return array Result with success status and message
*/
public function requestPasswordReset($email)
{
try {
$email = VSecurity::validateInput($email, 'email');
if (!$email) {
return ['success' => false, 'message' => 'Invalid email address'];
}
// Rate limiting for password reset requests
$rateLimitKey = "password_reset_{$email}";
if (!VSecurity::checkRateLimit($rateLimitKey, 3, 3600, 'password_reset')) {
return ['success' => false, 'message' => 'Too many password reset requests. Please try again later.'];
}
// Find user by email
$sql = "SELECT `user_id`, `username`, `email` FROM `db_users` WHERE `email` = ? AND `status` = 'active'";
$result = $this->db->dbConnection()->Execute($sql, [$email]);
if (!$result || $result->EOF) {
// Don't reveal if email exists or not
return ['success' => true, 'message' => 'If the email exists, a password reset link has been sent.'];
}
$user = $result->fields;
// Generate reset token
$resetToken = bin2hex(random_bytes(32));
$expiresAt = date('Y-m-d H:i:s', time() + 3600); // 1 hour expiry
// Store reset token
$tokenSql = "UPDATE `db_users` SET `reset_token` = ?, `reset_expires` = ? WHERE `user_id` = ?";
$tokenResult = $this->db->dbConnection()->Execute($tokenSql, [$resetToken, $expiresAt, $user['user_id']]);
if (!$tokenResult) {
return ['success' => false, 'message' => 'Failed to generate reset token'];
}
// Send reset email
$this->sendPasswordResetEmail($user['email'], $user['username'], $resetToken);
$this->logger->info('Password reset requested', [
'user_id' => $user['user_id'],
'email' => $email
]);
return ['success' => true, 'message' => 'If the email exists, a password reset link has been sent.'];
} catch (Exception $e) {
$this->logger->error('Password reset request error', [
'error' => $e->getMessage(),
'email' => $email ?? 'unknown'
]);
return ['success' => false, 'message' => 'An error occurred while processing your request'];
}
}
/**
* Reset password with token
* @param string $token Reset token
* @param string $newPassword New password
* @return array Result with success status and message
*/
public function resetPassword($token, $newPassword)
{
try {
$token = VSecurity::validateInput($token, 'alphanum');
if (!$token) {
return ['success' => false, 'message' => 'Invalid reset token'];
}
if (strlen($newPassword) < 8) {
return ['success' => false, 'message' => 'Password must be at least 8 characters long'];
}
if (!$this->isPasswordStrong($newPassword)) {
return ['success' => false, 'message' => 'Password must contain at least one uppercase letter, one lowercase letter, one number, and one special character'];
}
// Find user by reset token
$sql = "SELECT `user_id`, `username`, `email` FROM `db_users`
WHERE `reset_token` = ? AND `reset_expires` > NOW() AND `status` = 'active'";
$result = $this->db->dbConnection()->Execute($sql, [$token]);
if (!$result || $result->EOF) {
$this->logger->logSecurityEvent('Invalid password reset attempt', ['token' => $token]);
return ['success' => false, 'message' => 'Invalid or expired reset token'];
}
$user = $result->fields;
// Hash new password
$passwordHash = password_hash($newPassword, PASSWORD_DEFAULT);
// Update password and clear reset token
$updateSql = "UPDATE `db_users` SET
`password_hash` = ?,
`reset_token` = NULL,
`reset_expires` = NULL,
`updated_at` = NOW()
WHERE `user_id` = ?";
$updateResult = $this->db->dbConnection()->Execute($updateSql, [$passwordHash, $user['user_id']]);
if (!$updateResult) {
return ['success' => false, 'message' => 'Failed to update password'];
}
// Invalidate all existing sessions for this user
$this->invalidateUserSessions($user['user_id']);
$this->logger->info('Password reset successfully', [
'user_id' => $user['user_id'],
'username' => $user['username']
]);
return ['success' => true, 'message' => 'Password reset successfully. Please log in with your new password.'];
} catch (Exception $e) {
$this->logger->error('Password reset error', [
'error' => $e->getMessage(),
'token' => $token ?? 'unknown'
]);
return ['success' => false, 'message' => 'An error occurred while resetting your password'];
}
}
/**
* Helper methods
*/
private function userExists($username, $email)
{
$sql = "SELECT COUNT(*) as count FROM `db_users` WHERE `username` = ? OR `email` = ?";
$result = $this->db->dbConnection()->Execute($sql, [$username, $email]);
return $result && !$result->EOF && $result->fields['count'] > 0;
}
private function isPasswordStrong($password)
{
return preg_match('/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]/', $password);
}
private function findUserByIdentifier($identifier)
{
$sql = "SELECT * FROM `db_users` WHERE (`username` = ? OR `email` = ?) AND `status` != 'deleted'";
$result = $this->db->dbConnection()->Execute($sql, [$identifier, $identifier]);
return ($result && !$result->EOF) ? $result->fields : null;
}
private function updateLastLogin($userId)
{
$sql = "UPDATE `db_users` SET `last_login` = NOW() WHERE `user_id` = ?";
$this->db->dbConnection()->Execute($sql, [$userId]);
}
private function invalidateUserSessions($userId)
{
$sql = "DELETE FROM `db_sessions` WHERE `user_id` = ?";
$this->db->dbConnection()->Execute($sql, [$userId]);
}
private function getClientIP()
{
$ipKeys = [
'HTTP_CF_CONNECTING_IP',
'HTTP_X_FORWARDED_FOR',
'HTTP_X_FORWARDED',
'HTTP_X_CLUSTER_CLIENT_IP',
'HTTP_FORWARDED_FOR',
'HTTP_FORWARDED',
'REMOTE_ADDR'
];
foreach ($ipKeys as $key) {
if (array_key_exists($key, $_SERVER) === true) {
$ip = $_SERVER[$key];
if (strpos($ip, ',') !== false) {
$ip = explode(',', $ip)[0];
}
$ip = trim($ip);
if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) {
return $ip;
}
}
}
return $_SERVER['REMOTE_ADDR'] ?? 'unknown';
}
private function sendVerificationEmail($email, $username, $token)
{
// Implementation depends on your email system
// This is a placeholder for the email sending logic
$this->logger->info('Verification email sent', [
'email' => $email,
'username' => $username,
'token' => substr($token, 0, 8) . '...' // Log partial token for debugging
]);
}
private function sendPasswordResetEmail($email, $username, $token)
{
// Implementation depends on your email system
// This is a placeholder for the email sending logic
$this->logger->info('Password reset email sent', [
'email' => $email,
'username' => $username,
'token' => substr($token, 0, 8) . '...' // Log partial token for debugging
]);
}
/**
* Generate JWT token for API authentication
* @param array $user User data
* @param int|null $expiryTime Optional custom expiry time in seconds
* @return string JWT token
*/
public function generateJWTToken($user, $expiryTime = null)
{
try {
$expiryTime = $expiryTime ?? (24 * 60 * 60); // Default 24 hours
$header = json_encode([
'typ' => 'JWT',
'alg' => 'HS256'
]);
$payload = json_encode([
'user_id' => $user['user_id'],
'username' => $user['username'],
'email' => $user['email'],
'role' => $user['role'] ?? 'member',
'iat' => time(),
'exp' => time() + $expiryTime
]);
$headerEncoded = $this->base64UrlEncode($header);
$payloadEncoded = $this->base64UrlEncode($payload);
$jwtSecret = getenv('JWT_SECRET') ?: (defined('JWT_SECRET') ? JWT_SECRET : 'change_this_jwt_secret');
$signature = hash_hmac('sha256', $headerEncoded . '.' . $payloadEncoded, $jwtSecret, true);
$signatureEncoded = $this->base64UrlEncode($signature);
return $headerEncoded . '.' . $payloadEncoded . '.' . $signatureEncoded;
} catch (Exception $e) {
$this->logger->error('JWT generation failed', [
'error' => $e->getMessage(),
'user_id' => $user['user_id'] ?? 'unknown'
]);
throw $e;
}
}
/**
* Validate JWT token
* @param string $token JWT token
* @return array|null User data or null if invalid
*/
public function validateJWTToken($token)
{
try {
$parts = explode('.', $token);
if (count($parts) !== 3) {
$this->logger->logSecurityEvent('Invalid JWT format', ['token' => substr($token, 0, 20) . '...']);
return null;
}
list($headerEncoded, $payloadEncoded, $signatureProvided) = $parts;
// Verify signature
$jwtSecret = getenv('JWT_SECRET') ?: (defined('JWT_SECRET') ? JWT_SECRET : 'change_this_jwt_secret');
$expectedSignature = hash_hmac('sha256', $headerEncoded . '.' . $payloadEncoded, $jwtSecret, true);
$expectedSignatureEncoded = $this->base64UrlEncode($expectedSignature);
if (!hash_equals($expectedSignatureEncoded, $signatureProvided)) {
$this->logger->logSecurityEvent('JWT signature verification failed', []);
return null;
}
// Decode payload
$payload = json_decode($this->base64UrlDecode($payloadEncoded), true);
if (!$payload || !isset($payload['user_id'])) {
$this->logger->logSecurityEvent('Invalid JWT payload', []);
return null;
}
// Check expiration
if (isset($payload['exp']) && $payload['exp'] < time()) {
$this->logger->logSecurityEvent('JWT token expired', ['user_id' => $payload['user_id']]);
return null;
}
// Verify user exists and is active
$sql = "SELECT `user_id`, `username`, `email`, `role`, `status`
FROM `db_users`
WHERE `user_id` = ? AND `status` = 'active'";
$result = $this->db->dbConnection()->Execute($sql, [$payload['user_id']]);
if (!$result || $result->EOF) {
$this->logger->logSecurityEvent('JWT user not found or inactive', ['user_id' => $payload['user_id']]);
return null;
}
$user = $result->fields;
return [
'user_id' => $user['user_id'],
'username' => $user['username'],
'email' => $user['email'],
'role' => $user['role']
];
} catch (Exception $e) {
$this->logger->error('JWT validation error', [
'error' => $e->getMessage(),
'token' => substr($token, 0, 20) . '...'
]);
return null;
}
}
/**
* Login with JWT token return (for API clients)
* @param string $identifier Username or email
* @param string $password Password
* @param int|null $expiryTime Optional token expiry time
* @return array Result with token and user data
*/
public function loginWithToken($identifier, $password, $expiryTime = null)
{
try {
// Use regular login to validate credentials
$loginResult = $this->login($identifier, $password, false);
if (!$loginResult['success']) {
return $loginResult;
}
// Generate JWT token
$token = $this->generateJWTToken($loginResult['user'], $expiryTime);
return [
'success' => true,
'message' => 'Login successful',
'token' => $token,
'token_type' => 'Bearer',
'expires_in' => $expiryTime ?? (24 * 60 * 60),
'user' => $loginResult['user']
];
} catch (Exception $e) {
$this->logger->error('Token login error', [
'error' => $e->getMessage(),
'identifier' => $identifier ?? 'unknown'
]);
return ['success' => false, 'message' => 'An error occurred during login'];
}
}
/**
* Authenticate request via Bearer token (for API requests)
* @param string|null $authHeader Authorization header value
* @return array|null User data or null if not authenticated
*/
public function authenticateBearer($authHeader = null)
{
try {
// Get Authorization header if not provided
if ($authHeader === null) {
$authHeader = $_SERVER['HTTP_AUTHORIZATION'] ?? $_SERVER['REDIRECT_HTTP_AUTHORIZATION'] ?? '';
// Apache mod_rewrite workaround
if (empty($authHeader) && function_exists('apache_request_headers')) {
$headers = apache_request_headers();
$authHeader = $headers['Authorization'] ?? $headers['authorization'] ?? '';
}
}
if (empty($authHeader)) {
return null;
}
// Extract Bearer token
if (strpos($authHeader, 'Bearer ') === 0) {
$token = substr($authHeader, 7);
return $this->validateJWTToken($token);
}
return null;
} catch (Exception $e) {
$this->logger->error('Bearer authentication error', ['error' => $e->getMessage()]);
return null;
}
}
/**
* Base64 URL-safe encoding
* @param string $data Data to encode
* @return string Encoded data
*/
private function base64UrlEncode($data)
{
return rtrim(strtr(base64_encode($data), '+/', '-_'), '=');
}
/**
* Base64 URL-safe decoding
* @param string $data Data to decode
* @return string Decoded data
*/
private function base64UrlDecode($data)
{
return base64_decode(strtr($data, '-_', '+/'));
}
}