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.
";
-
- } 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.
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.