public
nobgit
read
NobMail
Based on mailcow: dockerized
Languages
Repository composition by tracked source files.
PHP
49%
JavaScript
35%
HTML
9%
CSS
4%
Shell
2%
Python
1%
Lua
0%
Perl
0%
Ruby
0%
SCSS
0%
Create file
Wiki Documentation
Clone
https://nobgit.com/orgs/nobgit/nobmail.git
ssh://[email protected]:2222/orgs/nobgit/nobmail.git
Commit
[Web] organize auth functions+api auth w/ dovecot
6adad79e
data/Dockerfiles/dovecot/Dockerfile | 1 +
data/Dockerfiles/dovecot/docker-entrypoint.sh | 150 ++++++--------
data/web/inc/functions.auth.inc.php | 278 ++++++++++++++++++++++++++
data/web/inc/functions.inc.php | 198 ------------------
data/web/inc/functions.mailbox.inc.php | 25 ++-
data/web/inc/init_db.inc.php | 1 +
data/web/inc/prerequisites.inc.php | 1 +
data/web/json_api.php | 20 ++
data/web/sogo-auth.php | 3 +-
data/web/templates/edit/mailbox.twig | 8 +-
data/web/templates/modals/mailbox.twig | 9 +
11 files changed, 400 insertions(+), 294 deletions(-)
create mode 100644 data/web/inc/functions.auth.inc.php
Diff
diff --git a/data/Dockerfiles/dovecot/Dockerfile b/data/Dockerfiles/dovecot/Dockerfile
index 9433dd2e..a3558968 100644
--- a/data/Dockerfiles/dovecot/Dockerfile
+++ b/data/Dockerfiles/dovecot/Dockerfile
@@ -26,6 +26,7 @@ RUN addgroup -g 5000 vmail \
curl \
jq \
lua \
+ lua-json \
lua-cjson \
lua-socket \
lua-sql-mysql \
diff --git a/data/Dockerfiles/dovecot/docker-entrypoint.sh b/data/Dockerfiles/dovecot/docker-entrypoint.sh
index a9545f33..f146a8a0 100755
--- a/data/Dockerfiles/dovecot/docker-entrypoint.sh
+++ b/data/Dockerfiles/dovecot/docker-entrypoint.sh
@@ -129,114 +129,86 @@ iterate_query = SELECT username FROM mailbox WHERE active = '1' OR active = '2';
EOF
cat <<EOF > /etc/dovecot/lua/passwd-verify.lua
-function auth_password_verify(req, pass)
-
- if req.domain == nil then
+function auth_password_verify(request, password)
+ if request.domain == nil then
return dovecot.auth.PASSDB_RESULT_USER_UNKNOWN, "No such user"
end
- if cur == nil then
- script_init()
- end
-
- if req.user == nil then
- req.user = ''
- end
-
- respbody = {}
+ json = require "json"
+ ltn12 = require "ltn12"
+ http = require "socket.http"
+ http.TIMEOUT = 5
+ mysql = require "luasql.mysql"
+ env = mysql.mysql()
+ con = env:connect("__DBNAME__","__DBUSER__","__DBPASS__","localhost")
+ local req = {
+ username = request.user,
+ password = password
+ }
+ local req_json = json.encode(req)
+ local res = {}
+
-- check against mailbox passwds
- local cur,errorString = con:execute(string.format([[SELECT password FROM mailbox
- WHERE username = '%s'
- AND active = '1'
- AND domain IN (SELECT domain FROM domain WHERE domain='%s' AND active='1')
- AND IFNULL(JSON_UNQUOTE(JSON_VALUE(mailbox.attributes, '$.force_pw_update')), 0) != '1'
- AND IFNULL(JSON_UNQUOTE(JSON_VALUE(attributes, '$.%s_access')), 1) = '1']], con:escape(req.user), con:escape(req.domain), con:escape(req.service)))
- local row = cur:fetch ({}, "a")
- while row do
- if req.password_verify(req, row.password, pass) == 1 then
- con:execute(string.format([[REPLACE INTO sasl_log (service, app_password, username, real_rip)
- VALUES ("%s", 0, "%s", "%s")]], con:escape(req.service), con:escape(req.user), con:escape(req.real_rip)))
- cur:close()
- con:close()
- return dovecot.auth.PASSDB_RESULT_OK, ""
- end
- row = cur:fetch (row, "a")
+ local b, c = http.request {
+ method = "POST",
+ url = "https://nginx/api/v1/process/login",
+ source = ltn12.source.string(req_json),
+ headers = {
+ ["content-type"] = "application/json",
+ ["content-length"] = tostring(#req_json)
+ },
+ sink = ltn12.sink.table(res)
+ }
+ local api_response = json.decode(table.concat(res))
+ if api_response.role == 'user' then
+ con:execute(string.format([[REPLACE INTO sasl_log (service, app_password, username, real_rip)
+ VALUES ("%s", 0, "%s", "%s")]], con:escape(request.service), con:escape(request.user), con:escape(request.real_rip)))
+ con:close()
+ return dovecot.auth.PASSDB_RESULT_OK, "password=" .. password
end
+
-- check against app passwds for imap and smtp
-- app passwords are only available for imap, smtp, sieve and pop3 when using sasl
- if req.service == "smtp" or req.service == "imap" or req.service == "sieve" or req.service == "pop3" then
- local cur,errorString = con:execute(string.format([[SELECT app_passwd.id, %s_access AS has_prot_access, app_passwd.password FROM app_passwd
- INNER JOIN mailbox ON mailbox.username = app_passwd.mailbox
- WHERE mailbox = '%s'
- AND app_passwd.active = '1'
- AND mailbox.active = '1'
- AND app_passwd.domain IN (SELECT domain FROM domain WHERE domain='%s' AND active='1')]], con:escape(req.service), con:escape(req.user), con:escape(req.domain)))
- local row = cur:fetch ({}, "a")
- while row do
- if req.password_verify(req, row.password, pass) == 1 then
- -- if password is valid and protocol access is 1 OR real_rip matches SOGo, proceed
- if tostring(req.real_rip) == "__IPV4_SOGO__" then
- cur:close()
- con:close()
- return dovecot.auth.PASSDB_RESULT_OK, ""
- elseif row.has_prot_access == "1" then
- con:execute(string.format([[REPLACE INTO sasl_log (service, app_password, username, real_rip)
- VALUES ("%s", %d, "%s", "%s")]], con:escape(req.service), row.id, con:escape(req.user), con:escape(req.real_rip)))
- cur:close()
- con:close()
- return dovecot.auth.PASSDB_RESULT_OK, ""
- end
+ if request.service == "smtp" or request.service == "imap" or request.service == "sieve" or request.service == "pop3" then
+ req.protocol = {}
+ req.protocol[request.service] = true
+ req_json = json.encode(req)
+
+ req.protocol.ignore_hasaccess = false
+ if tostring(req.real_rip) == "__IPV4_SOGO__" then
+ req.protocol.ignore_hasaccess = true
+ end
+
+ local b, c = http.request {
+ method = "POST",
+ url = "https://nginx/api/v1/process/login",
+ source = ltn12.source.string(req_json),
+ headers = {
+ ["content-type"] = "application/json",
+ ["content-length"] = tostring(#req_json)
+ },
+ sink = ltn12.sink.table(res)
+ }
+ local api_response = json.decode(table.concat(res))
+ if api_response.role == 'user' then
+ if req.protocol.ignore_hasaccess == false then
+ con:execute(string.format([[REPLACE INTO sasl_log (service, app_password, username, real_rip)
+ VALUES ("%s", %d, "%s", "%s")]], con:escape(req.service), row.id, con:escape(req.user), con:escape(req.real_rip)))
end
- row = cur:fetch (row, "a")
+ con:close()
+ return dovecot.auth.PASSDB_RESULT_OK, "password=" .. password
end
end
-
- cur:close()
+
con:close()
-
return dovecot.auth.PASSDB_RESULT_PASSWORD_MISMATCH, "Failed to authenticate"
-
- -- PoC
- -- local reqbody = string.format([[{
- -- "success":0,
- -- "service":"%s",
- -- "app_password":false,
- -- "username":"%s",
- -- "real_rip":"%s"
- -- }]], con:escape(req.service), con:escape(req.user), con:escape(req.real_rip))
- -- http.request {
- -- method = "POST",
- -- url = "http://nginx:8081/sasl_log.php",
- -- source = ltn12.source.string(reqbody),
- -- headers = {
- -- ["content-type"] = "application/json",
- -- ["content-length"] = tostring(#reqbody)
- -- },
- -- sink = ltn12.sink.table(respbody)
- -- }
-
end
function auth_passdb_lookup(req)
return dovecot.auth.PASSDB_RESULT_USER_UNKNOWN, ""
end
-
-function script_init()
- mysql = require "luasql.mysql"
- http = require "socket.http"
- http.TIMEOUT = 5
- ltn12 = require "ltn12"
- env = mysql.mysql()
- con = env:connect("__DBNAME__","__DBUSER__","__DBPASS__","localhost")
- return 0
-end
-
-function script_deinit()
- con:close()
- env:close()
-end
EOF
# Replace patterns in app-passdb.lua
diff --git a/data/web/inc/functions.auth.inc.php b/data/web/inc/functions.auth.inc.php
new file mode 100644
index 00000000..3f4f2f3b
--- /dev/null
+++ b/data/web/inc/functions.auth.inc.php
@@ -0,0 +1,278 @@
+<?php
+function check_login($user, $pass, $app_passwd_data = false) {
+ global $pdo;
+ global $redis;
+
+ if (!filter_var($user, FILTER_VALIDATE_EMAIL) && !ctype_alnum(str_replace(array('_', '.', '-'), '', $user))) {
+ $_SESSION['return'][] = array(
+ 'type' => 'danger',
+ 'log' => array(__FUNCTION__, $user, '*'),
+ 'msg' => 'malformed_username'
+ );
+ return false;
+ }
+
+ // Validate admin
+ $result = mailcow_admin_login($user, $pass);
+ if ($result){
+ return $result;
+ }
+
+ // Validate domain admin
+ $result = mailcow_domainadmin_login($user, $pass);
+ if ($result){
+ return $result;
+ }
+
+ // Validate mailbox user
+ // skip log & ldelay if requests comes from dovecot
+ $is_dovecot = false;
+ $request_ip = ($_SERVER['HTTP_X_REAL_IP'] ?? $_SERVER['REMOTE_ADDR']);
+ if ($request_ip == getenv('IPV4_NETWORK').'.250'){
+ $is_dovecot = true;
+ }
+ // check authsource
+ $stmt = $pdo->prepare("SELECT authsource FROM `mailbox`
+ INNER JOIN domain on mailbox.domain = domain.domain
+ WHERE `kind` NOT REGEXP 'location|thing|group'
+ AND `mailbox`.`active`='1'
+ AND `domain`.`active`='1'
+ AND `username` = :user");
+ $stmt->execute(array(':user' => $user));
+ $row = $stmt->fetch(PDO::FETCH_ASSOC);
+ if ($row['authsource'] == 'keycloak'){
+ $result = keycloak_mbox_login($user, $pass, $is_dovecot);
+ if ($result){
+ return $result;
+ }
+ } else {
+ $result = mailcow_mbox_login($user, $pass, $app_passwd_data, $is_dovecot);
+ if ($result){
+ return $result;
+ }
+ }
+
+ // skip log and only return false
+ // netfilter uses dovecot error log for banning
+ if ($is_dovecot){
+ return false;
+ }
+ if (!isset($_SESSION['ldelay'])) {
+ $_SESSION['ldelay'] = "0";
+ $redis->publish("F2B_CHANNEL", "mailcow UI: Invalid password for " . $user . " by " . $_SERVER['REMOTE_ADDR']);
+ error_log("mailcow UI: Invalid password for " . $user . " by " . $_SERVER['REMOTE_ADDR']);
+ }
+ elseif (!isset($_SESSION['mailcow_cc_username'])) {
+ $_SESSION['ldelay'] = $_SESSION['ldelay']+0.5;
+ $redis->publish("F2B_CHANNEL", "mailcow UI: Invalid password for " . $user . " by " . $_SERVER['REMOTE_ADDR']);
+ error_log("mailcow UI: Invalid password for " . $user . " by " . $_SERVER['REMOTE_ADDR']);
+ }
+ $_SESSION['return'][] = array(
+ 'type' => 'danger',
+ 'log' => array(__FUNCTION__, $user, '*'),
+ 'msg' => 'login_failed'
+ );
+
+ sleep($_SESSION['ldelay']);
+ return false;
+}
+
+function mailcow_mbox_login($user, $pass, $app_passwd_data = false, $is_internal = false){
+ global $pdo;
+
+ $stmt = $pdo->prepare("SELECT * FROM `mailbox`
+ INNER JOIN domain on mailbox.domain = domain.domain
+ WHERE `kind` NOT REGEXP 'location|thing|group'
+ AND `mailbox`.`active`='1'
+ AND `domain`.`active`='1'
+ AND (`mailbox`.`authsource`='mailcow' OR `mailbox`.`authsource` IS NULL)
+ AND `username` = :user");
+ $stmt->execute(array(':user' => $user));
+ $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
+
+ // check if password is app password
+ $is_app_passwd = false;
+ if ($app_passwd_data['eas']){
+ $is_app_passwd = 'eas';
+ } else if ($app_passwd_data['dav']){
+ $is_app_passwd = 'dav';
+ } else if ($app_passwd_data['smtp']){
+ $is_app_passwd = 'smtp';
+ } else if ($app_passwd_data['imap']){
+ $is_app_passwd = 'imap';
+ } else if ($app_passwd_data['sieve']){
+ $is_app_passwd = 'sieve';
+ } else if ($app_passwd_data['pop3']){
+ $is_app_passwd = 'pop3';
+ }
+ if ($is_app_passwd){
+ // fetch app password data
+ $app_passwd_query = "SELECT `app_passwd`.`password` as `password`, `app_passwd`.`id` as `app_passwd_id` FROM `app_passwd`
+ INNER JOIN `mailbox` ON `mailbox`.`username` = `app_passwd`.`mailbox`
+ INNER JOIN `domain` ON `mailbox`.`domain` = `domain`.`domain`
+ WHERE `mailbox`.`kind` NOT REGEXP 'location|thing|group'
+ AND `mailbox`.`active` = '1'
+ AND `domain`.`active` = '1'
+ AND `app_passwd`.`active` = '1'
+ AND `app_passwd`.`mailbox` = :user";
+ // check if app password has protocol access
+ // skip if $app_passwd_data['ignore_hasaccess'] is true and the call is not external
+ if (!$app_passwd_data['ignore_hasaccess'] || !$is_internal){
+ $app_passwd_query = $app_passwd_query . " AND `app_passwd`.`" . $is_app_passwd . "_access` = '1'";
+ }
+ // fetch password data
+ $stmt = $pdo->prepare($app_passwd_query);
+ $stmt->execute(array(':user' => $user));
+ $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
+ }
+
+ foreach ($rows as $row) {
+ // verify password
+ if (verify_hash($row['password'], $pass) !== false) {
+ if (!$is_app_passwd){
+ // password is not a app password
+ // check for tfa authenticators
+ $authenticators = get_tfa($user);
+ if (isset($authenticators['additional']) && is_array($authenticators['additional']) && count($authenticators['additional']) > 0 && !$is_internal) {
+ // authenticators found, init TFA flow
+ $_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 (!isset($authenticators['additional']) || !is_array($authenticators['additional']) || count($authenticators['additional']) == 0) {
+ // no authenticators found, login successfull
+ if (!$is_internal){
+ 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));
+ // skip log
+ $_SESSION['return'][] = array(
+ 'type' => 'success',
+ 'log' => array(__FUNCTION__, $user, '*'),
+ 'msg' => array('logged_in_as', $user)
+ );
+ }
+ return "user";
+ }
+ } elseif ($is_app_passwd) {
+ // password is a app password
+ if ($is_internal){
+ // skip log
+ return "user";
+ }
+
+ $service = strtoupper($is_app_passwd);
+ $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'])
+ ));
+
+ unset($_SESSION['ldelay']);
+ return "user";
+ }
+ }
+ }
+
+ return false;
+}
+function mailcow_domainadmin_login($user, $pass){
+ global $pdo;
+
+ $stmt = $pdo->prepare("SELECT `password` FROM `admin`
+ WHERE `superadmin` = '0'
+ AND `active`='1'
+ AND `username` = :user");
+ $stmt->execute(array(':user' => $user));
+ $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
+ 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'] = "domainadmin";
+ $_SESSION['pending_tfa_methods'] = $authenticators['additional'];
+ unset($_SESSION['ldelay']);
+ $_SESSION['return'][] = array(
+ 'type' => 'info',
+ 'log' => array(__FUNCTION__, $user, '*'),
+ 'msg' => 'awaiting_tfa_confirmation'
+ );
+ return "pending";
+ }
+ else {
+ 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));
+ $_SESSION['return'][] = array(
+ 'type' => 'success',
+ 'log' => array(__FUNCTION__, $user, '*'),
+ 'msg' => array('logged_in_as', $user)
+ );
+ return "domainadmin";
+ }
+ }
+ }
+
+ return false;
+}
+function mailcow_admin_login($user, $pass){
+ global $pdo;
+
+ $user = strtolower(trim($user));
+ $stmt = $pdo->prepare("SELECT `password` FROM `admin`
+ WHERE `superadmin` = '1'
+ AND `active` = '1'
+ AND `username` = :user");
+ $stmt->execute(array(':user' => $user));
+ $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
+ foreach ($rows as $row) {
+ // verify password
+ 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) {
+ // active tfa authenticators found, set pending user login
+ $_SESSION['pending_mailcow_cc_username'] = $user;
+ $_SESSION['pending_mailcow_cc_role'] = "admin";
+ $_SESSION['pending_tfa_methods'] = $authenticators['additional'];
+ unset($_SESSION['ldelay']);
+ $_SESSION['return'][] = array(
+ 'type' => 'info',
+ 'log' => array(__FUNCTION__, $user, '*'),
+ 'msg' => 'awaiting_tfa_confirmation'
+ );
+ return "pending";
+ } else {
+ 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));
+ $_SESSION['return'][] = array(
+ 'type' => 'success',
+ 'log' => array(__FUNCTION__, $user, '*'),
+ 'msg' => array('logged_in_as', $user)
+ );
+ return "admin";
+ }
+ }
+ }
+
+ return false;
+}
+
+function keycloak_mbox_login($user, $pass, $is_internal = false){
+ return false;
+}
diff --git a/data/web/inc/functions.inc.php b/data/web/inc/functions.inc.php
index ee1aab23..28cdc179 100644
--- a/data/web/inc/functions.inc.php
+++ b/data/web/inc/functions.inc.php
@@ -811,204 +811,6 @@ function verify_hash($hash, $password) {
}
return false;
}
-function check_login($user, $pass, $app_passwd_data = false) {
- global $pdo;
- global $redis;
- global $imap_server;
-
- if (!filter_var($user, FILTER_VALIDATE_EMAIL) && !ctype_alnum(str_replace(array('_', '.', '-'), '', $user))) {
- $_SESSION['return'][] = array(
- 'type' => 'danger',
- 'log' => array(__FUNCTION__, $user, '*'),
- 'msg' => 'malformed_username'
- );
- return false;
- }
-
- // Validate admin
- $user = strtolower(trim($user));
- $stmt = $pdo->prepare("SELECT `password` FROM `admin`
- WHERE `superadmin` = '1'
- AND `active` = '1'
- AND `username` = :user");
- $stmt->execute(array(':user' => $user));
- $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
- foreach ($rows as $row) {
- // verify password
- 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) {
- // active tfa authenticators found, set pending user login
- $_SESSION['pending_mailcow_cc_username'] = $user;
- $_SESSION['pending_mailcow_cc_role'] = "admin";
- $_SESSION['pending_tfa_methods'] = $authenticators['additional'];
- unset($_SESSION['ldelay']);
- $_SESSION['return'][] = array(
- 'type' => 'info',
- 'log' => array(__FUNCTION__, $user, '*'),
- 'msg' => 'awaiting_tfa_confirmation'
- );
- return "pending";
- } else {
- 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));
- $_SESSION['return'][] = array(
- 'type' => 'success',
- 'log' => array(__FUNCTION__, $user, '*'),
- 'msg' => array('logged_in_as', $user)
- );
- return "admin";
- }
- }
- }
-
- // Validate domain admin
- $stmt = $pdo->prepare("SELECT `password` FROM `admin`
- WHERE `superadmin` = '0'
- AND `active`='1'
- AND `username` = :user");
- $stmt->execute(array(':user' => $user));
- $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
- 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'] = "domainadmin";
- $_SESSION['pending_tfa_methods'] = $authenticators['additional'];
- unset($_SESSION['ldelay']);
- $_SESSION['return'][] = array(
- 'type' => 'info',
- 'log' => array(__FUNCTION__, $user, '*'),
- 'msg' => 'awaiting_tfa_confirmation'
- );
- return "pending";
- }
- else {
- 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));
- $_SESSION['return'][] = array(
- 'type' => 'success',
- 'log' => array(__FUNCTION__, $user, '*'),
- 'msg' => array('logged_in_as', $user)
- );
- return "domainadmin";
- }
- }
- }
-
- // Validate mailbox user
- $stmt = $pdo->prepare("SELECT `password` FROM `mailbox`
- INNER JOIN domain on mailbox.domain = domain.domain
- WHERE `kind` NOT REGEXP 'location|thing|group'
- AND `mailbox`.`active`='1'
- AND `domain`.`active`='1'
- AND `username` = :user");
- $stmt->execute(array(':user' => $user));
- $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
- if ($app_passwd_data['eas'] === true) {
- $stmt = $pdo->prepare("SELECT `app_passwd`.`password` as `password`, `app_passwd`.`id` as `app_passwd_id` FROM `app_passwd`
- INNER JOIN `mailbox` ON `mailbox`.`username` = `app_passwd`.`mailbox`
- INNER JOIN `domain` ON `mailbox`.`domain` = `domain`.`domain`
- WHERE `mailbox`.`kind` NOT REGEXP 'location|thing|group'
- AND `mailbox`.`active` = '1'
- AND `domain`.`active` = '1'
- AND `app_passwd`.`active` = '1'
- AND `app_passwd`.`eas_access` = '1'
- AND `app_passwd`.`mailbox` = :user");
- $stmt->execute(array(':user' => $user));
- $rows = array_merge($rows, $stmt->fetchAll(PDO::FETCH_ASSOC));
- }
- elseif ($app_passwd_data['dav'] === true) {
- $stmt = $pdo->prepare("SELECT `app_passwd`.`password` as `password`, `app_passwd`.`id` as `app_passwd_id` FROM `app_passwd`
- INNER JOIN `mailbox` ON `mailbox`.`username` = `app_passwd`.`mailbox`
- INNER JOIN `domain` ON `mailbox`.`domain` = `domain`.`domain`
- WHERE `mailbox`.`kind` NOT REGEXP 'location|thing|group'
- AND `mailbox`.`active` = '1'
- AND `domain`.`active` = '1'
- AND `app_passwd`.`active` = '1'
- AND `app_passwd`.`dav_access` = '1'
- AND `app_passwd`.`mailbox` = :user");
- $stmt->execute(array(':user' => $user));
- $rows = array_merge($rows, $stmt->fetchAll(PDO::FETCH_ASSOC));
- }
- foreach ($rows as $row) {
- // verify password
- if (verify_hash($row['password'], $pass) !== false) {
- if (!array_key_exists("app_passwd_id", $row)){
- // password is not a app password
- // check for tfa authenticators
- $authenticators = get_tfa($user);
- if (isset($authenticators['additional']) && is_array($authenticators['additional']) && count($authenticators['additional']) > 0 &&
- $app_passwd_data['eas'] !== true && $app_passwd_data['dav'] !== true) {
- // authenticators found, init TFA flow
- $_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 (!isset($authenticators['additional']) || !is_array($authenticators['additional']) || count($authenticators['additional']) == 0) {
- unset($_SESSION['ldelay']);
- // no authenticators found, login successfull
- // 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));
- $_SESSION['return'][] = array(
- 'type' => 'success',
- 'log' => array(__FUNCTION__, $user, '*'),
- 'msg' => array('logged_in_as', $user)
- );
- return "user";
- }
- } elseif ($app_passwd_data['eas'] === true || $app_passwd_data['dav'] === true) {
- // password is a app password
- $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'])
- ));
-
- unset($_SESSION['ldelay']);
- return "user";
- }
- }
- }
-
- if (!isset($_SESSION['ldelay'])) {
- $_SESSION['ldelay'] = "0";
- $redis->publish("F2B_CHANNEL", "mailcow UI: Invalid password for " . $user . " by " . $_SERVER['REMOTE_ADDR']);
- error_log("mailcow UI: Invalid password for " . $user . " by " . $_SERVER['REMOTE_ADDR']);
- }
- elseif (!isset($_SESSION['mailcow_cc_username'])) {
- $_SESSION['ldelay'] = $_SESSION['ldelay']+0.5;
- $redis->publish("F2B_CHANNEL", "mailcow UI: Invalid password for " . $user . " by " . $_SERVER['REMOTE_ADDR']);
- error_log("mailcow UI: Invalid password for " . $user . " by " . $_SERVER['REMOTE_ADDR']);
- }
-
- $_SESSION['return'][] = array(
- 'type' => 'danger',
- 'log' => array(__FUNCTION__, $user, '*'),
- 'msg' => 'login_failed'
- );
-
- sleep($_SESSION['ldelay']);
- return false;
-}
function formatBytes($size, $precision = 2) {
if(!is_numeric($size)) {
return "0";
diff --git a/data/web/inc/functions.mailbox.inc.php b/data/web/inc/functions.mailbox.inc.php
index 0f48efbd..a3809402 100644
--- a/data/web/inc/functions.mailbox.inc.php
+++ b/data/web/inc/functions.mailbox.inc.php
@@ -1002,6 +1002,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
$local_part = strtolower(trim($_data['local_part']));
$domain = idn_to_ascii(strtolower(trim($_data['domain'])), 0, INTL_IDNA_VARIANT_UTS46);
$username = $local_part . '@' . $domain;
+ $authsource = 'mailcow';
if (!filter_var($username, FILTER_VALIDATE_EMAIL)) {
$_SESSION['return'][] = array(
'type' => 'danger',
@@ -1018,6 +1019,9 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
);
return false;
}
+ if (in_array($_data['authsource'], array('mailcow', 'keycloak'))){
+ $authsource = $_data['authsource'];
+ }
if (empty($name)) {
$name = $local_part;
}
@@ -1035,6 +1039,11 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
$name = ltrim(rtrim($_data['name'], '>'), '<');
$tags = (isset($_data['tags'])) ? $_data['tags'] : $MAILBOX_DEFAULT_ATTRIBUTES['tags'];
$quota_m = (isset($_data['quota'])) ? intval($_data['quota']) : intval($MAILBOX_DEFAULT_ATTRIBUTES['quota']) / 1024 ** 2;
+ if ($authsource != 'mailcow'){
+ $password = '';
+ $password2 = '';
+ $password_hashed = '';
+ }
if ((!isset($_SESSION['acl']['unlimited_quota']) || $_SESSION['acl']['unlimited_quota'] != "1") && $quota_m === 0) {
$_SESSION['return'][] = array(
'type' => 'danger',
@@ -1153,10 +1162,12 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
);
return false;
}
- if (password_check($password, $password2) !== true) {
- return false;
+ if ($authsource == 'mailcow'){
+ if (password_check($password, $password2) !== true) {
+ return false;
+ }
+ $password_hashed = hash_password($password);
}
- $password_hashed = hash_password($password);
if ($MailboxData['count'] >= $DomainData['mailboxes']) {
$_SESSION['return'][] = array(
'type' => 'danger',
@@ -1182,8 +1193,8 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
);
return false;
}
- $stmt = $pdo->prepare("INSERT INTO `mailbox` (`username`, `password`, `name`, `quota`, `local_part`, `domain`, `attributes`, `active`)
- VALUES (:username, :password_hashed, :name, :quota_b, :local_part, :domain, :mailbox_attrs, :active)");
+ $stmt = $pdo->prepare("INSERT INTO `mailbox` (`username`, `password`, `name`, `quota`, `local_part`, `domain`, `attributes`, `authsource`, `active`)
+ VALUES (:username, :password_hashed, :name, :quota_b, :local_part, :domain, :mailbox_attrs, :authsource, :active)");
$stmt->execute(array(
':username' => $username,
':password_hashed' => $password_hashed,
@@ -1192,6 +1203,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
':local_part' => $local_part,
':domain' => $domain,
':mailbox_attrs' => $mailbox_attrs,
+ ':authsource' => $authsource,
':active' => $active
));
$stmt = $pdo->prepare("UPDATE `mailbox` SET
@@ -4422,6 +4434,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
`mailbox`.`quota`,
`mailbox`.`created`,
`mailbox`.`modified`,
+ `mailbox`.`authsource`,
`quota2`.`bytes`,
`attributes`,
`custom_attributes`,
@@ -4443,6 +4456,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
`mailbox`.`quota`,
`mailbox`.`created`,
`mailbox`.`modified`,
+ `mailbox`.`authsource`,
`quota2replica`.`bytes`,
`attributes`,
`custom_attributes`,
@@ -4472,6 +4486,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
$mailboxdata['percent_in_use'] = ($row['quota'] == 0) ? '- ' : round((intval($row['bytes']) / intval($row['quota'])) * 100);
$mailboxdata['created'] = $row['created'];
$mailboxdata['modified'] = $row['modified'];
+ $mailboxdata['authsource'] = ($row['authsource']) ? $row['authsource'] : 'mailcow';
if ($mailboxdata['percent_in_use'] === '- ') {
$mailboxdata['percent_class'] = "info";
diff --git a/data/web/inc/init_db.inc.php b/data/web/inc/init_db.inc.php
index 6fec0c2f..f7bd86d2 100644
--- a/data/web/inc/init_db.inc.php
+++ b/data/web/inc/init_db.inc.php
@@ -362,6 +362,7 @@ function init_db_schema() {
"custom_attributes" => "JSON NOT NULL DEFAULT ('{}')",
"kind" => "VARCHAR(100) NOT NULL DEFAULT ''",
"multiple_bookings" => "INT NOT NULL DEFAULT -1",
+ "authsource" => "ENUM('mailcow', 'keycloak') DEFAULT 'mailcow'",
"created" => "DATETIME(0) NOT NULL DEFAULT NOW(0)",
"modified" => "DATETIME ON UPDATE CURRENT_TIMESTAMP",
"active" => "TINYINT(1) NOT NULL DEFAULT '1'"
diff --git a/data/web/inc/prerequisites.inc.php b/data/web/inc/prerequisites.inc.php
index 9c5203e7..1a18ba92 100644
--- a/data/web/inc/prerequisites.inc.php
+++ b/data/web/inc/prerequisites.inc.php
@@ -177,6 +177,7 @@ function get_remote_ip() {
// Load core functions first
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.inc.php';
+require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.auth.inc.php';
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/sessions.inc.php';
// IMAP lib
diff --git a/data/web/json_api.php b/data/web/json_api.php
index 28f8cac5..0b80bafd 100644
--- a/data/web/json_api.php
+++ b/data/web/json_api.php
@@ -401,6 +401,26 @@ if (isset($_GET['query'])) {
);
echo json_encode($return);
break;
+ case "login":
+ header('Content-Type: application/json');
+ $post = trim(file_get_contents('php://input'));
+ if ($post) {
+ $post = json_decode($post, true);
+ }
+
+ $return = array("success" => false, "role" => false);
+ if(!isset($post['username']) || !isset($post['password'])){
+ echo json_encode($return);
+ return;
+ }
+ $result = check_login($post['username'], $post['password'], $post['protocol']);
+ if ($result) {
+ $return = array("success" => true, "role" => $result);
+ }
+
+ echo json_encode($return);
+ return;
+ break;
}
break;
case "get":
diff --git a/data/web/sogo-auth.php b/data/web/sogo-auth.php
index c34a60de..af6f851b 100644
--- a/data/web/sogo-auth.php
+++ b/data/web/sogo-auth.php
@@ -11,7 +11,8 @@ $session_var_pass = 'sogo-sso-pass';
// validate credentials for basic auth requests
if (isset($_SERVER['PHP_AUTH_USER'])) {
// load prerequisites only when required
- require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php';
+ require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.inc.php';
+ require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.auth.inc.php';
$username = $_SERVER['PHP_AUTH_USER'];
$password = $_SERVER['PHP_AUTH_PW'];
$is_eas = false;
diff --git a/data/web/templates/edit/mailbox.twig b/data/web/templates/edit/mailbox.twig
index 8960ee93..52b4096c 100644
--- a/data/web/templates/edit/mailbox.twig
+++ b/data/web/templates/edit/mailbox.twig
@@ -24,7 +24,13 @@
<input type="hidden" value="default" name="sender_acl">
<input type="hidden" value="0" name="force_pw_update">
<input type="hidden" value="0" name="sogo_access">
- <input type="hidden" value="0" name="protocol_access">
+ <input type="hidden" value="0" name="protocol_access">
+ <div class="row mb-2">
+ <label class="control-label col-sm-2">{{ lang.edit.full_name }}</label>
+ <div class="col-sm-10">
+ <span>{{ result.authsource }}</span>
+ </div>
+ </div>
<div class="row mb-2">
<label class="control-label col-sm-2" for="name">{{ lang.edit.full_name }}</label>
<div class="col-sm-10">
diff --git a/data/web/templates/modals/mailbox.twig b/data/web/templates/modals/mailbox.twig
index 22807c8d..5d6ea535 100644
--- a/data/web/templates/modals/mailbox.twig
+++ b/data/web/templates/modals/mailbox.twig
@@ -28,6 +28,15 @@
</select>
</div>
</div>
+ <div class="row mb-2">
+ <label class="control-label col-sm-2 text-sm-end text-sm-end" for="authsource">{{ lang.add.domain }}</label>
+ <div class="col-sm-10">
+ <select class="full-width-select" data-live-search="true" id="addAuthsource" name="authsource" required>
+ <option selected>mailcow</option>
+ <option>keycloak</option>
+ </select>
+ </div>
+ </div>
<div class="row mb-4">
<label class="control-label col-sm-2 text-sm-end text-sm-end" for="name">{{ lang.add.full_name }}</label>
<div class="col-sm-10">