Fix Login flow where user agent and client exists and agent has MFA but will not let them continue, also update some wording and button colors. Also dont show email password fields again after success and login as agent and client is shown.

This commit is contained in:
johnnyq 2025-12-20 14:30:57 -05:00
parent cab81ca170
commit e60a7a59f9
1 changed files with 433 additions and 336 deletions

769
login.php
View File

@ -2,13 +2,10 @@
// Unified login (Agent + Client) using one email & password
// Enforce a Content Security Policy for security against cross-site scripting
header("Content-Security-Policy: default-src 'self'");
// Check if the config.php file exists
if (!file_exists('config.php')) {
// Redirect to the setup page if config.php doesn't exist
header("Location: /setup"); // Must use header as functions aren't included yet
header("Location: /setup");
exit();
}
@ -16,12 +13,9 @@ require_once "config.php";
require_once "functions.php";
require_once "plugins/totp/totp.php";
// Sessions & cookies
if (session_status() === PHP_SESSION_NONE) {
// HTTP-Only cookies
ini_set("session.cookie_httponly", true);
// Tell client to only send cookie(s) over HTTPS
if ($config_https_only || !isset($config_https_only)) {
ini_set("session.cookie_secure", true);
}
@ -29,28 +23,25 @@ if (session_status() === PHP_SESSION_NONE) {
session_start();
}
// Check if setup mode is enabled or the variable is missing
if (!isset($config_enable_setup) || $config_enable_setup == 1) {
// Redirect to the setup page
header("Location: /setup");
exit();
}
// Check if the application is configured for HTTPS-only access
if ($config_https_only && (!isset($_SERVER['HTTPS']) || $_SERVER['HTTPS'] !== 'on') && (!isset($_SERVER['HTTP_X_FORWARDED_PROTO']) || $_SERVER['HTTP_X_FORWARDED_PROTO'] !== 'https')) {
if (
$config_https_only
&& (!isset($_SERVER['HTTPS']) || $_SERVER['HTTPS'] !== 'on')
&& (!isset($_SERVER['HTTP_X_FORWARDED_PROTO']) || $_SERVER['HTTP_X_FORWARDED_PROTO'] !== 'https')
) {
echo "Login is restricted as ITFlow defaults to HTTPS-only for enhanced security. To login using HTTP, modify the config.php file by setting config_https_only to false. However, this is strongly discouraged, especially when accessing from potentially unsafe networks like the internet.";
exit;
}
// Set Timezone after session_start
require_once "includes/inc_set_timezone.php";
// IP & User Agent for logging
$session_ip = sanitizeInput(getIP());
$session_user_agent = sanitizeInput($_SERVER['HTTP_USER_AGENT'] ?? '');
// Block brute force password attacks - check recent failed login attempts for this IP
// Block access if more than 15 failed login attempts have happened in the last 10 minutes
$row = mysqli_fetch_assoc(mysqli_query(
$mysqli,
"SELECT COUNT(log_id) AS failed_login_count
@ -63,15 +54,12 @@ $row = mysqli_fetch_assoc(mysqli_query(
$failed_login_count = intval($row['failed_login_count']);
if ($failed_login_count >= 15) {
logAction("Login", "Blocked", "$session_ip was blocked access to login due to IP lockout");
// Inform user & quit processing page
header("HTTP/1.1 429 Too Many Requests");
exit("<h2>$config_app_name</h2>Your IP address has been blocked due to repeated failed login attempts. Please try again later. <br><br>This action has been logged.");
}
// Query Settings for company
// Settings
$sql_settings = mysqli_query($mysqli, "
SELECT settings.*, companies.company_name, companies.company_logo
FROM settings
@ -80,13 +68,11 @@ $sql_settings = mysqli_query($mysqli, "
");
$row = mysqli_fetch_array($sql_settings);
// Company info
$company_name = $row['company_name'];
$company_logo = $row['company_logo'];
$config_start_page = nullable_htmlentities($row['config_start_page']);
$config_login_message = nullable_htmlentities($row['config_login_message']);
// Mail
$config_smtp_host = $row['config_smtp_host'];
$config_smtp_port = intval($row['config_smtp_port']);
$config_smtp_encryption = $row['config_smtp_encryption'];
@ -95,49 +81,96 @@ $config_smtp_password = $row['config_smtp_password'];
$config_mail_from_email = sanitizeInput($row['config_mail_from_email']);
$config_mail_from_name = sanitizeInput($row['config_mail_from_name']);
// Client Portal Enabled
$config_client_portal_enable = intval($row['config_client_portal_enable']);
$config_login_remember_me_expire = intval($row['config_login_remember_me_expire']);
// Login key (if setup)
$config_login_key_required = $row['config_login_key_required'];
$config_login_key_secret = $row['config_login_key_secret'];
$config_login_key_secret = $row['config_login_key_secret'];
// Azure / Entra for client
$azure_client_id = $row['config_azure_client_id'] ?? null;
$response = null;
$token_field = null;
$show_role_choice = false;
$email = '';
$password = '';
$response = null;
$token_field = null;
$show_role_choice = false;
// Handle POST login request (normal login or role choice)
if ($_SERVER['REQUEST_METHOD'] === 'POST' && (isset($_POST['login']) || isset($_POST['role_choice']))) {
$email = '';
$password = ''; // only ever used in the initial POST request
$email = sanitizeInput($_POST['email'] ?? '');
$password = $_POST['password'] ?? '';
$role_choice = $_POST['role_choice'] ?? null; // 'agent' or 'client'
// Helpers
function pendingExpired($sess, $ttl_seconds = 120) {
return !$sess || empty($sess['created']) || (time() - intval($sess['created']) > $ttl_seconds);
}
// Basic validation
if (empty($email) || empty($password) || !filter_var($email, FILTER_VALIDATE_EMAIL)) {
header("HTTP/1.1 401 Unauthorized");
$response = "
<div class='alert alert-danger'>
Incorrect username or password.
<button class='close' data-dismiss='alert'>&times;</button>
</div>";
} else {
// POST handling
if ($_SERVER['REQUEST_METHOD'] === 'POST' && (isset($_POST['login']) || isset($_POST['role_choice']) || isset($_POST['mfa_login']))) {
/*
* Unified lookup:
* - user_type = 1 Agent
* - user_type = 2 Client (must not be archived, client not archived)
* We fetch all possible matches for this email, then verify password per row.
* If both an agent and a client match with the same password:
* - First, show choice buttons (Agent / Client).
* - When user clicks a choice, we honor role_choice.
*/
$role_choice = $_POST['role_choice'] ?? null;
$is_login_step = isset($_POST['login']);
$is_role_step = isset($_POST['role_choice']) && !$is_login_step && !isset($_POST['mfa_login']);
$is_mfa_step = isset($_POST['mfa_login']);
// -----------------------------------
// STEP 2: ROLE CHOICE (no email/pass)
// -----------------------------------
if ($is_role_step) {
$posted_token = $_POST['pending_login_token'] ?? '';
$sess = $_SESSION['pending_dual_login'] ?? null;
if (pendingExpired($sess) || empty($posted_token) || empty($sess['token']) || !hash_equals($sess['token'], $posted_token)) {
unset($_SESSION['pending_dual_login']);
header("HTTP/1.1 401 Unauthorized");
$response = "
<div class='alert alert-danger'>
Your login session expired. Please sign in again.
</div>";
} else {
$email = sanitizeInput($sess['email'] ?? '');
}
}
// -----------------------------------
// STEP 3: MFA SUBMIT (no email/pass)
// -----------------------------------
if ($is_mfa_step && empty($response)) {
$posted_token = $_POST['pending_mfa_token'] ?? '';
$sess = $_SESSION['pending_mfa_login'] ?? null;
if (pendingExpired($sess) || empty($posted_token) || empty($sess['token']) || !hash_equals($sess['token'], $posted_token)) {
unset($_SESSION['pending_mfa_login']);
header("HTTP/1.1 401 Unauthorized");
$response = "
<div class='alert alert-danger'>
Your MFA session expired. Please sign in again.
</div>";
} else {
$email = sanitizeInput($sess['email'] ?? '');
$role_choice = 'agent';
}
}
// -----------------------------------
// STEP 1: INITIAL CREDENTIALS
// -----------------------------------
if ($is_login_step && empty($response)) {
$email = sanitizeInput($_POST['email'] ?? '');
$password = $_POST['password'] ?? '';
if (empty($email) || empty($password) || !filter_var($email, FILTER_VALIDATE_EMAIL)) {
header("HTTP/1.1 401 Unauthorized");
$response = "
<div class='alert alert-danger'>
Incorrect username or password.
</div>";
}
}
// Continue only if no response error
if (empty($response)) {
// Query all possible matches for that email
$sql = mysqli_query($mysqli, "
SELECT users.*,
user_settings.*,
@ -159,307 +192,372 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && (isset($_POST['login']) || isset($_
$agentRow = null;
$clientRow = null;
// Step 1: verify password. Step 2/3: use stored allowed ids.
$allowed_agent_id = null;
$allowed_client_id = null;
if ($is_role_step) {
$sess = $_SESSION['pending_dual_login'] ?? null;
$allowed_agent_id = isset($sess['agent_user_id']) ? intval($sess['agent_user_id']) : null;
$allowed_client_id = isset($sess['client_user_id']) ? intval($sess['client_user_id']) : null;
}
if ($is_mfa_step) {
$sess = $_SESSION['pending_mfa_login'] ?? null;
$allowed_agent_id = isset($sess['agent_user_id']) ? intval($sess['agent_user_id']) : null;
}
while ($r = mysqli_fetch_assoc($sql)) {
if (!password_verify($password, $r['user_password'])) {
continue;
$ut = intval($r['user_type']);
if ($is_login_step) {
// Only Step 1 checks password
if (!password_verify($password, $r['user_password'])) {
continue;
}
} else {
// Step 2/3: restrict to ids we previously verified
if ($ut === 1 && $allowed_agent_id !== null && intval($r['user_id']) !== $allowed_agent_id) {
continue;
}
if ($ut === 2 && $allowed_client_id !== null && intval($r['user_id']) !== $allowed_client_id) {
continue;
}
}
if (intval($r['user_type']) === 1 && $agentRow === null) {
if ($ut === 1 && $agentRow === null) {
$agentRow = $r;
}
if (intval($r['user_type']) === 2 && $clientRow === null) {
if ($ut === 2 && $clientRow === null) {
$clientRow = $r;
}
}
$selectedRow = null;
$selectedType = null; // 1 = agent, 2 = client
if ($agentRow === null && $clientRow === null) {
// No matching user/password combo
header("HTTP/1.1 401 Unauthorized");
logAction("Login", "Failed", "Failed login attempt using $email");
$response = "
<div class='alert alert-danger'>
Incorrect username or password.
<button class='close' data-dismiss='alert'>&times;</button>
</div>";
} elseif ($agentRow !== null && $clientRow !== null) {
// Both agent and client accounts share same email + password
if ($role_choice === 'agent') {
$selectedRow = $agentRow;
$selectedType = 1;
} elseif ($role_choice === 'client') {
$selectedRow = $clientRow;
$selectedType = 2;
} else {
// First time we realise this is a dual-role account: ask user to pick
$show_role_choice = true;
$response = "
<div class='alert alert-info'>
This login can be used as either an Agent account or a Client Portal account.
Please choose how you want to continue.
<button class='close' data-dismiss='alert'>&times;</button>
</div>";
}
} else {
// Only one valid row (agent OR client)
if ($agentRow !== null) {
$selectedRow = $agentRow;
$selectedType = 1;
} else {
$selectedRow = $clientRow;
$selectedType = 2;
}
}
// If we have a specific user selected, proceed with actual login
if ($selectedRow !== null && $selectedType !== null) {
$selectedRow = null;
$selectedType = null; // 1 agent, 2 client
$user_id = intval($selectedRow['user_id']);
$user_email = sanitizeInput($selectedRow['user_email']);
$session_user_id = $user_id; // to pass the user_id to logAction function
// Dual role
if ($agentRow !== null && $clientRow !== null) {
// =========================
// AGENT LOGIN FLOW
// =========================
if ($selectedType === 1) {
// Login key verification
// If no/incorrect 'key' is supplied, send to client portal instead
if ($config_login_key_required) {
if (!isset($_GET['key']) || $_GET['key'] !== $config_login_key_secret) {
redirect();
if ($role_choice === 'agent') {
$selectedRow = $agentRow;
$selectedType = 1;
} elseif ($role_choice === 'client') {
$selectedRow = $clientRow;
$selectedType = 2;
} else {
// Show role choice screen
$show_role_choice = true;
// If this is the first time (Step 1), we need to stash allowed ids and (optional) decrypted agent encryption key
// WITHOUT storing password.
if ($is_login_step) {
$pending_token = bin2hex(random_bytes(32));
// If agent has user-specific encryption ciphertext, decrypt it NOW while password is present.
$agent_master_key = null;
$agent_cipher = $agentRow['user_specific_encryption_ciphertext'] ?? null;
if (!empty($agent_cipher)) {
$agent_master_key = decryptUserSpecificKey($agent_cipher, $password);
}
$_SESSION['pending_dual_login'] = [
'email' => $email,
'agent_user_id' => intval($agentRow['user_id']),
'client_user_id' => intval($clientRow['user_id']),
'agent_master_key' => $agent_master_key, // may be null
'token' => $pending_token,
'created' => time()
];
}
$response = "
<div class='alert alert-light'>
This login can be used as either an Agent account or a Client Portal account.
Please choose how you want to continue.
</div>";
}
$user_name = sanitizeInput($selectedRow['user_name']);
$token = sanitizeInput($selectedRow['user_token']);
$force_mfa = intval($selectedRow['user_config_force_mfa']);
$user_role_id = intval($selectedRow['user_role_id']);
$user_encryption_ciphertext = $selectedRow['user_specific_encryption_ciphertext'];
$user_extension_key = $selectedRow['user_extension_key'];
$current_code = 0;
if (isset($_POST['current_code'])) {
$current_code = intval($_POST['current_code']);
} else {
// Single role
if ($agentRow !== null) {
$selectedRow = $agentRow;
$selectedType = 1;
} else {
$selectedRow = $clientRow;
$selectedType = 2;
}
}
$mfa_is_complete = false;
$extended_log = '';
// Proceed if selected
if ($selectedRow !== null && $selectedType !== null) {
if (empty($token)) {
// MFA is not configured
$mfa_is_complete = true;
}
// Clear dual pending once we actually proceed
unset($_SESSION['pending_dual_login']);
// Validate MFA via a remember-me cookie
if (isset($_COOKIE['rememberme'])) {
$remember_tokens = mysqli_query($mysqli, "
SELECT remember_token_token
FROM remember_tokens
WHERE remember_token_user_id = $user_id
AND remember_token_created_at > (NOW() - INTERVAL $config_login_remember_me_expire DAY)
");
while ($remember_row = mysqli_fetch_assoc($remember_tokens)) {
if (hash_equals($remember_row['remember_token_token'], $_COOKIE['rememberme'])) {
$mfa_is_complete = true;
$extended_log = 'with 2FA remember-me cookie';
break;
$user_id = intval($selectedRow['user_id']);
$user_email = sanitizeInput($selectedRow['user_email']);
// =========================
// AGENT FLOW
// =========================
if ($selectedType === 1) {
if ($config_login_key_required) {
if (!isset($_GET['key']) || $_GET['key'] !== $config_login_key_secret) {
redirect();
}
}
}
// Validate MFA code
if (!empty($current_code) && TokenAuth6238::verify($token, $current_code)) {
$mfa_is_complete = true;
$extended_log = 'with MFA';
}
$user_name = sanitizeInput($selectedRow['user_name']);
$token = sanitizeInput($selectedRow['user_token']);
$force_mfa = intval($selectedRow['user_config_force_mfa']);
$user_encryption_ciphertext = $selectedRow['user_specific_encryption_ciphertext'];
if ($mfa_is_complete) {
// FULL AGENT LOGIN SUCCESS
// Create a remember me token, if requested
if (isset($_POST['remember_me'])) {
$newRememberToken = bin2hex(random_bytes(64));
setcookie(
'rememberme',
$newRememberToken,
time() + 86400 * $config_login_remember_me_expire,
"/",
null,
true,
true
);
mysqli_query($mysqli, "
INSERT INTO remember_tokens
SET remember_token_user_id = $user_id,
remember_token_token = '$newRememberToken'
");
$extended_log .= ", generated a new remember-me token";
$current_code = 0;
if (isset($_POST['current_code'])) {
$current_code = intval($_POST['current_code']);
}
// Check this login isn't suspicious
$sql_ip_prev_logins = mysqli_fetch_assoc(mysqli_query($mysqli, "
SELECT COUNT(log_id) AS ip_previous_logins
FROM logs
WHERE log_type = 'Login'
AND log_action = 'Success'
AND log_ip = '$session_ip'
AND log_user_id = $user_id
"));
$ip_previous_logins = sanitizeInput($sql_ip_prev_logins['ip_previous_logins']);
$mfa_is_complete = false;
$extended_log = '';
$sql_ua_prev_logins = mysqli_fetch_assoc(mysqli_query($mysqli, "
SELECT COUNT(log_id) AS ua_previous_logins
FROM logs
WHERE log_type = 'Login'
AND log_action = 'Success'
AND log_user_agent = '$session_user_agent'
AND log_user_id = $user_id
"));
$ua_prev_logins = sanitizeInput($sql_ua_prev_logins['ua_previous_logins']);
if (empty($token)) {
$mfa_is_complete = true; // no MFA configured
}
// Notify if both the user agent and IP are different
if (!empty($config_smtp_host) && $ip_previous_logins == 0 && $ua_prev_logins == 0) {
$subject = "$config_app_name new login for $user_name";
$body = "Hi $user_name, <br><br>A recent successful login to your $config_app_name account was considered a little unusual. If this was you, you can safely ignore this email!<br><br>IP Address: $session_ip<br> User Agent: $session_user_agent <br><br>If you did not perform this login, your credentials may be compromised. <br><br>Thanks, <br>ITFlow";
// remember-me cookie allows bypass
if (isset($_COOKIE['rememberme'])) {
$remember_tokens = mysqli_query($mysqli, "
SELECT remember_token_token
FROM remember_tokens
WHERE remember_token_user_id = $user_id
AND remember_token_created_at > (NOW() - INTERVAL $config_login_remember_me_expire DAY)
");
while ($remember_row = mysqli_fetch_assoc($remember_tokens)) {
if (hash_equals($remember_row['remember_token_token'], $_COOKIE['rememberme'])) {
$mfa_is_complete = true;
$extended_log = 'with 2FA remember-me cookie';
break;
}
}
}
$data = [
[
// Validate MFA code
if (!empty($current_code) && TokenAuth6238::verify($token, $current_code)) {
$mfa_is_complete = true;
$extended_log = 'with MFA';
}
if ($mfa_is_complete) {
// Clear pending MFA if exists
unset($_SESSION['pending_mfa_login']);
// Remember me token creation
if (isset($_POST['remember_me'])) {
$newRememberToken = bin2hex(random_bytes(64));
setcookie('rememberme', $newRememberToken, time() + 86400 * $config_login_remember_me_expire, "/", null, true, true);
mysqli_query($mysqli, "
INSERT INTO remember_tokens
SET remember_token_user_id = $user_id,
remember_token_token = '$newRememberToken'
");
$extended_log .= ", generated a new remember-me token";
}
// Suspicious login checks / email notify (kept from your code)
$sql_ip_prev_logins = mysqli_fetch_assoc(mysqli_query($mysqli, "
SELECT COUNT(log_id) AS ip_previous_logins
FROM logs
WHERE log_type = 'Login'
AND log_action = 'Success'
AND log_ip = '$session_ip'
AND log_user_id = $user_id
"));
$ip_previous_logins = sanitizeInput($sql_ip_prev_logins['ip_previous_logins']);
$sql_ua_prev_logins = mysqli_fetch_assoc(mysqli_query($mysqli, "
SELECT COUNT(log_id) AS ua_previous_logins
FROM logs
WHERE log_type = 'Login'
AND log_action = 'Success'
AND log_user_agent = '$session_user_agent'
AND log_user_id = $user_id
"));
$ua_prev_logins = sanitizeInput($sql_ua_prev_logins['ua_previous_logins']);
if (!empty($config_smtp_host) && $ip_previous_logins == 0 && $ua_prev_logins == 0) {
$subject = "$config_app_name new login for $user_name";
$body = "Hi $user_name, <br><br>A recent successful login to your $config_app_name account was considered a little unusual. If this was you, you can safely ignore this email!<br><br>IP Address: $session_ip<br> User Agent: $session_user_agent <br><br>If you did not perform this login, your credentials may be compromised. <br><br>Thanks, <br>ITFlow";
$data = [[
'from' => $config_mail_from_email,
'from_name' => $config_mail_from_name,
'recipient' => $user_email,
'recipient_name' => $user_name,
'subject' => $subject,
'body' => $body
]
];
addToMailQueue($data);
}
]];
addToMailQueue($data);
}
logAction("Login", "Success", "$user_name successfully logged in $extended_log", 0, $user_id);
logAction("Login", "Success", "$user_name successfully logged in $extended_log", 0, $user_id);
// Session info
$_SESSION['user_id'] = $user_id;
$_SESSION['csrf_token'] = randomString(32);
$_SESSION['logged'] = true;
$_SESSION['user_id'] = $user_id;
$_SESSION['csrf_token'] = randomString(32);
$_SESSION['logged'] = true;
// Forcing MFA
if ($force_mfa == 1 && $token == NULL) {
$config_start_page = "user/mfa_enforcement.php";
}
if ($force_mfa == 1 && $token == NULL) {
$config_start_page = "user/mfa_enforcement.php";
}
// Setup encryption session key
if (!empty($user_encryption_ciphertext)) {
$site_encryption_master_key = decryptUserSpecificKey($user_encryption_ciphertext, $password);
generateUserSessionKey($site_encryption_master_key);
}
// ✅ Setup encryption session key WITHOUT PASSWORD IN SESSION
// If we are coming from MFA step, master key is in pending_mfa_login.
// If we are coming from login step with no MFA, decrypt now.
$site_encryption_master_key = null;
// Redirect to last visited or config home
if (isset($_GET['last_visited']) && (str_starts_with(base64_decode($_GET['last_visited']), '/agent') || str_starts_with(base64_decode($_GET['last_visited']), '/admin'))) {
if ($is_mfa_step) {
$sess = $_SESSION['pending_mfa_login'] ?? null;
if ($sess && isset($sess['agent_master_key'])) {
$site_encryption_master_key = $sess['agent_master_key'];
}
} else {
// No MFA step: password exists in this request (Step 1)
if (!empty($user_encryption_ciphertext)) {
$site_encryption_master_key = decryptUserSpecificKey($user_encryption_ciphertext, $password);
}
}
redirect($_SERVER["REQUEST_SCHEME"] . "://" . $config_base_url . base64_decode($_GET['last_visited']));
if (!empty($site_encryption_master_key)) {
generateUserSessionKey($site_encryption_master_key);
}
// Redirect
if (isset($_GET['last_visited']) && (str_starts_with(base64_decode($_GET['last_visited']), '/agent') || str_starts_with(base64_decode($_GET['last_visited']), '/admin'))) {
redirect($_SERVER["REQUEST_SCHEME"] . "://" . $config_base_url . base64_decode($_GET['last_visited']));
} else {
redirect("agent/$config_start_page");
}
} else {
redirect("agent/$config_start_page");
}
} else {
// MFA required — store *only what we need*, not password
$pending_mfa_token = bin2hex(random_bytes(32));
// MFA is configured and needs to be confirmed, or was unsuccessful
// If we arrived here from role-choice step, the agent master key may be in pending_dual_login
// If we arrived from initial login step, decrypt now (password in memory) and store master key.
$agent_master_key = null;
// HTML code for the token input field
$token_field = "
<div class='input-group mb-3'>
<input type='text' inputmode='numeric' pattern='[0-9]*' maxlength='6'
class='form-control' placeholder='Enter your 2FA code'
name='current_code' required autofocus>
<div class='input-group-append'>
<div class='input-group-text'>
<span class='fas fa-key'></span>
</div>
</div>
</div>";
if ($is_role_step) {
$sess = $_SESSION['pending_dual_login'] ?? null;
if ($sess && isset($sess['agent_master_key'])) {
$agent_master_key = $sess['agent_master_key'];
}
} else {
if (!empty($user_encryption_ciphertext)) {
$agent_master_key = decryptUserSpecificKey($user_encryption_ciphertext, $password);
}
}
if ($current_code !== 0) {
// Logging
logAction("Login", "MFA Failed", "$user_email failed MFA", 0, $user_id);
$_SESSION['pending_mfa_login'] = [
'email' => $user_email,
'agent_user_id' => $user_id,
'agent_master_key'=> $agent_master_key, // may be null
'token' => $pending_mfa_token,
'created' => time()
];
// Email the tech to advise their credentials may be compromised
if (!empty($config_smtp_host)) {
$subject = "Important: $config_app_name failed 2FA login attempt for $user_name";
$body = "Hi $user_name, <br><br>A recent login to your $config_app_name account was unsuccessful due to an incorrect 2FA code. If you did not attempt this login, your credentials may be compromised. <br><br>Thanks, <br>ITFlow";
$data = [
[
$token_field = "
<div class='input-group mb-3'>
<input type='text' inputmode='numeric' pattern='[0-9]*' maxlength='6'
class='form-control' placeholder='Verify your 2FA code'
name='current_code' required autofocus>
<div class='input-group-append'>
<div class='input-group-text'>
<span class='fas fa-key'></span>
</div>
</div>
</div>";
if ($current_code !== 0) {
logAction("Login", "MFA Failed", "$user_email failed MFA", 0, $user_id);
if (!empty($config_smtp_host)) {
$subject = "Important: $config_app_name failed 2FA login attempt for $user_name";
$body = "Hi $user_name, <br><br>A recent login to your $config_app_name account was unsuccessful due to an incorrect 2FA code. If you did not attempt this login, your credentials may be compromised. <br><br>Thanks, <br>ITFlow";
$data = [[
'from' => $config_mail_from_email,
'from_name' => $config_mail_from_name,
'recipient' => $user_email,
'recipient_name' => $user_name,
'subject' => $subject,
'body' => $body
]
];
addToMailQueue($data);
]];
addToMailQueue($data);
}
$response = "
<div class='alert alert-danger'>
Please enter a valid 2FA code.
</div>";
}
$response = "
<div class='alert alert-warning'>
Please Enter 2FA Code!
<button class='close' data-dismiss='alert'>&times;</button>
</div>";
}
}
// =========================
// CLIENT LOGIN FLOW
// =========================
} elseif ($selectedType === 2) {
if ($config_client_portal_enable != 1) {
// Client portal disabled
header("HTTP/1.1 401 Unauthorized");
logAction("Client Login", "Failed", "Client portal disabled; login attempt using $email");
$response = "
<div class='alert alert-danger'>
Incorrect username or password.
<button class='close' data-dismiss='alert'>&times;</button>
</div>";
} else {
$client_id = intval($selectedRow['contact_client_id']);
$contact_id = intval($selectedRow['contact_id']);
$user_auth_method = sanitizeInput($selectedRow['user_auth_method']);
if ($client_id && $contact_id && $user_auth_method === 'local') {
$_SESSION['client_logged_in'] = true;
$_SESSION['client_id'] = $client_id;
$_SESSION['user_id'] = $user_id;
$_SESSION['user_type'] = 2;
$_SESSION['contact_id'] = $contact_id;
$_SESSION['login_method'] = "local";
logAction("Client Login", "Success", "Client contact $user_email successfully logged in locally", $client_id, $user_id);
header("Location: client/index.php");
exit();
} else {
// Not allowed or invalid
logAction("Client Login", "Failed", "Failed client portal login attempt using $email (invalid auth method or missing contact/client)", $client_id ?? 0, $user_id);
// =========================
// CLIENT FLOW
// =========================
} elseif ($selectedType === 2) {
if ($config_client_portal_enable != 1) {
header("HTTP/1.1 401 Unauthorized");
logAction("Client Login", "Failed", "Client portal disabled; login attempt using $email");
$response = "
<div class='alert alert-danger'>
Incorrect username or password.
<button class='close' data-dismiss='alert'>&times;</button>
</div>";
} else {
$client_id = intval($selectedRow['contact_client_id']);
$contact_id = intval($selectedRow['contact_id']);
$user_auth_method = sanitizeInput($selectedRow['user_auth_method']);
if ($client_id && $contact_id && $user_auth_method === 'local') {
$_SESSION['client_logged_in'] = true;
$_SESSION['client_id'] = $client_id;
$_SESSION['user_id'] = $user_id;
$_SESSION['user_type'] = 2;
$_SESSION['contact_id'] = $contact_id;
$_SESSION['login_method'] = "local";
logAction("Client Login", "Success", "Client contact $user_email successfully logged in locally", $client_id, $user_id);
header("Location: client/index.php");
exit();
} else {
logAction("Client Login", "Failed", "Failed client portal login attempt using $email (invalid auth method or missing contact/client)", $client_id ?? 0, $user_id);
header("HTTP/1.1 401 Unauthorized");
$response = "
<div class='alert alert-danger'>
Incorrect username or password.
</div>";
}
}
}
}
@ -467,6 +565,10 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && (isset($_POST['login']) || isset($_
}
}
// Form state
$show_mfa_form = (isset($token_field) && !empty($token_field));
$show_login_form = (!$show_role_choice && !$show_mfa_form);
?>
<!DOCTYPE html>
<html lang="en">
@ -474,21 +576,16 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && (isset($_POST['login']) || isset($_
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title><?php echo nullable_htmlentities($company_name); ?> | Login</title>
<!-- Tell the browser to be responsive to screen width -->
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="robots" content="noindex">
<!-- Font Awesome -->
<link rel="stylesheet" href="plugins/fontawesome-free/css/all.min.css">
<!-- Favicon -->
<?php if(file_exists('uploads/favicon.ico')) { ?>
<link rel="icon" type="image/x-icon" href="/uploads/favicon.ico">
<?php } ?>
<!-- Theme style -->
<link rel="stylesheet" href="plugins/adminlte/css/adminlte.min.css">
</head>
<body class="hold-transition login-page">
@ -505,66 +602,73 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && (isset($_POST['login']) || isset($_
<div class="card-body login-card-body">
<?php if (!empty($config_login_message)){ ?>
<p class="login-box-msg px-0"><?php echo nl2br($config_login_message); ?></p>
<p class="login-box-msg px-0"><?php echo nl2br($config_login_message); ?></p>
<?php } ?>
<?php if (isset($response)) { ?>
<p><?php echo $response; ?></p>
<p><?php echo $response; ?></p>
<?php } ?>
<form method="post">
<div class="input-group mb-3" <?php if (isset($token_field) && $token_field) { echo "hidden"; } ?>>
<input type="text" class="form-control"
placeholder="<?php if ($config_login_key_required) { if (!isset($_GET['key']) || $_GET['key'] !== $config_login_key_secret) { echo "Client "; } } echo "Email"; ?>"
name="email"
value="<?php echo htmlspecialchars($email ?? '', ENT_QUOTES); ?>"
required <?php if (!isset($token_field) || !$token_field) { echo "autofocus"; } ?>
>
<div class="input-group-append">
<div class="input-group-text">
<span class="fas fa-envelope"></span>
<?php if ($show_login_form): ?>
<!-- STEP 1: Email + Password -->
<div class="input-group mb-3">
<input type="text" class="form-control"
placeholder="<?php if ($config_login_key_required) { if (!isset($_GET['key']) || $_GET['key'] !== $config_login_key_secret) { echo "Client "; } } echo "Email"; ?>"
name="email"
value="<?php echo htmlspecialchars($email ?? '', ENT_QUOTES); ?>"
required autofocus
>
<div class="input-group-append">
<div class="input-group-text">
<span class="fas fa-envelope"></span>
</div>
</div>
</div>
</div>
<div class="input-group mb-3" <?php if (isset($token_field) && $token_field) { echo "hidden"; } ?>>
<input type="password" class="form-control" placeholder="Password" name="password"
value="<?php echo isset($token_field) && $token_field ? htmlspecialchars($password ?? '', ENT_QUOTES) : ''; ?>"
required>
<div class="input-group-append">
<div class="input-group-text">
<span class="fas fa-lock"></span>
<div class="input-group mb-3">
<input type="password" class="form-control" placeholder="Password" name="password" required>
<div class="input-group-append">
<div class="input-group-text">
<span class="fas fa-lock"></span>
</div>
</div>
</div>
</div>
<?php
// If agent needs MFA, show 2FA field + remember me
if (isset($token_field) && $token_field) {
echo $token_field;
?>
<div class="form-group mb-3">
<div class="custom-control custom-checkbox">
<input type="checkbox" class="custom-control-input" id="remember_me" name="remember_me">
<label class="custom-control-label" for="remember_me">Remember Me</label>
</div>
</div>
<?php } ?>
<button type="submit" class="btn btn-primary btn-block mb-3" name="login">Sign In</button>
<?php endif; ?>
<?php if ($show_role_choice): ?>
<!-- When both agent & client accounts exist with same email/password -->
<!-- STEP 2: Role choice only -->
<input type="hidden" name="pending_login_token"
value="<?php echo htmlspecialchars($_SESSION['pending_dual_login']['token'] ?? '', ENT_QUOTES); ?>">
<div class="mb-2 text-center">
<button type="submit" class="btn btn-primary btn-block mb-2" name="role_choice" value="agent">
<button type="submit" class="btn btn-dark btn-block mb-2" name="role_choice" value="agent">
Log in as Agent
</button>
<button type="submit" class="btn btn-success btn-block" name="role_choice" value="client">
<button type="submit" class="btn btn-light btn-block" name="role_choice" value="client">
Log in as Client
</button>
</div>
<?php else: ?>
<button type="submit" class="btn btn-primary btn-block mb-3" name="login">Sign In</button>
<?php endif; ?>
<?php if ($show_mfa_form): ?>
<!-- STEP 3: MFA only -->
<?php echo $token_field; ?>
<input type="hidden" name="pending_mfa_token"
value="<?php echo htmlspecialchars($_SESSION['pending_mfa_login']['token'] ?? '', ENT_QUOTES); ?>">
<div class="form-group mb-3">
<div class="custom-control custom-checkbox">
<input type="checkbox" class="custom-control-input" id="remember_me" name="remember_me">
<label class="custom-control-label" for="remember_me">Remember Me</label>
</div>
</div>
<button type="submit" class="btn btn-dark btn-block mb-3" name="mfa_login">Verify & Sign In</button>
<?php endif; ?>
</form>
@ -593,16 +697,9 @@ if (!$config_whitelabel_enabled) {
}
?>
<!-- jQuery -->
<script src="plugins/jquery/jquery.min.js"></script>
<!-- Bootstrap 4 -->
<script src="plugins/bootstrap/js/bootstrap.bundle.min.js"></script>
<!-- AdminLTE App -->
<script src="plugins/adminlte/js/adminlte.min.js"></script>
<!-- Prevents resubmit on refresh or back -->
<script src="js/login_prevent_resubmit.js"></script>
</body>