(NOW() - INTERVAL 10 MINUTE)"
));
$failed_login_count = intval($row['failed_login_count']);
if ($failed_login_count >= 15) {
// Make sure global session_user_id is not required here (will be 0 anyway)
logAction("Login", "Blocked", "$session_ip was blocked access to login due to IP lockout");
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.
";
}
}
// Continue only if no response error
if (empty($response)) {
// Query all possible matches for that email
$sql = mysqli_query($mysqli, "
SELECT users.*,
user_settings.*,
contacts.*,
clients.*
FROM users
LEFT JOIN user_settings ON users.user_id = user_settings.user_id
LEFT JOIN contacts ON users.user_id = contacts.contact_user_id
LEFT JOIN clients ON contacts.contact_client_id = clients.client_id
WHERE user_email = '$email'
AND user_archived_at IS NULL
AND user_status = 1
AND (
user_type = 1
OR (user_type = 2 AND client_archived_at IS NULL)
)
");
$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)) {
$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 ($ut === 1 && $agentRow === null) {
$agentRow = $r;
}
if ($ut === 2 && $clientRow === null) {
$clientRow = $r;
}
}
if ($agentRow === null && $clientRow === null) {
header("HTTP/1.1 401 Unauthorized");
// Option B not possible here (we don't know user_id reliably)
logAction("Login", "Failed", "Failed login attempt using $email");
$response = "
Incorrect username or password.
";
} else {
$selectedRow = null;
$selectedType = null; // 1 agent, 2 client
// Dual role
if ($agentRow !== null && $clientRow !== null) {
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()
];
}
}
} else {
// Single role
if ($agentRow !== null) {
$selectedRow = $agentRow;
$selectedType = 1;
} else {
$selectedRow = $clientRow;
$selectedType = 2;
}
}
// Proceed if selected
if ($selectedRow !== null && $selectedType !== null) {
// Cache pending sessions BEFORE unsetting anything (needed for role->MFA and MFA success)
$pending_dual = $_SESSION['pending_dual_login'] ?? null;
$pending_mfa = $_SESSION['pending_mfa_login'] ?? null;
// NOTE: do NOT unset pending_dual_login here anymore; we may still need agent_master_key for MFA or role-step agent login
$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();
}
}
$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'];
$current_code = 0;
if (isset($_POST['current_code'])) {
$current_code = intval($_POST['current_code']);
}
$mfa_is_complete = false;
$extended_log = '';
if (empty($token)) {
$mfa_is_complete = true; // no MFA configured
}
// 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;
}
}
}
// Validate MFA code
if (!empty($current_code) && TokenAuth6238::verify($token, $current_code)) {
$mfa_is_complete = true;
$extended_log = 'with MFA';
}
if ($mfa_is_complete) {
// 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
$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);
}
// Option B: set session_user_id BEFORE logAction()
$session_user_id = $user_id;
logAction("Login", "Success", "$user_name successfully logged in $extended_log", 0, $user_id);
$_SESSION['user_id'] = $user_id;
$_SESSION['csrf_token'] = randomString(32);
$_SESSION['logged'] = true;
if ($force_mfa == 1 && $token == NULL) {
$config_start_page = "user/mfa_enforcement.php";
}
// Setup encryption session key WITHOUT PASSWORD IN SESSION
$site_encryption_master_key = null;
if ($is_mfa_step) {
// Step 3: MFA submit
$sess = $pending_mfa ?? ($_SESSION['pending_mfa_login'] ?? null);
if ($sess && !empty($sess['agent_master_key'])) {
$site_encryption_master_key = $sess['agent_master_key'];
}
} elseif ($is_role_step) {
// Step 2: role choice (no password available)
$sess = $pending_dual ?? ($_SESSION['pending_dual_login'] ?? null);
if ($sess && !empty($sess['agent_master_key'])) {
$site_encryption_master_key = $sess['agent_master_key'];
}
} else {
// Step 1: initial login (password available in this request)
if (!empty($user_encryption_ciphertext)) {
$site_encryption_master_key = decryptUserSpecificKey($user_encryption_ciphertext, $password);
}
}
if (!empty($site_encryption_master_key)) {
generateUserSessionKey($site_encryption_master_key);
}
// NOW safe to clear pending sessions AFTER we used them
unset($_SESSION['pending_mfa_login']);
unset($_SESSION['pending_dual_login']);
// 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 {
// MFA required — store *only what we need*, not password
$pending_mfa_token = bin2hex(random_bytes(32));
// If we arrived here from role-choice step, the agent master key may be in cached pending_dual
// If we arrived from initial login step, decrypt now (password in memory) and store master key.
$agent_master_key = null;
if ($is_role_step) {
$sess = $pending_dual ?? ($_SESSION['pending_dual_login'] ?? null);
if ($sess && array_key_exists('agent_master_key', $sess)) {
$agent_master_key = $sess['agent_master_key'];
}
} else {
if (!empty($user_encryption_ciphertext)) {
$agent_master_key = decryptUserSpecificKey($user_encryption_ciphertext, $password);
}
}
$_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()
];
// Now that we've transferred what we need, it's safe to clear the dual-role pending session.
unset($_SESSION['pending_dual_login']);
$token_field = "
";
if ($current_code !== 0) {
// Option B: set session_user_id BEFORE logAction()
$session_user_id = $user_id;
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.