diff --git a/data/web/inc/ajax/destroy_pw_update.php b/data/web/inc/ajax/destroy_pw_update.php new file mode 100644 index 000000000..b06c3679a --- /dev/null +++ b/data/web/inc/ajax/destroy_pw_update.php @@ -0,0 +1,6 @@ + diff --git a/data/web/inc/footer.inc.php b/data/web/inc/footer.inc.php index cd689cd14..c1249a402 100644 --- a/data/web/inc/footer.inc.php +++ b/data/web/inc/footer.inc.php @@ -32,11 +32,11 @@ foreach($_SESSION['pending_tfa_methods'] as $authdata){ if (isset($pending_tfa_authmechs['webauthn'])) { $pending_tfa_authmechs['webauthn'] = true; } -if (!isset($pending_tfa_authmechs['webauthn']) +if (!isset($pending_tfa_authmechs['webauthn']) && isset($pending_tfa_authmechs['yubi_otp'])) { $pending_tfa_authmechs['yubi_otp'] = true; } -if (!isset($pending_tfa_authmechs['webauthn']) +if (!isset($pending_tfa_authmechs['webauthn']) && !isset($pending_tfa_authmechs['yubi_otp']) && isset($pending_tfa_authmechs['totp'])) { $pending_tfa_authmechs['totp'] = true; @@ -60,6 +60,7 @@ $globalVariables = [ ), 'js_path' => '/cache/'.basename($JSPath), 'pending_tfa_methods' => @$_SESSION['pending_tfa_methods'], + 'pending_pw_update' => @$_SESSION['pending_pw_update'], 'pending_tfa_authmechs' => $pending_tfa_authmechs, 'pending_mailcow_cc_username' => @$_SESSION['pending_mailcow_cc_username'], 'lang_footer' => json_encode($lang['footer']), diff --git a/data/web/inc/functions.inc.php b/data/web/inc/functions.inc.php index 25d08b9fe..dfc3c6a6e 100644 --- a/data/web/inc/functions.inc.php +++ b/data/web/inc/functions.inc.php @@ -905,7 +905,7 @@ function check_login($user, $pass, $app_passwd_data = false) { } // Validate mailbox user - $stmt = $pdo->prepare("SELECT `password` FROM `mailbox` + $stmt = $pdo->prepare("SELECT `password`, JSON_UNQUOTE(JSON_EXTRACT(`attributes`, '$.force_pw_update')) AS `force_pw_update` FROM `mailbox` INNER JOIN domain on mailbox.domain = domain.domain WHERE `kind` NOT REGEXP 'location|thing|group' AND `mailbox`.`active`='1' @@ -939,11 +939,17 @@ function check_login($user, $pass, $app_passwd_data = false) { $stmt->execute(array(':user' => $user)); $rows = array_merge($rows, $stmt->fetchAll(PDO::FETCH_ASSOC)); } - foreach ($rows as $row) { + foreach ($rows as $row) { // verify password if (verify_hash($row['password'], $pass) !== false) { - if (!array_key_exists("app_passwd_id", $row)){ + if (!array_key_exists("app_passwd_id", $row)){ // password is not a app password + + // check if pw update is required + if (array_key_exists('force_pw_update', $row) && intval($row['force_pw_update']) == 1) { + $_SESSION['pending_pw_update'] = True; + } + // check for tfa authenticators $authenticators = get_tfa($user); if (isset($authenticators['additional']) && is_array($authenticators['additional']) && count($authenticators['additional']) > 0 && @@ -1028,7 +1034,7 @@ function update_sogo_static_view($mailbox = null) { // Check if the mailbox exists $stmt = $pdo->prepare("SELECT username FROM mailbox WHERE username = :mailbox AND active = '1'"); $stmt->execute(array(':mailbox' => $mailbox)); - $row = $stmt->fetch(PDO::FETCH_ASSOC); + $row = $stmt->fetch(PDO::FETCH_ASSOC); if ($row){ $mailbox_exists = true; } @@ -1056,7 +1062,7 @@ function update_sogo_static_view($mailbox = null) { LEFT OUTER JOIN grouped_sender_acl_external external_acl ON external_acl.username = mailbox.username WHERE mailbox.active = '1'"; - + if ($mailbox_exists) { $query .= " AND mailbox.username = :mailbox"; $stmt = $pdo->prepare($query); @@ -1065,9 +1071,9 @@ function update_sogo_static_view($mailbox = null) { $query .= " GROUP BY mailbox.username"; $stmt = $pdo->query($query); } - + $stmt = $pdo->query("DELETE FROM _sogo_static_view WHERE `c_uid` NOT IN (SELECT `username` FROM `mailbox` WHERE `active` = '1');"); - + flush_memcached(); } function edit_user_account($_data) { @@ -1079,9 +1085,10 @@ function edit_user_account($_data) { !isset($_data_log['user_new_pass2']) ?: $_data_log['user_new_pass2'] = '*'; !isset($_data_log['user_old_pass']) ?: $_data_log['user_old_pass'] = '*'; - $username = $_SESSION['mailcow_cc_username']; - $role = $_SESSION['mailcow_cc_role']; + $username = (!empty($_data['username'])) ? $_data['username'] : $_SESSION['mailcow_cc_username']; + $role = (!empty($_data['role'])) ? $_data['role'] : $_SESSION['mailcow_cc_role']; $password_old = $_data['user_old_pass']; + $skip_old_password_check = $_data['skip_old_password_check']; $pw_recovery_email = $_data['pw_recovery_email']; if (filter_var($username, FILTER_VALIDATE_EMAIL === false) || $role != 'user') { @@ -1094,22 +1101,24 @@ function edit_user_account($_data) { } // edit password - if (!empty($password_old) && !empty($_data['user_new_pass']) && !empty($_data['user_new_pass2'])) { - $stmt = $pdo->prepare("SELECT `password` FROM `mailbox` - WHERE `kind` NOT REGEXP 'location|thing|group' - AND `username` = :user"); - $stmt->execute(array(':user' => $username)); - $row = $stmt->fetch(PDO::FETCH_ASSOC); - - if (!verify_hash($row['password'], $password_old)) { - $_SESSION['return'][] = array( - 'type' => 'danger', - 'log' => array(__FUNCTION__, $_data_log), - 'msg' => 'access_denied' - ); - return false; + if ((!empty($password_old) || $skip_old_password_check) && !empty($_data['user_new_pass']) && !empty($_data['user_new_pass2'])) { + if (!$skip_old_password_check) { + $stmt = $pdo->prepare("SELECT `password` FROM `mailbox` + WHERE `kind` NOT REGEXP 'location|thing|group' + AND `username` = :user"); + $stmt->execute(array(':user' => $username)); + $row = $stmt->fetch(PDO::FETCH_ASSOC); + + if (!verify_hash($row['password'], $password_old)) { + $_SESSION['return'][] = array( + 'type' => 'danger', + 'log' => array(__FUNCTION__, $_data_log), + 'msg' => 'access_denied' + ); + return false; + } } - + $password_new = $_data['user_new_pass']; $password_new2 = $_data['user_new_pass2']; if (password_check($password_new, $password_new2) !== true) { @@ -1124,7 +1133,7 @@ function edit_user_account($_data) { ':password_hashed' => $password_hashed, ':username' => $username )); - + update_sogo_static_view(); } // edit password recovery email @@ -1152,6 +1161,7 @@ function edit_user_account($_data) { 'log' => array(__FUNCTION__, $_data_log), 'msg' => array('mailbox_modified', htmlspecialchars($username)) ); + return true; } function user_get_alias_details($username) { global $pdo; @@ -1374,7 +1384,7 @@ function set_tfa($_data) { $_data['registration']->certificate, 0 )); - + $_SESSION['return'][] = array( 'type' => 'success', 'log' => array(__FUNCTION__, $_data_log), @@ -1544,7 +1554,7 @@ function unset_tfa_key($_data) { try { if (!is_numeric($id)) $access_denied = true; - + // set access_denied error if ($access_denied){ $_SESSION['return'][] = array( @@ -1553,7 +1563,7 @@ function unset_tfa_key($_data) { 'msg' => 'access_denied' ); return false; - } + } // check if it's last key $stmt = $pdo->prepare("SELECT COUNT(*) AS `keys` FROM `tfa` @@ -1602,7 +1612,7 @@ function get_tfa($username = null, $id = null) { WHERE `username` = :username AND `active` = '1'"); $stmt->execute(array(':username' => $username)); $results = $stmt->fetchAll(PDO::FETCH_ASSOC); - + // no tfa methods found if (count($results) == 0) { $data['name'] = 'none'; @@ -1810,8 +1820,8 @@ function verify_tfa_login($username, $_data) { 'msg' => array('webauthn_authenticator_failed') ); return false; - } - + } + if (empty($process_webauthn['publicKey']) || $process_webauthn['publicKey'] === false) { $_SESSION['return'][] = array( 'type' => 'danger', @@ -2173,7 +2183,7 @@ function cors($action, $data = null) { 'msg' => 'access_denied' ); return false; - } + } $allowed_origins = isset($data['allowed_origins']) ? $data['allowed_origins'] : array($_SERVER['SERVER_NAME']); $allowed_origins = !is_array($allowed_origins) ? array_filter(array_map('trim', explode("\n", $allowed_origins))) : $allowed_origins; @@ -2206,7 +2216,7 @@ function cors($action, $data = null) { $redis->hMSet('CORS_SETTINGS', array( 'allowed_origins' => implode(', ', $allowed_origins), 'allowed_methods' => implode(', ', $allowed_methods) - )); + )); } catch (RedisException $e) { $_SESSION['return'][] = array( 'type' => 'danger', @@ -2258,10 +2268,10 @@ function cors($action, $data = null) { header('Access-Control-Allow-Headers: Accept, Content-Type, X-Api-Key, Origin'); // Access-Control settings requested, this is just a preflight request - if ($_SERVER['REQUEST_METHOD'] == 'OPTIONS' && + if ($_SERVER['REQUEST_METHOD'] == 'OPTIONS' && isset($_SERVER['HTTP_ACCESS_CONTROL_REQUEST_METHOD']) && isset($_SERVER['HTTP_ACCESS_CONTROL_REQUEST_HEADERS'])) { - + $allowed_methods = explode(', ', $cors_settings["allowed_methods"]); if (in_array($_SERVER['HTTP_ACCESS_CONTROL_REQUEST_METHOD'], $allowed_methods, true)) // method allowed send 200 OK @@ -2315,7 +2325,7 @@ function reset_password($action, $data = null) { break; case 'issue': $username = $data; - + // perform cleanup $stmt = $pdo->prepare("DELETE FROM `reset_password` WHERE created < DATE_SUB(NOW(), INTERVAL :lifetime MINUTE);"); $stmt->execute(array(':lifetime' => $PW_RESET_TOKEN_LIFETIME)); @@ -2397,8 +2407,8 @@ function reset_password($action, $data = null) { $request_date = new DateTime(); $locale_date = locale_get_default(); $date_formatter = new IntlDateFormatter( - $locale_date, - IntlDateFormatter::FULL, + $locale_date, + IntlDateFormatter::FULL, IntlDateFormatter::FULL ); $formatted_request_date = $date_formatter->format($request_date); @@ -2514,7 +2524,7 @@ function reset_password($action, $data = null) { $stmt->execute(array( ':username' => $username )); - + $_SESSION['return'][] = array( 'type' => 'success', 'log' => array(__FUNCTION__, $action, $_data_log), @@ -2557,7 +2567,7 @@ function reset_password($action, $data = null) { $text = $data['text']; $html = $data['html']; $subject = $data['subject']; - + if (!filter_var($from, FILTER_VALIDATE_EMAIL)) { $_SESSION['return'][] = array( 'type' => 'danger', @@ -2590,7 +2600,7 @@ function reset_password($action, $data = null) { ); return false; } - + ini_set('max_execution_time', 0); ini_set('max_input_time', 0); $mail = new PHPMailer; @@ -2622,7 +2632,7 @@ function reset_password($action, $data = null) { return false; } $mail->ClearAllRecipients(); - + return true; break; } diff --git a/data/web/inc/triggers.inc.php b/data/web/inc/triggers.inc.php index 5c625e414..ec6522366 100644 --- a/data/web/inc/triggers.inc.php +++ b/data/web/inc/triggers.inc.php @@ -10,15 +10,37 @@ if (!empty($_GET['sso_token'])) { } } + +if (isset($_POST["forced_pw_update"]) && !empty($_POST['new_password']) && !empty($_POST['new_password2'])) { + $result = edit_user_account(array( + 'username' => $_SESSION['pending_mailcow_cc_username'], + 'role' => $_SESSION['pending_mailcow_cc_role'], + 'user_new_pass' => $_POST['new_password'], + 'user_new_pass2' => $_POST['new_password2'], + 'skip_old_password_check' => True + )); + + if ($result) { + $_SESSION['mailcow_cc_username'] = $_SESSION['pending_mailcow_cc_username']; + $_SESSION['mailcow_cc_role'] = $_SESSION['pending_mailcow_cc_role']; + unset($_SESSION['pending_mailcow_cc_username']); + unset($_SESSION['pending_mailcow_cc_role']); + unset($_SESSION['pending_pw_update']); + } + + header("Location: /"); + exit; +} + if (isset($_POST["pw_reset_request"]) && !empty($_POST['username'])) { - reset_password("issue", $_POST['username']); + $resultreset_password("issue", $_POST['username']); header("Location: /"); exit; } if (isset($_POST["pw_reset"])) { $username = reset_password("check", $_POST['token']); $reset_result = reset_password("reset", array( - 'new_password' => $_POST['new_password'], + 'new_password' => $_POST['new_password'], 'new_password2' => $_POST['new_password2'], 'token' => $_POST['token'], 'username' => $username, @@ -47,13 +69,15 @@ if (isset($_POST["verify_tfa_login"])) { header("Location: /"); exit; } else { - $_SESSION['mailcow_cc_username'] = $_SESSION['pending_mailcow_cc_username']; - $_SESSION['mailcow_cc_role'] = $_SESSION['pending_mailcow_cc_role']; - unset($_SESSION['pending_mailcow_cc_username']); - unset($_SESSION['pending_mailcow_cc_role']); - unset($_SESSION['pending_tfa_methods']); - - header("Location: /user"); + if (!$_SESSION['pending_pw_update']) { + $_SESSION['mailcow_cc_username'] = $_SESSION['pending_mailcow_cc_username']; + $_SESSION['mailcow_cc_role'] = $_SESSION['pending_mailcow_cc_role']; + unset($_SESSION['pending_mailcow_cc_username']); + unset($_SESSION['pending_mailcow_cc_role']); + unset($_SESSION['pending_tfa_methods']); + + header("Location: /user"); + } } } else { unset($_SESSION['pending_pw_reset_token']); @@ -97,24 +121,30 @@ if (isset($_POST["login_user"]) && isset($_POST["pass_user"])) { header("Location: /mailbox"); } elseif ($as == "user") { - $_SESSION['mailcow_cc_username'] = $login_user; - $_SESSION['mailcow_cc_role'] = "user"; - $http_parameters = explode('&', $_SESSION['index_query_string']); - unset($_SESSION['index_query_string']); - if (in_array('mobileconfig', $http_parameters)) { - if (in_array('only_email', $http_parameters)) { - header("Location: /mobileconfig.php?only_email"); - die(); - } - header("Location: /mobileconfig.php"); - die(); + if ($_SESSION['pending_pw_update']) { + $_SESSION['pending_mailcow_cc_username'] = $login_user; + $_SESSION['pending_mailcow_cc_role'] = "user"; + } else { + $_SESSION['mailcow_cc_username'] = $login_user; + $_SESSION['mailcow_cc_role'] = "user"; + $http_parameters = explode('&', $_SESSION['index_query_string']); + unset($_SESSION['index_query_string']); + if (in_array('mobileconfig', $http_parameters)) { + if (in_array('only_email', $http_parameters)) { + header("Location: /mobileconfig.php?only_email"); + die(); } - header("Location: /user"); + header("Location: /mobileconfig.php"); + die(); + } + header("Location: /user"); + } } elseif ($as != "pending") { unset($_SESSION['pending_mailcow_cc_username']); unset($_SESSION['pending_mailcow_cc_role']); unset($_SESSION['pending_tfa_methods']); + unset($_SESSION['pending_pw_update']); unset($_SESSION['mailcow_cc_username']); unset($_SESSION['mailcow_cc_role']); } diff --git a/data/web/templates/base.twig b/data/web/templates/base.twig index bd1f5b413..8703fa9ef 100644 --- a/data/web/templates/base.twig +++ b/data/web/templates/base.twig @@ -211,8 +211,31 @@ function recursiveBase64StrToArrayBuffer(obj) { mailcow_alert_box('{{ alert_msg|raw|e("js") }}', '{{ alert_type }}'); {% endfor %} + + // Confirm PW Update modal + {% if pending_pw_update %} + new bootstrap.Modal(document.getElementById("ConfirmPWUpdateModal"), { + backdrop: 'static', + keyboard: false + }).show(); + + $('#ConfirmPWUpdateModal').on('hidden.bs.modal', function(){ + // cancel pending login + $.ajax({ + type: "GET", + cache: false, + dataType: 'script', + url: '/inc/ajax/destroy_pw_update.php', + complete: function(data){ + window.location = window.location.href.split("#")[0]; + } + }); + }); + {% endif %} + + // Confirm TFA modal - {% if pending_tfa_methods %} + {% if pending_tfa_methods %} new bootstrap.Modal(document.getElementById("ConfirmTFAModal"), { backdrop: 'static', keyboard: false @@ -235,7 +258,7 @@ function recursiveBase64StrToArrayBuffer(obj) { $(".totp-authenticator-selection").click(function(){ $(".totp-authenticator-selection").removeClass("active"); $(this).addClass("active"); - + var id = $(this).children('input').first().val(); $("#totp_selected_id").val(id); @@ -244,7 +267,7 @@ function recursiveBase64StrToArrayBuffer(obj) { if ($('.totp-authenticator-selection').length == 1 && $('#pending_tfa_tab_yubi_otp').length == 0 && $('.webauthn-authenticator-selection').length == 0){ - + // select default if only one authenticator exists $('.totp-authenticator-selection').addClass("active"); @@ -257,7 +280,7 @@ function recursiveBase64StrToArrayBuffer(obj) { $('#pending_tfa_tab_totp').on('shown.bs.tab', function() { // autofocus setTimeout(function() { $("#collapseTotpTFA").find('input[name="token"]').focus(); }, 200); - }); + }); // validate Yubi OTP tfa if ($('.webauthn-authenticator-selection').length == 0){ // autofocus @@ -276,10 +299,10 @@ function recursiveBase64StrToArrayBuffer(obj) { $(".webauthn-authenticator-selection").click(function(){ $(".webauthn-authenticator-selection").removeClass("active"); $(this).addClass("active"); - + var id = $(this).children('input').first().val(); $("#webauthn_selected_id").val(id); - + var webauthn_status_auth = document.getElementById('webauthn_status_auth'); webauthn_status_auth.style.setProperty('display', 'flex', 'important'); var webauthn_return_code = document.getElementById('webauthn_return_code'); @@ -302,7 +325,7 @@ function recursiveBase64StrToArrayBuffer(obj) { console.log(json); if (json.success === false) throw new Error(); if (json.type === "error") throw new Error(json.msg); - + recursiveBase64StrToArrayBuffer(json); return json; }).then(getCredentialArgs => { @@ -329,7 +352,7 @@ function recursiveBase64StrToArrayBuffer(obj) { webauthn_return_code.style.setProperty('display', 'block', 'important'); webauthn_return_code.innerHTML = lang_tfa.error_code + ': ' + err + ' ' + lang_tfa.reload_retry; }); - } + } }); $('#ConfirmTFAModal').on('hidden.bs.modal', function(){ // cancel pending login @@ -343,7 +366,7 @@ function recursiveBase64StrToArrayBuffer(obj) { } }); }); - {% endif %} + {% endif %} // Validate FIDO2 @@ -540,7 +563,7 @@ function recursiveBase64StrToArrayBuffer(obj) { Version: {{ mailcow_info.version_tag }} - {% endif %} + {% endif %} {% if mailcow_cc_username and mailcow_info.mailcow_branch|lower == "nightly" and mailcow_info.version_tag|default %} 🛠️🐮 + 🐋 = 💕 diff --git a/data/web/templates/modals/footer.twig b/data/web/templates/modals/footer.twig index 8ff112d5d..7818fbe58 100644 --- a/data/web/templates/modals/footer.twig +++ b/data/web/templates/modals/footer.twig @@ -139,8 +139,7 @@ - - +