Add remember me feature (#7750)

This commit is contained in:
Alex
2026-02-24 13:43:18 -07:00
committed by GitHub
parent 1ee116c3ee
commit 416dd107fd
10 changed files with 256 additions and 8 deletions

View File

@@ -135,6 +135,33 @@ $text['label-password_description']['zh-cn'] = "輸入您的密碼。";
$text['label-password_description']['ja-jp'] = "パスワードを入力してください。";
$text['label-password_description']['ko-kr'] = "비밀번호를 입력하세요.";
$text['label-remember_me']['en-us'] = "Remember Me";
$text['label-remember_me']['en-gb'] = "Remember Me";
$text['label-remember_me']['ar-eg'] = "تذكرني";
$text['label-remember_me']['de-at'] = "Angemeldet bleiben";
$text['label-remember_me']['de-ch'] = "Angemeldet bleiben";
$text['label-remember_me']['de-de'] = "Angemeldet bleiben";
$text['label-remember_me']['el-gr'] = "Θύστε μου";
$text['label-remember_me']['es-cl'] = "Recuérdame";
$text['label-remember_me']['es-mx'] = "Recuérdame";
$text['label-remember_me']['fr-ca'] = "Se souvenir de moi";
$text['label-remember_me']['fr-fr'] = "Se souvenir de moi";
$text['label-remember_me']['he-il'] = "זכור אותי";
$text['label-remember_me']['it-it'] = "Ricordami";
$text['label-remember_me']['ka-ge'] = "დაიხმარე შემთხვევით";
$text['label-remember_me']['nl-nl'] = "Onthoud me";
$text['label-remember_me']['pl-pl'] = "Pamiętaj mnie";
$text['label-remember_me']['pt-br'] = "Lembre-me";
$text['label-remember_me']['pt-pt'] = "Lembre-me";
$text['label-remember_me']['ro-ro'] = "Ține-mă minte";
$text['label-remember_me']['ru-ru'] = "Запомнить меня";
$text['label-remember_me']['sv-se'] = "Kom ihåg mig";
$text['label-remember_me']['uk-ua'] = "Пам'ятати мене";
$text['label-remember_me']['tr-tr'] = "Beni hatırla";
$text['label-remember_me']['zh-cn'] = "记住我";
$text['label-remember_me']['ja-jp'] = "私を覚えて";
$text['label-remember_me']['ko-kr'] = "나를 기억해줘";
$text['description-totp']['en-us'] = "Scan the code with an authentication application or password manager. Then use it to generate the token for the login.";
$text['description-totp']['en-gb'] = "Scan the code with an authentication application or password manager. Then use it to generate the token for the login.";
$text['description-totp']['ar-eg'] = "امسح الرمز ضوئيًا باستخدام تطبيق المصادقة أو مدير كلمات المرور. ثم استخدمه لإنشاء الرمز المميز لتسجيل الدخول.";

View File

@@ -17,7 +17,7 @@
The Initial Developer of the Original Code is
Mark J Crane <markjcrane@fusionpbx.com>
Portions created by the Initial Developer are Copyright (C) 2008-2024
Portions created by the Initial Developer are Copyright (C) 2008-2026
the Initial Developer. All Rights Reserved.
Contributor(s):
@@ -101,8 +101,122 @@ class authentication {
//check if contacts app exists
$contacts_exists = file_exists(dirname(__DIR__, 4) . '/core/contacts/');
//check for remember me cookie
if (isset($_COOKIE['remember'])) {
//set variables
$plugin_name = 'remember';
$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 ";
$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' ";
$sql .= "limit 1 ";
$parameters['remember_selector'] = $cookie_selector;
$parameters['remote_address'] = $remote_address;
$parameters['user_agent'] = $user_agent;
$user_logs = $this->database->select($sql, $parameters, 'row');
unset($sql, $parameters);
//validate the token
if (!empty($user_logs) && password_verify($cookie_validator, $user_logs['remember_validator'])) {
//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 .= "and u.user_enabled = 'true' \n";
$parameters['user_uuid'] = $user_logs['user_uuid'];
$row = $this->database->select($sql, $parameters, 'row');
unset($sql, $parameters);
//get the contact details
if ($contacts_exists && !empty($row["contact_uuid"])) {
$sql = "select * from v_contacts \n";
$sql .= "where contact_uuid = :contact_uuid \n";
$sql .= "and domain_uuid = :domain_uuid \n";
$parameters['contact_uuid'] = $row["contact_uuid"];
$parameters['domain_uuid'] = $row["domain_uuid"];
$contact = $this->database->select($sql, $parameters, 'row');
unset($sql, $parameters);
}
//build a result array
$result['plugin'] = $plugin_name;
$result['domain_name'] = $row["domain_name"];
$result['username'] = $row['username'];
$result['user_uuid'] = $row['user_uuid'];
$result['contact_uuid'] = $row["contact_uuid"];
if ($contacts_exists) {
$result["contact_organization"] = $contact["contact_organization"] ?? '';
$result["contact_name_given"] = $contact["contact_name_given"] ?? '';
$result["contact_name_family"] = $contact["contact_name_family"] ?? '';
$result["contact_image"] = $contact["contact_image"] ?? '';
}
$result['domain_uuid'] = $row['domain_uuid'];
$result['authorized'] = true;
//set the domain_uuid
$this->domain_uuid = $row["domain_uuid"];
//set the user_uuid
$this->user_uuid = $row["user_uuid"];
//save the result to the authentication plugin
$_SESSION['authentication']['methods'] = [];
$_SESSION['authentication']['methods'][] = $plugin_name;
$_SESSION['authentication']['plugin'] = [];
$_SESSION['authentication']['plugin'][$plugin_name] = $result;
//create the session
self::create_user_session($result, $this->settings);
//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'
]);
}
}
//use the authentication plugins
foreach ($_SESSION['authentication']['methods'] as $name) {
//skip the loop if already authorized
if (isset($result['authorized']) && $result['authorized']) {
break;
}
//already processed the plugin move to the next plugin
if (!empty($_SESSION['authentication']['plugin'][$name]['authorized']) && $_SESSION['authentication']['plugin'][$name]['authorized']) {
continue;
@@ -178,7 +292,7 @@ class authentication {
}
//debug information
//view_array($_SESSION['authentication'], false);
// view_array($_SESSION['authentication'], false);
//set authorized to false if any authentication method failed
$authorized = false;
@@ -212,6 +326,60 @@ class authentication {
}
}
//create remember me token
if ($authorized && isset($_SESSION['username']) && isset($_SESSION['remember'])) {
//set session variables
$input_username = $_SESSION['username'];
$remember = $_SESSION['remember'];
//match the username
$sql = "select user_uuid from v_users ";
$sql .= "where username = :username";
$parameters['username'] = $input_username;
$user = $this->database->select($sql, $parameters, 'row');
unset($sql, $parameters);
if ($remember && $user) {
//generate the token
$selector = uuid();
$validator = generate_password(32);
$hashed_validator = password_hash($validator, PASSWORD_DEFAULT);
$token = $selector.':'.$validator;
$remote_address = $_SERVER['REMOTE_ADDR'] ?? '';
$user_agent = $_SERVER['HTTP_USER_AGENT'] ?? '';
//save token to the user logs
$sql = "update v_user_logs ";
$sql .= "set remember_selector = :remember_selector, ";
$sql .= "remember_validator = :remember_validator ";
$sql .= "where user_log_uuid = ( ";
$sql .= " select user_log_uuid FROM v_user_logs ";
$sql .= " where result = 'success' ";
$sql .= " and remote_address = :remote_address ";
$sql .= " and user_agent = :user_agent ";
$sql .= " and user_uuid = :user_uuid ";
$sql .= " and timestamp > NOW() - INTERVAL '7 days' ";
$sql .= " order by timestamp desc limit 1 ";
$sql .= ") ";
$parameters['remember_selector'] = $selector;
$parameters['remember_validator'] = $hashed_validator;
$parameters['remote_address'] = $remote_address;
$parameters['user_agent'] = $user_agent;
$parameters['user_uuid'] = $user['user_uuid'];
$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'
]);
}
}
//set a session variable to indicate whether or not we are authorized
$_SESSION['authorized'] = $authorized;

