Forráskód Böngészése

rework auth - move dovecot sasl log to php

FreddleSpl0it 2 éve
szülő
commit
7b47159478

+ 6 - 51
data/Dockerfiles/dovecot/docker-entrypoint.sh

@@ -138,18 +138,17 @@ function auth_password_verify(request, password)
   ltn12 = require "ltn12"
   ltn12 = require "ltn12"
   https = require "ssl.https"
   https = require "ssl.https"
   https.TIMEOUT = 5
   https.TIMEOUT = 5
-  mysql = require "luasql.mysql"
-  env  = mysql.mysql()
-  con = env:connect("__DBNAME__","__DBUSER__","__DBPASS__","localhost")
 
 
   local req = {
   local req = {
     username = request.user,
     username = request.user,
-    password = password
+    password = password,
+    real_rip = request.real_rip,
+    protocol = {}
   }
   }
+  req.protocol[request.service] = true
   local req_json = json.encode(req)
   local req_json = json.encode(req)
   local res = {} 
   local res = {} 
   
   
-  -- check against mailbox passwds
   local b, c = https.request {
   local b, c = https.request {
     method = "POST",
     method = "POST",
     url = "https://nginx:9082",
     url = "https://nginx:9082",
@@ -162,48 +161,10 @@ function auth_password_verify(request, password)
     insecure = true
     insecure = true
   }
   }
   local api_response = json.decode(table.concat(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 request.service == "smtp" or request.service == "imap" or request.service == "sieve" or request.service == "pop3" then
-    skip_sasl_log = false
-    req.protocol = {}
-    if tostring(req.real_rip) ~= "__IPV4_SOGO__" then
-      skip_sasl_log = true
-      req.protocol[request.service] = true
-    end
-    req_json = json.encode(req)
-
-    local b, c = https.request {
-      method = "POST",
-      url = "https://nginx:9082",
-      source = ltn12.source.string(req_json),
-      headers = {
-        ["content-type"] = "application/json",
-        ["content-length"] = tostring(#req_json)
-      },
-      sink = ltn12.sink.table(res),
-      insecure = true
-    }
-    local api_response = json.decode(table.concat(res))
-    if api_response.role == 'user' then
-      if skip_sasl_log == 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
-      con:close()
-      return dovecot.auth.PASSDB_RESULT_OK, "password=" .. password
-    end
+  if api_response.success == true then
+    return dovecot.auth.PASSDB_RESULT_OK, ""
   end
   end
   
   
-  con:close()
   return dovecot.auth.PASSDB_RESULT_PASSWORD_MISMATCH, "Failed to authenticate"
   return dovecot.auth.PASSDB_RESULT_PASSWORD_MISMATCH, "Failed to authenticate"
 end
 end
 
 
@@ -212,12 +173,6 @@ function auth_passdb_lookup(req)
 end
 end
 EOF
 EOF
 
 
-# Replace patterns in app-passdb.lua
-sed -i "s/__DBUSER__/${DBUSER}/g" /etc/dovecot/auth/passwd-verify.lua
-sed -i "s/__DBPASS__/${DBPASS}/g" /etc/dovecot/auth/passwd-verify.lua
-sed -i "s/__DBNAME__/${DBNAME}/g" /etc/dovecot/auth/passwd-verify.lua
-sed -i "s/__IPV4_SOGO__/${IPV4_NETWORK}.248/g" /etc/dovecot/auth/passwd-verify.lua
-
 
 
 # Migrate old sieve_after file
 # Migrate old sieve_after file
 [[ -f /etc/dovecot/sieve_after ]] && mv /etc/dovecot/sieve_after /etc/dovecot/global_sieve_after
 [[ -f /etc/dovecot/sieve_after ]] && mv /etc/dovecot/sieve_after /etc/dovecot/global_sieve_after

+ 27 - 8
data/conf/dovecot/auth/mailcowauth.php

@@ -1,4 +1,5 @@
 <?php
 <?php
+ini_set('error_reporting', 0);
 header('Content-Type: application/json');
 header('Content-Type: application/json');
 
 
 $post = trim(file_get_contents('php://input'));
 $post = trim(file_get_contents('php://input'));
@@ -6,8 +7,11 @@ if ($post) {
   $post = json_decode($post, true);
   $post = json_decode($post, true);
 }
 }
 
 
-$return = array("success" => false, "role" => false);
-if(!isset($post['username']) || !isset($post['password'])){
+
+$return = array("success" => false);
+if(!isset($post['username']) || !isset($post['password']) || !isset($post['real_rip'])){
+  error_log("MAILCOWAUTH: Bad Request");
+  http_response_code(400); // Bad Request
   echo json_encode($return); 
   echo json_encode($return); 
   exit();
   exit();
 }
 }
@@ -18,9 +22,7 @@ if (file_exists('../../../web/inc/vars.local.inc.php')) {
 }
 }
 require_once '../../../web/inc/lib/vendor/autoload.php';
 require_once '../../../web/inc/lib/vendor/autoload.php';
 
 
-ini_set('error_reporting', 0);
 // Init database
 // Init database
-//$dsn = $database_type . ':host=' . $database_host . ';dbname=' . $database_name;
 $dsn = $database_type . ":unix_socket=" . $database_sock . ";dbname=" . $database_name;
 $dsn = $database_type . ":unix_socket=" . $database_sock . ";dbname=" . $database_name;
 $opt = [
 $opt = [
     PDO::ATTR_ERRMODE            => PDO::ERRMODE_EXCEPTION,
     PDO::ATTR_ERRMODE            => PDO::ERRMODE_EXCEPTION,
@@ -31,7 +33,8 @@ try {
   $pdo = new PDO($dsn, $database_user, $database_pass, $opt);
   $pdo = new PDO($dsn, $database_user, $database_pass, $opt);
 }
 }
 catch (PDOException $e) {
 catch (PDOException $e) {
-  $return = array("success" => false, "role" => '');
+  error_log("MAILCOWAUTH: " . $e . PHP_EOL);
+  http_response_code(500); // Internal Server Error
   echo json_encode($return); 
   echo json_encode($return); 
   exit;
   exit;
 }
 }
@@ -44,12 +47,28 @@ require_once 'sessions.inc.php';
 // Init provider
 // Init provider
 $iam_provider = identity_provider('init');
 $iam_provider = identity_provider('init');
 
 
-$result = check_login($post['username'], $post['password'], $post['protocol'], true);
+
+$protocol = $post['protocol'];
+if ($post['real_rip'] == getenv('IPV4_NETWORK') . '.248') {
+  $protocol = null;
+}
+$result = user_login($post['username'], $post['password'], $protocol, array('is_internal' => true));
+if ($result === false){
+  $result = apppass_login($post['username'], $post['password'], $protocol, array(
+    'is_internal' => true,
+    'remote_addr' => $post['real_rip']
+  ));
+}
+
 if ($result) {
 if ($result) {
-  $return = array("success" => true, "role" => $result);
+  http_response_code(200); // OK
+  $return['success'] = true;
 } else {
 } else {
-  $return = array("success" => false, "role" => '');
+  error_log("MAILCOWAUTH: Login failed for user " . $post['username']);
+  http_response_code(401); // Unauthorized
 }
 }
 
 
+
 echo json_encode($return); 
 echo json_encode($return); 
+session_destroy();
 exit;
 exit;

+ 194 - 155
data/web/inc/functions.auth.inc.php

@@ -1,64 +1,29 @@
 <?php
 <?php
-function check_login($user, $pass, $app_passwd_data = false, $is_internal = false) {
+function check_login($user, $pass, $app_passwd_data = false, $extra = null) {
   global $pdo;
   global $pdo;
   global $redis;
   global $redis;
+  
+  $is_internal = $extra['is_internal'];
 
 
-  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);
+  // Try validate admin
+  $result = admin_login($user, $pass);
   if ($result !== false) return $result;
   if ($result !== false) return $result;
 
 
-  // Validate domain admin
-  $result = mailcow_domainadmin_login($user, $pass);
+  // Try validate domain admin
+  $result = domainadmin_login($user, $pass);
   if ($result !== false) return $result;
   if ($result !== false) return $result;
 
 
-  // Validate mailbox user
-  // check authsource
-  $stmt = $pdo->prepare("SELECT authsource, mailbox.active AS mailbox_active, domain.active AS domain_active FROM `mailbox`
-      INNER JOIN domain on mailbox.domain = domain.domain
-      WHERE `kind` NOT REGEXP 'location|thing|group'
-        AND `username` = :user");
-  $stmt->execute(array(':user' => $user));
-  $row = $stmt->fetch(PDO::FETCH_ASSOC);
-  if (!$row && $row['domain_active'] == 1){
-    // mbox does not exist, call keycloak login and create mbox if possible via rest flow
-    $iam_settings = identity_provider('get');
-    if ($iam_settings['authsource'] == 'keycloak' && intval($iam_settings['mailboxpassword_flow']) == 1){
-      $result = keycloak_mbox_login_rest($user, $pass, $iam_settings, $is_internal, true);
-      if ($result !== false) return $result;
-    }
-  } else if ($row && $row['mailbox_active'] == 1 && $row['domain_active'] == 1) {
-    // mbox does exist and is active
-    if (isset($app_passwd_data)){
-      // first check if password is app_password
-      $result = mailcow_mbox_apppass_login($user, $pass, $app_passwd_data, $is_internal);
-      if ($result !== false) return $result;
-    }
+  // Try validate user
+  $result = user_login($user, $pass);
+  if ($result !== false) return $result;
 
 
-    if ($row['authsource'] == 'mailcow') {
-      // mbox authsource is mailcow
-      $result = mailcow_mbox_login($user, $pass, $app_passwd_data, $is_internal);
-      if ($result !== false) return $result;
-    } else if ($row['authsource'] == 'keycloak'){
-      // mbox authsource is keycloak, try using via rest flow
-      $iam_settings = identity_provider('get');
-      if (intval($iam_settings['mailboxpassword_flow']) == 1){
-        $result = keycloak_mbox_login_rest($user, $pass, $iam_settings, $is_internal);
-        if ($result !== false) return $result;
-      }
-    }
-  }
+  // Try validate app password
+  $result = apppass_login($user, $pass, $app_passwd_data);
+  if ($result !== false) 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;
+
   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']);
     $redis->publish("F2B_CHANNEL", "mailcow UI: Invalid password for " . $user . " by " . $_SERVER['REMOTE_ADDR']);
@@ -79,146 +44,207 @@ function check_login($user, $pass, $app_passwd_data = false, $is_internal = fals
   return false;
   return false;
 }
 }
 
 
-function mailcow_admin_login($user, $pass){
+function admin_login($user, $pass){
   global $pdo;
   global $pdo;
 
 
+  if (!ctype_alnum(str_replace(array('_', '.', '-'), '', $user))) {
+    $_SESSION['return'][] =  array(
+      'type' => 'danger',
+      'log' => array(__FUNCTION__, $user, '*'),
+      'msg' => 'malformed_username'
+    );
+    return false;
+  }
+
   $user = strtolower(trim($user));
   $user = strtolower(trim($user));
   $stmt = $pdo->prepare("SELECT `password` FROM `admin`
   $stmt = $pdo->prepare("SELECT `password` FROM `admin`
       WHERE `superadmin` = '1'
       WHERE `superadmin` = '1'
       AND `active` = '1'
       AND `active` = '1'
       AND `username` = :user");
       AND `username` = :user");
   $stmt->execute(array(':user' => $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";
-      }
+  $row = $stmt->fetch(PDO::FETCH_ASSOC);
+
+  // 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;
   return false;
 }
 }
-function mailcow_domainadmin_login($user, $pass){
+function domainadmin_login($user, $pass){
   global $pdo;
   global $pdo;
 
 
+  if (!ctype_alnum(str_replace(array('_', '.', '-'), '', $user))) {
+    $_SESSION['return'][] =  array(
+      'type' => 'danger',
+      'log' => array(__FUNCTION__, $user, '*'),
+      'msg' => 'malformed_username'
+    );
+    return false;
+  }
+
   $stmt = $pdo->prepare("SELECT `password` FROM `admin`
   $stmt = $pdo->prepare("SELECT `password` FROM `admin`
       WHERE `superadmin` = '0'
       WHERE `superadmin` = '0'
       AND `active`='1'
       AND `active`='1'
       AND `username` = :user");
       AND `username` = :user");
   $stmt->execute(array(':user' => $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";
-      }
+  $row = $stmt->fetch(PDO::FETCH_ASSOC);
+
+  // 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;
   return false;
 }
 }
-function mailcow_mbox_login($user, $pass, $app_passwd_data = false, $is_internal = false){
+function user_login($user, $pass, $extra = null){
   global $pdo;
   global $pdo;
 
 
+  $is_internal = $extra['is_internal'];
+
+  if (!filter_var($user, FILTER_VALIDATE_EMAIL) && !ctype_alnum(str_replace(array('_', '.', '-'), '', $user))) {
+    if (!$is_internal){
+      $_SESSION['return'][] =  array(
+        'type' => 'danger',
+        'log' => array(__FUNCTION__, $user, '*'),
+        'msg' => 'malformed_username'
+      );
+    }
+    return false;
+  }
+
   $stmt = $pdo->prepare("SELECT * FROM `mailbox`
   $stmt = $pdo->prepare("SELECT * FROM `mailbox`
       INNER JOIN domain on mailbox.domain = domain.domain
       INNER JOIN domain on mailbox.domain = domain.domain
       WHERE `kind` NOT REGEXP 'location|thing|group'
       WHERE `kind` NOT REGEXP 'location|thing|group'
-        AND `mailbox`.`active`='1'
         AND `domain`.`active`='1'
         AND `domain`.`active`='1'
-        AND (`mailbox`.`authsource`='mailcow' OR `mailbox`.`authsource` IS NULL)
         AND `username` = :user");
         AND `username` = :user");
   $stmt->execute(array(':user' => $user));
   $stmt->execute(array(':user' => $user));
-  $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
+  $row = $stmt->fetch(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 && !$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'];
+  // user does not exist, try call keycloak login and create user if possible via rest flow
+  if (!$row){
+    $iam_settings = identity_provider('get');
+    if ($iam_settings['authsource'] == 'keycloak' && intval($iam_settings['mailboxpassword_flow']) == 1){
+      $result = keycloak_mbox_login_rest($user, $pass, $iam_settings, array('is_internal' => $is_internal, 'create' => true));
+      if ($result !== false) return $result;
+    }
+  } 
+  if ($row['active'] != 1) {
+    return false;
+  }
+
+  if ($row['authsource'] == 'keycloak'){
+    // user authsource is keycloak, try using via rest flow
+    $iam_settings = identity_provider('get');
+    if (intval($iam_settings['mailboxpassword_flow']) == 1){
+      $result = keycloak_mbox_login_rest($user, $pass, $iam_settings, array('is_internal' => $is_internal));
+      return $result;
+    } else {
+      return false;
+    }
+  }
+ 
+  // 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 && !$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']);
         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(
         $_SESSION['return'][] =  array(
           'type' => 'success',
           'type' => 'success',
           'log' => array(__FUNCTION__, $user, '*'),
           'log' => array(__FUNCTION__, $user, '*'),
           'msg' => array('logged_in_as', $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));
-          $_SESSION['return'][] =  array(
-            'type' => 'success',
-            'log' => array(__FUNCTION__, $user, '*'),
-            'msg' => array('logged_in_as', $user)
-          );
-        }
-        return "user";
       }
       }
+      return "user";
     }
     }
   }
   }
 
 
   return false;
   return false;
 }
 }
-function mailcow_mbox_apppass_login($user, $pass, $app_passwd_data, $is_internal = false){
+function apppass_login($user, $pass, $app_passwd_data, $extra = null){
   global $pdo;
   global $pdo;
 
 
+  $is_internal = $extra['is_internal'];
+
+  if (!filter_var($user, FILTER_VALIDATE_EMAIL) && !ctype_alnum(str_replace(array('_', '.', '-'), '', $user))) {
+    if (!$is_internal){
+      $_SESSION['return'][] =  array(
+        'type' => 'danger',
+        'log' => array(__FUNCTION__, $user, '*'),
+        'msg' => 'malformed_username'
+      );
+    }
+    return false;
+  }
+
   $protocol = false;
   $protocol = false;
   if ($app_passwd_data['eas']){
   if ($app_passwd_data['eas']){
     $protocol = 'eas';
     $protocol = 'eas';
@@ -236,34 +262,33 @@ function mailcow_mbox_apppass_login($user, $pass, $app_passwd_data, $is_internal
     return false;
     return false;
   }
   }
 
 
-
   // fetch app password data
   // fetch app password data
-  $stmt = $pdo->prepare("SELECT `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` 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 `mailbox`.`active` = '1'
       AND `domain`.`active` = '1'
       AND `domain`.`active` = '1'
       AND `app_passwd`.`active` = '1'
       AND `app_passwd`.`active` = '1'
-      AND `app_passwd`.`mailbox` = :user
-      :has_access_query"
+      AND `app_passwd`.`mailbox` = :user"
   );
   );
-  // check if app password has protocol access
-  // skip if protocol is false and the call is internal
-  $has_access_query = ($is_internal && $protocol === false) ? "" : " AND `app_passwd`.`" . $protocol . "_access` = '1'";
   // fetch password data
   // fetch password data
   $stmt->execute(array(
   $stmt->execute(array(
     ':user' => $user,
     ':user' => $user,
-    ':has_access_query' => $has_access_query
   ));
   ));
-  $rows = array_merge($rows, $stmt->fetchAll(PDO::FETCH_ASSOC));
+  $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
   
   
-  foreach ($rows as $row) { 
+  foreach ($rows as $row) {
+    if ($protocol && $row[$protocol . '_access'] != '1'){
+      continue;
+    }
+
     // verify password
     // verify password
     if (verify_hash($row['password'], $pass) !== false) {
     if (verify_hash($row['password'], $pass) !== false) {
       if ($is_internal){
       if ($is_internal){
-        // skip sasl_log, dovecot does the job
-        return "user";
+        $remote_addr = $extra['remote_addr'];
+      } else {
+        $remote_addr = ($_SERVER['HTTP_X_REAL_IP'] ?? $_SERVER['REMOTE_ADDR']);
       }
       }
 
 
       $service = strtoupper($is_app_passwd);
       $service = strtoupper($is_app_passwd);
@@ -272,7 +297,7 @@ function mailcow_mbox_apppass_login($user, $pass, $app_passwd_data, $is_internal
         ':service' => $service,
         ':service' => $service,
         ':app_id' => $row['app_passwd_id'],
         ':app_id' => $row['app_passwd_id'],
         ':username' => $user,
         ':username' => $user,
-        ':remote_addr' => ($_SERVER['HTTP_X_REAL_IP'] ?? $_SERVER['REMOTE_ADDR'])
+        ':remote_addr' => $remote_addr
       ));
       ));
 
 
       unset($_SESSION['ldelay']);
       unset($_SESSION['ldelay']);
@@ -285,9 +310,23 @@ function mailcow_mbox_apppass_login($user, $pass, $app_passwd_data, $is_internal
 // Keycloak REST Api Flow - auth user by mailcow_password attribute
 // Keycloak REST Api Flow - auth user by mailcow_password attribute
 // This password will be used for direct UI, IMAP and SMTP Auth
 // This password will be used for direct UI, IMAP and SMTP Auth
 // To use direct user credentials, only Authorization Code Flow is valid
 // To use direct user credentials, only Authorization Code Flow is valid
-function keycloak_mbox_login_rest($user, $pass, $iam_settings, $is_internal = false, $create = false){
+function keycloak_mbox_login_rest($user, $pass, $iam_settings, $extra = null){
   global $pdo;
   global $pdo;
 
 
+  $is_internal = $extra['is_internal'];
+  $create = $extra['create'];
+  
+  if (!filter_var($user, FILTER_VALIDATE_EMAIL) && !ctype_alnum(str_replace(array('_', '.', '-'), '', $user))) {
+    if (!$is_internal){
+      $_SESSION['return'][] =  array(
+        'type' => 'danger',
+        'log' => array(__FUNCTION__, $user, '*'),
+        'msg' => 'malformed_username'
+      );
+    }
+    return false;
+  }
+
   // get access_token for service account of mailcow client
   // get access_token for service account of mailcow client
   $admin_token = identity_provider("get-keycloak-admin-token");
   $admin_token = identity_provider("get-keycloak-admin-token");