auth.php 38 KB

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