You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

1402 lines
41 KiB

  1. <?php
  2. /**
  3. * Authentication library
  4. *
  5. * Including this file will automatically try to login
  6. * a user by calling auth_login()
  7. *
  8. * @license GPL 2 (http://www.gnu.org/licenses/gpl.html)
  9. * @author Andreas Gohr <andi@splitbrain.org>
  10. */
  11. use dokuwiki\ErrorHandler;
  12. use dokuwiki\JWT;
  13. use dokuwiki\Utf8\PhpString;
  14. use dokuwiki\Extension\AuthPlugin;
  15. use dokuwiki\Extension\Event;
  16. use dokuwiki\Extension\PluginController;
  17. use dokuwiki\PassHash;
  18. use dokuwiki\Subscriptions\RegistrationSubscriptionSender;
  19. use phpseclib3\Crypt\AES;
  20. use phpseclib3\Crypt\Common\SymmetricKey;
  21. use phpseclib3\Exception\BadDecryptionException;
  22. /**
  23. * Initialize the auth system.
  24. *
  25. * This function is automatically called at the end of init.php
  26. *
  27. * This used to be the main() of the auth.php
  28. *
  29. * @todo backend loading maybe should be handled by the class autoloader
  30. * @todo maybe split into multiple functions at the XXX marked positions
  31. * @triggers AUTH_LOGIN_CHECK
  32. * @return bool
  33. */
  34. function auth_setup()
  35. {
  36. global $conf;
  37. /* @var AuthPlugin $auth */
  38. global $auth;
  39. /* @var Input $INPUT */
  40. global $INPUT;
  41. global $AUTH_ACL;
  42. global $lang;
  43. /* @var PluginController $plugin_controller */
  44. global $plugin_controller;
  45. $AUTH_ACL = [];
  46. if (!$conf['useacl']) return false;
  47. // try to load auth backend from plugins
  48. foreach ($plugin_controller->getList('auth') as $plugin) {
  49. if ($conf['authtype'] === $plugin) {
  50. $auth = $plugin_controller->load('auth', $plugin);
  51. break;
  52. }
  53. }
  54. if (!$auth instanceof AuthPlugin) {
  55. msg($lang['authtempfail'], -1);
  56. return false;
  57. }
  58. if ($auth->success == false) {
  59. // degrade to unauthenticated user
  60. $auth = null;
  61. auth_logoff();
  62. msg($lang['authtempfail'], -1);
  63. return false;
  64. }
  65. // do the login either by cookie or provided credentials XXX
  66. $INPUT->set('http_credentials', false);
  67. if (!$conf['rememberme']) $INPUT->set('r', false);
  68. // Populate Basic Auth user/password from Authorization header
  69. // Note: with FastCGI, data is in REDIRECT_HTTP_AUTHORIZATION instead of HTTP_AUTHORIZATION
  70. $header = $INPUT->server->str('HTTP_AUTHORIZATION') ?: $INPUT->server->str('REDIRECT_HTTP_AUTHORIZATION');
  71. if (preg_match('~^Basic ([a-z\d/+]*={0,2})$~i', $header, $matches)) {
  72. $userpass = explode(':', base64_decode($matches[1]));
  73. [$_SERVER['PHP_AUTH_USER'], $_SERVER['PHP_AUTH_PW']] = $userpass;
  74. }
  75. // if no credentials were given try to use HTTP auth (for SSO)
  76. if (!$INPUT->str('u') && empty($_COOKIE[DOKU_COOKIE]) && !empty($INPUT->server->str('PHP_AUTH_USER'))) {
  77. $INPUT->set('u', $INPUT->server->str('PHP_AUTH_USER'));
  78. $INPUT->set('p', $INPUT->server->str('PHP_AUTH_PW'));
  79. $INPUT->set('http_credentials', true);
  80. }
  81. // apply cleaning (auth specific user names, remove control chars)
  82. if (true === $auth->success) {
  83. $INPUT->set('u', $auth->cleanUser(stripctl($INPUT->str('u'))));
  84. $INPUT->set('p', stripctl($INPUT->str('p')));
  85. }
  86. if (!auth_tokenlogin()) {
  87. $ok = null;
  88. if ($auth instanceof AuthPlugin && $auth->canDo('external')) {
  89. $ok = $auth->trustExternal($INPUT->str('u'), $INPUT->str('p'), $INPUT->bool('r'));
  90. }
  91. if ($ok === null) {
  92. // external trust mechanism not in place, or returns no result,
  93. // then attempt auth_login
  94. $evdata = [
  95. 'user' => $INPUT->str('u'),
  96. 'password' => $INPUT->str('p'),
  97. 'sticky' => $INPUT->bool('r'),
  98. 'silent' => $INPUT->bool('http_credentials')
  99. ];
  100. Event::createAndTrigger('AUTH_LOGIN_CHECK', $evdata, 'auth_login_wrapper');
  101. }
  102. }
  103. //load ACL into a global array XXX
  104. $AUTH_ACL = auth_loadACL();
  105. return true;
  106. }
  107. /**
  108. * Loads the ACL setup and handle user wildcards
  109. *
  110. * @author Andreas Gohr <andi@splitbrain.org>
  111. *
  112. * @return array
  113. */
  114. function auth_loadACL()
  115. {
  116. global $config_cascade;
  117. global $USERINFO;
  118. /* @var Input $INPUT */
  119. global $INPUT;
  120. if (!is_readable($config_cascade['acl']['default'])) return [];
  121. $acl = file($config_cascade['acl']['default']);
  122. $out = [];
  123. foreach ($acl as $line) {
  124. $line = trim($line);
  125. if (empty($line) || ($line[0] == '#')) continue; // skip blank lines & comments
  126. [$id, $rest] = preg_split('/[ \t]+/', $line, 2);
  127. // substitute user wildcard first (its 1:1)
  128. if (strstr($line, '%USER%')) {
  129. // if user is not logged in, this ACL line is meaningless - skip it
  130. if (!$INPUT->server->has('REMOTE_USER')) continue;
  131. $id = str_replace('%USER%', cleanID($INPUT->server->str('REMOTE_USER')), $id);
  132. $rest = str_replace('%USER%', auth_nameencode($INPUT->server->str('REMOTE_USER')), $rest);
  133. }
  134. // substitute group wildcard (its 1:m)
  135. if (strstr($line, '%GROUP%')) {
  136. // if user is not logged in, grps is empty, no output will be added (i.e. skipped)
  137. if (isset($USERINFO['grps'])) {
  138. foreach ((array) $USERINFO['grps'] as $grp) {
  139. $nid = str_replace('%GROUP%', cleanID($grp), $id);
  140. $nrest = str_replace('%GROUP%', '@' . auth_nameencode($grp), $rest);
  141. $out[] = "$nid\t$nrest";
  142. }
  143. }
  144. } else {
  145. $out[] = "$id\t$rest";
  146. }
  147. }
  148. return $out;
  149. }
  150. /**
  151. * Try a token login
  152. *
  153. * @return bool true if token login succeeded
  154. */
  155. function auth_tokenlogin()
  156. {
  157. global $USERINFO;
  158. global $INPUT;
  159. /** @var DokuWiki_Auth_Plugin $auth */
  160. global $auth;
  161. if (!$auth) return false;
  162. // see if header has token
  163. $header = '';
  164. if (function_exists('getallheaders')) {
  165. // Authorization headers are not in $_SERVER for mod_php
  166. $headers = array_change_key_case(getallheaders());
  167. if (isset($headers['authorization'])) $header = $headers['authorization'];
  168. } else {
  169. $header = $INPUT->server->str('HTTP_AUTHORIZATION');
  170. }
  171. if (!$header) return false;
  172. [$type, $token] = sexplode(' ', $header, 2);
  173. if ($type !== 'Bearer') return false;
  174. // check token
  175. try {
  176. $authtoken = JWT::validate($token);
  177. } catch (Exception $e) {
  178. msg(hsc($e->getMessage()), -1);
  179. return false;
  180. }
  181. // fetch user info from backend
  182. $user = $authtoken->getUser();
  183. $USERINFO = $auth->getUserData($user);
  184. if (!$USERINFO) return false;
  185. // the code is correct, set up user
  186. $INPUT->server->set('REMOTE_USER', $user);
  187. $_SESSION[DOKU_COOKIE]['auth']['user'] = $user;
  188. $_SESSION[DOKU_COOKIE]['auth']['pass'] = 'nope';
  189. $_SESSION[DOKU_COOKIE]['auth']['info'] = $USERINFO;
  190. return true;
  191. }
  192. /**
  193. * Event hook callback for AUTH_LOGIN_CHECK
  194. *
  195. * @param array $evdata
  196. * @return bool
  197. * @throws Exception
  198. */
  199. function auth_login_wrapper($evdata)
  200. {
  201. return auth_login(
  202. $evdata['user'],
  203. $evdata['password'],
  204. $evdata['sticky'],
  205. $evdata['silent']
  206. );
  207. }
  208. /**
  209. * This tries to login the user based on the sent auth credentials
  210. *
  211. * The authentication works like this: if a username was given
  212. * a new login is assumed and user/password are checked. If they
  213. * are correct the password is encrypted with blowfish and stored
  214. * together with the username in a cookie - the same info is stored
  215. * in the session, too. Additonally a browserID is stored in the
  216. * session.
  217. *
  218. * If no username was given the cookie is checked: if the username,
  219. * crypted password and browserID match between session and cookie
  220. * no further testing is done and the user is accepted
  221. *
  222. * If a cookie was found but no session info was availabe the
  223. * blowfish encrypted password from the cookie is decrypted and
  224. * together with username rechecked by calling this function again.
  225. *
  226. * On a successful login $_SERVER[REMOTE_USER] and $USERINFO
  227. * are set.
  228. *
  229. * @param string $user Username
  230. * @param string $pass Cleartext Password
  231. * @param bool $sticky Cookie should not expire
  232. * @param bool $silent Don't show error on bad auth
  233. * @return bool true on successful auth
  234. * @throws Exception
  235. *
  236. * @author Andreas Gohr <andi@splitbrain.org>
  237. */
  238. function auth_login($user, $pass, $sticky = false, $silent = false)
  239. {
  240. global $USERINFO;
  241. global $conf;
  242. global $lang;
  243. /* @var AuthPlugin $auth */
  244. global $auth;
  245. /* @var Input $INPUT */
  246. global $INPUT;
  247. if (!$auth instanceof AuthPlugin) return false;
  248. if (!empty($user)) {
  249. //usual login
  250. if (!empty($pass) && $auth->checkPass($user, $pass)) {
  251. // make logininfo globally available
  252. $INPUT->server->set('REMOTE_USER', $user);
  253. $secret = auth_cookiesalt(!$sticky, true); //bind non-sticky to session
  254. auth_setCookie($user, auth_encrypt($pass, $secret), $sticky);
  255. return true;
  256. } else {
  257. //invalid credentials - log off
  258. if (!$silent) {
  259. http_status(403, 'Login failed');
  260. msg($lang['badlogin'], -1);
  261. }
  262. auth_logoff();
  263. return false;
  264. }
  265. } else {
  266. // read cookie information
  267. [$user, $sticky, $pass] = auth_getCookie();
  268. if ($user && $pass) {
  269. // we got a cookie - see if we can trust it
  270. // get session info
  271. if (isset($_SESSION[DOKU_COOKIE])) {
  272. $session = $_SESSION[DOKU_COOKIE]['auth'];
  273. if (
  274. isset($session) &&
  275. $auth->useSessionCache($user) &&
  276. ($session['time'] >= time() - $conf['auth_security_timeout']) &&
  277. ($session['user'] == $user) &&
  278. ($session['pass'] == sha1($pass)) && //still crypted
  279. ($session['buid'] == auth_browseruid())
  280. ) {
  281. // he has session, cookie and browser right - let him in
  282. $INPUT->server->set('REMOTE_USER', $user);
  283. $USERINFO = $session['info']; //FIXME move all references to session
  284. return true;
  285. }
  286. }
  287. // no we don't trust it yet - recheck pass but silent
  288. $secret = auth_cookiesalt(!$sticky, true); //bind non-sticky to session
  289. $pass = auth_decrypt($pass, $secret);
  290. return auth_login($user, $pass, $sticky, true);
  291. }
  292. }
  293. //just to be sure
  294. auth_logoff(true);
  295. return false;
  296. }
  297. /**
  298. * Builds a pseudo UID from browser and IP data
  299. *
  300. * This is neither unique nor unfakable - still it adds some
  301. * security. Using the first part of the IP makes sure
  302. * proxy farms like AOLs are still okay.
  303. *
  304. * @author Andreas Gohr <andi@splitbrain.org>
  305. *
  306. * @return string a SHA256 sum of various browser headers
  307. */
  308. function auth_browseruid()
  309. {
  310. /* @var Input $INPUT */
  311. global $INPUT;
  312. $ip = clientIP(true);
  313. // convert IP string to packed binary representation
  314. $pip = inet_pton($ip);
  315. $uid = implode("\n", [
  316. $INPUT->server->str('HTTP_USER_AGENT'),
  317. $INPUT->server->str('HTTP_ACCEPT_LANGUAGE'),
  318. substr($pip, 0, strlen($pip) / 2), // use half of the IP address (works for both IPv4 and IPv6)
  319. ]);
  320. return hash('sha256', $uid);
  321. }
  322. /**
  323. * Creates a random key to encrypt the password in cookies
  324. *
  325. * This function tries to read the password for encrypting
  326. * cookies from $conf['metadir'].'/_htcookiesalt'
  327. * if no such file is found a random key is created and
  328. * and stored in this file.
  329. *
  330. * @param bool $addsession if true, the sessionid is added to the salt
  331. * @param bool $secure if security is more important than keeping the old value
  332. * @return string
  333. * @throws Exception
  334. *
  335. * @author Andreas Gohr <andi@splitbrain.org>
  336. */
  337. function auth_cookiesalt($addsession = false, $secure = false)
  338. {
  339. if (defined('SIMPLE_TEST')) {
  340. return 'test';
  341. }
  342. global $conf;
  343. $file = $conf['metadir'] . '/_htcookiesalt';
  344. if ($secure || !file_exists($file)) {
  345. $file = $conf['metadir'] . '/_htcookiesalt2';
  346. }
  347. $salt = io_readFile($file);
  348. if (empty($salt)) {
  349. $salt = bin2hex(auth_randombytes(64));
  350. io_saveFile($file, $salt);
  351. }
  352. if ($addsession) {
  353. $salt .= session_id();
  354. }
  355. return $salt;
  356. }
  357. /**
  358. * Return cryptographically secure random bytes.
  359. *
  360. * @param int $length number of bytes
  361. * @return string cryptographically secure random bytes
  362. * @throws Exception
  363. *
  364. * @author Niklas Keller <me@kelunik.com>
  365. */
  366. function auth_randombytes($length)
  367. {
  368. return random_bytes($length);
  369. }
  370. /**
  371. * Cryptographically secure random number generator.
  372. *
  373. * @param int $min
  374. * @param int $max
  375. * @return int
  376. * @throws Exception
  377. *
  378. * @author Niklas Keller <me@kelunik.com>
  379. */
  380. function auth_random($min, $max)
  381. {
  382. return random_int($min, $max);
  383. }
  384. /**
  385. * Encrypt data using the given secret using AES
  386. *
  387. * The mode is CBC with a random initialization vector, the key is derived
  388. * using pbkdf2.
  389. *
  390. * @param string $data The data that shall be encrypted
  391. * @param string $secret The secret/password that shall be used
  392. * @return string The ciphertext
  393. * @throws Exception
  394. */
  395. function auth_encrypt($data, $secret)
  396. {
  397. $iv = auth_randombytes(16);
  398. $cipher = new AES('cbc');
  399. $cipher->setPassword($secret, 'pbkdf2', 'sha1', 'phpseclib');
  400. $cipher->setIV($iv);
  401. /*
  402. this uses the encrypted IV as IV as suggested in
  403. http://csrc.nist.gov/publications/nistpubs/800-38a/sp800-38a.pdf, Appendix C
  404. for unique but necessarily random IVs. The resulting ciphertext is
  405. compatible to ciphertext that was created using a "normal" IV.
  406. */
  407. return $cipher->encrypt($iv . $data);
  408. }
  409. /**
  410. * Decrypt the given AES ciphertext
  411. *
  412. * The mode is CBC, the key is derived using pbkdf2
  413. *
  414. * @param string $ciphertext The encrypted data
  415. * @param string $secret The secret/password that shall be used
  416. * @return string|null The decrypted data
  417. */
  418. function auth_decrypt($ciphertext, $secret)
  419. {
  420. $iv = substr($ciphertext, 0, 16);
  421. $cipher = new AES('cbc');
  422. $cipher->setPassword($secret, 'pbkdf2', 'sha1', 'phpseclib');
  423. $cipher->setIV($iv);
  424. try {
  425. return $cipher->decrypt(substr($ciphertext, 16));
  426. } catch (BadDecryptionException $e) {
  427. ErrorHandler::logException($e);
  428. return null;
  429. }
  430. }
  431. /**
  432. * Log out the current user
  433. *
  434. * This clears all authentication data and thus log the user
  435. * off. It also clears session data.
  436. *
  437. * @author Andreas Gohr <andi@splitbrain.org>
  438. *
  439. * @param bool $keepbc - when true, the breadcrumb data is not cleared
  440. */
  441. function auth_logoff($keepbc = false)
  442. {
  443. global $conf;
  444. global $USERINFO;
  445. /* @var AuthPlugin $auth */
  446. global $auth;
  447. /* @var Input $INPUT */
  448. global $INPUT;
  449. // make sure the session is writable (it usually is)
  450. @session_start();
  451. if (isset($_SESSION[DOKU_COOKIE]['auth']['user']))
  452. unset($_SESSION[DOKU_COOKIE]['auth']['user']);
  453. if (isset($_SESSION[DOKU_COOKIE]['auth']['pass']))
  454. unset($_SESSION[DOKU_COOKIE]['auth']['pass']);
  455. if (isset($_SESSION[DOKU_COOKIE]['auth']['info']))
  456. unset($_SESSION[DOKU_COOKIE]['auth']['info']);
  457. if (!$keepbc && isset($_SESSION[DOKU_COOKIE]['bc']))
  458. unset($_SESSION[DOKU_COOKIE]['bc']);
  459. $INPUT->server->remove('REMOTE_USER');
  460. $USERINFO = null; //FIXME
  461. $cookieDir = empty($conf['cookiedir']) ? DOKU_REL : $conf['cookiedir'];
  462. setcookie(DOKU_COOKIE, '', [
  463. 'expires' => time() - 600000,
  464. 'path' => $cookieDir,
  465. 'secure' => ($conf['securecookie'] && is_ssl()),
  466. 'httponly' => true,
  467. 'samesite' => $conf['samesitecookie'] ?: null, // null means browser default
  468. ]);
  469. if ($auth instanceof AuthPlugin) {
  470. $auth->logOff();
  471. }
  472. }
  473. /**
  474. * Check if a user is a manager
  475. *
  476. * Should usually be called without any parameters to check the current
  477. * user.
  478. *
  479. * The info is available through $INFO['ismanager'], too
  480. *
  481. * @param string $user Username
  482. * @param array $groups List of groups the user is in
  483. * @param bool $adminonly when true checks if user is admin
  484. * @param bool $recache set to true to refresh the cache
  485. * @return bool
  486. * @see auth_isadmin
  487. *
  488. * @author Andreas Gohr <andi@splitbrain.org>
  489. */
  490. function auth_ismanager($user = null, $groups = null, $adminonly = false, $recache = false)
  491. {
  492. global $conf;
  493. global $USERINFO;
  494. /* @var AuthPlugin $auth */
  495. global $auth;
  496. /* @var Input $INPUT */
  497. global $INPUT;
  498. if (!$auth instanceof AuthPlugin) return false;
  499. if (is_null($user)) {
  500. if (!$INPUT->server->has('REMOTE_USER')) {
  501. return false;
  502. } else {
  503. $user = $INPUT->server->str('REMOTE_USER');
  504. }
  505. }
  506. if (is_null($groups)) {
  507. // checking the logged in user, or another one?
  508. if ($USERINFO && $user === $INPUT->server->str('REMOTE_USER')) {
  509. $groups = (array) $USERINFO['grps'];
  510. } else {
  511. $groups = $auth->getUserData($user);
  512. $groups = $groups ? $groups['grps'] : [];
  513. }
  514. }
  515. // prefer cached result
  516. static $cache = [];
  517. $cachekey = serialize([$user, $adminonly, $groups]);
  518. if (!isset($cache[$cachekey]) || $recache) {
  519. // check superuser match
  520. $ok = auth_isMember($conf['superuser'], $user, $groups);
  521. // check managers
  522. if (!$ok && !$adminonly) {
  523. $ok = auth_isMember($conf['manager'], $user, $groups);
  524. }
  525. $cache[$cachekey] = $ok;
  526. }
  527. return $cache[$cachekey];
  528. }
  529. /**
  530. * Check if a user is admin
  531. *
  532. * Alias to auth_ismanager with adminonly=true
  533. *
  534. * The info is available through $INFO['isadmin'], too
  535. *
  536. * @param string $user Username
  537. * @param array $groups List of groups the user is in
  538. * @param bool $recache set to true to refresh the cache
  539. * @return bool
  540. * @author Andreas Gohr <andi@splitbrain.org>
  541. * @see auth_ismanager()
  542. *
  543. */
  544. function auth_isadmin($user = null, $groups = null, $recache = false)
  545. {
  546. return auth_ismanager($user, $groups, true, $recache);
  547. }
  548. /**
  549. * Match a user and his groups against a comma separated list of
  550. * users and groups to determine membership status
  551. *
  552. * Note: all input should NOT be nameencoded.
  553. *
  554. * @param string $memberlist commaseparated list of allowed users and groups
  555. * @param string $user user to match against
  556. * @param array $groups groups the user is member of
  557. * @return bool true for membership acknowledged
  558. */
  559. function auth_isMember($memberlist, $user, array $groups)
  560. {
  561. /* @var AuthPlugin $auth */
  562. global $auth;
  563. if (!$auth instanceof AuthPlugin) return false;
  564. // clean user and groups
  565. if (!$auth->isCaseSensitive()) {
  566. $user = PhpString::strtolower($user);
  567. $groups = array_map([PhpString::class, 'strtolower'], $groups);
  568. }
  569. $user = $auth->cleanUser($user);
  570. $groups = array_map([$auth, 'cleanGroup'], $groups);
  571. // extract the memberlist
  572. $members = explode(',', $memberlist);
  573. $members = array_map('trim', $members);
  574. $members = array_unique($members);
  575. $members = array_filter($members);
  576. // compare cleaned values
  577. foreach ($members as $member) {
  578. if ($member == '@ALL') return true;
  579. if (!$auth->isCaseSensitive()) $member = PhpString::strtolower($member);
  580. if ($member[0] == '@') {
  581. $member = $auth->cleanGroup(substr($member, 1));
  582. if (in_array($member, $groups)) return true;
  583. } else {
  584. $member = $auth->cleanUser($member);
  585. if ($member == $user) return true;
  586. }
  587. }
  588. // still here? not a member!
  589. return false;
  590. }
  591. /**
  592. * Convinience function for auth_aclcheck()
  593. *
  594. * This checks the permissions for the current user
  595. *
  596. * @author Andreas Gohr <andi@splitbrain.org>
  597. *
  598. * @param string $id page ID (needs to be resolved and cleaned)
  599. * @return int permission level
  600. */
  601. function auth_quickaclcheck($id)
  602. {
  603. global $conf;
  604. global $USERINFO;
  605. /* @var Input $INPUT */
  606. global $INPUT;
  607. # if no ACL is used always return upload rights
  608. if (!$conf['useacl']) return AUTH_UPLOAD;
  609. return auth_aclcheck($id, $INPUT->server->str('REMOTE_USER'), is_array($USERINFO) ? $USERINFO['grps'] : []);
  610. }
  611. /**
  612. * Returns the maximum rights a user has for the given ID or its namespace
  613. *
  614. * @author Andreas Gohr <andi@splitbrain.org>
  615. *
  616. * @triggers AUTH_ACL_CHECK
  617. * @param string $id page ID (needs to be resolved and cleaned)
  618. * @param string $user Username
  619. * @param array|null $groups Array of groups the user is in
  620. * @return int permission level
  621. */
  622. function auth_aclcheck($id, $user, $groups)
  623. {
  624. $data = [
  625. 'id' => $id ?? '',
  626. 'user' => $user,
  627. 'groups' => $groups
  628. ];
  629. return Event::createAndTrigger('AUTH_ACL_CHECK', $data, 'auth_aclcheck_cb');
  630. }
  631. /**
  632. * default ACL check method
  633. *
  634. * DO NOT CALL DIRECTLY, use auth_aclcheck() instead
  635. *
  636. * @author Andreas Gohr <andi@splitbrain.org>
  637. *
  638. * @param array $data event data
  639. * @return int permission level
  640. */
  641. function auth_aclcheck_cb($data)
  642. {
  643. $id =& $data['id'];
  644. $user =& $data['user'];
  645. $groups =& $data['groups'];
  646. global $conf;
  647. global $AUTH_ACL;
  648. /* @var AuthPlugin $auth */
  649. global $auth;
  650. // if no ACL is used always return upload rights
  651. if (!$conf['useacl']) return AUTH_UPLOAD;
  652. if (!$auth instanceof AuthPlugin) return AUTH_NONE;
  653. if (!is_array($AUTH_ACL)) return AUTH_NONE;
  654. //make sure groups is an array
  655. if (!is_array($groups)) $groups = [];
  656. //if user is superuser or in superusergroup return 255 (acl_admin)
  657. if (auth_isadmin($user, $groups)) {
  658. return AUTH_ADMIN;
  659. }
  660. if (!$auth->isCaseSensitive()) {
  661. $user = PhpString::strtolower($user);
  662. $groups = array_map([PhpString::class, 'strtolower'], $groups);
  663. }
  664. $user = auth_nameencode($auth->cleanUser($user));
  665. $groups = array_map([$auth, 'cleanGroup'], $groups);
  666. //prepend groups with @ and nameencode
  667. foreach ($groups as &$group) {
  668. $group = '@' . auth_nameencode($group);
  669. }
  670. $ns = getNS($id);
  671. $perm = -1;
  672. //add ALL group
  673. $groups[] = '@ALL';
  674. //add User
  675. if ($user) $groups[] = $user;
  676. //check exact match first
  677. $matches = preg_grep('/^' . preg_quote($id, '/') . '[ \t]+([^ \t]+)[ \t]+/', $AUTH_ACL);
  678. if (count($matches)) {
  679. foreach ($matches as $match) {
  680. $match = preg_replace('/#.*$/', '', $match); //ignore comments
  681. $acl = preg_split('/[ \t]+/', $match);
  682. if (!$auth->isCaseSensitive() && $acl[1] !== '@ALL') {
  683. $acl[1] = PhpString::strtolower($acl[1]);
  684. }
  685. if (!in_array($acl[1], $groups)) {
  686. continue;
  687. }
  688. if ($acl[2] > AUTH_DELETE) $acl[2] = AUTH_DELETE; //no admins in the ACL!
  689. if ($acl[2] > $perm) {
  690. $perm = $acl[2];
  691. }
  692. }
  693. if ($perm > -1) {
  694. //we had a match - return it
  695. return (int) $perm;
  696. }
  697. }
  698. //still here? do the namespace checks
  699. if ($ns) {
  700. $path = $ns . ':*';
  701. } else {
  702. $path = '*'; //root document
  703. }
  704. do {
  705. $matches = preg_grep('/^' . preg_quote($path, '/') . '[ \t]+([^ \t]+)[ \t]+/', $AUTH_ACL);
  706. if (count($matches)) {
  707. foreach ($matches as $match) {
  708. $match = preg_replace('/#.*$/', '', $match); //ignore comments
  709. $acl = preg_split('/[ \t]+/', $match);
  710. if (!$auth->isCaseSensitive() && $acl[1] !== '@ALL') {
  711. $acl[1] = PhpString::strtolower($acl[1]);
  712. }
  713. if (!in_array($acl[1], $groups)) {
  714. continue;
  715. }
  716. if ($acl[2] > AUTH_DELETE) $acl[2] = AUTH_DELETE; //no admins in the ACL!
  717. if ($acl[2] > $perm) {
  718. $perm = $acl[2];
  719. }
  720. }
  721. //we had a match - return it
  722. if ($perm != -1) {
  723. return (int) $perm;
  724. }
  725. }
  726. //get next higher namespace
  727. $ns = getNS($ns);
  728. if ($path != '*') {
  729. $path = $ns . ':*';
  730. if ($path == ':*') $path = '*';
  731. } else {
  732. //we did this already
  733. //looks like there is something wrong with the ACL
  734. //break here
  735. msg('No ACL setup yet! Denying access to everyone.');
  736. return AUTH_NONE;
  737. }
  738. } while (1); //this should never loop endless
  739. return AUTH_NONE;
  740. }
  741. /**
  742. * Encode ASCII special chars
  743. *
  744. * Some auth backends allow special chars in their user and groupnames
  745. * The special chars are encoded with this function. Only ASCII chars
  746. * are encoded UTF-8 multibyte are left as is (different from usual
  747. * urlencoding!).
  748. *
  749. * Decoding can be done with rawurldecode
  750. *
  751. * @author Andreas Gohr <gohr@cosmocode.de>
  752. * @see rawurldecode()
  753. *
  754. * @param string $name
  755. * @param bool $skip_group
  756. * @return string
  757. */
  758. function auth_nameencode($name, $skip_group = false)
  759. {
  760. global $cache_authname;
  761. $cache =& $cache_authname;
  762. $name = (string) $name;
  763. // never encode wildcard FS#1955
  764. if ($name == '%USER%') return $name;
  765. if ($name == '%GROUP%') return $name;
  766. if (!isset($cache[$name][$skip_group])) {
  767. if ($skip_group && $name[0] == '@') {
  768. $cache[$name][$skip_group] = '@' . preg_replace_callback(
  769. '/([\x00-\x2f\x3a-\x40\x5b-\x60\x7b-\x7f])/',
  770. 'auth_nameencode_callback',
  771. substr($name, 1)
  772. );
  773. } else {
  774. $cache[$name][$skip_group] = preg_replace_callback(
  775. '/([\x00-\x2f\x3a-\x40\x5b-\x60\x7b-\x7f])/',
  776. 'auth_nameencode_callback',
  777. $name
  778. );
  779. }
  780. }
  781. return $cache[$name][$skip_group];
  782. }
  783. /**
  784. * callback encodes the matches
  785. *
  786. * @param array $matches first complete match, next matching subpatterms
  787. * @return string
  788. */
  789. function auth_nameencode_callback($matches)
  790. {
  791. return '%' . dechex(ord(substr($matches[1], -1)));
  792. }
  793. /**
  794. * Create a pronouncable password
  795. *
  796. * The $foruser variable might be used by plugins to run additional password
  797. * policy checks, but is not used by the default implementation
  798. *
  799. * @param string $foruser username for which the password is generated
  800. * @return string pronouncable password
  801. * @throws Exception
  802. *
  803. * @link http://www.phpbuilder.com/annotate/message.php3?id=1014451
  804. * @triggers AUTH_PASSWORD_GENERATE
  805. *
  806. * @author Andreas Gohr <andi@splitbrain.org>
  807. */
  808. function auth_pwgen($foruser = '')
  809. {
  810. $data = [
  811. 'password' => '',
  812. 'foruser' => $foruser
  813. ];
  814. $evt = new Event('AUTH_PASSWORD_GENERATE', $data);
  815. if ($evt->advise_before(true)) {
  816. $c = 'bcdfghjklmnprstvwz'; //consonants except hard to speak ones
  817. $v = 'aeiou'; //vowels
  818. $a = $c . $v; //both
  819. $s = '!$%&?+*~#-_:.;,'; // specials
  820. //use thre syllables...
  821. for ($i = 0; $i < 3; $i++) {
  822. $data['password'] .= $c[auth_random(0, strlen($c) - 1)];
  823. $data['password'] .= $v[auth_random(0, strlen($v) - 1)];
  824. $data['password'] .= $a[auth_random(0, strlen($a) - 1)];
  825. }
  826. //... and add a nice number and special
  827. $data['password'] .= $s[auth_random(0, strlen($s) - 1)] . auth_random(10, 99);
  828. }
  829. $evt->advise_after();
  830. return $data['password'];
  831. }
  832. /**
  833. * Sends a password to the given user
  834. *
  835. * @author Andreas Gohr <andi@splitbrain.org>
  836. *
  837. * @param string $user Login name of the user
  838. * @param string $password The new password in clear text
  839. * @return bool true on success
  840. */
  841. function auth_sendPassword($user, $password)
  842. {
  843. global $lang;
  844. /* @var AuthPlugin $auth */
  845. global $auth;
  846. if (!$auth instanceof AuthPlugin) return false;
  847. $user = $auth->cleanUser($user);
  848. $userinfo = $auth->getUserData($user, false);
  849. if (!$userinfo['mail']) return false;
  850. $text = rawLocale('password');
  851. $trep = [
  852. 'FULLNAME' => $userinfo['name'],
  853. 'LOGIN' => $user,
  854. 'PASSWORD' => $password
  855. ];
  856. $mail = new Mailer();
  857. $mail->to($mail->getCleanName($userinfo['name']) . ' <' . $userinfo['mail'] . '>');
  858. $mail->subject($lang['regpwmail']);
  859. $mail->setBody($text, $trep);
  860. return $mail->send();
  861. }
  862. /**
  863. * Register a new user
  864. *
  865. * This registers a new user - Data is read directly from $_POST
  866. *
  867. * @return bool true on success, false on any error
  868. * @throws Exception
  869. *
  870. * @author Andreas Gohr <andi@splitbrain.org>
  871. */
  872. function register()
  873. {
  874. global $lang;
  875. global $conf;
  876. /* @var AuthPlugin $auth */
  877. global $auth;
  878. global $INPUT;
  879. if (!$INPUT->post->bool('save')) return false;
  880. if (!actionOK('register')) return false;
  881. // gather input
  882. $login = trim($auth->cleanUser($INPUT->post->str('login')));
  883. $fullname = trim(preg_replace('/[\x00-\x1f:<>&%,;]+/', '', $INPUT->post->str('fullname')));
  884. $email = trim(preg_replace('/[\x00-\x1f:<>&%,;]+/', '', $INPUT->post->str('email')));
  885. $pass = $INPUT->post->str('pass');
  886. $passchk = $INPUT->post->str('passchk');
  887. if (empty($login) || empty($fullname) || empty($email)) {
  888. msg($lang['regmissing'], -1);
  889. return false;
  890. }
  891. if ($conf['autopasswd']) {
  892. $pass = auth_pwgen($login); // automatically generate password
  893. } elseif (empty($pass) || empty($passchk)) {
  894. msg($lang['regmissing'], -1); // complain about missing passwords
  895. return false;
  896. } elseif ($pass != $passchk) {
  897. msg($lang['regbadpass'], -1); // complain about misspelled passwords
  898. return false;
  899. }
  900. //check mail
  901. if (!mail_isvalid($email)) {
  902. msg($lang['regbadmail'], -1);
  903. return false;
  904. }
  905. //okay try to create the user
  906. if (!$auth->triggerUserMod('create', [$login, $pass, $fullname, $email])) {
  907. msg($lang['regfail'], -1);
  908. return false;
  909. }
  910. // send notification about the new user
  911. $subscription = new RegistrationSubscriptionSender();
  912. $subscription->sendRegister($login, $fullname, $email);
  913. // are we done?
  914. if (!$conf['autopasswd']) {
  915. msg($lang['regsuccess2'], 1);
  916. return true;
  917. }
  918. // autogenerated password? then send password to user
  919. if (auth_sendPassword($login, $pass)) {
  920. msg($lang['regsuccess'], 1);
  921. return true;
  922. } else {
  923. msg($lang['regmailfail'], -1);
  924. return false;
  925. }
  926. }
  927. /**
  928. * Update user profile
  929. *
  930. * @throws Exception
  931. *
  932. * @author Christopher Smith <chris@jalakai.co.uk>
  933. */
  934. function updateprofile()
  935. {
  936. global $conf;
  937. global $lang;
  938. /* @var AuthPlugin $auth */
  939. global $auth;
  940. /* @var Input $INPUT */
  941. global $INPUT;
  942. if (!$INPUT->post->bool('save')) return false;
  943. if (!checkSecurityToken()) return false;
  944. if (!actionOK('profile')) {
  945. msg($lang['profna'], -1);
  946. return false;
  947. }
  948. $changes = [];
  949. $changes['pass'] = $INPUT->post->str('newpass');
  950. $changes['name'] = $INPUT->post->str('fullname');
  951. $changes['mail'] = $INPUT->post->str('email');
  952. // check misspelled passwords
  953. if ($changes['pass'] != $INPUT->post->str('passchk')) {
  954. msg($lang['regbadpass'], -1);
  955. return false;
  956. }
  957. // clean fullname and email
  958. $changes['name'] = trim(preg_replace('/[\x00-\x1f:<>&%,;]+/', '', $changes['name']));
  959. $changes['mail'] = trim(preg_replace('/[\x00-\x1f:<>&%,;]+/', '', $changes['mail']));
  960. // no empty name and email (except the backend doesn't support them)
  961. if (
  962. (empty($changes['name']) && $auth->canDo('modName')) ||
  963. (empty($changes['mail']) && $auth->canDo('modMail'))
  964. ) {
  965. msg($lang['profnoempty'], -1);
  966. return false;
  967. }
  968. if (!mail_isvalid($changes['mail']) && $auth->canDo('modMail')) {
  969. msg($lang['regbadmail'], -1);
  970. return false;
  971. }
  972. $changes = array_filter($changes);
  973. // check for unavailable capabilities
  974. if (!$auth->canDo('modName')) unset($changes['name']);
  975. if (!$auth->canDo('modMail')) unset($changes['mail']);
  976. if (!$auth->canDo('modPass')) unset($changes['pass']);
  977. // anything to do?
  978. if ($changes === []) {
  979. msg($lang['profnochange'], -1);
  980. return false;
  981. }
  982. if ($conf['profileconfirm']) {
  983. if (!$auth->checkPass($INPUT->server->str('REMOTE_USER'), $INPUT->post->str('oldpass'))) {
  984. msg($lang['badpassconfirm'], -1);
  985. return false;
  986. }
  987. }
  988. if (!$auth->triggerUserMod('modify', [$INPUT->server->str('REMOTE_USER'), &$changes])) {
  989. msg($lang['proffail'], -1);
  990. return false;
  991. }
  992. if (array_key_exists('pass', $changes) && $changes['pass']) {
  993. // update cookie and session with the changed data
  994. [/* user */, $sticky, /* pass */] = auth_getCookie();
  995. $pass = auth_encrypt($changes['pass'], auth_cookiesalt(!$sticky, true));
  996. auth_setCookie($INPUT->server->str('REMOTE_USER'), $pass, (bool) $sticky);
  997. } else {
  998. // make sure the session is writable
  999. @session_start();
  1000. // invalidate session cache
  1001. $_SESSION[DOKU_COOKIE]['auth']['time'] = 0;
  1002. session_write_close();
  1003. }
  1004. return true;
  1005. }
  1006. /**
  1007. * Delete the current logged-in user
  1008. *
  1009. * @return bool true on success, false on any error
  1010. */
  1011. function auth_deleteprofile()
  1012. {
  1013. global $conf;
  1014. global $lang;
  1015. /* @var AuthPlugin $auth */
  1016. global $auth;
  1017. /* @var Input $INPUT */
  1018. global $INPUT;
  1019. if (!$INPUT->post->bool('delete')) return false;
  1020. if (!checkSecurityToken()) return false;
  1021. // action prevented or auth module disallows
  1022. if (!actionOK('profile_delete') || !$auth->canDo('delUser')) {
  1023. msg($lang['profnodelete'], -1);
  1024. return false;
  1025. }
  1026. if (!$INPUT->post->bool('confirm_delete')) {
  1027. msg($lang['profconfdeletemissing'], -1);
  1028. return false;
  1029. }
  1030. if ($conf['profileconfirm']) {
  1031. if (!$auth->checkPass($INPUT->server->str('REMOTE_USER'), $INPUT->post->str('oldpass'))) {
  1032. msg($lang['badpassconfirm'], -1);
  1033. return false;
  1034. }
  1035. }
  1036. $deleted = [];
  1037. $deleted[] = $INPUT->server->str('REMOTE_USER');
  1038. if ($auth->triggerUserMod('delete', [$deleted])) {
  1039. // force and immediate logout including removing the sticky cookie
  1040. auth_logoff();
  1041. return true;
  1042. }
  1043. return false;
  1044. }
  1045. /**
  1046. * Send a new password
  1047. *
  1048. * This function handles both phases of the password reset:
  1049. *
  1050. * - handling the first request of password reset
  1051. * - validating the password reset auth token
  1052. *
  1053. * @return bool true on success, false on any error
  1054. * @throws Exception
  1055. *
  1056. * @author Andreas Gohr <andi@splitbrain.org>
  1057. * @author Benoit Chesneau <benoit@bchesneau.info>
  1058. * @author Chris Smith <chris@jalakai.co.uk>
  1059. */
  1060. function act_resendpwd()
  1061. {
  1062. global $lang;
  1063. global $conf;
  1064. /* @var AuthPlugin $auth */
  1065. global $auth;
  1066. /* @var Input $INPUT */
  1067. global $INPUT;
  1068. if (!actionOK('resendpwd')) {
  1069. msg($lang['resendna'], -1);
  1070. return false;
  1071. }
  1072. $token = preg_replace('/[^a-f0-9]+/', '', $INPUT->str('pwauth'));
  1073. if ($token) {
  1074. // we're in token phase - get user info from token
  1075. $tfile = $conf['cachedir'] . '/' . $token[0] . '/' . $token . '.pwauth';
  1076. if (!file_exists($tfile)) {
  1077. msg($lang['resendpwdbadauth'], -1);
  1078. $INPUT->remove('pwauth');
  1079. return false;
  1080. }
  1081. // token is only valid for 3 days
  1082. if ((time() - filemtime($tfile)) > (3 * 60 * 60 * 24)) {
  1083. msg($lang['resendpwdbadauth'], -1);
  1084. $INPUT->remove('pwauth');
  1085. @unlink($tfile);
  1086. return false;
  1087. }
  1088. $user = io_readfile($tfile);
  1089. $userinfo = $auth->getUserData($user, false);
  1090. if (!$userinfo['mail']) {
  1091. msg($lang['resendpwdnouser'], -1);
  1092. return false;
  1093. }
  1094. if (!$conf['autopasswd']) { // we let the user choose a password
  1095. $pass = $INPUT->str('pass');
  1096. // password given correctly?
  1097. if (!$pass) return false;
  1098. if ($pass != $INPUT->str('passchk')) {
  1099. msg($lang['regbadpass'], -1);
  1100. return false;
  1101. }
  1102. // change it
  1103. if (!$auth->triggerUserMod('modify', [$user, ['pass' => $pass]])) {
  1104. msg($lang['proffail'], -1);
  1105. return false;
  1106. }
  1107. } else { // autogenerate the password and send by mail
  1108. $pass = auth_pwgen($user);
  1109. if (!$auth->triggerUserMod('modify', [$user, ['pass' => $pass]])) {
  1110. msg($lang['proffail'], -1);
  1111. return false;
  1112. }
  1113. if (auth_sendPassword($user, $pass)) {
  1114. msg($lang['resendpwdsuccess'], 1);
  1115. } else {
  1116. msg($lang['regmailfail'], -1);
  1117. }
  1118. }
  1119. @unlink($tfile);
  1120. return true;
  1121. } else {
  1122. // we're in request phase
  1123. if (!$INPUT->post->bool('save')) return false;
  1124. if (!$INPUT->post->str('login')) {
  1125. msg($lang['resendpwdmissing'], -1);
  1126. return false;
  1127. } else {
  1128. $user = trim($auth->cleanUser($INPUT->post->str('login')));
  1129. }
  1130. $userinfo = $auth->getUserData($user, false);
  1131. if (!$userinfo['mail']) {
  1132. msg($lang['resendpwdnouser'], -1);
  1133. return false;
  1134. }
  1135. // generate auth token
  1136. $token = md5(auth_randombytes(16)); // random secret
  1137. $tfile = $conf['cachedir'] . '/' . $token[0] . '/' . $token . '.pwauth';
  1138. $url = wl('', ['do' => 'resendpwd', 'pwauth' => $token], true, '&');
  1139. io_saveFile($tfile, $user);
  1140. $text = rawLocale('pwconfirm');
  1141. $trep = ['FULLNAME' => $userinfo['name'], 'LOGIN' => $user, 'CONFIRM' => $url];
  1142. $mail = new Mailer();
  1143. $mail->to($userinfo['name'] . ' <' . $userinfo['mail'] . '>');
  1144. $mail->subject($lang['regpwmail']);
  1145. $mail->setBody($text, $trep);
  1146. if ($mail->send()) {
  1147. msg($lang['resendpwdconfirm'], 1);
  1148. } else {
  1149. msg($lang['regmailfail'], -1);
  1150. }
  1151. return true;
  1152. }
  1153. // never reached
  1154. }
  1155. /**
  1156. * Encrypts a password using the given method and salt
  1157. *
  1158. * If the selected method needs a salt and none was given, a random one
  1159. * is chosen.
  1160. *
  1161. * @author Andreas Gohr <andi@splitbrain.org>
  1162. *
  1163. * @param string $clear The clear text password
  1164. * @param string $method The hashing method
  1165. * @param string $salt A salt, null for random
  1166. * @return string The crypted password
  1167. */
  1168. function auth_cryptPassword($clear, $method = '', $salt = null)
  1169. {
  1170. global $conf;
  1171. if (empty($method)) $method = $conf['passcrypt'];
  1172. $pass = new PassHash();
  1173. $call = 'hash_' . $method;
  1174. if (!method_exists($pass, $call)) {
  1175. msg("Unsupported crypt method $method", -1);
  1176. return false;
  1177. }
  1178. return $pass->$call($clear, $salt);
  1179. }
  1180. /**
  1181. * Verifies a cleartext password against a crypted hash
  1182. *
  1183. * @param string $clear The clear text password
  1184. * @param string $crypt The hash to compare with
  1185. * @return bool true if both match
  1186. * @throws Exception
  1187. *
  1188. * @author Andreas Gohr <andi@splitbrain.org>
  1189. */
  1190. function auth_verifyPassword($clear, $crypt)
  1191. {
  1192. $pass = new PassHash();
  1193. return $pass->verify_hash($clear, $crypt);
  1194. }
  1195. /**
  1196. * Set the authentication cookie and add user identification data to the session
  1197. *
  1198. * @param string $user username
  1199. * @param string $pass encrypted password
  1200. * @param bool $sticky whether or not the cookie will last beyond the session
  1201. * @return bool
  1202. */
  1203. function auth_setCookie($user, $pass, $sticky)
  1204. {
  1205. global $conf;
  1206. /* @var AuthPlugin $auth */
  1207. global $auth;
  1208. global $USERINFO;
  1209. if (!$auth instanceof AuthPlugin) return false;
  1210. $USERINFO = $auth->getUserData($user);
  1211. // set cookie
  1212. $cookie = base64_encode($user) . '|' . ((int) $sticky) . '|' . base64_encode($pass);
  1213. $cookieDir = empty($conf['cookiedir']) ? DOKU_REL : $conf['cookiedir'];
  1214. $time = $sticky ? (time() + 60 * 60 * 24 * 365) : 0; //one year
  1215. setcookie(DOKU_COOKIE, $cookie, [
  1216. 'expires' => $time,
  1217. 'path' => $cookieDir,
  1218. 'secure' => ($conf['securecookie'] && is_ssl()),
  1219. 'httponly' => true,
  1220. 'samesite' => $conf['samesitecookie'] ?: null, // null means browser default
  1221. ]);
  1222. // set session
  1223. $_SESSION[DOKU_COOKIE]['auth']['user'] = $user;
  1224. $_SESSION[DOKU_COOKIE]['auth']['pass'] = sha1($pass);
  1225. $_SESSION[DOKU_COOKIE]['auth']['buid'] = auth_browseruid();
  1226. $_SESSION[DOKU_COOKIE]['auth']['info'] = $USERINFO;
  1227. $_SESSION[DOKU_COOKIE]['auth']['time'] = time();
  1228. return true;
  1229. }
  1230. /**
  1231. * Returns the user, (encrypted) password and sticky bit from cookie
  1232. *
  1233. * @returns array
  1234. */
  1235. function auth_getCookie()
  1236. {
  1237. if (!isset($_COOKIE[DOKU_COOKIE])) {
  1238. return [null, null, null];
  1239. }
  1240. [$user, $sticky, $pass] = sexplode('|', $_COOKIE[DOKU_COOKIE], 3, '');
  1241. $sticky = (bool) $sticky;
  1242. $pass = base64_decode($pass);
  1243. $user = base64_decode($user);
  1244. return [$user, $sticky, $pass];
  1245. }
  1246. //Setup VIM: ex: et ts=2 :