mirror of
https://github.com/mailcow/mailcow-dockerized.git
synced 2026-11-05 07:58:20 +00:00
[Web][Dovecot] Do not count denied logins as failed logins
This commit is contained in:
2 files changed
+52
-37
No files matched your search
@@ -71,6 +71,8 @@ $isSOGoRequest = $post['real_rip'] == getenv('IPV4_NETWORK') . '.248';
|
|||||||
// real_rip is the address that connected to Dovecot (for proxied connections the proxy), client_ip the client (for logs)
|
// real_rip is the address that connected to Dovecot (for proxied connections the proxy), client_ip the client (for logs)
|
||||||
$client_ip = filter_var($post['client_ip'] ?? '', FILTER_VALIDATE_IP) ? $post['client_ip'] : $post['real_rip'];
|
$client_ip = filter_var($post['client_ip'] ?? '', FILTER_VALIDATE_IP) ? $post['client_ip'] : $post['real_rip'];
|
||||||
$result = false;
|
$result = false;
|
||||||
|
// set when the password is right, but the account does not allow this login
|
||||||
|
$denied = false;
|
||||||
if ($isSOGoRequest) {
|
if ($isSOGoRequest) {
|
||||||
// This is a SOGo Auth request. First check for SSO password.
|
// This is a SOGo Auth request. First check for SSO password.
|
||||||
$sogo_sso_pass = file_get_contents("/etc/sogo-sso/sogo-sso.pass");
|
$sogo_sso_pass = file_get_contents("/etc/sogo-sso/sogo-sso.pass");
|
||||||
@@ -93,7 +95,7 @@ if ($result === false){
|
|||||||
'service' => $post['service'],
|
'service' => $post['service'],
|
||||||
'is_internal' => true,
|
'is_internal' => true,
|
||||||
'remote_addr' => $post['real_rip']
|
'remote_addr' => $post['real_rip']
|
||||||
));
|
), $denied);
|
||||||
if ($result) {
|
if ($result) {
|
||||||
error_log('MAILCOWAUTH: App auth for user ' . $post['username'] . " with service " . $service . " from IP " . $client_ip);
|
error_log('MAILCOWAUTH: App auth for user ' . $post['username'] . " with service " . $service . " from IP " . $client_ip);
|
||||||
set_sasl_log($post['username'], $client_ip, $service);
|
set_sasl_log($post['username'], $client_ip, $service);
|
||||||
@@ -104,7 +106,7 @@ if ($result === false){
|
|||||||
$iam_provider = identity_provider('init');
|
$iam_provider = identity_provider('init');
|
||||||
$iam_settings = identity_provider('get');
|
$iam_settings = identity_provider('get');
|
||||||
try {
|
try {
|
||||||
$result = user_login($post['username'], $post['password'], array('is_internal' => true, 'service' => $post['service']));
|
$result = user_login($post['username'], $post['password'], array('is_internal' => true, 'service' => $post['service']), $denied);
|
||||||
} catch (\Mailcow\Auth\IdpUnavailable $e) {
|
} catch (\Mailcow\Auth\IdpUnavailable $e) {
|
||||||
// temporary failure for Dovecot, no failed login
|
// temporary failure for Dovecot, no failed login
|
||||||
error_log("MAILCOWAUTH: Identity provider unavailable for user " . $post['username'] . " with service " . $post['service'] . " from IP " . $client_ip . ": " . $e->getMessage());
|
error_log("MAILCOWAUTH: Identity provider unavailable for user " . $post['username'] . " with service " . $post['service'] . " from IP " . $client_ip . ": " . $e->getMessage());
|
||||||
@@ -122,6 +124,10 @@ if ($result === false){
|
|||||||
if ($result) {
|
if ($result) {
|
||||||
http_response_code(200); // OK
|
http_response_code(200); // OK
|
||||||
$return['success'] = true;
|
$return['success'] = true;
|
||||||
|
} elseif ($denied) {
|
||||||
|
// Logged, but no failed login for netfilter: e.g. a client of a shared mailbox or without IMAP access
|
||||||
|
error_log("MAILCOWAUTH: Login denied for user " . $post['username'] . " with service " . $service . " from IP " . $client_ip);
|
||||||
|
http_response_code(401); // Unauthorized
|
||||||
} else {
|
} else {
|
||||||
// Same line for the log and netfilter, which counts it for IMAP, POP3, Sieve and SMTP (Postfix asks Dovecot);
|
// Same line for the log and netfilter, which counts it for IMAP, POP3, Sieve and SMTP (Postfix asks Dovecot);
|
||||||
// SOGo logins have their own rule
|
// SOGo logins have their own rule
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ function check_login($user, $pass, $extra = null) {
|
|||||||
$is_internal = $extra['is_internal'];
|
$is_internal = $extra['is_internal'];
|
||||||
$role = $extra['role'];
|
$role = $extra['role'];
|
||||||
$extra['service'] = !isset($extra['service']) ? 'NONE' : $extra['service'];
|
$extra['service'] = !isset($extra['service']) ? 'NONE' : $extra['service'];
|
||||||
|
// set when the password is right, but the account does not allow this login
|
||||||
|
$denied = false;
|
||||||
|
|
||||||
// Try validate admin
|
// Try validate admin
|
||||||
if (!isset($role) || $role == "admin") {
|
if (!isset($role) || $role == "admin") {
|
||||||
@@ -26,7 +28,7 @@ function check_login($user, $pass, $extra = null) {
|
|||||||
|
|
||||||
// Try validate app password
|
// Try validate app password
|
||||||
if (!isset($role) || $role == "app") {
|
if (!isset($role) || $role == "app") {
|
||||||
$result = apppass_login($user, $pass, $extra);
|
$result = apppass_login($user, $pass, $extra, $denied);
|
||||||
if ($result !== false) {
|
if ($result !== false) {
|
||||||
$real_rip = ($_SERVER['HTTP_X_REAL_IP'] ?? $_SERVER['REMOTE_ADDR']);
|
$real_rip = ($_SERVER['HTTP_X_REAL_IP'] ?? $_SERVER['REMOTE_ADDR']);
|
||||||
set_sasl_log($user, $real_rip, $extra['service'], $pass);
|
set_sasl_log($user, $real_rip, $extra['service'], $pass);
|
||||||
@@ -37,7 +39,7 @@ function check_login($user, $pass, $extra = null) {
|
|||||||
// Try validate user
|
// Try validate user
|
||||||
if (!isset($role) || $role == "user") {
|
if (!isset($role) || $role == "user") {
|
||||||
try {
|
try {
|
||||||
$result = user_login($user, $pass, $extra);
|
$result = user_login($user, $pass, $extra, $denied);
|
||||||
} catch (\Mailcow\Auth\IdpUnavailable $e) {
|
} catch (\Mailcow\Auth\IdpUnavailable $e) {
|
||||||
// no failed login, netfilter must not count it
|
// no failed login, netfilter must not count it
|
||||||
error_log("mailcow UI: Identity provider unavailable for " . $user . " by " . $_SERVER['REMOTE_ADDR'] . ": " . $e->getMessage());
|
error_log("mailcow UI: Identity provider unavailable for " . $user . " by " . $_SERVER['REMOTE_ADDR'] . ": " . $e->getMessage());
|
||||||
@@ -54,19 +56,20 @@ function check_login($user, $pass, $extra = null) {
|
|||||||
return $result;
|
return $result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// skip log and only return false if it's an internal request
|
// skip log and only return false if it's an internal request
|
||||||
if ($is_internal == true) return false;
|
if ($is_internal == true) return false;
|
||||||
|
|
||||||
|
// A denied login is logged, but not counted by netfilter (e.g. EAS/DAV clients of accounts without access)
|
||||||
|
$msg = ($denied ? "mailcow UI: Login denied for " : "mailcow UI: Invalid password for ") . $user . " by " . $_SERVER['REMOTE_ADDR'];
|
||||||
if (!isset($_SESSION['ldelay'])) {
|
if (!isset($_SESSION['ldelay'])) {
|
||||||
$_SESSION['ldelay'] = "0";
|
$_SESSION['ldelay'] = "0";
|
||||||
$redis->publish("F2B_CHANNEL", "mailcow UI: Invalid password for " . $user . " by " . $_SERVER['REMOTE_ADDR']);
|
if (!$denied) $redis->publish("F2B_CHANNEL", $msg);
|
||||||
error_log("mailcow UI: Invalid password for " . $user . " by " . $_SERVER['REMOTE_ADDR']);
|
error_log($msg);
|
||||||
}
|
}
|
||||||
elseif (!isset($_SESSION['mailcow_cc_username'])) {
|
elseif (!isset($_SESSION['mailcow_cc_username'])) {
|
||||||
$_SESSION['ldelay'] = $_SESSION['ldelay']+0.5;
|
$_SESSION['ldelay'] = $_SESSION['ldelay']+0.5;
|
||||||
$redis->publish("F2B_CHANNEL", "mailcow UI: Invalid password for " . $user . " by " . $_SERVER['REMOTE_ADDR']);
|
if (!$denied) $redis->publish("F2B_CHANNEL", $msg);
|
||||||
error_log("mailcow UI: Invalid password for " . $user . " by " . $_SERVER['REMOTE_ADDR']);
|
error_log($msg);
|
||||||
}
|
}
|
||||||
$_SESSION['return'][] = array(
|
$_SESSION['return'][] = array(
|
||||||
'type' => 'danger',
|
'type' => 'danger',
|
||||||
@@ -207,7 +210,7 @@ function domainadmin_login($user, $pass){
|
|||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
function user_login($user, $pass, $extra = null){
|
function user_login($user, $pass, $extra = null, &$denied = false){
|
||||||
global $pdo;
|
global $pdo;
|
||||||
global $iam_provider;
|
global $iam_provider;
|
||||||
global $iam_settings;
|
global $iam_settings;
|
||||||
@@ -261,11 +264,14 @@ function user_login($user, $pass, $extra = null){
|
|||||||
if ($extra['service'] != 'NONE') {
|
if ($extra['service'] != 'NONE') {
|
||||||
$key = strtolower($extra['service']) . "_access";
|
$key = strtolower($extra['service']) . "_access";
|
||||||
if (isset($row['attributes'][$key]) && $row['attributes'][$key] != '1') {
|
if (isset($row['attributes'][$key]) && $row['attributes'][$key] != '1') {
|
||||||
|
$denied = true;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
// the IdP accepted the password, the mailbox is inactive
|
||||||
|
$denied = true;
|
||||||
}
|
}
|
||||||
clear_session();
|
clear_session();
|
||||||
return false;
|
return false;
|
||||||
@@ -273,18 +279,16 @@ function user_login($user, $pass, $extra = null){
|
|||||||
|
|
||||||
// check if user has access to service (imap, smtp, pop3, sieve) if service is set
|
// check if user has access to service (imap, smtp, pop3, sieve) if service is set
|
||||||
$row['attributes'] = json_decode($row['attributes'], true);
|
$row['attributes'] = json_decode($row['attributes'], true);
|
||||||
// Shared mailboxes have no login of their own, members open them through group shares
|
$key = strtolower($extra['service']) . "_access";
|
||||||
if (($row['attributes']['shared'] ?? '0') == '1') {
|
// Shared mailboxes have no login of their own, members open them through group shares.
|
||||||
return false;
|
// Checked before asking an IdP (no template side effects).
|
||||||
}
|
if (($row['attributes']['shared'] ?? '0') == '1'
|
||||||
if ($extra['service'] != 'NONE') {
|
|| ($extra['service'] != 'NONE' && isset($row['attributes'][$key]) && $row['attributes'][$key] != '1')
|
||||||
$key = strtolower($extra['service']) . "_access";
|
|| $row['active'] != '1' || $row['d_active'] != '1') {
|
||||||
if (isset($row['attributes'][$key]) && $row['attributes'][$key] != '1') {
|
// Denied, not failed, unless the password is wrong; IdP passwords are not checked for that
|
||||||
return false;
|
if ($row['authsource'] != 'mailcow' || verify_hash($row['password'], $pass) !== false) {
|
||||||
|
$denied = true;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
// IdP accounts: refuse inactive mailboxes and domains before asking the IdP (no template side effects)
|
|
||||||
if ($row['authsource'] != 'mailcow' && ($row['active'] != '1' || $row['d_active'] != '1')) {
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
switch ($row['authsource']) {
|
switch ($row['authsource']) {
|
||||||
@@ -303,6 +307,7 @@ function user_login($user, $pass, $extra = null){
|
|||||||
$stmt->execute(array(':user' => $user));
|
$stmt->execute(array(':user' => $user));
|
||||||
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
if (empty($row)) {
|
if (empty($row)) {
|
||||||
|
$denied = true;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -361,6 +366,7 @@ function user_login($user, $pass, $extra = null){
|
|||||||
$stmt->execute(array(':user' => $user));
|
$stmt->execute(array(':user' => $user));
|
||||||
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
if (empty($row)) {
|
if (empty($row)) {
|
||||||
|
$denied = true;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -403,9 +409,6 @@ function user_login($user, $pass, $extra = null){
|
|||||||
return $result;
|
return $result;
|
||||||
break;
|
break;
|
||||||
case 'mailcow':
|
case 'mailcow':
|
||||||
if ($row['active'] != 1 || $row['d_active'] != 1) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
// verify password
|
// verify password
|
||||||
if (verify_hash($row['password'], $pass) !== false) {
|
if (verify_hash($row['password'], $pass) !== false) {
|
||||||
|
|
||||||
@@ -446,13 +449,15 @@ function user_login($user, $pass, $extra = null){
|
|||||||
}
|
}
|
||||||
return "user";
|
return "user";
|
||||||
}
|
}
|
||||||
|
// TFA accounts use app passwords in mail clients
|
||||||
|
$denied = true;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
function apppass_login($user, $pass, $extra = null){
|
function apppass_login($user, $pass, $extra = null, &$denied = false){
|
||||||
global $pdo;
|
global $pdo;
|
||||||
|
|
||||||
$is_internal = $extra['is_internal'];
|
$is_internal = $extra['is_internal'];
|
||||||
@@ -473,15 +478,12 @@ function apppass_login($user, $pass, $extra = null){
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// fetch app password data
|
// fetch app password data, inactive ones too: their right password is denied, not failed
|
||||||
$stmt = $pdo->prepare("SELECT `app_passwd`.*, `app_passwd`.`password` as `password`, `app_passwd`.`id` as `app_passwd_id` FROM `app_passwd`
|
$stmt = $pdo->prepare("SELECT `app_passwd`.*, `app_passwd`.`password` as `password`, `app_passwd`.`id` as `app_passwd_id`,
|
||||||
|
`mailbox`.`active` as `mailbox_active`, `domain`.`active` as `domain_active`, `mailbox`.`attributes` as `mailbox_attributes` FROM `app_passwd`
|
||||||
INNER JOIN `mailbox` ON `mailbox`.`username` = `app_passwd`.`mailbox`
|
INNER JOIN `mailbox` ON `mailbox`.`username` = `app_passwd`.`mailbox`
|
||||||
INNER JOIN `domain` ON `mailbox`.`domain` = `domain`.`domain`
|
INNER JOIN `domain` ON `mailbox`.`domain` = `domain`.`domain`
|
||||||
WHERE `mailbox`.`kind` NOT REGEXP 'location|thing|group'
|
WHERE `mailbox`.`kind` NOT REGEXP 'location|thing|group'
|
||||||
AND `mailbox`.`active` = '1'
|
|
||||||
AND `domain`.`active` = '1'
|
|
||||||
AND `app_passwd`.`active` = '1'
|
|
||||||
AND COALESCE(JSON_UNQUOTE(JSON_VALUE(`mailbox`.`attributes`, '$.shared')), '0') != '1'
|
|
||||||
AND `app_passwd`.`mailbox` = :user"
|
AND `app_passwd`.`mailbox` = :user"
|
||||||
);
|
);
|
||||||
// fetch password data
|
// fetch password data
|
||||||
@@ -491,16 +493,23 @@ function apppass_login($user, $pass, $extra = null){
|
|||||||
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
foreach ($rows as $row) {
|
foreach ($rows as $row) {
|
||||||
if ($extra['service'] != 'NONE' && $row[strtolower($extra['service']) . '_access'] != '1'){
|
// verify password
|
||||||
|
if (verify_hash($row['password'], $pass) === false) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// verify password
|
$mailbox_attributes = json_decode($row['mailbox_attributes'], true);
|
||||||
if (verify_hash($row['password'], $pass) !== false) {
|
if ($row['active'] != '1' || $row['mailbox_active'] != '1' || $row['domain_active'] != '1'
|
||||||
$_SESSION['app_passwd_id'] = $row['app_passwd_id'];
|
|| ($mailbox_attributes['shared'] ?? '0') == '1'
|
||||||
unset($_SESSION['ldelay']);
|
|| ($extra['service'] != 'NONE' && $row[strtolower($extra['service']) . '_access'] != '1')) {
|
||||||
return "user";
|
// right password, but the app password does not allow this login
|
||||||
|
$denied = true;
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$_SESSION['app_passwd_id'] = $row['app_passwd_id'];
|
||||||
|
unset($_SESSION['ldelay']);
|
||||||
|
return "user";
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
Reference in new issue
Block a user