functions.auth.inc.php 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418
  1. <?php
  2. function check_login($user, $pass, $app_passwd_data = false, $is_internal = false) {
  3. global $pdo;
  4. global $redis;
  5. if (!filter_var($user, FILTER_VALIDATE_EMAIL) && !ctype_alnum(str_replace(array('_', '.', '-'), '', $user))) {
  6. $_SESSION['return'][] = array(
  7. 'type' => 'danger',
  8. 'log' => array(__FUNCTION__, $user, '*'),
  9. 'msg' => 'malformed_username'
  10. );
  11. return false;
  12. }
  13. // Validate admin
  14. $result = mailcow_admin_login($user, $pass);
  15. if ($result){
  16. return $result;
  17. }
  18. // Validate domain admin
  19. $result = mailcow_domainadmin_login($user, $pass);
  20. if ($result){
  21. return $result;
  22. }
  23. // Validate mailbox user
  24. // check authsource
  25. $stmt = $pdo->prepare("SELECT authsource FROM `mailbox`
  26. INNER JOIN domain on mailbox.domain = domain.domain
  27. WHERE `kind` NOT REGEXP 'location|thing|group'
  28. AND `mailbox`.`active`='1'
  29. AND `domain`.`active`='1'
  30. AND `username` = :user");
  31. $stmt->execute(array(':user' => $user));
  32. $row = $stmt->fetch(PDO::FETCH_ASSOC);
  33. if (!$row){
  34. // mbox does not exist, call keycloak login and create mbox if possible
  35. $identity_provider_settings = identity_provider('get');
  36. if ($identity_provider_settings['login_flow'] == 'ropc'){
  37. $result = keycloak_mbox_login_ropc($user, $pass, $identity_provider_settings, $is_internal, true);
  38. } else {
  39. $result = keycloak_mbox_login_rest($user, $pass, $identity_provider_settings, $is_internal, true);
  40. }
  41. if ($result){
  42. return $result;
  43. }
  44. } else if ($row['authsource'] == 'keycloak'){
  45. if ($app_passwd_data){
  46. // first check if password is app_password
  47. $result = mailcow_mbox_apppass_login($user, $pass, $app_passwd_data, $is_internal);
  48. if ($result){
  49. return $result;
  50. }
  51. }
  52. $identity_provider_settings = identity_provider('get');
  53. if ($identity_provider_settings['login_flow'] == 'ropc'){
  54. $result = keycloak_mbox_login_ropc($user, $pass, $identity_provider_settings, $is_internal);
  55. } else {
  56. $result = keycloak_mbox_login_rest($user, $pass, $identity_provider_settings, $is_internal);
  57. }
  58. if ($result){
  59. return $result;
  60. }
  61. } else {
  62. if ($app_passwd_data){
  63. // first check if password is app_password
  64. $result = mailcow_mbox_apppass_login($user, $pass, $app_passwd_data, $is_internal);
  65. if ($result){
  66. return $result;
  67. }
  68. }
  69. $result = mailcow_mbox_login($user, $pass, $app_passwd_data, $is_internal);
  70. if ($result){
  71. return $result;
  72. }
  73. }
  74. // skip log and only return false if it's an internal request
  75. if ($is_internal){
  76. return false;
  77. }
  78. if (!isset($_SESSION['ldelay'])) {
  79. $_SESSION['ldelay'] = "0";
  80. $redis->publish("F2B_CHANNEL", "mailcow UI: Invalid password for " . $user . " by " . $_SERVER['REMOTE_ADDR']);
  81. error_log("mailcow UI: Invalid password for " . $user . " by " . $_SERVER['REMOTE_ADDR']);
  82. }
  83. elseif (!isset($_SESSION['mailcow_cc_username'])) {
  84. $_SESSION['ldelay'] = $_SESSION['ldelay']+0.5;
  85. $redis->publish("F2B_CHANNEL", "mailcow UI: Invalid password for " . $user . " by " . $_SERVER['REMOTE_ADDR']);
  86. error_log("mailcow UI: Invalid password for " . $user . " by " . $_SERVER['REMOTE_ADDR']);
  87. }
  88. $_SESSION['return'][] = array(
  89. 'type' => 'danger',
  90. 'log' => array(__FUNCTION__, $user, '*'),
  91. 'msg' => 'login_failed'
  92. );
  93. sleep($_SESSION['ldelay']);
  94. return false;
  95. }
  96. function mailcow_mbox_login($user, $pass, $app_passwd_data = false, $is_internal = false){
  97. global $pdo;
  98. $stmt = $pdo->prepare("SELECT * FROM `mailbox`
  99. INNER JOIN domain on mailbox.domain = domain.domain
  100. WHERE `kind` NOT REGEXP 'location|thing|group'
  101. AND `mailbox`.`active`='1'
  102. AND `domain`.`active`='1'
  103. AND (`mailbox`.`authsource`='mailcow' OR `mailbox`.`authsource` IS NULL)
  104. AND `username` = :user");
  105. $stmt->execute(array(':user' => $user));
  106. $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
  107. foreach ($rows as $row) {
  108. // verify password
  109. if (verify_hash($row['password'], $pass) !== false) {
  110. if (!array_key_exists("app_passwd_id", $row)){
  111. // password is not a app password
  112. // check for tfa authenticators
  113. $authenticators = get_tfa($user);
  114. if (isset($authenticators['additional']) && is_array($authenticators['additional']) && count($authenticators['additional']) > 0 && !$is_internal) {
  115. // authenticators found, init TFA flow
  116. $_SESSION['pending_mailcow_cc_username'] = $user;
  117. $_SESSION['pending_mailcow_cc_role'] = "user";
  118. $_SESSION['pending_tfa_methods'] = $authenticators['additional'];
  119. unset($_SESSION['ldelay']);
  120. $_SESSION['return'][] = array(
  121. 'type' => 'success',
  122. 'log' => array(__FUNCTION__, $user, '*'),
  123. 'msg' => array('logged_in_as', $user)
  124. );
  125. return "pending";
  126. } else if (!isset($authenticators['additional']) || !is_array($authenticators['additional']) || count($authenticators['additional']) == 0) {
  127. // no authenticators found, login successfull
  128. if (!$is_internal){
  129. unset($_SESSION['ldelay']);
  130. // Reactivate TFA if it was set to "deactivate TFA for next login"
  131. $stmt = $pdo->prepare("UPDATE `tfa` SET `active`='1' WHERE `username` = :user");
  132. $stmt->execute(array(':user' => $user));
  133. $_SESSION['return'][] = array(
  134. 'type' => 'success',
  135. 'log' => array(__FUNCTION__, $user, '*'),
  136. 'msg' => array('logged_in_as', $user)
  137. );
  138. }
  139. return "user";
  140. }
  141. }
  142. }
  143. }
  144. return false;
  145. }
  146. function mailcow_domainadmin_login($user, $pass){
  147. global $pdo;
  148. $stmt = $pdo->prepare("SELECT `password` FROM `admin`
  149. WHERE `superadmin` = '0'
  150. AND `active`='1'
  151. AND `username` = :user");
  152. $stmt->execute(array(':user' => $user));
  153. $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
  154. foreach ($rows as $row) {
  155. // verify password
  156. if (verify_hash($row['password'], $pass) !== false) {
  157. // check for tfa authenticators
  158. $authenticators = get_tfa($user);
  159. if (isset($authenticators['additional']) && is_array($authenticators['additional']) && count($authenticators['additional']) > 0) {
  160. $_SESSION['pending_mailcow_cc_username'] = $user;
  161. $_SESSION['pending_mailcow_cc_role'] = "domainadmin";
  162. $_SESSION['pending_tfa_methods'] = $authenticators['additional'];
  163. unset($_SESSION['ldelay']);
  164. $_SESSION['return'][] = array(
  165. 'type' => 'info',
  166. 'log' => array(__FUNCTION__, $user, '*'),
  167. 'msg' => 'awaiting_tfa_confirmation'
  168. );
  169. return "pending";
  170. }
  171. else {
  172. unset($_SESSION['ldelay']);
  173. // Reactivate TFA if it was set to "deactivate TFA for next login"
  174. $stmt = $pdo->prepare("UPDATE `tfa` SET `active`='1' WHERE `username` = :user");
  175. $stmt->execute(array(':user' => $user));
  176. $_SESSION['return'][] = array(
  177. 'type' => 'success',
  178. 'log' => array(__FUNCTION__, $user, '*'),
  179. 'msg' => array('logged_in_as', $user)
  180. );
  181. return "domainadmin";
  182. }
  183. }
  184. }
  185. return false;
  186. }
  187. function mailcow_admin_login($user, $pass){
  188. global $pdo;
  189. $user = strtolower(trim($user));
  190. $stmt = $pdo->prepare("SELECT `password` FROM `admin`
  191. WHERE `superadmin` = '1'
  192. AND `active` = '1'
  193. AND `username` = :user");
  194. $stmt->execute(array(':user' => $user));
  195. $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
  196. foreach ($rows as $row) {
  197. // verify password
  198. if (verify_hash($row['password'], $pass)) {
  199. // check for tfa authenticators
  200. $authenticators = get_tfa($user);
  201. if (isset($authenticators['additional']) && is_array($authenticators['additional']) && count($authenticators['additional']) > 0) {
  202. // active tfa authenticators found, set pending user login
  203. $_SESSION['pending_mailcow_cc_username'] = $user;
  204. $_SESSION['pending_mailcow_cc_role'] = "admin";
  205. $_SESSION['pending_tfa_methods'] = $authenticators['additional'];
  206. unset($_SESSION['ldelay']);
  207. $_SESSION['return'][] = array(
  208. 'type' => 'info',
  209. 'log' => array(__FUNCTION__, $user, '*'),
  210. 'msg' => 'awaiting_tfa_confirmation'
  211. );
  212. return "pending";
  213. } else {
  214. unset($_SESSION['ldelay']);
  215. // Reactivate TFA if it was set to "deactivate TFA for next login"
  216. $stmt = $pdo->prepare("UPDATE `tfa` SET `active`='1' WHERE `username` = :user");
  217. $stmt->execute(array(':user' => $user));
  218. $_SESSION['return'][] = array(
  219. 'type' => 'success',
  220. 'log' => array(__FUNCTION__, $user, '*'),
  221. 'msg' => array('logged_in_as', $user)
  222. );
  223. return "admin";
  224. }
  225. }
  226. }
  227. return false;
  228. }
  229. function mailcow_mbox_apppass_login($user, $pass, $app_passwd_data, $is_internal = false){
  230. global $pdo;
  231. $protocol = false;
  232. if ($app_passwd_data['eas']){
  233. $protocol = 'eas';
  234. } else if ($app_passwd_data['dav']){
  235. $protocol = 'dav';
  236. } else if ($app_passwd_data['smtp']){
  237. $protocol = 'smtp';
  238. } else if ($app_passwd_data['imap']){
  239. $protocol = 'imap';
  240. } else if ($app_passwd_data['sieve']){
  241. $protocol = 'sieve';
  242. } else if ($app_passwd_data['pop3']){
  243. $protocol = 'pop3';
  244. }
  245. // fetch app password data
  246. $stmt = $pdo->prepare("SELECT `app_passwd`.`password` as `password`, `app_passwd`.`id` as `app_passwd_id` FROM `app_passwd`
  247. INNER JOIN `mailbox` ON `mailbox`.`username` = `app_passwd`.`mailbox`
  248. INNER JOIN `domain` ON `mailbox`.`domain` = `domain`.`domain`
  249. WHERE `mailbox`.`kind` NOT REGEXP 'location|thing|group'
  250. AND `mailbox`.`active` = '1'
  251. AND `domain`.`active` = '1'
  252. AND `app_passwd`.`active` = '1'
  253. AND `app_passwd`.`mailbox` = :user
  254. :has_access_query"
  255. );
  256. // check if app password has protocol access
  257. // skip if protocol is false and the call is not external
  258. $has_access_query = '';
  259. if (!$is_internal || ($is_internal && !empty($protocol))){
  260. $has_access_query = " AND `app_passwd`.`" . $protocol . "_access` = '1'";
  261. }
  262. // fetch password data
  263. $stmt->execute(array(
  264. ':user' => $user,
  265. ':has_access_query' => $has_access_query
  266. ));
  267. $rows = array_merge($rows, $stmt->fetchAll(PDO::FETCH_ASSOC));
  268. foreach ($rows as $row) {
  269. // verify password
  270. if (verify_hash($row['password'], $pass) !== false) {
  271. if ($is_internal){
  272. // skip sasl_log, dovecot does the job
  273. return "user";
  274. }
  275. $service = strtoupper($is_app_passwd);
  276. $stmt = $pdo->prepare("REPLACE INTO sasl_log (`service`, `app_password`, `username`, `real_rip`) VALUES (:service, :app_id, :username, :remote_addr)");
  277. $stmt->execute(array(
  278. ':service' => $service,
  279. ':app_id' => $row['app_passwd_id'],
  280. ':username' => $user,
  281. ':remote_addr' => ($_SERVER['HTTP_X_REAL_IP'] ?? $_SERVER['REMOTE_ADDR'])
  282. ));
  283. unset($_SESSION['ldelay']);
  284. return "user";
  285. }
  286. }
  287. return false;
  288. }
  289. // Keycloak REST Api Flow - auth user by mailcow_password attribute
  290. // This password will be used for direct UI, IMAP and SMTP Auth
  291. // To use direct user credentials, only Authorization Code Flow is valid
  292. function keycloak_mbox_login_rest($user, $pass, $iam_settings, $is_internal = false, $create = false){
  293. global $pdo;
  294. // get access_token for service account of mailcow client
  295. $url = "{$iam_settings['server_url']}/realms/{$iam_settings['realm']}/protocol/openid-connect/token";
  296. $req = http_build_query(array(
  297. 'grant_type' => 'client_credentials',
  298. 'client_id' => $iam_settings['client_id'],
  299. 'client_secret' => $iam_settings['client_secret']
  300. ));
  301. $curl = curl_init();
  302. curl_setopt($curl, CURLOPT_URL, $url);
  303. curl_setopt($curl, CURLOPT_POST, 1);
  304. curl_setopt($curl, CURLOPT_POSTFIELDS, $req);
  305. curl_setopt($curl, CURLOPT_HTTPHEADER, array('Content-Type: application/x-www-form-urlencoded'));
  306. curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
  307. $mcclient_res = json_decode(curl_exec($curl), true);
  308. $code = curl_getinfo($curl, CURLINFO_HTTP_CODE);
  309. curl_close ($curl);
  310. if ($code != 200) {
  311. return false;
  312. }
  313. // get the mailcow_password attribute from keycloak user
  314. $url = "{$iam_settings['server_url']}/admin/realms/{$iam_settings['realm']}/users";
  315. $queryParams = array('email' => $user, 'exact' => true);
  316. $queryString = http_build_query($queryParams);
  317. $curl = curl_init();
  318. curl_setopt($curl, CURLOPT_URL, $url . '?' . $queryString);
  319. curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
  320. curl_setopt($curl, CURLOPT_HTTPHEADER, array(
  321. 'Authorization: Bearer ' . $mcclient_res['access_token'],
  322. 'Content-Type: application/json'
  323. ));
  324. $user_res = json_decode(curl_exec($curl), true)[0];
  325. $code = curl_getinfo($curl, CURLINFO_HTTP_CODE);
  326. curl_close($curl);
  327. if ($code != 200) {
  328. return false;
  329. }
  330. // validate mailcow_password
  331. $mailcow_password = $user_res['attributes']['mailcow_password'][0];
  332. if (!verify_hash($mailcow_password, $pass)) {
  333. return false;
  334. }
  335. // get mapped template, if not set return false
  336. // also return false if no mappers were defined
  337. $user_template = $user_data['attributes']['mailcow_template'][0];
  338. if ($create && (empty($iam_settings['mappers']) || $user_template)){
  339. return false;
  340. } else if (!$create) {
  341. // login success - dont create mailbox
  342. $_SESSION['return'][] = array(
  343. 'type' => 'success',
  344. 'log' => array(__FUNCTION__, $user, '*'),
  345. 'msg' => array('logged_in_as', $user)
  346. );
  347. return 'user';
  348. }
  349. // try to create mbox on successfull login
  350. $mbox_template = null;
  351. // check if matching attribute mapping exists
  352. foreach ($iam_settings['mappers'] as $index => $mapper){
  353. if (in_array($mapper, $iam_settings['mappers'])) {
  354. $mbox_template = $iam_settings['templates'][$index];
  355. break;
  356. }
  357. }
  358. if (!$mbox_template){
  359. // no matching template found
  360. return false;
  361. }
  362. $stmt = $pdo->prepare("SELECT * FROM `templates`
  363. WHERE `template` = :template AND type = 'mailbox'");
  364. $stmt->execute(array(
  365. ":template" => $mbox_template
  366. ));
  367. $mbox_template_data = $stmt->fetch(PDO::FETCH_ASSOC);
  368. if (empty($mbox_template_data)){
  369. return false;
  370. }
  371. $mbox_template_data = json_decode($mbox_template_data["attributes"], true);
  372. $mbox_template_data['domain'] = explode('@', $user)[1];
  373. $mbox_template_data['local_part'] = explode('@', $user)[0];
  374. $mbox_template_data['authsource'] = 'keycloak';
  375. $_SESSION['iam_create_login'] = true;
  376. $create_res = mailbox('add', 'mailbox', $mbox_template_data);
  377. $_SESSION['iam_create_login'] = false;
  378. if (!$create_res){
  379. return false;
  380. }
  381. $_SESSION['return'][] = array(
  382. 'type' => 'success',
  383. 'log' => array(__FUNCTION__, $user, '*'),
  384. 'msg' => array('logged_in_as', $user)
  385. );
  386. return 'user';
  387. }