View File

@@ -17,7 +17,7 @@
The Initial Developer of the Original Code is
Mark J Crane <markjcrane@fusionpbx.com>
Portions created by the Initial Developer are Copyright (C) 2008-2024
Portions created by the Initial Developer are Copyright (C) 2008-2026
the Initial Developer. All Rights Reserved.
Contributor(s):
@@ -68,6 +68,7 @@ class plugin_database {
$theme_background_video = (isset($background_videos[0])) ? $background_videos[0] : '';
$login_domain_name_visible = $settings->get('login', 'domain_name_visible', false);
$login_domain_name = $settings->get('login', 'domain_name');
$login_remember_me = $settings->get('login', 'remember_me');
$login_destination = $settings->get('login', 'destination');
$users_unique = $settings->get('users', 'unique', '');
@@ -115,6 +116,7 @@ class plugin_database {
$view->assign("label_username", $text['label-username']);
$view->assign("label_password", $text['label-password']);
$view->assign("label_domain", $text['label-domain']);
$view->assign("label_remember_me", $text['label-remember_me']);
$view->assign("button_login", $text['button-login']);
//assign default values to the template
@@ -122,6 +124,7 @@ class plugin_database {
$view->assign("login_destination_url", $login_destination);
$view->assign("login_domain_name_visible", $login_domain_name_visible);
$view->assign("login_domain_names", $login_domain_name);
$view->assign("login_remember_me", $login_remember_me);
$view->assign("login_password_reset_enabled", $login_password_reset_enabled);
$view->assign("favicon", $theme_favicon);
$view->assign("login_logo_width", $theme_login_logo_width);
@@ -186,6 +189,9 @@ class plugin_database {
if (isset($_REQUEST["password"])) {
$this->password = $_REQUEST["password"];
}
if (isset($_POST["remember"])) {
$_SESSION['remember'] = $_POST["remember"];
}
if (isset($_REQUEST["key"])) {
$this->key = $_REQUEST["key"];
}

View File

@@ -91,6 +91,14 @@
{/if}
{/if}
</div>
{if !empty($login_remember_me)}
<div style="display: flex;">
<span style='padding-left: 15px;'>
<input type='checkbox' id='remember' name='remember'>
<label for='remember' class='login_text' style='padding-bottom: 1px;'>{$label_remember_me}</label>
</span>
</div>
{/if}
<div>
<input type='submit' id='btn_login' class='btn' style='width: 100px; margin-top: 15px;' value='{$button_login}' /><br />
{if !empty($login_password_reset_enabled) && $login_password_reset_enabled}

View File

@@ -249,6 +249,14 @@
$apps[$x]['default_settings'][$y]['default_setting_enabled'] = "false";
$apps[$x]['default_settings'][$y]['default_setting_description'] = "Set the domain to use in the Password Reset link sent via email.";
$y++;
$apps[$x]['default_settings'][$y]['default_setting_uuid'] = "d8b18b38-8510-489a-9bba-33534550ddef";
$apps[$x]['default_settings'][$y]['default_setting_category'] = "login";
$apps[$x]['default_settings'][$y]['default_setting_subcategory'] = "remember_me";
$apps[$x]['default_settings'][$y]['default_setting_name'] = "boolean";
$apps[$x]['default_settings'][$y]['default_setting_value'] = "true";
$apps[$x]['default_settings'][$y]['default_setting_enabled'] = "true";
$apps[$x]['default_settings'][$y]['default_setting_description'] = "Display a Remember Me checkbox on the login box.";
$y++;
$apps[$x]['default_settings'][$y]['default_setting_uuid'] = "962ac32c-74ce-4cce-b1d9-89f4d921493d";
$apps[$x]['default_settings'][$y]['default_setting_category'] = "login";
$apps[$x]['default_settings'][$y]['default_setting_subcategory'] = "domain_name_visible";

View File

@@ -93,6 +93,16 @@
$apps[$x]['db'][$y]['fields'][$z]['search_by'] = 'true';
$apps[$x]['db'][$y]['fields'][$z]['description']['en-us'] = '';
$z++;
$apps[$x]['db'][$y]['fields'][$z]['name'] = 'remember_selector';
$apps[$x]['db'][$y]['fields'][$z]['type']['pgsql'] = "uuid";
$apps[$x]['db'][$y]['fields'][$z]['type']['sqlite'] = "text";
$apps[$x]['db'][$y]['fields'][$z]['type']['mysql'] = "char(36)";
$apps[$x]['db'][$y]['fields'][$z]['description']['en-us'] = "Remember me selector";
$z++;
$apps[$x]['db'][$y]['fields'][$z]['name'] = 'remember_validator';
$apps[$x]['db'][$y]['fields'][$z]['type'] = 'text';
$apps[$x]['db'][$y]['fields'][$z]['description']['en-us'] = "Remember me validator";
$z++;
$apps[$x]['db'][$y]['fields'][$z]['name'] = "insert_date";
$apps[$x]['db'][$y]['fields'][$z]['type']['pgsql'] = 'timestamptz';
$apps[$x]['db'][$y]['fields'][$z]['type']['sqlite'] = 'date';

View File

@@ -17,7 +17,7 @@
The Initial Developer of the Original Code is
Mark J Crane <markjcrane@fusionpbx.com>
Portions created by the Initial Developer are Copyright (C) 2019-2024
Portions created by the Initial Developer are Copyright (C) 2019-2026
the Initial Developer. All Rights Reserved.
Contributor(s):
@@ -113,6 +113,7 @@ class user_logs {
$array['user_logs'][0]["remote_address"] = $_SERVER['REMOTE_ADDR'];
$array['user_logs'][0]["user_agent"] = $_SERVER['HTTP_USER_AGENT'];
$array['user_logs'][0]["session_id"] = session_id();
$array['user_logs'][0]["remember_token"] = $result['remember_token'];
if ($result["authorized"]) {
$array['user_logs'][0]["result"] = 'success';
} else {

View File

@@ -17,7 +17,7 @@
The Initial Developer of the Original Code is
Mark J Crane <markjcrane@fusionpbx.com>
Portions created by the Initial Developer are Copyright (C) 2008-2019
Portions created by the Initial Developer are Copyright (C) 2008-2026
the Initial Developer. All Rights Reserved.
Contributor(s):
@@ -30,6 +30,16 @@
//use custom logout destination if set otherwise redirect to the index page
$logout_destination = $settings->get('login', 'logout_destination', PROJECT_PATH.'/');
//remove remember me token
setcookie('remember', '', time() - 3600, '/');
$sql = "update v_user_logs ";
$sql .= "set remember_selector = null, ";
$sql .= "remember_validator = null ";
$sql .= "where user_uuid = :user_uuid ";
$parameters['user_uuid'] = $_SESSION['user_uuid'];
$database->execute($sql, $parameters);
unset($sql, $parameters);
//destroy session
session_unset();
session_destroy();

View File

@@ -17,7 +17,7 @@
The Initial Developer of the Original Code is
Mark J Crane <markjcrane@fusionpbx.com>
Portions created by the Initial Developer are Copyright (C) 2008-2025
Portions created by the Initial Developer are Copyright (C) 2008-2026
the Initial Developer. All Rights Reserved.
Contributor(s):
@@ -145,7 +145,7 @@
settings::clear_cache();
//if logged in, redirect to login destination
if (!isset($_REQUEST["key"])) {
if (!isset($_REQUEST["key"]) && !isset($_COOKIE['remember'])) {
//connect to the settings object
$settings = new settings(['database' => $database, 'domain_uuid' => $domain_uuid, 'user_uuid' => $user_uuid]);

View File

@@ -34,7 +34,7 @@
//set the current domain_uuid
$domain_uuid = $_SESSION['domain_uuid'] ?? '';
//initialize the settigns object
//initialize the settings object
$settings = new settings(['database' => $database, 'domain_uuid' => $domain_uuid]);
//get action, if any - define, request, reset
@@ -259,6 +259,16 @@
$database->execute($sql, $parameters);
unset($sql, $parameters);
//remove remember me token
setcookie('remember', '', time() - 3600, '/');
$sql = "update v_user_logs ";
$sql .= "set remember_selector = null, ";
$sql .= "remember_validator = null ";
$sql .= "where username = :username ";
$parameters['username'] = $_SESSION['valid_username'];
$database->execute($sql, $parameters);
unset($sql, $parameters);
//build the user log array
$log_array['type'] = 'Password Changed';
$log_array['domain_uuid'] = $_SESSION['valid_domain'];