Improve remember me token handling and validation (#7831)

- validate cookie format
- check if token was found first before validation to prevent race condition
- separate the expired/ip/user agent check for different handling
This commit is contained in:
Alex
2026-04-06 20:33:30 +00:00
committed by GitHub
parent 562c837bd0
commit f5d17de1d2
@@ -101,45 +101,99 @@ class authentication {
//check if contacts app exists
$contacts_exists = file_exists(dirname(__DIR__, 4) . '/core/contacts/');
//check for remember me cookie
// Check for remember me cookie
if (isset($_COOKIE['remember'])) {
//set variables
$plugin_name = 'remember';
// Validate cookie format
$parts = explode(':', $_COOKIE['remember'], 2);
if (count($parts) !== 2 || !is_uuid($parts[0])) {
// Invalid format
user_logs::add(['authorized' => false, 'domain_uuid' => $_SESSION['domain_uuid']], "Invalid remember me token format");
unset($_COOKIE['remember']);
setcookie('remember', '', time() - 3600, '/');
return false;
}
// Set variables
[$cookie_selector, $cookie_validator] = $parts;
$remote_address = $_SERVER['REMOTE_ADDR'] ?? '';
$user_agent = $_SERVER['HTTP_USER_AGENT'] ?? '';
list($cookie_selector, $cookie_validator) = explode(":", $_COOKIE['remember']);
//get user logs
$sql = "select user_uuid, remember_validator from v_user_logs ";
// Get the user log
$sql = "select \n";
$sql .= "user_uuid, \n";
$sql .= "remember_validator, \n";
$sql .= "(timestamp < now() - interval '7 days')::int as expired, \n";
$sql .= "(remote_address is distinct from :remote_address)::int as invalid_remote_address, \n";
$sql .= "(user_agent is distinct from :user_agent)::int as invalid_user_agent \n";
$sql .= "from v_user_logs \n";
$sql .= "where remember_selector = :remember_selector \n";
$sql .= "and remote_address = :remote_address ";
$sql .= "and user_agent = :user_agent ";
$sql .= "and timestamp > NOW() - INTERVAL '7 days' ";
$sql .= "and result = 'success' ";
$parameters['remember_selector'] = $cookie_selector;
$parameters['remote_address'] = $remote_address;
$parameters['user_agent'] = $user_agent;
$user_log = $this->database->select($sql, $parameters, 'row');
unset($sql, $parameters);
//validate the token
if (!empty($user_log['remember_validator']) && password_verify($cookie_validator, $user_log['remember_validator'])) {
//get the user details
// Check if a token was found
if (!empty($user_log['remember_validator'])) {
// Validate the token
if (!password_verify($cookie_validator, $user_log['remember_validator'])) {
// Invalid token
user_logs::add(['authorized' => false, 'domain_uuid' => $_SESSION['domain_uuid']], "Invalid remember me token");
unset($_COOKIE['remember']);
setcookie('remember', '', time() - 3600, '/');
return false;
}
else if ($user_log['expired'] || $user_log['invalid_remote_address'] || $user_log['invalid_user_agent']) {
unset($_COOKIE['remember']);
setcookie('remember', '', time() - 3600, '/');
return false;
}
// Generate new token
$selector = uuid();
$validator = generate_password(32);
$hashed_validator = password_hash($validator, PASSWORD_DEFAULT);
$token = $selector.':'.$validator;
// Update the user log
$sql = "update v_user_logs \n";
$sql .= "set remember_selector = :remember_selector, \n";
$sql .= "remember_validator = :remember_validator \n";
$sql .= "where remember_selector = :old_selector \n";
$parameters['remember_selector'] = $selector;
$parameters['remember_validator'] = $hashed_validator;
$parameters['old_selector'] = $cookie_selector;
$this->database->execute($sql, $parameters);
unset($sql, $parameters);
// Set the cookie
setcookie('remember', $token, [
'expires' => strtotime('+7 days'),
'path' => '/',
'secure' => true,
'httponly' => true,
'samesite' => 'Strict'
]);
// Get the user details
$sql = "select \n";
$sql .= "u.domain_uuid, \n";
$sql .= "d.domain_name, \n";
$sql .= "u.user_uuid, \n";
$sql .= "u.username, \n";
$sql .= "u.contact_uuid \n";
$sql .= "from v_users as u, v_domains as d \n";
$sql .= "where user_uuid = :user_uuid \n";
$sql .= "and u.domain_uuid = d.domain_uuid \n";
$sql .= "from v_users as u \n";
$sql .= "inner join v_domains as d on u.domain_uuid = d.domain_uuid \n";
$sql .= "where u.user_uuid = :user_uuid \n";
$sql .= "and u.user_enabled = 'true' \n";
$parameters['user_uuid'] = $user_log['user_uuid'];
$row = $this->database->select($sql, $parameters, 'row');
unset($sql, $parameters);
//get the contact details
// Get the contact details
if ($contacts_exists && !empty($row["contact_uuid"])) {
$sql = "select * from v_contacts \n";
$sql .= "where contact_uuid = :contact_uuid \n";
@@ -150,8 +204,8 @@ class authentication {
unset($sql, $parameters);
}
//build a result array
$result['plugin'] = $plugin_name;
// Build a result array
$result['plugin'] = 'remember';
$result['domain_name'] = $row["domain_name"];
$result['username'] = $row['username'];
$result['user_uuid'] = $row['user_uuid'];
@@ -165,58 +219,21 @@ class authentication {
$result['domain_uuid'] = $row['domain_uuid'];
$result['authorized'] = true;
//set the domain_uuid
// Set the domain_uuid
$this->domain_uuid = $row["domain_uuid"];
//set the user_uuid
// Set the user_uuid
$this->user_uuid = $row["user_uuid"];
//generate new token
$selector = uuid();
$validator = generate_password(32);
$hashed_validator = password_hash($validator, PASSWORD_DEFAULT);
$token = $selector.':'.$validator;
//update the user logs
$sql = "update v_user_logs ";
$sql .= "set remember_selector = :remember_selector, ";
$sql .= "remember_validator = :remember_validator ";
$sql .= "where remember_selector = :cookie_selector ";
$parameters['remember_selector'] = $selector;
$parameters['remember_validator'] = $hashed_validator;
$parameters['cookie_selector'] = $cookie_selector;
$this->database->execute($sql, $parameters);
unset($sql, $parameters);
//set the cookie
setcookie('remember', $token, [
'expires' => strtotime('+7 days'),
'path' => '/',
'secure' => true,
'httponly' => true,
'samesite' => 'Strict'
]);
//create the session
// Create the session
self::create_user_session($result, $this->settings);
//set the session authorized to true
// Set the session authorized to true
$_SESSION['authorized'] = true;
//return the result
// Return the result
return $result;
}
else {
//invalid token
unset($_COOKIE['remember']);
setcookie('remember', '', time() - 3600, '/');
//log the attempt
$log_array['domain_uuid'] = $_SESSION['domain_uuid'];
$log_array['authorized'] = false;
$failed_login_message = "Invalid remember me token";
user_logs::add($log_array, $failed_login_message);
}
}
//use the authentication plugins
@@ -330,13 +347,13 @@ class authentication {
}
}
//create remember me token
// Create remember me token
if ($authorized && isset($_SESSION['username']) && isset($_SESSION['remember'])) {
//set session variables
// Set session variables
$input_username = $_SESSION['username'];
$remember = $_SESSION['remember'];
//match the username
// Match the username
$sql = "select user_uuid from v_users ";
$sql .= "where username = :username";
$parameters['username'] = $input_username;
@@ -344,17 +361,17 @@ class authentication {
unset($sql, $parameters);
if ($remember && $user) {
//generate the token
// Generate the token
$selector = uuid();
$validator = generate_password(32);
$hashed_validator = password_hash($validator, PASSWORD_DEFAULT);
$token = $selector.':'.$validator;
//save token to the user log array
// Save token to the user log array
$_SESSION['authentication']['plugin'][$name]['remember_selector'] = $selector;
$_SESSION['authentication']['plugin'][$name]['remember_validator'] = $hashed_validator;
//set the cookie
// Set the cookie
setcookie('remember', $token, [
'expires' => strtotime('+7 days'),
'path' => '/',