From e60a7a59f937c1a2917ab28463e330fa8556d870 Mon Sep 17 00:00:00 2001 From: johnnyq Date: Sat, 20 Dec 2025 14:30:57 -0500 Subject: [PATCH] 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. --- login.php | 769 ++++++++++++++++++++++++++++++------------------------ 1 file changed, 433 insertions(+), 336 deletions(-) diff --git a/login.php b/login.php index a1d6a2bb..618e66e0 100644 --- a/login.php +++ b/login.php @@ -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("

$config_app_name

Your IP address has been blocked due to repeated failed login attempts. Please try again later.

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 = " -
- Incorrect username or password. - -
"; - } 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 = " +
+ Your login session expired. Please sign in again. +
"; + } 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 = " +
+ Your MFA session expired. Please sign in again. +
"; + } 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 = " +
+ Incorrect username or password. +
"; + } + } + + // 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 = "
Incorrect username or password. -
"; - - } 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 = " -
- This login can be used as either an Agent account or a Client Portal account. - Please choose how you want to continue. - -
"; - } - } 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 = " +
+ This login can be used as either an Agent account or a Client Portal account. + Please choose how you want to continue. +
"; } - $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,

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!

IP Address: $session_ip
User Agent: $session_user_agent

If you did not perform this login, your credentials may be compromised.

Thanks,
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,

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!

IP Address: $session_ip
User Agent: $session_user_agent

If you did not perform this login, your credentials may be compromised.

Thanks,
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 = " -
- -
-
- -
-
-
"; + 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,

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.

Thanks,
ITFlow"; - $data = [ - [ + $token_field = " +
+ +
+
+ +
+
+
"; + + 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,

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.

Thanks,
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 = " +
+ Please enter a valid 2FA code. +
"; } - - $response = " -
- Please Enter 2FA Code! - -
"; } - } - // ========================= - // 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 = " -
- Incorrect username or password. - -
"; - } 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 = "
Incorrect username or password. -
"; + } 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 = " +
+ Incorrect username or password. +
"; + } } } } @@ -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); + ?> @@ -474,21 +576,16 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && (isset($_POST['login']) || isset($_ <?php echo nullable_htmlentities($company_name); ?> | Login - - - - - @@ -505,66 +602,73 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && (isset($_POST['login']) || isset($_
- + -

+

-
> - " - name="email" - value="" - required - > -
-
- + + +
+ " + name="email" + value="" + required autofocus + > +
+
+ +
-
-
> - -
-
- +
+ +
+
+ +
-
- -
-
- - -
-
- + + - + + +
- -
- - + + + + + + + + +
+
+ + +
+
+ + @@ -593,16 +697,9 @@ if (!$config_whitelabel_enabled) { } ?> - - - - - - -