functions.auth.inc.php 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495
  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. // ROPC Flow (deprecated oAuth2.1)
  290. // uses direct user credentials for UI, IMAP and SMTP Auth
  291. function keycloak_mbox_login_ropc($user, $pass, $iam_settings, $is_internal = false, $create = false){
  292. global $pdo;
  293. $url = "{$iam_settings['server_url']}/realms/{$iam_settings['realm']}/protocol/openid-connect/token";
  294. $req = http_build_query(array(
  295. 'grant_type' => 'password',
  296. 'client_id' => $iam_settings['client_id'],
  297. 'client_secret' => $iam_settings['client_secret'],
  298. 'username' => $user,
  299. 'password' => $pass,
  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. $res = json_decode(curl_exec($curl), true);
  308. $code = curl_getinfo($curl, CURLINFO_HTTP_CODE);
  309. curl_close ($curl);
  310. if ($code == 200) {
  311. // decode jwt
  312. $user_data = json_decode(base64_decode(str_replace('_', '/', str_replace('-','+',explode('.', $res['access_token'])[1]))), true);
  313. if ($user != $user_data['email']){
  314. // check if $user is email address, only accept email address as username
  315. return false;
  316. }
  317. if ($create && !empty($iam_settings['mappers'])){
  318. // try to create mbox on successfull login
  319. $mbox_template = null;
  320. // check if matching attribute mapping exists
  321. foreach ($iam_settings['mappers'] as $index => $mapper){
  322. if (in_array($mapper, $iam_settings['mappers'])) {
  323. $mbox_template = $iam_settings['templates'][$index];
  324. break;
  325. }
  326. }
  327. if (!$mbox_template){
  328. // no matching template found
  329. return false;
  330. }
  331. $stmt = $pdo->prepare("SELECT * FROM `templates`
  332. WHERE `template` = :template AND type = 'mailbox'");
  333. $stmt->execute(array(
  334. ":template" => $mbox_template
  335. ));
  336. $mbox_template_data = $stmt->fetch(PDO::FETCH_ASSOC);
  337. if (!empty($mbox_template_data)){
  338. $mbox_template_data = json_decode($mbox_template_data["attributes"], true);
  339. $mbox_template_data['domain'] = explode('@', $user)[1];
  340. $mbox_template_data['local_part'] = explode('@', $user)[0];
  341. $mbox_template_data['authsource'] = 'keycloak';
  342. $_SESSION['iam_create_login'] = true;
  343. $create_res = mailbox('add', 'mailbox', $mbox_template_data);
  344. $_SESSION['iam_create_login'] = false;
  345. if (!$create_res){
  346. return false;
  347. }
  348. }
  349. }
  350. $_SESSION['return'][] = array(
  351. 'type' => 'success',
  352. 'log' => array(__FUNCTION__, $user, '*'),
  353. 'msg' => array('logged_in_as', $user)
  354. );
  355. return 'user';
  356. } else {
  357. return false;
  358. }
  359. }
  360. // Keycloak REST Api Flow - auth user by mailcow_password attribute
  361. // This password will be used for direct UI, IMAP and SMTP Auth
  362. // To use direct user credentials, only Authorization Code Flow is valid
  363. function keycloak_mbox_login_rest($user, $pass, $iam_settings, $is_internal = false, $create = false){
  364. global $pdo;
  365. // get access_token for service account of mailcow client
  366. $url = "{$iam_settings['server_url']}/realms/{$iam_settings['realm']}/protocol/openid-connect/token";
  367. $req = http_build_query(array(
  368. 'grant_type' => 'client_credentials',
  369. 'client_id' => $iam_settings['client_id'],
  370. 'client_secret' => $iam_settings['client_secret']
  371. ));
  372. $curl = curl_init();
  373. curl_setopt($curl, CURLOPT_URL, $url);
  374. curl_setopt($curl, CURLOPT_POST, 1);
  375. curl_setopt($curl, CURLOPT_POSTFIELDS, $req);
  376. curl_setopt($curl, CURLOPT_HTTPHEADER, array('Content-Type: application/x-www-form-urlencoded'));
  377. curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
  378. $mcclient_res = json_decode(curl_exec($curl), true);
  379. $code = curl_getinfo($curl, CURLINFO_HTTP_CODE);
  380. curl_close ($curl);
  381. if ($code != 200) {
  382. return false;
  383. }
  384. // get the mailcow_password attribute from keycloak user
  385. $url = "{$iam_settings['server_url']}/admin/realms/{$iam_settings['realm']}/users";
  386. $queryParams = array('email' => $user, 'exact' => true);
  387. $queryString = http_build_query($queryParams);
  388. $curl = curl_init();
  389. curl_setopt($curl, CURLOPT_URL, $url . '?' . $queryString);
  390. curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
  391. curl_setopt($curl, CURLOPT_HTTPHEADER, array(
  392. 'Authorization: Bearer ' . $mcclient_res['access_token'],
  393. 'Content-Type: application/json'
  394. ));
  395. $user_res = json_decode(curl_exec($curl), true)[0];
  396. $code = curl_getinfo($curl, CURLINFO_HTTP_CODE);
  397. curl_close($curl);
  398. if ($code != 200) {
  399. return false;
  400. }
  401. // validate mailcow_password
  402. $mailcow_password = $user_res['attributes']['mailcow_password'][0];
  403. if (!verify_hash($mailcow_password, $pass)) {
  404. return false;
  405. }
  406. // get mapped template, if not set return false
  407. // also return false if no mappers were defined
  408. $user_template = $user_data['attributes']['mailcow_template'][0];
  409. if ($create && (empty($iam_settings['mappers']) || $user_template)){
  410. return false;
  411. } else if (!$create) {
  412. // login success - dont create mailbox
  413. $_SESSION['return'][] = array(
  414. 'type' => 'success',
  415. 'log' => array(__FUNCTION__, $user, '*'),
  416. 'msg' => array('logged_in_as', $user)
  417. );
  418. return 'user';
  419. }
  420. // try to create mbox on successfull login
  421. $mbox_template = null;
  422. // check if matching attribute mapping exists
  423. foreach ($iam_settings['mappers'] as $index => $mapper){
  424. if (in_array($mapper, $iam_settings['mappers'])) {
  425. $mbox_template = $iam_settings['templates'][$index];
  426. break;
  427. }
  428. }
  429. if (!$mbox_template){
  430. // no matching template found
  431. return false;
  432. }
  433. $stmt = $pdo->prepare("SELECT * FROM `templates`
  434. WHERE `template` = :template AND type = 'mailbox'");
  435. $stmt->execute(array(
  436. ":template" => $mbox_template
  437. ));
  438. $mbox_template_data = $stmt->fetch(PDO::FETCH_ASSOC);
  439. if (empty($mbox_template_data)){
  440. return false;
  441. }
  442. $mbox_template_data = json_decode($mbox_template_data["attributes"], true);
  443. $mbox_template_data['domain'] = explode('@', $user)[1];
  444. $mbox_template_data['local_part'] = explode('@', $user)[0];
  445. $mbox_template_data['authsource'] = 'keycloak';
  446. $_SESSION['iam_create_login'] = true;
  447. $create_res = mailbox('add', 'mailbox', $mbox_template_data);
  448. $_SESSION['iam_create_login'] = false;
  449. if (!$create_res){
  450. return false;
  451. }
  452. $_SESSION['return'][] = array(
  453. 'type' => 'success',
  454. 'log' => array(__FUNCTION__, $user, '*'),
  455. 'msg' => array('logged_in_as', $user)
  456. );
  457. return 'user';
  458. }