Browse Source

[Web] organize auth functions+api auth w/ dovecot

FreddleSpl0it 2 năm trước cách đây
mục cha
commit
7218095041

+ 1 - 0
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 \

+ 61 - 89
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

+ 278 - 0
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;
+}

+ 0 - 198
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";

+ 20 - 5
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";

+ 1 - 0
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'"

+ 1 - 0
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

+ 20 - 0
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":

+ 2 - 1
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;

+ 7 - 1
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">

+ 9 - 0
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">