| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495 | <?phpfunction check_login($user, $pass, $app_passwd_data = false, $is_internal = 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  // 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){    // mbox does not exist, call keycloak login and create mbox if possible    $identity_provider_settings = identity_provider('get');    if ($identity_provider_settings['login_flow'] == 'ropc'){      $result = keycloak_mbox_login_ropc($user, $pass, $identity_provider_settings, $is_internal, true);    } else {      $result = keycloak_mbox_login_rest($user, $pass, $identity_provider_settings, $is_internal, true);    }    if ($result){      return $result;    }  } else if ($row['authsource'] == 'keycloak'){    if ($app_passwd_data){      // first check if password is app_password      $result = mailcow_mbox_apppass_login($user, $pass, $app_passwd_data, $is_internal);      if ($result){        return $result;      }    }    $identity_provider_settings = identity_provider('get');    if ($identity_provider_settings['login_flow'] == 'ropc'){      $result = keycloak_mbox_login_ropc($user, $pass, $identity_provider_settings, $is_internal);    } else {      $result = keycloak_mbox_login_rest($user, $pass, $identity_provider_settings, $is_internal);    }    if ($result){      return $result;    }  } else {    if ($app_passwd_data){      // first check if password is app_password      $result = mailcow_mbox_apppass_login($user, $pass, $app_passwd_data, $is_internal);      if ($result){        return $result;      }    }    $result = mailcow_mbox_login($user, $pass, $app_passwd_data, $is_internal);    if ($result){      return $result;    }  }  // skip log and only return false if it's an internal request  if ($is_internal){    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);  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 && !$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));            $_SESSION['return'][] =  array(              'type' => 'success',              'log' => array(__FUNCTION__, $user, '*'),              'msg' => array('logged_in_as', $user)            );          }          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 mailcow_mbox_apppass_login($user, $pass, $app_passwd_data, $is_internal = false){  global $pdo;  $protocol = false;  if ($app_passwd_data['eas']){    $protocol = 'eas';  } else if ($app_passwd_data['dav']){    $protocol = 'dav';  } else if ($app_passwd_data['smtp']){    $protocol = 'smtp';  } else if ($app_passwd_data['imap']){    $protocol = 'imap';  } else if ($app_passwd_data['sieve']){    $protocol = 'sieve';  } else if ($app_passwd_data['pop3']){    $protocol = 'pop3';  }  // fetch app password data  $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`.`mailbox` = :user      :has_access_query"  );      // check if app password has protocol access  // skip if protocol is false and the call is not external  $has_access_query = '';  if (!$is_internal || ($is_internal && !empty($protocol))){    $has_access_query = " AND `app_passwd`.`" . $protocol . "_access` = '1'";  }  // fetch password data  $stmt->execute(array(    ':user' => $user,    ':has_access_query' => $has_access_query  ));  $rows = array_merge($rows, $stmt->fetchAll(PDO::FETCH_ASSOC));    foreach ($rows as $row) {     // verify password    if (verify_hash($row['password'], $pass) !== false) {      if ($is_internal){        // skip sasl_log, dovecot does the job        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;}// ROPC Flow (deprecated oAuth2.1)// uses direct user credentials for UI, IMAP and SMTP Authfunction keycloak_mbox_login_ropc($user, $pass, $iam_settings, $is_internal = false, $create = false){  global $pdo;  $url = "{$iam_settings['server_url']}/realms/{$iam_settings['realm']}/protocol/openid-connect/token";  $req = http_build_query(array(    'grant_type'    => 'password',    'client_id'     => $iam_settings['client_id'],    'client_secret' => $iam_settings['client_secret'],    'username'      => $user,    'password'      => $pass,  ));  $curl = curl_init();  curl_setopt($curl, CURLOPT_URL, $url);  curl_setopt($curl, CURLOPT_POST, 1);  curl_setopt($curl, CURLOPT_POSTFIELDS, $req);  curl_setopt($curl, CURLOPT_HTTPHEADER, array('Content-Type: application/x-www-form-urlencoded'));  curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);  $res = json_decode(curl_exec($curl), true);  $code = curl_getinfo($curl, CURLINFO_HTTP_CODE);  curl_close ($curl);  if ($code == 200) {    // decode jwt    $user_data = json_decode(base64_decode(str_replace('_', '/', str_replace('-','+',explode('.', $res['access_token'])[1]))), true);    if ($user != $user_data['email']){      // check if $user is email address, only accept email address as username      return false;    }    if ($create && !empty($iam_settings['mappers'])){      // try to create mbox on successfull login      $mbox_template = null;      // check if matching attribute mapping exists      foreach ($iam_settings['mappers'] as $index => $mapper){        if (in_array($mapper, $iam_settings['mappers'])) {          $mbox_template = $iam_settings['templates'][$index];          break;        }      }      if (!$mbox_template){        // no matching template found        return false;      }            $stmt = $pdo->prepare("SELECT * FROM `templates`       WHERE `template` = :template AND type = 'mailbox'");      $stmt->execute(array(        ":template" => $mbox_template      ));      $mbox_template_data = $stmt->fetch(PDO::FETCH_ASSOC);      if (!empty($mbox_template_data)){        $mbox_template_data = json_decode($mbox_template_data["attributes"], true);        $mbox_template_data['domain'] = explode('@', $user)[1];        $mbox_template_data['local_part'] = explode('@', $user)[0];        $mbox_template_data['authsource'] = 'keycloak';        $_SESSION['iam_create_login'] = true;        $create_res = mailbox('add', 'mailbox', $mbox_template_data);        $_SESSION['iam_create_login'] = false;        if (!$create_res){          return false;        }      }    }    $_SESSION['return'][] =  array(      'type' => 'success',      'log' => array(__FUNCTION__, $user, '*'),      'msg' => array('logged_in_as', $user)    );    return 'user';  } else {    return false;  }}// Keycloak REST Api Flow - auth user by mailcow_password attribute// This password will be used for direct UI, IMAP and SMTP Auth// To use direct user credentials, only Authorization Code Flow is validfunction keycloak_mbox_login_rest($user, $pass, $iam_settings, $is_internal = false, $create = false){  global $pdo;  // get access_token for service account of mailcow client  $url = "{$iam_settings['server_url']}/realms/{$iam_settings['realm']}/protocol/openid-connect/token";  $req = http_build_query(array(    'grant_type'    => 'client_credentials',    'client_id'     => $iam_settings['client_id'],    'client_secret' => $iam_settings['client_secret']  ));  $curl = curl_init();  curl_setopt($curl, CURLOPT_URL, $url);  curl_setopt($curl, CURLOPT_POST, 1);  curl_setopt($curl, CURLOPT_POSTFIELDS, $req);  curl_setopt($curl, CURLOPT_HTTPHEADER, array('Content-Type: application/x-www-form-urlencoded'));  curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);  $mcclient_res = json_decode(curl_exec($curl), true);  $code = curl_getinfo($curl, CURLINFO_HTTP_CODE);  curl_close ($curl);  if ($code != 200) {    return false;  }  // get the mailcow_password attribute from keycloak user  $url = "{$iam_settings['server_url']}/admin/realms/{$iam_settings['realm']}/users";  $queryParams = array('email' => $user, 'exact' => true);  $queryString = http_build_query($queryParams);  $curl = curl_init();  curl_setopt($curl, CURLOPT_URL, $url . '?' . $queryString);  curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);  curl_setopt($curl, CURLOPT_HTTPHEADER, array(      'Authorization: Bearer ' . $mcclient_res['access_token'],      'Content-Type: application/json'  ));  $user_res = json_decode(curl_exec($curl), true)[0];  $code = curl_getinfo($curl, CURLINFO_HTTP_CODE);  curl_close($curl);  if ($code != 200) {    return false;  }    // validate mailcow_password  $mailcow_password = $user_res['attributes']['mailcow_password'][0];  if (!verify_hash($mailcow_password, $pass)) {    return false;  }  // get mapped template, if not set return false  // also return false if no mappers were defined  $user_template = $user_data['attributes']['mailcow_template'][0];  if ($create && (empty($iam_settings['mappers']) || $user_template)){    return false;  } else if (!$create) {    // login success - dont create mailbox    $_SESSION['return'][] =  array(      'type' => 'success',      'log' => array(__FUNCTION__, $user, '*'),      'msg' => array('logged_in_as', $user)    );    return 'user';  }  // try to create mbox on successfull login  $mbox_template = null;  // check if matching attribute mapping exists  foreach ($iam_settings['mappers'] as $index => $mapper){    if (in_array($mapper, $iam_settings['mappers'])) {      $mbox_template = $iam_settings['templates'][$index];      break;    }  }  if (!$mbox_template){    // no matching template found    return false;  }  $stmt = $pdo->prepare("SELECT * FROM `templates`   WHERE `template` = :template AND type = 'mailbox'");  $stmt->execute(array(    ":template" => $mbox_template  ));  $mbox_template_data = $stmt->fetch(PDO::FETCH_ASSOC);  if (empty($mbox_template_data)){    return false;  }  $mbox_template_data = json_decode($mbox_template_data["attributes"], true);  $mbox_template_data['domain'] = explode('@', $user)[1];  $mbox_template_data['local_part'] = explode('@', $user)[0];  $mbox_template_data['authsource'] = 'keycloak';  $_SESSION['iam_create_login'] = true;  $create_res = mailbox('add', 'mailbox', $mbox_template_data);  $_SESSION['iam_create_login'] = false;  if (!$create_res){    return false;  }  $_SESSION['return'][] =  array(    'type' => 'success',    'log' => array(__FUNCTION__, $user, '*'),    'msg' => array('logged_in_as', $user)  );  return 'user';}
 |