From be08742653f1678addbf7dc4a612c1f7e94dbea5 Mon Sep 17 00:00:00 2001 From: FreddleSpl0it Date: Thu, 14 Jul 2022 18:37:21 +0200 Subject: [PATCH 1/5] exclude oauth clients & app passwords from mailbox tfa --- data/web/inc/functions.inc.php | 38 +++++++++++++++++------------- data/web/inc/prerequisites.inc.php | 2 +- 2 files changed, 22 insertions(+), 18 deletions(-) diff --git a/data/web/inc/functions.inc.php b/data/web/inc/functions.inc.php index 08963888..b2c5eee5 100644 --- a/data/web/inc/functions.inc.php +++ b/data/web/inc/functions.inc.php @@ -807,7 +807,7 @@ function verify_hash($hash, $password) { } return false; } -function check_login($user, $pass, $app_passwd_data = false) { +function check_login($user, $pass, $app_passwd_data = false, $skip_tfa = false) { global $pdo; global $redis; global $imap_server; @@ -938,19 +938,22 @@ function check_login($user, $pass, $app_passwd_data = false) { foreach ($rows as $row) { // verify password if (verify_hash($row['password'], $pass) !== false) { - // check for tfa authenticators - $authenticators = get_tfa($user); - if (isset($authenticators['additional']) && is_array($authenticators['additional']) && count($authenticators['additional']) > 0) { - $_SESSION['pending_mailcow_cc_username'] = $user; - $_SESSION['pending_mailcow_cc_role'] = "user"; - $_SESSION['pending_tfa_methods'] = $authenticators['additional']; - unset($_SESSION['ldelay']); - $_SESSION['return'][] = array( - 'type' => 'success', - 'log' => array(__FUNCTION__, $user, '*'), - 'msg' => array('logged_in_as', $user) - ); - return "pending"; + + if ($app_passwd_data['eas'] !== true && $app_passwd_data['dav'] !== true && !$skip_tfa){ + // check for tfa authenticators + $authenticators = get_tfa($user); + if (isset($authenticators['additional']) && is_array($authenticators['additional']) && count($authenticators['additional']) > 0) { + $_SESSION['pending_mailcow_cc_username'] = $user; + $_SESSION['pending_mailcow_cc_role'] = "user"; + $_SESSION['pending_tfa_methods'] = $authenticators['additional']; + unset($_SESSION['ldelay']); + $_SESSION['return'][] = array( + 'type' => 'success', + 'log' => array(__FUNCTION__, $user, '*'), + 'msg' => array('logged_in_as', $user) + ); + return "pending"; + } } else { if ($app_passwd_data['eas'] === true || $app_passwd_data['dav'] === true) { $service = ($app_passwd_data['eas'] === true) ? 'EAS' : 'DAV'; @@ -961,12 +964,13 @@ function check_login($user, $pass, $app_passwd_data = false) { ':username' => $user, ':remote_addr' => ($_SERVER['HTTP_X_REAL_IP'] ?? $_SERVER['REMOTE_ADDR']) )); + } elseif (!$skip_tfa) { + // Reactivate TFA if it was set to "deactivate TFA for next login" + $stmt = $pdo->prepare("UPDATE `tfa` SET `active`='1' WHERE `username` = :user"); + $stmt->execute(array(':user' => $user)); } unset($_SESSION['ldelay']); - // Reactivate TFA if it was set to "deactivate TFA for next login" - $stmt = $pdo->prepare("UPDATE `tfa` SET `active`='1' WHERE `username` = :user"); - $stmt->execute(array(':user' => $user)); return "user"; } } diff --git a/data/web/inc/prerequisites.inc.php b/data/web/inc/prerequisites.inc.php index 2897444b..cb02ba16 100644 --- a/data/web/inc/prerequisites.inc.php +++ b/data/web/inc/prerequisites.inc.php @@ -131,7 +131,7 @@ class mailcowPdo extends OAuth2\Storage\Pdo { $this->config['user_table'] = 'mailbox'; } public function checkUserCredentials($username, $password) { - if (check_login($username, $password) == 'user') { + if (check_login($username, $password, false, true) == 'user') { return true; } return false; From 0342ae926c8c02b13f02ec5f711b9d4d81d29334 Mon Sep 17 00:00:00 2001 From: FreddleSpl0it Date: Thu, 14 Jul 2022 18:55:35 +0200 Subject: [PATCH 2/5] exclude oauth clients & app passwords from mailbox tfa --- data/web/inc/functions.inc.php | 40 +++++++++++++++++----------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/data/web/inc/functions.inc.php b/data/web/inc/functions.inc.php index b2c5eee5..f705af03 100644 --- a/data/web/inc/functions.inc.php +++ b/data/web/inc/functions.inc.php @@ -834,7 +834,7 @@ function check_login($user, $pass, $app_passwd_data = false, $skip_tfa = false) if (verify_hash($row['password'], $pass)) { // check for tfa authenticators $authenticators = get_tfa($user); - if (isset($authenticators['additional']) && is_array($authenticators['additional']) && count($authenticators['additional']) > 0) { + if (isset($authenticators['additional']) && is_array($authenticators['additional']) && count($authenticators['additional']) > 0 && !$skip_tfa) { // active tfa authenticators found, set pending user login $_SESSION['pending_mailcow_cc_username'] = $user; $_SESSION['pending_mailcow_cc_role'] = "admin"; @@ -873,7 +873,7 @@ function check_login($user, $pass, $app_passwd_data = false, $skip_tfa = false) if (verify_hash($row['password'], $pass) !== false) { // check for tfa authenticators $authenticators = get_tfa($user); - if (isset($authenticators['additional']) && is_array($authenticators['additional']) && count($authenticators['additional']) > 0) { + if (isset($authenticators['additional']) && is_array($authenticators['additional']) && count($authenticators['additional']) > 0 && !$skip_tfa) { $_SESSION['pending_mailcow_cc_username'] = $user; $_SESSION['pending_mailcow_cc_role'] = "domainadmin"; $_SESSION['pending_tfa_methods'] = $authenticators['additional']; @@ -954,25 +954,25 @@ function check_login($user, $pass, $app_passwd_data = false, $skip_tfa = false) ); return "pending"; } - } else { - if ($app_passwd_data['eas'] === true || $app_passwd_data['dav'] === true) { - $service = ($app_passwd_data['eas'] === true) ? 'EAS' : 'DAV'; - $stmt = $pdo->prepare("REPLACE INTO sasl_log (`service`, `app_password`, `username`, `real_rip`) VALUES (:service, :app_id, :username, :remote_addr)"); - $stmt->execute(array( - ':service' => $service, - ':app_id' => $row['app_passwd_id'], - ':username' => $user, - ':remote_addr' => ($_SERVER['HTTP_X_REAL_IP'] ?? $_SERVER['REMOTE_ADDR']) - )); - } elseif (!$skip_tfa) { - // Reactivate TFA if it was set to "deactivate TFA for next login" - $stmt = $pdo->prepare("UPDATE `tfa` SET `active`='1' WHERE `username` = :user"); - $stmt->execute(array(':user' => $user)); - } - - unset($_SESSION['ldelay']); - return "user"; } + + if ($app_passwd_data['eas'] === true || $app_passwd_data['dav'] === true) { + $service = ($app_passwd_data['eas'] === true) ? 'EAS' : 'DAV'; + $stmt = $pdo->prepare("REPLACE INTO sasl_log (`service`, `app_password`, `username`, `real_rip`) VALUES (:service, :app_id, :username, :remote_addr)"); + $stmt->execute(array( + ':service' => $service, + ':app_id' => $row['app_passwd_id'], + ':username' => $user, + ':remote_addr' => ($_SERVER['HTTP_X_REAL_IP'] ?? $_SERVER['REMOTE_ADDR']) + )); + } elseif (!$skip_tfa) { + // Reactivate TFA if it was set to "deactivate TFA for next login" + $stmt = $pdo->prepare("UPDATE `tfa` SET `active`='1' WHERE `username` = :user"); + $stmt->execute(array(':user' => $user)); + } + + unset($_SESSION['ldelay']); + return "user"; } } From 1ca566f670d31280b37378fb76b40338436a9866 Mon Sep 17 00:00:00 2001 From: FreddleSpl0it Date: Fri, 15 Jul 2022 13:02:13 +0200 Subject: [PATCH 3/5] autoselect authenticator if only one exists --- data/web/css/build/008-mailcow.css | 1 + data/web/templates/base.twig | 51 ++++++++++++++++++++++++++++-- 2 files changed, 50 insertions(+), 2 deletions(-) diff --git a/data/web/css/build/008-mailcow.css b/data/web/css/build/008-mailcow.css index f557084f..b118ca01 100644 --- a/data/web/css/build/008-mailcow.css +++ b/data/web/css/build/008-mailcow.css @@ -269,6 +269,7 @@ code { padding: 10px; background: #fbfbfb; border: 1px solid #ededed; + min-height: 110px; } .tag-box { diff --git a/data/web/templates/base.twig b/data/web/templates/base.twig index 37c27e21..482b4e24 100644 --- a/data/web/templates/base.twig +++ b/data/web/templates/base.twig @@ -182,13 +182,21 @@ function recursiveBase64StrToArrayBuffer(obj) { keyboard: false }); + // validate Yubi OTP tfa $("#pending_tfa_tab_yubi_otp").click(function(){ $(".totp-authenticator-selection").removeClass("active"); $(".webauthn-authenticator-selection").removeClass("active"); - $("#collapseTotpTFA").collapse('hide'); $("#collapseWebAuthnTFA").collapse('hide'); + + // select default if only one authenticator exists + if ($('.yubi-authenticator-selection').length == 1){ + $('.yubi-authenticator-selection').addClass("active"); + var id = $('.yubi-authenticator-selection').children('input').first().val(); + $("#yubi_selected_id").val(id); + $("#collapseYubiTFA").collapse('show'); + } }); $(".yubi-authenticator-selection").click(function(){ $(".yubi-authenticator-selection").removeClass("active"); @@ -198,14 +206,37 @@ function recursiveBase64StrToArrayBuffer(obj) { $("#yubi_selected_id").val(id); $("#collapseYubiTFA").collapse('show'); + $("#collapseYubiTFA").children('input[name="token"]').focus(); + }); + if ($('.yubi-authenticator-selection').length == 1 && + $('.webauthn-authenticator-selection').length == 0){ + + // select default if only one authenticator exists + $('.yubi-authenticator-selection').addClass("active"); + + var id = $('.yubi-authenticator-selection').children('input').first().val(); + $("#yubi_selected_id").val(id); + + $("#collapseYubiTFA").collapse('show'); + } + $('#collapseYubiTFA').on('shown.bs.collapse', function() { + // autofocus + setTimeout(function() { $("#collapseYubiTFA").find('input[name="token"]').focus(); }, 200); }); // validate Time based OTP tfa $("#pending_tfa_tab_totp").click(function(){ $(".yubi-authenticator-selection").removeClass("active"); $(".webauthn-authenticator-selection").removeClass("active"); - $("#collapseYubiTFA").collapse('hide'); $("#collapseWebAuthnTFA").collapse('hide'); + + // select default if only one authenticator exists + if ($('.totp-authenticator-selection').length == 1){ + $('.totp-authenticator-selection').addClass("active"); + var id = $('.totp-authenticator-selection').children('input').first().val(); + $("#totp_selected_id").val(id); + $("#collapseTotpTFA").collapse('show'); + } }); $(".totp-authenticator-selection").click(function(){ $(".totp-authenticator-selection").removeClass("active"); @@ -216,6 +247,22 @@ function recursiveBase64StrToArrayBuffer(obj) { $("#collapseTotpTFA").collapse('show'); }); + if ($('.totp-authenticator-selection').length == 1 && + $('.yubi-authenticator-selection').length == 0 && + $('.webauthn-authenticator-selection').length == 0){ + + // select default if only one authenticator exists + $('.totp-authenticator-selection').addClass("active"); + + var id = $('.totp-authenticator-selection').children('input').first().val(); + $("#totp_selected_id").val(id); + + $("#collapseTotpTFA").collapse('show'); + } + $('#collapseTotpTFA').on('shown.bs.collapse', function() { + // autofocus + setTimeout(function() { $("#collapseTotpTFA").find('input[name="token"]').focus(); }, 200); + }); // validate WebAuthn tfa $("#pending_tfa_tab_webauthn").click(function(){ $(".totp-authenticator-selection").removeClass("active"); From c8620a066d447ed6aaf6054b184fd16146574e5b Mon Sep 17 00:00:00 2001 From: FreddleSpl0it Date: Fri, 15 Jul 2022 16:45:28 +0200 Subject: [PATCH 4/5] yubi_otp undo authenticator selection --- data/web/inc/functions.inc.php | 9 +--- data/web/templates/base.twig | 60 ++++++--------------------- data/web/templates/modals/footer.twig | 15 +------ 3 files changed, 17 insertions(+), 67 deletions(-) diff --git a/data/web/inc/functions.inc.php b/data/web/inc/functions.inc.php index f705af03..ca371303 100644 --- a/data/web/inc/functions.inc.php +++ b/data/web/inc/functions.inc.php @@ -1630,12 +1630,8 @@ function verify_tfa_login($username, $_data) { global $WebAuthn; if ($_data['tfa_method'] != 'u2f'){ - $stmt = $pdo->prepare("SELECT `authmech` FROM `tfa` - WHERE `username` = :username AND `id` = :id AND `active` = '1'"); - $stmt->execute(array(':username' => $username, ':id' => $_data['id'])); - $row = $stmt->fetch(PDO::FETCH_ASSOC); - switch ($row["authmech"]) { + switch ($_data["tfa_method"]) { case "yubi_otp": if (!ctype_alnum($_data['token']) || strlen($_data['token']) != 44) { $_SESSION['return'][] = array( @@ -1649,10 +1645,9 @@ function verify_tfa_login($username, $_data) { $stmt = $pdo->prepare("SELECT `id`, `secret` FROM `tfa` WHERE `username` = :username AND `authmech` = 'yubi_otp' - AND `id` = :id AND `active` = '1' AND `secret` LIKE :modhex"); - $stmt->execute(array(':username' => $username, ':modhex' => '%' . $yubico_modhex_id, ':id' => $_data['id'])); + $stmt->execute(array(':username' => $username, ':modhex' => '%' . $yubico_modhex_id)); $row = $stmt->fetch(PDO::FETCH_ASSOC); $yubico_auth = explode(':', $row['secret']); $yubi = new Auth_Yubico($yubico_auth[0], $yubico_auth[1]); diff --git a/data/web/templates/base.twig b/data/web/templates/base.twig index 482b4e24..770decde 100644 --- a/data/web/templates/base.twig +++ b/data/web/templates/base.twig @@ -183,51 +183,9 @@ function recursiveBase64StrToArrayBuffer(obj) { }); - // validate Yubi OTP tfa - $("#pending_tfa_tab_yubi_otp").click(function(){ - $(".totp-authenticator-selection").removeClass("active"); - $(".webauthn-authenticator-selection").removeClass("active"); - $("#collapseTotpTFA").collapse('hide'); - $("#collapseWebAuthnTFA").collapse('hide'); - - // select default if only one authenticator exists - if ($('.yubi-authenticator-selection').length == 1){ - $('.yubi-authenticator-selection').addClass("active"); - var id = $('.yubi-authenticator-selection').children('input').first().val(); - $("#yubi_selected_id").val(id); - $("#collapseYubiTFA").collapse('show'); - } - }); - $(".yubi-authenticator-selection").click(function(){ - $(".yubi-authenticator-selection").removeClass("active"); - $(this).addClass("active"); - - var id = $(this).children('input').first().val(); - $("#yubi_selected_id").val(id); - - $("#collapseYubiTFA").collapse('show'); - $("#collapseYubiTFA").children('input[name="token"]').focus(); - }); - if ($('.yubi-authenticator-selection').length == 1 && - $('.webauthn-authenticator-selection').length == 0){ - - // select default if only one authenticator exists - $('.yubi-authenticator-selection').addClass("active"); - - var id = $('.yubi-authenticator-selection').children('input').first().val(); - $("#yubi_selected_id").val(id); - - $("#collapseYubiTFA").collapse('show'); - } - $('#collapseYubiTFA').on('shown.bs.collapse', function() { - // autofocus - setTimeout(function() { $("#collapseYubiTFA").find('input[name="token"]').focus(); }, 200); - }); // validate Time based OTP tfa $("#pending_tfa_tab_totp").click(function(){ - $(".yubi-authenticator-selection").removeClass("active"); $(".webauthn-authenticator-selection").removeClass("active"); - $("#collapseYubiTFA").collapse('hide'); $("#collapseWebAuthnTFA").collapse('hide'); // select default if only one authenticator exists @@ -248,9 +206,9 @@ function recursiveBase64StrToArrayBuffer(obj) { $("#collapseTotpTFA").collapse('show'); }); if ($('.totp-authenticator-selection').length == 1 && - $('.yubi-authenticator-selection').length == 0 && + $('#pending_tfa_tab_yubi_otp').length == 0 && $('.webauthn-authenticator-selection').length == 0){ - + // select default if only one authenticator exists $('.totp-authenticator-selection').addClass("active"); @@ -258,18 +216,26 @@ function recursiveBase64StrToArrayBuffer(obj) { $("#totp_selected_id").val(id); $("#collapseTotpTFA").collapse('show'); + setTimeout(function() { $("#collapseTotpTFA").find('input[name="token"]').focus(); }, 1000); } - $('#collapseTotpTFA').on('shown.bs.collapse', function() { + $('#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 + setTimeout(function() { $("#collapseYubiTFA").find('input[name="token"]').focus(); }, 1000); + } + $('#pending_tfa_tab_yubi_otp').on('shown.bs.tab', function() { + // autofocus + $("#collapseYubiTFA").find('input[name="token"]').focus(); }); // validate WebAuthn tfa $("#pending_tfa_tab_webauthn").click(function(){ $(".totp-authenticator-selection").removeClass("active"); - $(".yubi-authenticator-selection").removeClass("active"); $("#collapseTotpTFA").collapse('hide'); - $("#collapseYubiTFA").collapse('hide'); }); $(".webauthn-authenticator-selection").click(function(){ $(".webauthn-authenticator-selection").removeClass("active"); diff --git a/data/web/templates/modals/footer.twig b/data/web/templates/modals/footer.twig index 67cc3482..52e89e00 100644 --- a/data/web/templates/modals/footer.twig +++ b/data/web/templates/modals/footer.twig @@ -206,20 +206,9 @@
- Authenticators + Authenticate -
- {% for authenticator in pending_tfa_methods %} - {% if authenticator["authmech"] == "yubi_otp" %} - - - {{ authenticator["key_id"] }} - - - {% endif %} - {% endfor %} -
-
+
Yubicon Icon From 555f4a8a6d300361985c8d4cb9aada97ea339483 Mon Sep 17 00:00:00 2001 From: FreddleSpl0it Date: Thu, 25 Aug 2022 14:26:45 +0200 Subject: [PATCH 5/5] [Web] Mailbox TFA fix --- data/web/inc/functions.inc.php | 48 ++++++++++++++++-------------- data/web/inc/prerequisites.inc.php | 2 +- 2 files changed, 27 insertions(+), 23 deletions(-) diff --git a/data/web/inc/functions.inc.php b/data/web/inc/functions.inc.php index ca371303..4f70b4c2 100644 --- a/data/web/inc/functions.inc.php +++ b/data/web/inc/functions.inc.php @@ -807,7 +807,7 @@ function verify_hash($hash, $password) { } return false; } -function check_login($user, $pass, $app_passwd_data = false, $skip_tfa = false) { +function check_login($user, $pass, $app_passwd_data = false) { global $pdo; global $redis; global $imap_server; @@ -834,7 +834,7 @@ function check_login($user, $pass, $app_passwd_data = false, $skip_tfa = false) if (verify_hash($row['password'], $pass)) { // check for tfa authenticators $authenticators = get_tfa($user); - if (isset($authenticators['additional']) && is_array($authenticators['additional']) && count($authenticators['additional']) > 0 && !$skip_tfa) { + if (isset($authenticators['additional']) && is_array($authenticators['additional']) && count($authenticators['additional']) > 0) { // active tfa authenticators found, set pending user login $_SESSION['pending_mailcow_cc_username'] = $user; $_SESSION['pending_mailcow_cc_role'] = "admin"; @@ -873,7 +873,7 @@ function check_login($user, $pass, $app_passwd_data = false, $skip_tfa = false) if (verify_hash($row['password'], $pass) !== false) { // check for tfa authenticators $authenticators = get_tfa($user); - if (isset($authenticators['additional']) && is_array($authenticators['additional']) && count($authenticators['additional']) > 0 && !$skip_tfa) { + if (isset($authenticators['additional']) && is_array($authenticators['additional']) && count($authenticators['additional']) > 0) { $_SESSION['pending_mailcow_cc_username'] = $user; $_SESSION['pending_mailcow_cc_role'] = "domainadmin"; $_SESSION['pending_tfa_methods'] = $authenticators['additional']; @@ -937,9 +937,8 @@ function check_login($user, $pass, $app_passwd_data = false, $skip_tfa = false) } foreach ($rows as $row) { // verify password - if (verify_hash($row['password'], $pass) !== false) { - - if ($app_passwd_data['eas'] !== true && $app_passwd_data['dav'] !== true && !$skip_tfa){ + if ($app_passwd_data['eas'] !== true && $app_passwd_data['dav'] !== true){ + if (verify_hash($row['password'], $pass) !== false) { // check for tfa authenticators $authenticators = get_tfa($user); if (isset($authenticators['additional']) && is_array($authenticators['additional']) && count($authenticators['additional']) > 0) { @@ -953,26 +952,31 @@ function check_login($user, $pass, $app_passwd_data = false, $skip_tfa = false) 'msg' => array('logged_in_as', $user) ); return "pending"; + } else { + // Reactivate TFA if it was set to "deactivate TFA for next login" + $stmt = $pdo->prepare("UPDATE `tfa` SET `active`='1' WHERE `username` = :user"); + $stmt->execute(array(':user' => $user)); + + unset($_SESSION['ldelay']); + return "user"; } } + } elseif ($app_passwd_data['eas'] === true || $app_passwd_data['dav'] === true) { + if (array_key_exists("app_passwd_id", $row)){ + if (verify_hash($row['password'], $pass) !== false) { + $service = ($app_passwd_data['eas'] === true) ? 'EAS' : 'DAV'; + $stmt = $pdo->prepare("REPLACE INTO sasl_log (`service`, `app_password`, `username`, `real_rip`) VALUES (:service, :app_id, :username, :remote_addr)"); + $stmt->execute(array( + ':service' => $service, + ':app_id' => $row['app_passwd_id'], + ':username' => $user, + ':remote_addr' => ($_SERVER['HTTP_X_REAL_IP'] ?? $_SERVER['REMOTE_ADDR']) + )); - if ($app_passwd_data['eas'] === true || $app_passwd_data['dav'] === true) { - $service = ($app_passwd_data['eas'] === true) ? 'EAS' : 'DAV'; - $stmt = $pdo->prepare("REPLACE INTO sasl_log (`service`, `app_password`, `username`, `real_rip`) VALUES (:service, :app_id, :username, :remote_addr)"); - $stmt->execute(array( - ':service' => $service, - ':app_id' => $row['app_passwd_id'], - ':username' => $user, - ':remote_addr' => ($_SERVER['HTTP_X_REAL_IP'] ?? $_SERVER['REMOTE_ADDR']) - )); - } elseif (!$skip_tfa) { - // Reactivate TFA if it was set to "deactivate TFA for next login" - $stmt = $pdo->prepare("UPDATE `tfa` SET `active`='1' WHERE `username` = :user"); - $stmt->execute(array(':user' => $user)); + unset($_SESSION['ldelay']); + return "user"; + } } - - unset($_SESSION['ldelay']); - return "user"; } } diff --git a/data/web/inc/prerequisites.inc.php b/data/web/inc/prerequisites.inc.php index cb02ba16..2897444b 100644 --- a/data/web/inc/prerequisites.inc.php +++ b/data/web/inc/prerequisites.inc.php @@ -131,7 +131,7 @@ class mailcowPdo extends OAuth2\Storage\Pdo { $this->config['user_table'] = 'mailbox'; } public function checkUserCredentials($username, $password) { - if (check_login($username, $password, false, true) == 'user') { + if (check_login($username, $password) == 'user') { return true; } return false;