Compare commits
No commits in common. "71b0448004094c5ed4c8d0058a944ec824588ede" and "e00599b4f03db32970350709801edeb3c6eabd75" have entirely different histories.
71b0448004
...
e00599b4f0
|
@ -8,12 +8,11 @@
|
||||||
*/
|
*/
|
||||||
class TwoFactorAuthentication {
|
class TwoFactorAuthentication {
|
||||||
private $db;
|
private $db;
|
||||||
private $secretLength = 20; // 160 bits for SHA1
|
private $secretLength = 32;
|
||||||
private $period = 30; // Time step in seconds (T0)
|
private $period = 30; // Time step in seconds
|
||||||
private $digits = 6; // Number of digits in TOTP code
|
private $digits = 6; // Number of digits in TOTP code
|
||||||
private $algorithm = 'sha1'; // HMAC algorithm
|
private $algorithm = 'sha1';
|
||||||
private $issuer = 'TotalMeet';
|
private $issuer = 'Jilo';
|
||||||
private $window = 1; // Time window of 1 step before/after
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructor
|
* Constructor
|
||||||
|
@ -32,11 +31,9 @@ class TwoFactorAuthentication {
|
||||||
* Enable 2FA for a user
|
* Enable 2FA for a user
|
||||||
*
|
*
|
||||||
* @param int $userId User ID
|
* @param int $userId User ID
|
||||||
* @param string $secret Secret key (base32 encoded)
|
* @return array Array containing success status and data (secret, QR code URL)
|
||||||
* @param string $code Verification code
|
|
||||||
* @return bool True if enabled successfully
|
|
||||||
*/
|
*/
|
||||||
public function enable($userId, $secret = null, $code = null) {
|
public function enable($userId) {
|
||||||
try {
|
try {
|
||||||
// Check if 2FA is already enabled
|
// Check if 2FA is already enabled
|
||||||
$stmt = $this->db->prepare('SELECT enabled FROM user_2fa WHERE user_id = ?');
|
$stmt = $this->db->prepare('SELECT enabled FROM user_2fa WHERE user_id = ?');
|
||||||
|
@ -44,23 +41,21 @@ class TwoFactorAuthentication {
|
||||||
$existing = $stmt->fetch(PDO::FETCH_ASSOC);
|
$existing = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
if ($existing && $existing['enabled']) {
|
if ($existing && $existing['enabled']) {
|
||||||
return false;
|
return ['success' => false, 'message' => '2FA is already enabled'];
|
||||||
}
|
}
|
||||||
|
|
||||||
// If no secret provided, generate one and return setup data
|
|
||||||
if ($secret === null) {
|
|
||||||
// Generate secret key
|
// Generate secret key
|
||||||
$secret = $this->generateSecret();
|
$secret = $this->generateSecret();
|
||||||
|
|
||||||
// Get user's username for the QR code
|
// Get user's username for the QR code
|
||||||
$stmt = $this->db->prepare('SELECT username FROM user WHERE id = ?');
|
$stmt = $this->db->prepare('SELECT username FROM users WHERE id = ?');
|
||||||
$stmt->execute([$userId]);
|
$stmt->execute([$userId]);
|
||||||
$user = $stmt->fetch(PDO::FETCH_ASSOC);
|
$user = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
// Generate backup codes
|
// Generate backup codes
|
||||||
$backupCodes = $this->generateBackupCodes();
|
$backupCodes = $this->generateBackupCodes();
|
||||||
|
|
||||||
// Store in database without enabling yet
|
// Store in database
|
||||||
$this->db->beginTransaction();
|
$this->db->beginTransaction();
|
||||||
|
|
||||||
$stmt = $this->db->prepare('
|
$stmt = $this->db->prepare('
|
||||||
|
@ -92,33 +87,12 @@ class TwoFactorAuthentication {
|
||||||
'backupCodes' => $backupCodes
|
'backupCodes' => $backupCodes
|
||||||
]
|
]
|
||||||
];
|
];
|
||||||
}
|
|
||||||
|
|
||||||
// If secret and code provided, verify the code and enable 2FA
|
|
||||||
if ($code !== null) {
|
|
||||||
// Verify the setup code
|
|
||||||
if (!$this->verify($userId, $code)) {
|
|
||||||
error_log("Code verification failed");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Enable 2FA
|
|
||||||
$stmt = $this->db->prepare('
|
|
||||||
UPDATE user_2fa
|
|
||||||
SET enabled = 1
|
|
||||||
WHERE user_id = ? AND secret_key = ?
|
|
||||||
');
|
|
||||||
return $stmt->execute([$userId, $secret]);
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
|
|
||||||
} catch (Exception $e) {
|
} catch (Exception $e) {
|
||||||
if ($this->db->inTransaction()) {
|
if ($this->db->inTransaction()) {
|
||||||
$this->db->rollBack();
|
$this->db->rollBack();
|
||||||
}
|
}
|
||||||
error_log('2FA enable error: ' . $e->getMessage());
|
return ['success' => false, 'message' => $e->getMessage()];
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -132,24 +106,37 @@ class TwoFactorAuthentication {
|
||||||
public function verify($userId, $code) {
|
public function verify($userId, $code) {
|
||||||
try {
|
try {
|
||||||
// Get user's 2FA settings
|
// Get user's 2FA settings
|
||||||
$settings = $this->getUserSettings($userId);
|
$stmt = $this->db->prepare('
|
||||||
if (!$settings) {
|
SELECT secret_key, backup_codes, enabled
|
||||||
|
FROM user_2fa
|
||||||
|
WHERE user_id = ?
|
||||||
|
');
|
||||||
|
$stmt->execute([$userId]);
|
||||||
|
$tfa = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
if (!$tfa || !$tfa['enabled']) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if code matches a backup code
|
// Check if it's a backup code
|
||||||
if ($this->verifyBackupCode($userId, $code)) {
|
if ($this->verifyBackupCode($userId, $code)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get current Unix timestamp
|
// Verify TOTP code
|
||||||
$currentTime = time();
|
$currentTime = time();
|
||||||
|
|
||||||
// Check time window
|
// Check current and adjacent time steps
|
||||||
for ($timeSlot = -$this->window; $timeSlot <= $this->window; $timeSlot++) {
|
for ($timeStep = -1; $timeStep <= 1; $timeStep++) {
|
||||||
$checkTime = $currentTime + ($timeSlot * $this->period);
|
$checkTime = $currentTime + ($timeStep * $this->period);
|
||||||
$generatedCode = $this->generateCode($settings['secret_key'], $checkTime);
|
if ($this->generateCode($tfa['secret_key'], $checkTime) === $code) {
|
||||||
if (hash_equals($generatedCode, $code)) {
|
// Update last used timestamp
|
||||||
|
$stmt = $this->db->prepare('
|
||||||
|
UPDATE user_2fa
|
||||||
|
SET last_used = NOW()
|
||||||
|
WHERE user_id = ?
|
||||||
|
');
|
||||||
|
$stmt->execute([$userId]);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -162,13 +149,49 @@ class TwoFactorAuthentication {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate a TOTP code for a given secret and time
|
||||||
|
*
|
||||||
|
* @param string $secret The secret key
|
||||||
|
* @param int $time Current Unix timestamp
|
||||||
|
* @return string Generated code
|
||||||
|
*/
|
||||||
|
private function generateCode($secret, $time) {
|
||||||
|
$timeStep = floor($time / $this->period);
|
||||||
|
$timeHex = str_pad(dechex($timeStep), 16, '0', STR_PAD_LEFT);
|
||||||
|
|
||||||
|
// Convert hex time to binary
|
||||||
|
$timeBin = '';
|
||||||
|
for ($i = 0; $i < strlen($timeHex); $i += 2) {
|
||||||
|
$timeBin .= chr(hexdec(substr($timeHex, $i, 2)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get binary secret
|
||||||
|
$secretBin = $this->base32Decode($secret);
|
||||||
|
|
||||||
|
// Calculate HMAC
|
||||||
|
$hash = hash_hmac($this->algorithm, $timeBin, $secretBin, true);
|
||||||
|
|
||||||
|
// Get offset
|
||||||
|
$offset = ord($hash[strlen($hash) - 1]) & 0xF;
|
||||||
|
|
||||||
|
// Generate 4-byte code
|
||||||
|
$code = (
|
||||||
|
((ord($hash[$offset]) & 0x7F) << 24) |
|
||||||
|
((ord($hash[$offset + 1]) & 0xFF) << 16) |
|
||||||
|
((ord($hash[$offset + 2]) & 0xFF) << 8) |
|
||||||
|
(ord($hash[$offset + 3]) & 0xFF)
|
||||||
|
) % pow(10, $this->digits);
|
||||||
|
|
||||||
|
return str_pad($code, $this->digits, '0', STR_PAD_LEFT);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate a random secret key
|
* Generate a random secret key
|
||||||
*
|
*
|
||||||
* @return string Base32 encoded secret
|
* @return string Base32 encoded secret
|
||||||
*/
|
*/
|
||||||
private function generateSecret() {
|
private function generateSecret() {
|
||||||
// Generate random bytes (160 bits for SHA1)
|
|
||||||
$random = random_bytes($this->secretLength);
|
$random = random_bytes($this->secretLength);
|
||||||
return $this->base32Encode($random);
|
return $this->base32Encode($random);
|
||||||
}
|
}
|
||||||
|
@ -191,19 +214,10 @@ class TwoFactorAuthentication {
|
||||||
|
|
||||||
// Process 5 bits at a time
|
// Process 5 bits at a time
|
||||||
for ($i = 0; $i < strlen($binary); $i += 5) {
|
for ($i = 0; $i < strlen($binary); $i += 5) {
|
||||||
$chunk = substr($binary, $i, 5);
|
$chunk = substr($binary . '0000', $i, 5);
|
||||||
if (strlen($chunk) < 5) {
|
|
||||||
$chunk = str_pad($chunk, 5, '0', STR_PAD_RIGHT);
|
|
||||||
}
|
|
||||||
$encoded .= $alphabet[bindec($chunk)];
|
$encoded .= $alphabet[bindec($chunk)];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add padding
|
|
||||||
$padding = strlen($encoded) % 8;
|
|
||||||
if ($padding > 0) {
|
|
||||||
$encoded .= str_repeat('=', 8 - $padding);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $encoded;
|
return $encoded;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -215,22 +229,16 @@ class TwoFactorAuthentication {
|
||||||
*/
|
*/
|
||||||
private function base32Decode($data) {
|
private function base32Decode($data) {
|
||||||
$alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
|
$alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
|
||||||
|
|
||||||
// Remove padding and uppercase
|
|
||||||
$data = rtrim(strtoupper($data), '=');
|
|
||||||
|
|
||||||
$binary = '';
|
$binary = '';
|
||||||
|
$decoded = '';
|
||||||
|
|
||||||
// Convert to binary
|
// Convert to binary
|
||||||
for ($i = 0; $i < strlen($data); $i++) {
|
for ($i = 0; $i < strlen($data); $i++) {
|
||||||
$position = strpos($alphabet, $data[$i]);
|
$position = strpos($alphabet, $data[$i]);
|
||||||
if ($position === false) {
|
if ($position === false) continue;
|
||||||
continue;
|
|
||||||
}
|
|
||||||
$binary .= str_pad(decbin($position), 5, '0', STR_PAD_LEFT);
|
$binary .= str_pad(decbin($position), 5, '0', STR_PAD_LEFT);
|
||||||
}
|
}
|
||||||
|
|
||||||
$decoded = '';
|
|
||||||
// Process 8 bits at a time
|
// Process 8 bits at a time
|
||||||
for ($i = 0; $i + 7 < strlen($binary); $i += 8) {
|
for ($i = 0; $i + 7 < strlen($binary); $i += 8) {
|
||||||
$chunk = substr($binary, $i, 8);
|
$chunk = substr($binary, $i, 8);
|
||||||
|
@ -240,60 +248,23 @@ class TwoFactorAuthentication {
|
||||||
return $decoded;
|
return $decoded;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Generate a TOTP code for a given secret and time
|
|
||||||
* RFC 6238 compliant implementation
|
|
||||||
*/
|
|
||||||
private function generateCode($secret, $time) {
|
|
||||||
// Calculate number of time steps since Unix epoch
|
|
||||||
$timeStep = (int)floor($time / $this->period);
|
|
||||||
|
|
||||||
// Pack time into 8 bytes (64-bit big-endian)
|
|
||||||
$timeBin = pack('J', $timeStep);
|
|
||||||
|
|
||||||
// Clean secret of any padding
|
|
||||||
$secret = rtrim($secret, '=');
|
|
||||||
|
|
||||||
// Get binary secret
|
|
||||||
$secretBin = $this->base32Decode($secret);
|
|
||||||
|
|
||||||
// Calculate HMAC
|
|
||||||
$hash = hash_hmac($this->algorithm, $timeBin, $secretBin, true);
|
|
||||||
|
|
||||||
// Get dynamic truncation offset
|
|
||||||
$offset = ord($hash[strlen($hash) - 1]) & 0xF;
|
|
||||||
|
|
||||||
// Generate 31-bit number
|
|
||||||
$code = (
|
|
||||||
((ord($hash[$offset]) & 0x7F) << 24) |
|
|
||||||
((ord($hash[$offset + 1]) & 0xFF) << 16) |
|
|
||||||
((ord($hash[$offset + 2]) & 0xFF) << 8) |
|
|
||||||
(ord($hash[$offset + 3]) & 0xFF)
|
|
||||||
) % pow(10, $this->digits);
|
|
||||||
|
|
||||||
$code = str_pad($code, $this->digits, '0', STR_PAD_LEFT);
|
|
||||||
|
|
||||||
return $code;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate otpauth URL for QR codes
|
* Generate otpauth URL for QR codes
|
||||||
* Format: otpauth://totp/ISSUER:ACCOUNT?secret=SECRET&issuer=ISSUER&algorithm=ALGORITHM&digits=DIGITS&period=PERIOD
|
*
|
||||||
|
* @param string $username Username
|
||||||
|
* @param string $secret Secret key
|
||||||
|
* @return string otpauth URL
|
||||||
*/
|
*/
|
||||||
private function generateOtpauthUrl($username, $secret) {
|
private function generateOtpauthUrl($username, $secret) {
|
||||||
$params = [
|
|
||||||
'secret' => $secret,
|
|
||||||
'issuer' => $this->issuer,
|
|
||||||
'algorithm' => strtoupper($this->algorithm),
|
|
||||||
'digits' => $this->digits,
|
|
||||||
'period' => $this->period
|
|
||||||
];
|
|
||||||
|
|
||||||
return sprintf(
|
return sprintf(
|
||||||
'otpauth://totp/%s:%s?%s',
|
'otpauth://totp/%s:%s?secret=%s&issuer=%s&algorithm=%s&digits=%d&period=%d',
|
||||||
rawurlencode($this->issuer),
|
urlencode($this->issuer),
|
||||||
rawurlencode($username),
|
urlencode($username),
|
||||||
http_build_query($params)
|
$secret,
|
||||||
|
urlencode($this->issuer),
|
||||||
|
strtoupper($this->algorithm),
|
||||||
|
$this->digits,
|
||||||
|
$this->period
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -364,15 +335,11 @@ class TwoFactorAuthentication {
|
||||||
*/
|
*/
|
||||||
public function disable($userId) {
|
public function disable($userId) {
|
||||||
try {
|
try {
|
||||||
// First check if user has 2FA settings
|
|
||||||
$settings = $this->getUserSettings($userId);
|
|
||||||
if (!$settings) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete the 2FA settings entirely instead of just disabling
|
|
||||||
$stmt = $this->db->prepare('
|
$stmt = $this->db->prepare('
|
||||||
DELETE FROM user_2fa
|
UPDATE user_2fa
|
||||||
|
SET enabled = 0,
|
||||||
|
secret_key = NULL,
|
||||||
|
backup_codes = NULL
|
||||||
WHERE user_id = ?
|
WHERE user_id = ?
|
||||||
');
|
');
|
||||||
return $stmt->execute([$userId]);
|
return $stmt->execute([$userId]);
|
||||||
|
@ -401,20 +368,4 @@ class TwoFactorAuthentication {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private function getUserSettings($userId) {
|
|
||||||
try {
|
|
||||||
$stmt = $this->db->prepare('
|
|
||||||
SELECT secret_key, backup_codes, enabled
|
|
||||||
FROM user_2fa
|
|
||||||
WHERE user_id = ?
|
|
||||||
');
|
|
||||||
$stmt->execute([$userId]);
|
|
||||||
return $stmt->fetch(PDO::FETCH_ASSOC);
|
|
||||||
|
|
||||||
} catch (Exception $e) {
|
|
||||||
error_log('Failed to get user 2FA settings: ' . $e->getMessage());
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -507,12 +507,10 @@ class User {
|
||||||
* Enable two-factor authentication for a user
|
* Enable two-factor authentication for a user
|
||||||
*
|
*
|
||||||
* @param int $userId User ID
|
* @param int $userId User ID
|
||||||
* @param string $secret Secret key to use
|
* @return array Result of enabling 2FA
|
||||||
* @param string $code Verification code to validate
|
|
||||||
* @return bool True if enabled successfully
|
|
||||||
*/
|
*/
|
||||||
public function enableTwoFactor($userId, $secret = null, $code = null) {
|
public function enableTwoFactor($userId) {
|
||||||
return $this->twoFactorAuth->enable($userId, $secret, $code);
|
return $this->twoFactorAuth->enable($userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -24,7 +24,7 @@ function applySessionMiddleware($config, $app_root) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if user is logged in
|
// Check if user is logged in
|
||||||
if (!isset($_SESSION['user_id'])) {
|
if (!isset($_SESSION['USER_ID'])) {
|
||||||
if (!$isTest) {
|
if (!$isTest) {
|
||||||
header('Location: ' . $app_root . '?page=login');
|
header('Location: ' . $app_root . '?page=login');
|
||||||
exit();
|
exit();
|
||||||
|
|
|
@ -14,24 +14,14 @@
|
||||||
* - `password`: Change password
|
* - `password`: Change password
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// Check if user is logged in
|
|
||||||
if (!isset($_SESSION['user_id'])) {
|
|
||||||
header("Location: $app_root?page=login");
|
|
||||||
exit();
|
|
||||||
}
|
|
||||||
|
|
||||||
$user_id = $_SESSION['user_id'];
|
|
||||||
|
|
||||||
// Initialize user object
|
|
||||||
$userObject = new User($dbWeb);
|
|
||||||
|
|
||||||
$action = $_REQUEST['action'] ?? '';
|
$action = $_REQUEST['action'] ?? '';
|
||||||
$item = $_REQUEST['item'] ?? '';
|
$item = $_REQUEST['item'] ?? '';
|
||||||
|
|
||||||
// if a form is submitted
|
// if a form is submitted
|
||||||
if ($_SERVER['REQUEST_METHOD'] == 'POST') {
|
if ($_SERVER['REQUEST_METHOD'] == 'POST') {
|
||||||
// Validate CSRF token
|
// Validate CSRF token
|
||||||
$security->verifyCsrfToken($_POST['csrf_token'] ?? '');
|
require_once '../app/helpers/security.php';
|
||||||
|
$security = SecurityHelper::getInstance();
|
||||||
if (!$security->verifyCsrfToken($_POST['csrf_token'] ?? '')) {
|
if (!$security->verifyCsrfToken($_POST['csrf_token'] ?? '')) {
|
||||||
Feedback::flash('ERROR', 'DEFAULT', 'Invalid security token. Please try again.');
|
Feedback::flash('ERROR', 'DEFAULT', 'Invalid security token. Please try again.');
|
||||||
header("Location: $app_root?page=credentials");
|
header("Location: $app_root?page=credentials");
|
||||||
|
@ -44,6 +34,8 @@ if ($_SERVER['REQUEST_METHOD'] == 'POST') {
|
||||||
|
|
||||||
switch ($item) {
|
switch ($item) {
|
||||||
case '2fa':
|
case '2fa':
|
||||||
|
require_once '../app/helpers/2fa.php';
|
||||||
|
|
||||||
switch ($action) {
|
switch ($action) {
|
||||||
case 'setup':
|
case 'setup':
|
||||||
// Validate the setup code
|
// Validate the setup code
|
||||||
|
@ -55,17 +47,13 @@ if ($_SERVER['REQUEST_METHOD'] == 'POST') {
|
||||||
header("Location: $app_root?page=credentials");
|
header("Location: $app_root?page=credentials");
|
||||||
exit();
|
exit();
|
||||||
} else {
|
} else {
|
||||||
// Only show error if code was actually submitted
|
|
||||||
if ($code !== '') {
|
|
||||||
Feedback::flash('ERROR', 'DEFAULT', 'Invalid verification code. Please try again.');
|
Feedback::flash('ERROR', 'DEFAULT', 'Invalid verification code. Please try again.');
|
||||||
}
|
header("Location: $app_root?page=credentials&action=edit");
|
||||||
header("Location: $app_root?page=credentials&action=setup");
|
|
||||||
exit();
|
exit();
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'verify':
|
case 'verify':
|
||||||
// This is a user-initiated verification
|
|
||||||
$code = $_POST['code'] ?? '';
|
$code = $_POST['code'] ?? '';
|
||||||
if ($userObject->verifyTwoFactor($user_id, $code)) {
|
if ($userObject->verifyTwoFactor($user_id, $code)) {
|
||||||
$_SESSION['2fa_verified'] = true;
|
$_SESSION['2fa_verified'] = true;
|
||||||
|
@ -139,16 +127,12 @@ if ($_SERVER['REQUEST_METHOD'] == 'POST') {
|
||||||
$has2fa = $userObject->isTwoFactorEnabled($user_id);
|
$has2fa = $userObject->isTwoFactorEnabled($user_id);
|
||||||
|
|
||||||
switch ($action) {
|
switch ($action) {
|
||||||
case 'setup':
|
case 'edit':
|
||||||
if (!$has2fa) {
|
if (!$has2fa) {
|
||||||
$result = $userObject->enableTwoFactor($user_id);
|
require_once '../app/helpers/2fa.php';
|
||||||
if ($result['success']) {
|
$secret = $userObject->generateTwoFactorSecret();
|
||||||
$setupData = $result['data'];
|
$qrCode = $userObject->generateTwoFactorQR($user_id, $secret);
|
||||||
} else {
|
$backupCodes = $userObject->generateBackupCodes();
|
||||||
Feedback::flash('ERROR', 'DEFAULT', $result['message'] ?? 'Failed to generate 2FA setup data');
|
|
||||||
header("Location: $app_root?page=credentials");
|
|
||||||
exit();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
// Get any new feedback messages
|
// Get any new feedback messages
|
||||||
include '../app/helpers/feedback.php';
|
include '../app/helpers/feedback.php';
|
||||||
|
|
|
@ -4,20 +4,20 @@
|
||||||
* User login
|
* User login
|
||||||
*
|
*
|
||||||
* This page ("login") handles user login, session management, cookie handling, and error logging.
|
* 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.
|
* Supports "remember me" functionality to extend session duration.
|
||||||
*
|
*
|
||||||
* Actions Performed:
|
* Actions Performed:
|
||||||
* - Validates login credentials
|
* - Validates login credentials.
|
||||||
* - Handles two-factor authentication if enabled
|
* - Manages session and cookies based on "remember me" option.
|
||||||
* - Manages session and cookies based on "remember me" option
|
* - Logs successful and failed login attempts.
|
||||||
* - Logs successful and failed login attempts
|
* - Displays login form and optional custom messages.
|
||||||
* - Displays login form and optional custom messages
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// clear the global error var before login
|
// clear the global error var before login
|
||||||
unset($error);
|
unset($error);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
||||||
// connect to database
|
// connect to database
|
||||||
$dbWeb = connectDB($config)['db'];
|
$dbWeb = connectDB($config)['db'];
|
||||||
|
|
||||||
|
@ -28,44 +28,7 @@ try {
|
||||||
// Get user IP
|
// Get user IP
|
||||||
$user_IP = getUserIP();
|
$user_IP = getUserIP();
|
||||||
|
|
||||||
$action = $_REQUEST['action'] ?? '';
|
if ( $_SERVER['REQUEST_METHOD'] == 'POST' ) {
|
||||||
|
|
||||||
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($dbWeb);
|
|
||||||
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
|
|
||||||
if ( $_SERVER['REQUEST_METHOD'] == 'POST' && $action !== 'verify' ) {
|
|
||||||
try {
|
try {
|
||||||
// Validate form data
|
// Validate form data
|
||||||
$security = SecurityHelper::getInstance();
|
$security = SecurityHelper::getInstance();
|
||||||
|
@ -81,7 +44,7 @@ try {
|
||||||
'password' => [
|
'password' => [
|
||||||
'type' => 'string',
|
'type' => 'string',
|
||||||
'required' => true,
|
'required' => true,
|
||||||
'min' => 5
|
'min' => 2
|
||||||
]
|
]
|
||||||
];
|
];
|
||||||
|
|
||||||
|
@ -109,64 +72,10 @@ try {
|
||||||
$rateLimiter->attempt($username, $user_IP);
|
$rateLimiter->attempt($username, $user_IP);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Attempt login
|
// login successful
|
||||||
$loginResult = $userObject->login($username, $password);
|
if ( $userObject->login($username, $password) ) {
|
||||||
|
// if remember_me is checked, max out the session
|
||||||
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'])) {
|
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
|
// 30*24*60*60 = 30 days
|
||||||
$cookie_lifetime = 30 * 24 * 60 * 60;
|
$cookie_lifetime = 30 * 24 * 60 * 60;
|
||||||
$setcookie_lifetime = time() + 30 * 24 * 60 * 60;
|
$setcookie_lifetime = time() + 30 * 24 * 60 * 60;
|
||||||
|
@ -190,17 +99,44 @@ function handleSuccessfulLogin($userId, $username, $rememberMe, $config, $logObj
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Set session variables
|
// Set session variables
|
||||||
$_SESSION['user_id'] = $userId;
|
$_SESSION['USER_ID'] = $userObject->getUserId($username)[0]['id'];
|
||||||
$_SESSION['USERNAME'] = $username;
|
$_SESSION['USERNAME'] = $username;
|
||||||
$_SESSION['LAST_ACTIVITY'] = time();
|
$_SESSION['LAST_ACTIVITY'] = time();
|
||||||
if ($rememberMe) {
|
if (isset($formData['remember_me'])) {
|
||||||
$_SESSION['REMEMBER_ME'] = true;
|
$_SESSION['REMEMBER_ME'] = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Log successful login
|
// Log successful login
|
||||||
$logObject->insertLog($userId, "Login: User \"$username\" logged in. IP: $userIP", 'user');
|
$user_id = $userObject->getUserId($username)[0]['id'];
|
||||||
|
$logObject->insertLog($user_id, "Login: User \"$username\" logged in. IP: $user_IP", 'user');
|
||||||
|
|
||||||
// Set success message and redirect
|
// Set success message and redirect
|
||||||
Feedback::flash('LOGIN', 'LOGIN_SUCCESS');
|
Feedback::flash('LOGIN', 'LOGIN_SUCCESS');
|
||||||
header('Location: ' . htmlspecialchars($app_root));
|
header('Location: ' . htmlspecialchars($app_root));
|
||||||
|
exit();
|
||||||
|
} else {
|
||||||
|
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';
|
||||||
|
|
|
@ -22,7 +22,7 @@
|
||||||
</div>
|
</div>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|
||||||
<?php if (isset($setupData) && is_array($setupData)): ?>
|
<?php if (!empty($setupData)): ?>
|
||||||
<div class="setup-steps">
|
<div class="setup-steps">
|
||||||
<h4>1. Install an authenticator app</h4>
|
<h4>1. Install an authenticator app</h4>
|
||||||
<p>If you haven't already, install an authenticator app on your mobile device:</p>
|
<p>If you haven't already, install an authenticator app on your mobile device:</p>
|
||||||
|
@ -46,7 +46,7 @@
|
||||||
<h4 class="mt-4">3. Verify setup</h4>
|
<h4 class="mt-4">3. Verify setup</h4>
|
||||||
<p>Enter the 6-digit code from your authenticator app to verify the setup:</p>
|
<p>Enter the 6-digit code from your authenticator app to verify the setup:</p>
|
||||||
|
|
||||||
<form method="post" action="?page=credentials&item=2fa&action=setup" class="mt-3">
|
<form method="post" action="?page=credentials&action=setup" class="mt-3">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<input type="text"
|
<input type="text"
|
||||||
name="code"
|
name="code"
|
||||||
|
@ -58,7 +58,6 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<input type="hidden" name="secret" value="<?php echo htmlspecialchars($setupData['secret']); ?>">
|
<input type="hidden" name="secret" value="<?php echo htmlspecialchars($setupData['secret']); ?>">
|
||||||
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars($_SESSION['csrf_token']); ?>">
|
|
||||||
|
|
||||||
<button type="submit" class="btn btn-primary mt-3">
|
<button type="submit" class="btn btn-primary mt-3">
|
||||||
Verify and enable 2FA
|
Verify and enable 2FA
|
||||||
|
@ -83,10 +82,12 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<?php else: ?>
|
<?php else: ?>
|
||||||
<div class="alert alert-danger">
|
<form method="post" action="?page=credentials&action=setup" class="mt-3">
|
||||||
Failed to generate 2FA setup data. Please try again.
|
<p>Click the button below to begin setting up two-factor authentication:</p>
|
||||||
</div>
|
<button type="submit" class="btn btn-primary">
|
||||||
<a href="?page=credentials" class="btn btn-primary">Back to credentials</a>
|
Begin setup
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -94,7 +95,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<?php if (isset($setupData) && is_array($setupData)): ?>
|
<?php if (!empty($setupData)): ?>
|
||||||
<script src="https://cdn.jsdelivr.net/npm/qrcodejs@1.0.0/qrcode.min.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/qrcodejs@1.0.0/qrcode.min.js"></script>
|
||||||
<script>
|
<script>
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
|
|
@ -20,7 +20,7 @@
|
||||||
|
|
||||||
<p>Enter the 6-digit code from your authenticator app:</p>
|
<p>Enter the 6-digit code from your authenticator app:</p>
|
||||||
|
|
||||||
<form method="post" action="?page=login&action=verify" class="mt-3">
|
<form method="post" action="?page=credentials&action=verify" class="mt-3">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<input type="text"
|
<input type="text"
|
||||||
name="code"
|
name="code"
|
||||||
|
@ -50,7 +50,7 @@
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div class="collapse mt-3" id="backupCodeForm">
|
<div class="collapse mt-3" id="backupCodeForm">
|
||||||
<form method="post" action="?page=login&action=verify" class="mt-3">
|
<form method="post" action="?page=credentials&action=verify" class="mt-3">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>Enter backup code:</label>
|
<label>Enter backup code:</label>
|
||||||
<input type="text"
|
<input type="text"
|
||||||
|
|
|
@ -77,7 +77,7 @@
|
||||||
<div class="alert alert-warning">
|
<div class="alert alert-warning">
|
||||||
<i class="fas fa-exclamation-triangle"></i> two-factor authentication is not enabled
|
<i class="fas fa-exclamation-triangle"></i> two-factor authentication is not enabled
|
||||||
</div>
|
</div>
|
||||||
<form method="post" action="?page=credentials&item=2fa&action=setup">
|
<form method="post" action="?page=credentials&item=2fa&action=edit">
|
||||||
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars($_SESSION['csrf_token']); ?>">
|
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars($_SESSION['csrf_token']); ?>">
|
||||||
<button type="submit" class="btn btn-primary">
|
<button type="submit" class="btn btn-primary">
|
||||||
set up two-factor authentication
|
set up two-factor authentication
|
||||||
|
|
|
@ -29,7 +29,7 @@
|
||||||
<label class="form-label"><small>name:</small></label>
|
<label class="form-label"><small>name:</small></label>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-8 text-start bg-light">
|
<div class="col-md-8 text-start bg-light">
|
||||||
<?= htmlspecialchars($userDetails[0]['name'] ?? '') ?>
|
<?= htmlspecialchars($userDetails[0]['name']) ?>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -38,7 +38,7 @@
|
||||||
<label class="form-label"><small>email:</small></label>
|
<label class="form-label"><small>email:</small></label>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-8 text-start bg-light">
|
<div class="col-md-8 text-start bg-light">
|
||||||
<?= htmlspecialchars($userDetails[0]['email'] ?? '') ?>
|
<?= htmlspecialchars($userDetails[0]['email']) ?>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue