jilo-web/app/pages/login.php

323 lines
12 KiB
PHP

<?php
/**
* User login
*
* This page ("login") handles user login, session management, cookie handling, and error logging.
* Supports "remember me" functionality to extend session duration and two-factor authentication.
*
* Actions Performed:
* - Validates login credentials
* - Handles two-factor authentication if enabled
* - Manages session and cookies based on "remember me" option
* - Logs successful and failed login attempts
* - Displays login form and optional custom messages
*/
// clear the global error var before login
unset($error);
try {
// connect to database
$db = connectDB($config)['db'];
// Initialize RateLimiter
require_once '../app/classes/ratelimiter.php';
$rateLimiter = new RateLimiter($db);
// Get user IP
$user_IP = getUserIP();
$action = $_REQUEST['action'] ?? '';
if ($action === 'verify' && isset($_SESSION['2fa_pending_user_id'])) {
// Handle 2FA verification
$code = $_POST['code'] ?? '';
$userId = $_SESSION['2fa_pending_user_id'];
$username = $_SESSION['2fa_pending_username'];
$rememberMe = isset($_SESSION['2fa_pending_remember']);
require_once '../app/classes/twoFactorAuth.php';
$twoFactorAuth = new TwoFactorAuthentication($db);
if ($twoFactorAuth->verify($userId, $code)) {
// Complete login
handleSuccessfulLogin($userId, $username, $rememberMe, $config, $logObject, $user_IP);
// Clean up 2FA session data
unset($_SESSION['2fa_pending_user_id']);
unset($_SESSION['2fa_pending_username']);
unset($_SESSION['2fa_pending_remember']);
exit();
}
// If we get here (and we have code submitted), verification failed
if (!empty($code)) {
Feedback::flash('ERROR', 'DEFAULT', 'Invalid verification code');
}
// Get any new feedback messages
include '../app/helpers/feedback.php';
// Load the 2FA verification template
include '../app/templates/credentials-2fa-verify.php';
exit();
} elseif ($action === 'forgot') {
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// Handle password reset request
try {
// Validate CSRF token
$security = SecurityHelper::getInstance();
if (!$security->verifyCsrfToken($_POST['csrf_token'] ?? '')) {
throw new Exception('Invalid security token. Please try again.');
}
// Apply rate limiting
if (!$rateLimiter->isIpWhitelisted($user_IP)) {
if ($rateLimiter->isIpBlacklisted($user_IP)) {
throw new Exception(Feedback::get('LOGIN', 'IP_BLACKLISTED')['message']);
}
if ($rateLimiter->tooManyAttempts('password_reset', $user_IP)) {
throw new Exception(Feedback::get('LOGIN', 'TOO_MANY_ATTEMPTS')['message']);
}
$rateLimiter->attempt('password_reset', $user_IP);
}
// Validate email
$email = filter_var($_POST['email'] ?? '', FILTER_VALIDATE_EMAIL);
if (!$email) {
throw new Exception('Please enter a valid email address.');
}
// Process reset request
require_once '../app/classes/passwordReset.php';
$resetHandler = new PasswordReset($db);
$result = $resetHandler->requestReset($email);
// Always show same message whether email exists or not for security
Feedback::flash('NOTICE', 'DEFAULT', $result['message']);
header("Location: $app_root?page=login");
exit();
} catch (Exception $e) {
Feedback::flash('ERROR', 'DEFAULT', $e->getMessage());
}
}
// Generate CSRF token
$security = SecurityHelper::getInstance();
$security->generateCsrfToken();
// Load the forgot password form
include '../app/helpers/feedback.php';
include '../app/templates/form-password-forgot.php';
exit();
} elseif ($action === 'reset' && isset($_GET['token'])) {
// Handle password reset
try {
require_once '../app/classes/passwordReset.php';
$resetHandler = new PasswordReset($db);
$token = $_GET['token'];
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// Validate CSRF token
$security = SecurityHelper::getInstance();
if (!$security->verifyCsrfToken($_POST['csrf_token'] ?? '')) {
throw new Exception('Invalid security token. Please try again.');
}
// Apply rate limiting
if (!$rateLimiter->isIpWhitelisted($user_IP)) {
if ($rateLimiter->tooManyAttempts('password_reset', $user_IP)) {
throw new Exception(Feedback::get('LOGIN', 'TOO_MANY_ATTEMPTS')['message']);
}
$rateLimiter->attempt('password_reset', $user_IP);
}
// Validate password
require_once '../app/classes/validator.php';
$validator = new Validator($_POST);
$rules = [
'new_password' => [
'required' => true,
'min' => 8
],
'confirm_password' => [
'required' => true,
'matches' => 'new_password'
]
];
if (!$validator->validate($rules)) {
throw new Exception($validator->getFirstError());
}
// Reset password
if ($resetHandler->resetPassword($token, $_POST['new_password'])) {
Feedback::flash('NOTICE', 'DEFAULT', 'Your password has been reset successfully. You can now log in.');
header("Location: $app_root?page=login");
exit();
}
throw new Exception('Invalid or expired reset link. Please request a new one.');
}
// Verify token is valid
$validation = $resetHandler->validateToken($token);
if (!$validation['valid']) {
throw new Exception('Invalid or expired reset link. Please request a new one.');
}
// Show reset password form
include '../app/helpers/feedback.php';
include '../app/templates/form-password-reset.php';
exit();
} catch (Exception $e) {
Feedback::flash('ERROR', 'DEFAULT', $e->getMessage());
header("Location: $app_root?page=login&action=forgot");
exit();
}
}
if ( $_SERVER['REQUEST_METHOD'] == 'POST' && $action !== 'verify' ) {
try {
// Validate form data
$security = SecurityHelper::getInstance();
$formData = $security->sanitizeArray($_POST, ['username', 'password', 'remember_me', 'csrf_token']);
$validationRules = [
'username' => [
'type' => 'string',
'required' => true,
'min' => 3,
'max' => 20
],
'password' => [
'type' => 'string',
'required' => true,
'min' => 5
]
];
$errors = $security->validateFormData($formData, $validationRules);
if (!empty($errors)) {
throw new Exception("Invalid input: " . implode(", ", $errors));
}
$username = $formData['username'];
$password = $formData['password'];
// Skip all checks if IP is whitelisted
if (!$rateLimiter->isIpWhitelisted($user_IP)) {
// Check if IP is blacklisted
if ($rateLimiter->isIpBlacklisted($user_IP)) {
throw new Exception(Feedback::get('LOGIN', 'IP_BLACKLISTED')['message']);
}
// Check rate limiting before recording attempt
if ($rateLimiter->tooManyAttempts($username, $user_IP)) {
throw new Exception(Feedback::get('LOGIN', 'TOO_MANY_ATTEMPTS')['message']);
}
// Record this attempt
$rateLimiter->attempt($username, $user_IP);
}
// Attempt login
$loginResult = $userObject->login($username, $password);
if (is_array($loginResult)) {
switch ($loginResult['status']) {
case 'requires_2fa':
// Store pending 2FA info
$_SESSION['2fa_pending_user_id'] = $loginResult['user_id'];
$_SESSION['2fa_pending_username'] = $loginResult['username'];
if (isset($formData['remember_me'])) {
$_SESSION['2fa_pending_remember'] = true;
}
// Redirect to 2FA verification
header('Location: ?page=login&action=verify');
exit();
case 'success':
// Complete login
handleSuccessfulLogin($loginResult['user_id'], $loginResult['username'],
isset($formData['remember_me']), $config, $logObject, $user_IP);
exit();
default:
throw new Exception($loginResult['message'] ?? 'Login failed');
}
}
throw new Exception(Feedback::get('LOGIN', 'LOGIN_FAILED')['message']);
} catch (Exception $e) {
// Log the failed attempt
Feedback::flash('ERROR', 'DEFAULT', $e->getMessage());
if (isset($username)) {
$user_id = $userObject->getUserId($username)[0]['id'] ?? 0;
$logObject->insertLog($user_id, "Login: Failed login attempt for user \"$username\". IP: $user_IP. Reason: {$e->getMessage()}", 'user');
}
}
}
} catch (Exception $e) {
Feedback::flash('ERROR', 'DEFAULT');
}
// Show configured login message if any
if (!empty($config['login_message'])) {
echo Feedback::render('NOTICE', 'DEFAULT', $config['login_message'], false);
}
// Get any new feedback messages
include '../app/helpers/feedback.php';
// Load the template
include '../app/templates/form-login.php';
/**
* Handle successful login by setting up session and cookies
*/
function handleSuccessfulLogin($userId, $username, $rememberMe, $config, $logObject, $userIP) {
if ($rememberMe) {
// 30*24*60*60 = 30 days
$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
$logObject->insertLog($userId, "Login: User \"$username\" logged in. IP: $userIP", 'user');
// Set success message and redirect
Feedback::flash('LOGIN', 'LOGIN_SUCCESS');
header('Location: ' . htmlspecialchars($app_root));
}