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