PageRenderTime 59ms CodeModel.GetById 24ms RepoModel.GetById 0ms app.codeStats 0ms

/horde-3.3.13/lib/Horde/Auth.php

#
PHP | 1375 lines | 721 code | 148 blank | 506 comment | 136 complexity | b4942f1ad38666718c5d99fceea8bc4e MD5 | raw file
Possible License(s): LGPL-2.0
  1. <?php
  2. /**
  3. * The parameter name for the logout reason.
  4. */
  5. define('AUTH_REASON_PARAM', 'logout_reason');
  6. /**
  7. * The parameter name for the logout message used with type
  8. * AUTH_REASON_MESSAGE.
  9. */
  10. define('AUTH_REASON_MSG_PARAM', 'logout_msg');
  11. /**
  12. * The 'badlogin' reason.
  13. *
  14. * The following 'reasons' for the logout screen are recognized:
  15. * <pre>
  16. * 'badlogin' -- Bad username and/or password
  17. * 'browser' -- A browser change was detected
  18. * 'failed' -- Login failed
  19. * 'expired' -- Password has expired
  20. * 'logout' -- Logout due to user request
  21. * 'message' -- Logout with custom message in AUTH_REASON_MSG_PARAM
  22. * 'session' -- Logout due to session expiration
  23. * 'sessionip' -- Logout due to change of IP address during session
  24. * </pre>
  25. */
  26. define('AUTH_REASON_BADLOGIN', 'badlogin');
  27. /**
  28. * The 'browser' reason.
  29. */
  30. define('AUTH_REASON_BROWSER', 'browser');
  31. /**
  32. * The 'failed' reason.
  33. */
  34. define('AUTH_REASON_FAILED', 'failed');
  35. /**
  36. * The 'expired' reason.
  37. */
  38. define('AUTH_REASON_EXPIRED', 'expired');
  39. /**
  40. * The 'logout' reason.
  41. */
  42. define('AUTH_REASON_LOGOUT', 'logout');
  43. /**
  44. * The 'message' reason.
  45. */
  46. define('AUTH_REASON_MESSAGE', 'message');
  47. /**
  48. * The 'session' reason.
  49. */
  50. define('AUTH_REASON_SESSION', 'session');
  51. /**
  52. * The 'sessionip' reason.
  53. */
  54. define('AUTH_REASON_SESSIONIP', 'sessionip');
  55. /**
  56. * The Auth:: class provides a common abstracted interface into the various
  57. * backends for the Horde authentication system.
  58. *
  59. * $Horde: framework/Auth/Auth.php,v 1.142.10.37 2009/10/26 11:58:58 jan Exp $
  60. *
  61. * Copyright 1999-2009 The Horde Project (http://www.horde.org/)
  62. *
  63. * See the enclosed file COPYING for license information (LGPL). If you
  64. * did not receive this file, see http://opensource.org/licenses/lgpl-license.php.
  65. *
  66. * @author Chuck Hagenbuch <chuck@horde.org>
  67. * @since Horde 1.3
  68. * @package Horde_Auth
  69. */
  70. class Auth {
  71. /**
  72. * An array of capabilities, so that the driver can report which
  73. * operations it supports and which it doesn't.
  74. *
  75. * @var array
  76. */
  77. var $capabilities = array('add' => false,
  78. 'update' => false,
  79. 'resetpassword' => false,
  80. 'remove' => false,
  81. 'list' => false,
  82. 'groups' => false,
  83. 'admins' => false,
  84. 'transparent' => false);
  85. /**
  86. * Hash containing parameters.
  87. *
  88. * @var array
  89. */
  90. var $_params = array();
  91. /**
  92. * The credentials currently being authenticated.
  93. *
  94. * @access protected
  95. *
  96. * @var array
  97. */
  98. var $_authCredentials = array();
  99. /**
  100. * Returns the name of the concrete Auth implementation.
  101. *
  102. * @return string The Auth implementation name.
  103. */
  104. function getDriver()
  105. {
  106. return str_replace('auth_', '', strtolower(get_class($this)));
  107. }
  108. /**
  109. * Finds out if a set of login credentials are valid, and if requested,
  110. * mark the user as logged in in the current session.
  111. *
  112. * @param string $userId The userId to check.
  113. * @param array $credentials The credentials to check.
  114. * @param boolean $login Whether to log the user in. If false, we'll
  115. * only test the credentials and won't modify
  116. * the current session. Defaults to true.
  117. * @param string $realm The authentication realm to check.
  118. *
  119. * @return boolean Whether or not the credentials are valid.
  120. */
  121. function authenticate($userId, $credentials, $login = true, $realm = null)
  122. {
  123. $auth = false;
  124. $userId = trim($userId);
  125. if (!empty($GLOBALS['conf']['hooks']['preauthenticate'])) {
  126. if (!Horde::callHook('_horde_hook_preauthenticate', array($userId, $credentials, $realm), 'horde', false)) {
  127. if ($this->_getAuthError() != AUTH_REASON_MESSAGE) {
  128. $this->_setAuthError(AUTH_REASON_FAILED);
  129. }
  130. return false;
  131. }
  132. }
  133. /* Store the credentials being checked so that subclasses can modify
  134. * them if necessary (like transparent auth does). */
  135. $this->_authCredentials = array(
  136. 'userId' => $userId,
  137. 'credentials' => $credentials,
  138. 'realm' => $realm,
  139. 'changeRequested' => false
  140. );
  141. if ($authenticated = $this->_authenticate($userId, $credentials)) {
  142. if (is_a($authenticated, 'PEAR_Error')) {
  143. return false;
  144. }
  145. if ($login) {
  146. $auth = $this->setAuth(
  147. $this->_authCredentials['userId'],
  148. $this->_authCredentials['credentials'],
  149. $this->_authCredentials['realm'],
  150. $this->_authCredentials['changeRequested']);
  151. } else {
  152. if (!$this->_checkSessionIP()) {
  153. $this->_setAuthError(AUTH_REASON_SESSIONIP);
  154. return false;
  155. } elseif (!$this->_checkBrowserString()) {
  156. $this->_setAuthError(AUTH_REASON_BROWSER);
  157. return false;
  158. }
  159. $auth = true;
  160. }
  161. }
  162. return $auth;
  163. }
  164. /**
  165. * Formats a password using the current encryption.
  166. *
  167. * @param string $plaintext The plaintext password to encrypt.
  168. * @param string $salt The salt to use to encrypt the password.
  169. * If not present, a new salt will be
  170. * generated.
  171. * @param string $encryption The kind of pasword encryption to use.
  172. * Defaults to md5-hex.
  173. * @param boolean $show_encrypt Some password systems prepend the kind of
  174. * encryption to the crypted password ({SHA},
  175. * etc). Defaults to false.
  176. *
  177. * @return string The encrypted password.
  178. */
  179. function getCryptedPassword($plaintext, $salt = '',
  180. $encryption = 'md5-hex', $show_encrypt = false)
  181. {
  182. /* Get the salt to use. */
  183. $salt = Auth::getSalt($encryption, $salt, $plaintext);
  184. /* Encrypt the password. */
  185. switch ($encryption) {
  186. case 'plain':
  187. return $plaintext;
  188. case 'msad':
  189. return String::convertCharset('"' . $plaintext . '"', 'ISO-8859-1', 'UTF-16LE');
  190. case 'sha':
  191. $encrypted = base64_encode(pack('H*', sha1($plaintext)));
  192. return ($show_encrypt) ? '{SHA}' . $encrypted : $encrypted;
  193. case 'crypt':
  194. case 'crypt-des':
  195. case 'crypt-md5':
  196. case 'crypt-blowfish':
  197. return ($show_encrypt ? '{crypt}' : '') . crypt($plaintext, $salt);
  198. case 'md5-base64':
  199. $encrypted = base64_encode(pack('H*', md5($plaintext)));
  200. return ($show_encrypt) ? '{MD5}' . $encrypted : $encrypted;
  201. case 'ssha':
  202. $encrypted = base64_encode(pack('H*', sha1($plaintext . $salt)) . $salt);
  203. return ($show_encrypt) ? '{SSHA}' . $encrypted : $encrypted;
  204. case 'smd5':
  205. $encrypted = base64_encode(pack('H*', md5($plaintext . $salt)) . $salt);
  206. return ($show_encrypt) ? '{SMD5}' . $encrypted : $encrypted;
  207. case 'aprmd5':
  208. $length = strlen($plaintext);
  209. $context = $plaintext . '$apr1$' . $salt;
  210. $binary = pack('H*', md5($plaintext . $salt . $plaintext));
  211. for ($i = $length; $i > 0; $i -= 16) {
  212. $context .= substr($binary, 0, ($i > 16 ? 16 : $i));
  213. }
  214. for ($i = $length; $i > 0; $i >>= 1) {
  215. $context .= ($i & 1) ? chr(0) : $plaintext[0];
  216. }
  217. $binary = pack('H*', md5($context));
  218. for ($i = 0; $i < 1000; $i++) {
  219. $new = ($i & 1) ? $plaintext : substr($binary, 0, 16);
  220. if ($i % 3) {
  221. $new .= $salt;
  222. }
  223. if ($i % 7) {
  224. $new .= $plaintext;
  225. }
  226. $new .= ($i & 1) ? substr($binary, 0, 16) : $plaintext;
  227. $binary = pack('H*', md5($new));
  228. }
  229. $p = array();
  230. for ($i = 0; $i < 5; $i++) {
  231. $k = $i + 6;
  232. $j = $i + 12;
  233. if ($j == 16) {
  234. $j = 5;
  235. }
  236. $p[] = Auth::_toAPRMD5((ord($binary[$i]) << 16) |
  237. (ord($binary[$k]) << 8) |
  238. (ord($binary[$j])),
  239. 5);
  240. }
  241. return '$apr1$' . $salt . '$' . implode('', $p) . Auth::_toAPRMD5(ord($binary[11]), 3);
  242. case 'md5-hex':
  243. default:
  244. return ($show_encrypt) ? '{MD5}' . md5($plaintext) : md5($plaintext);
  245. }
  246. }
  247. /**
  248. * Returns a salt for the appropriate kind of password encryption.
  249. * Optionally takes a seed and a plaintext password, to extract the seed
  250. * of an existing password, or for encryption types that use the plaintext
  251. * in the generation of the salt.
  252. *
  253. * @param string $encryption The kind of pasword encryption to use.
  254. * Defaults to md5-hex.
  255. * @param string $seed The seed to get the salt from (probably a
  256. * previously generated password). Defaults to
  257. * generating a new seed.
  258. * @param string $plaintext The plaintext password that we're generating
  259. * a salt for. Defaults to none.
  260. *
  261. * @return string The generated or extracted salt.
  262. */
  263. function getSalt($encryption = 'md5-hex', $seed = '', $plaintext = '')
  264. {
  265. switch ($encryption) {
  266. case 'crypt':
  267. case 'crypt-des':
  268. if ($seed) {
  269. return substr(preg_replace('|^{crypt}|i', '', $seed), 0, 2);
  270. } else {
  271. return substr(md5(mt_rand()), 0, 2);
  272. }
  273. case 'crypt-md5':
  274. if ($seed) {
  275. return substr(preg_replace('|^{crypt}|i', '', $seed), 0, 12);
  276. } else {
  277. return '$1$' . substr(md5(mt_rand()), 0, 8) . '$';
  278. }
  279. case 'crypt-blowfish':
  280. if ($seed) {
  281. return substr(preg_replace('|^{crypt}|i', '', $seed), 0, 16);
  282. } else {
  283. return '$2$' . substr(md5(mt_rand()), 0, 12) . '$';
  284. }
  285. case 'ssha':
  286. if ($seed) {
  287. return substr(base64_decode(preg_replace('|^{SSHA}|i', '', $seed)), 20);
  288. } else {
  289. $salt = substr(pack('h*', md5(mt_rand())), 0, 8);
  290. return substr(pack('H*', sha1($salt . $plaintext)), 0, 4);
  291. }
  292. case 'smd5':
  293. if ($seed) {
  294. return substr(base64_decode(preg_replace('|^{SMD5}|i', '', $seed)), 16);
  295. } else {
  296. $salt = substr(pack('h*', md5(mt_rand())), 0, 8);
  297. return substr(pack('H*', md5($salt . $plaintext)), 0, 4);
  298. }
  299. case 'aprmd5':
  300. /* 64 characters that are valid for APRMD5 passwords. */
  301. $APRMD5 = './0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
  302. if ($seed) {
  303. return substr(preg_replace('/^\$apr1\$(.{8}).*/', '\\1', $seed), 0, 8);
  304. } else {
  305. $salt = '';
  306. for ($i = 0; $i < 8; $i++) {
  307. $salt .= $APRMD5[mt_rand(0, 63)];
  308. }
  309. return $salt;
  310. }
  311. default:
  312. return '';
  313. }
  314. }
  315. /**
  316. * Generates a random, hopefully pronounceable, password. This can be used
  317. * when resetting automatically a user's password.
  318. *
  319. * @return string A random password
  320. */
  321. function genRandomPassword()
  322. {
  323. $vowels = 'aeiouy';
  324. $constants = 'bcdfghjklmnpqrstvwxz';
  325. $numbers = '0123456789';
  326. /* Alternate constant and vowel random chars with two random numbers
  327. * at the end. This should produce a fairly pronounceable password. */
  328. $chars[0] = substr($constants, mt_rand(0, strlen($constants) - 1), 1);
  329. $chars[1] = substr($vowels, mt_rand(0, strlen($vowels) - 1), 1);
  330. $chars[2] = substr($constants, mt_rand(0, strlen($constants) - 1), 1);
  331. $chars[3] = substr($vowels, mt_rand(0, strlen($vowels) - 1), 1);
  332. $chars[4] = substr($constants, mt_rand(0, strlen($constants) - 1), 1);
  333. $chars[5] = substr($numbers, mt_rand(0, strlen($numbers) - 1), 1);
  334. $chars[6] = substr($numbers, mt_rand(0, strlen($numbers) - 1), 1);
  335. return implode('', $chars);
  336. }
  337. /**
  338. * Adds a set of authentication credentials.
  339. *
  340. * @abstract
  341. *
  342. * @param string $userId The userId to add.
  343. * @param array $credentials The credentials to use.
  344. *
  345. * @return mixed True on success or a PEAR_Error object on failure.
  346. */
  347. function addUser($userId, $credentials)
  348. {
  349. return PEAR::raiseError('unsupported');
  350. }
  351. /**
  352. * Updates a set of authentication credentials.
  353. *
  354. * @abstract
  355. *
  356. * @param string $oldID The old userId.
  357. * @param string $newID The new userId.
  358. * @param array $credentials The new credentials
  359. *
  360. * @return mixed True on success or a PEAR_Error object on failure.
  361. */
  362. function updateUser($oldID, $newID, $credentials)
  363. {
  364. return PEAR::raiseError('unsupported');
  365. }
  366. /**
  367. * Deletes a set of authentication credentials.
  368. *
  369. * @abstract
  370. *
  371. * @param string $userId The userId to delete.
  372. *
  373. * @return mixed True on success or a PEAR_Error object on failure.
  374. */
  375. function removeUser($userId)
  376. {
  377. return PEAR::raiseError('unsupported');
  378. }
  379. /**
  380. * Calls all applications' removeUser API methods.
  381. *
  382. * @param string $userId The userId to delete.
  383. *
  384. * @return mixed True on success or a PEAR_Error object on failure.
  385. */
  386. function removeUserData($userId)
  387. {
  388. global $registry;
  389. $errApps = array();
  390. foreach ($registry->listApps(array('notoolbar', 'hidden', 'active', 'admin')) as $app) {
  391. if ($registry->hasMethod('removeUserData', $app)) {
  392. if (is_a($result = $registry->callByPackage($app, 'removeUserData', array($userId)), 'PEAR_Error')) {
  393. Horde::logMessage($result, __FILE__, __LINE__, PEAR_LOG_ERR);
  394. $errApps[] = $app;
  395. }
  396. }
  397. }
  398. if (count($errApps)) {
  399. $err = implode(', ', $errApps);
  400. return PEAR::raiseError(sprintf(_("The following applications encountered errors removing user data: %s"), $err));
  401. } else {
  402. return true;
  403. }
  404. }
  405. /**
  406. * Lists all users in the system.
  407. *
  408. * @abstract
  409. *
  410. * @return mixed The array of userIds, or a PEAR_Error object on failure.
  411. */
  412. function listUsers()
  413. {
  414. return PEAR::raiseError('unsupported');
  415. }
  416. /**
  417. * Checks if $userId exists in the system.
  418. *
  419. * @abstract
  420. *
  421. * @param string $userId User ID for which to check
  422. *
  423. * @return boolean Whether or not $userId already exists.
  424. */
  425. function exists($userId)
  426. {
  427. $users = $this->listUsers();
  428. if (is_a($users, 'PEAR_Error')) {
  429. return $users;
  430. }
  431. return in_array($userId, $users);
  432. }
  433. /**
  434. * Automatic authentication.
  435. *
  436. * @abstract
  437. *
  438. * @return boolean Whether or not the user is authenticated automatically.
  439. */
  440. function transparent()
  441. {
  442. return false;
  443. }
  444. /**
  445. * Checks if there is a session with valid auth information. for the
  446. * specified user. If there isn't, but the configured Auth driver supports
  447. * transparent authentication, then we try that.
  448. *
  449. * @param string $realm The authentication realm to check.
  450. *
  451. * @return boolean Whether or not the user is authenticated.
  452. */
  453. function isAuthenticated($realm = null)
  454. {
  455. if (isset($_SESSION['__auth'])) {
  456. if (!empty($_SESSION['__auth']['authenticated']) &&
  457. !empty($_SESSION['__auth']['userId']) &&
  458. ($_SESSION['__auth']['realm'] == $realm)) {
  459. if (!Auth::_checkSessionIP()) {
  460. Auth::_setAuthError(AUTH_REASON_SESSIONIP);
  461. return false;
  462. } elseif (!Auth::_checkBrowserString()) {
  463. Auth::_setAuthError(AUTH_REASON_BROWSER);
  464. return false;
  465. } else {
  466. return true;
  467. }
  468. }
  469. }
  470. // Try transparent authentication now.
  471. $auth = &Auth::singleton($GLOBALS['conf']['auth']['driver']);
  472. if ($auth->hasCapability('transparent') && $auth->transparent()) {
  473. return Auth::isAuthenticated($realm);
  474. }
  475. return false;
  476. }
  477. /**
  478. * Returns the currently logged in user, if there is one.
  479. *
  480. * @return mixed The userId of the current user, or false if no user is
  481. * logged in.
  482. */
  483. function getAuth()
  484. {
  485. if (isset($_SESSION['__auth'])) {
  486. if (!empty($_SESSION['__auth']['authenticated']) &&
  487. !empty($_SESSION['__auth']['userId'])) {
  488. return $_SESSION['__auth']['userId'];
  489. }
  490. }
  491. return false;
  492. }
  493. /**
  494. * Return whether the authentication backend requested a password change.
  495. *
  496. * @return boolean Whether the backend requested a password change.
  497. */
  498. function isPasswordChangeRequested()
  499. {
  500. if (isset($_SESSION['__auth']) &&
  501. !empty($_SESSION['__auth']['authenticated']) &&
  502. !empty($_SESSION['__auth']['changeRequested'])) {
  503. return true;
  504. }
  505. return false;
  506. }
  507. /**
  508. * Returns the curently logged-in user without any domain information
  509. * (e.g., bob@example.com would be returned as 'bob').
  510. *
  511. * @return mixed The user ID of the current user, or false if no user
  512. * is logged in.
  513. */
  514. function getBareAuth()
  515. {
  516. $user = Auth::getAuth();
  517. if ($user) {
  518. $pos = strpos($user, '@');
  519. if ($pos !== false) {
  520. $user = substr($user, 0, $pos);
  521. }
  522. }
  523. return $user;
  524. }
  525. /**
  526. * Returns the domain of currently logged-in user (e.g., bob@example.com
  527. * would be returned as 'example.com').
  528. *
  529. * @since Horde 3.0.6
  530. *
  531. * @return mixed The domain suffix of the current user, or false.
  532. */
  533. function getAuthDomain()
  534. {
  535. if ($user = Auth::getAuth()) {
  536. $pos = strpos($user, '@');
  537. if ($pos !== false) {
  538. return substr($user, $pos + 1);
  539. }
  540. }
  541. return false;
  542. }
  543. /**
  544. * Returns the requested credential for the currently logged in user, if
  545. * present.
  546. *
  547. * @param string $credential The credential to retrieve.
  548. *
  549. * @return mixed The requested credential, or false if no user is
  550. * logged in.
  551. */
  552. function getCredential($credential)
  553. {
  554. if (!empty($_SESSION['__auth']) &&
  555. !empty($_SESSION['__auth']['authenticated'])) {
  556. require_once 'Horde/Secret.php';
  557. $credentials = Secret::read(Secret::getKey('auth'), $_SESSION['__auth']['credentials']);
  558. $credentials = @unserialize($credentials);
  559. } else {
  560. return false;
  561. }
  562. if (is_array($credentials) &&
  563. isset($credentials[$credential])) {
  564. return $credentials[$credential];
  565. } else {
  566. return false;
  567. }
  568. }
  569. /**
  570. * Sets the requested credential for the currently logged in user.
  571. *
  572. * @param string $credential The credential to set.
  573. * @param string $value The value to set the credential to.
  574. */
  575. function setCredential($credential, $value)
  576. {
  577. if (!empty($_SESSION['__auth']) &&
  578. !empty($_SESSION['__auth']['authenticated'])) {
  579. require_once 'Horde/Secret.php';
  580. $credentials = @unserialize(Secret::read(Secret::getKey('auth'), $_SESSION['__auth']['credentials']));
  581. if (is_array($credentials)) {
  582. $credentials[$credential] = $value;
  583. } else {
  584. $credentials = array($credential => $value);
  585. }
  586. $_SESSION['__auth']['credentials'] = Secret::write(Secret::getKey('auth'), serialize($credentials));
  587. }
  588. }
  589. /**
  590. * Sets a variable in the session saying that authorization has succeeded,
  591. * note which userId was authorized, and note when the login took place.
  592. *
  593. * If a user name hook was defined in the configuration, it gets applied
  594. * to the userId at this point.
  595. *
  596. * @param string $userId The userId who has been authorized.
  597. * @param array $credentials The credentials of the user.
  598. * @param string $realm The authentication realm to use.
  599. * @param boolean $changeRequested Whether to request that the user change
  600. * their password.
  601. */
  602. function setAuth($userId, $credentials, $realm = null, $changeRequested = false)
  603. {
  604. $userId = trim($userId);
  605. $userId = Auth::addHook($userId);
  606. if (!empty($GLOBALS['conf']['hooks']['postauthenticate'])) {
  607. if (!Horde::callHook('_horde_hook_postauthenticate', array($userId, $credentials, $realm), 'horde', false)) {
  608. if ($this->_getAuthError() != AUTH_REASON_MESSAGE) {
  609. $this->_setAuthError(AUTH_REASON_FAILED);
  610. }
  611. return false;
  612. }
  613. }
  614. /* If we're already set with this userId, don't continue. */
  615. if (isset($_SESSION['__auth']['userId']) &&
  616. $_SESSION['__auth']['userId'] == $userId) {
  617. return true;
  618. }
  619. /* Clear any existing info. */
  620. $this->clearAuth($realm);
  621. require_once 'Horde/Secret.php';
  622. $credentials = Secret::write(Secret::getKey('auth'), serialize($credentials));
  623. if (!empty($realm)) {
  624. $userId .= '@' . $realm;
  625. }
  626. $_SESSION['__auth'] = array(
  627. 'authenticated' => true,
  628. 'userId' => $userId,
  629. 'credentials' => $credentials,
  630. 'realm' => $realm,
  631. 'timestamp' => time(),
  632. 'remote_addr' => isset($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : null,
  633. 'browser' => $GLOBALS['browser']->getAgentString(),
  634. 'changeRequested' => $changeRequested
  635. );
  636. /* Reload preferences for the new user. */
  637. $GLOBALS['registry']->loadPrefs();
  638. NLS::setLang($GLOBALS['prefs']->getValue('language'));
  639. /* Fetch the user's last login time. */
  640. $old_login = @unserialize($GLOBALS['prefs']->getValue('last_login'));
  641. /* Display it, if we have a notification object and the
  642. * show_last_login preference is active. */
  643. if (isset($GLOBALS['notification']) && $GLOBALS['prefs']->getValue('show_last_login')) {
  644. if (empty($old_login['time'])) {
  645. $GLOBALS['notification']->push(_("Last login: Never"), 'horde.message');
  646. } else {
  647. if (empty($old_login['host'])) {
  648. $GLOBALS['notification']->push(sprintf(_("Last login: %s"), strftime('%c', $old_login['time'])), 'horde.message');
  649. } else {
  650. $GLOBALS['notification']->push(sprintf(_("Last login: %s from %s"), strftime('%c', $old_login['time']), $old_login['host']), 'horde.message');
  651. }
  652. }
  653. }
  654. /* Set the user's last_login information. */
  655. $host = empty($_SERVER['HTTP_X_FORWARDED_FOR'])
  656. ? $_SERVER['REMOTE_ADDR']
  657. : $_SERVER['HTTP_X_FORWARDED_FOR'];
  658. if ((@include_once 'Net/DNS.php')) {
  659. $resolver = new Net_DNS_Resolver();
  660. $resolver->retry = isset($GLOBALS['conf']['dns']['retry']) ? $GLOBALS['conf']['dns']['retry'] : 1;
  661. $resolver->retrans = isset($GLOBALS['conf']['dns']['retrans']) ? $GLOBALS['conf']['dns']['retrans'] : 1;
  662. $response = $resolver->query($host, 'PTR');
  663. $ptrdname = $response ? $response->answer[0]->ptrdname : $host;
  664. } else {
  665. $ptrdname = @gethostbyaddr($host);
  666. }
  667. $last_login = array('time' => time(),
  668. 'host' => $ptrdname);
  669. $GLOBALS['prefs']->setValue('last_login', serialize($last_login));
  670. if ($changeRequested) {
  671. $GLOBALS['notification']->push(_("Your password has expired."),
  672. 'horde.message');
  673. if ($this->hasCapability('update')) {
  674. /* A bit of a kludge. URL is set from the login screen, but
  675. * we aren't completely certain we got here from the login
  676. * screen. So any screen which calls setAuth() which has a
  677. * url will end up going there. Should be OK. */
  678. $url_param = Util::getFormData('url');
  679. if ($url_param) {
  680. $url = Horde::url(Util::removeParameter($url_param,
  681. session_name()),
  682. true);
  683. $return_to = $GLOBALS['registry']->get('webroot', 'horde') .
  684. '/index.php';
  685. $return_to = Util::addParameter($return_to, 'url', $url);
  686. } else {
  687. $return_to = Horde::url($GLOBALS['registry']->get('webroot', 'horde')
  688. . '/index.php');
  689. }
  690. $url = Horde::applicationUrl('services/changepassword.php');
  691. $url = Util::addParameter($url,
  692. array('return_to' => $return_to),
  693. null, false);
  694. header('Location: ' . $url);
  695. exit;
  696. }
  697. }
  698. return true;
  699. }
  700. /**
  701. * Clears any authentication tokens in the current session.
  702. *
  703. * @param string $realm The authentication realm to clear.
  704. */
  705. function clearAuth($realm = null)
  706. {
  707. if (!empty($realm) && isset($_SESSION['__auth'][$realm])) {
  708. $_SESSION['__auth'][$realm] = array();
  709. $_SESSION['__auth'][$realm]['authenticated'] = false;
  710. } elseif (isset($_SESSION['__auth'])) {
  711. $_SESSION['__auth'] = array();
  712. $_SESSION['__auth']['authenticated'] = false;
  713. }
  714. /* Remove the user's cached preferences if they are present. */
  715. if (isset($GLOBALS['registry'])) {
  716. $GLOBALS['registry']->unloadPrefs();
  717. }
  718. }
  719. /**
  720. * Is the current user an administrator?
  721. *
  722. * @param string $permission Allow users with this permission admin access
  723. * in the current context.
  724. * @param integer $permlevel The level of permissions to check for
  725. * (PERMS_EDIT, PERMS_DELETE, etc). Defaults
  726. * to PERMS_EDIT.
  727. * @param string $user The user to check. Defaults to Auth::getAuth().
  728. *
  729. * @return boolean Whether or not this is an admin user.
  730. */
  731. function isAdmin($permission = null, $permlevel = null, $user = null)
  732. {
  733. if (is_null($user)) {
  734. $user = Auth::getAuth();
  735. }
  736. if ($user
  737. && @is_array($GLOBALS['conf']['auth']['admins'])
  738. && in_array($user, $GLOBALS['conf']['auth']['admins'])) {
  739. return true;
  740. }
  741. if ($user) {
  742. $auth = &Auth::singleton($GLOBALS['conf']['auth']['driver']);
  743. if ($auth->hasCapability('admins')
  744. && $auth->_isAdmin($permission, $permlevel, $user)) {
  745. return true;
  746. }
  747. }
  748. if (!is_null($permission)) {
  749. if (is_null($permlevel)) {
  750. $permlevel = PERMS_EDIT;
  751. }
  752. return $GLOBALS['perms']->hasPermission($permission, $user, $permlevel);
  753. }
  754. return false;
  755. }
  756. /**
  757. * Applies a hook defined by the function _username_hook_frombackend() to
  758. * the given user name if this function exists and user hooks are enabled.
  759. *
  760. * This method should be called if a authentication backend's user name
  761. * needs to be converted to a (unique) Horde user name. The backend's user
  762. * name is what the user sees and uses, but internally we use the Horde
  763. * user name.
  764. *
  765. * @param string $userId The authentication backend's user name.
  766. *
  767. * @return string The internal Horde user name.
  768. */
  769. function addHook($userId)
  770. {
  771. if (!empty($GLOBALS['conf']['hooks']['username'])) {
  772. $newId = Horde::callHook('_username_hook_frombackend', array($userId));
  773. if (!is_a($newId, 'PEAR_Error')) {
  774. return $newId;
  775. }
  776. }
  777. return $userId;
  778. }
  779. /**
  780. * Applies a hook defined by the function _username_hook_tobackend() to
  781. * the given user name if this function exists and user hooks are enabled.
  782. *
  783. * This method should be called if a Horde user name needs to be converted
  784. * to an authentication backend's user name or displayed to the user. The
  785. * backend's user name is what the user sees and uses, but internally we
  786. * use the Horde user name.
  787. *
  788. * @param string $userId The internal Horde user name.
  789. *
  790. * @return string The authentication backend's user name.
  791. */
  792. function removeHook($userId)
  793. {
  794. if (!empty($GLOBALS['conf']['hooks']['username'])) {
  795. $newId = Horde::callHook('_username_hook_tobackend', array($userId));
  796. if (!is_a($newId, 'PEAR_Error')) {
  797. return $newId;
  798. }
  799. }
  800. return $userId;
  801. }
  802. /**
  803. * Queries the current Auth object to find out if it supports the given
  804. * capability.
  805. *
  806. * @param string $capability The capability to test for.
  807. *
  808. * @return boolean Whether or not the capability is supported.
  809. */
  810. function hasCapability($capability)
  811. {
  812. return !empty($this->capabilities[$capability]);
  813. }
  814. /**
  815. * Returns the URI of the login screen for the current authentication
  816. * method.
  817. *
  818. * @param string $app The application to use.
  819. * @param string $url The URL to redirect to after login.
  820. *
  821. * @return string The login screen URI.
  822. */
  823. function getLoginScreen($app = 'horde', $url = '')
  824. {
  825. $auth = &Auth::singleton($GLOBALS['conf']['auth']['driver']);
  826. return $auth->_getLoginScreen($app, $url);
  827. }
  828. /**
  829. * Returns the named parameter for the current auth driver.
  830. *
  831. * @param string $param The parameter to fetch.
  832. *
  833. * @return string The parameter's value.
  834. */
  835. function getParam($param)
  836. {
  837. return isset($this->_params[$param]) ? $this->_params[$param] : null;
  838. }
  839. /**
  840. * Returns the name of the authentication provider.
  841. *
  842. * @param string $driver Used by recursive calls when untangling composite
  843. * auth.
  844. * @param array $params Used by recursive calls when untangling composite
  845. * auth.
  846. *
  847. * @return string The name of the driver currently providing
  848. * authentication.
  849. */
  850. function getProvider($driver = null, $params = null)
  851. {
  852. if (is_null($driver)) {
  853. $driver = $GLOBALS['conf']['auth']['driver'];
  854. }
  855. if (is_null($params)) {
  856. $params = Horde::getDriverConfig('auth',
  857. is_array($driver) ? $driver[1] : $driver);
  858. }
  859. if ($driver == 'application') {
  860. return isset($params['app']) ? $params['app'] : 'application';
  861. } elseif ($driver == 'composite') {
  862. if (($login_driver = Auth::_getDriverByParam('loginscreen_switch', $params)) &&
  863. !empty($params['drivers'][$login_driver])) {
  864. return Auth::getProvider($params['drivers'][$login_driver]['driver'],
  865. isset($params['drivers'][$login_driver]['params']) ? $params['drivers'][$login_driver]['params'] : null);
  866. }
  867. return 'composite';
  868. } else {
  869. return $driver;
  870. }
  871. }
  872. /**
  873. * Returns the logout reason.
  874. *
  875. * @return string One of the logout reasons (see the AUTH_LOGOUT_*
  876. * constants for the valid reasons). Returns null if there
  877. * is no logout reason present.
  878. */
  879. function getLogoutReason()
  880. {
  881. if (isset($GLOBALS['__autherror']['type'])) {
  882. return $GLOBALS['__autherror']['type'];
  883. } else {
  884. return Util::getFormData(AUTH_REASON_PARAM);
  885. }
  886. }
  887. /**
  888. * Returns the status string to use for logout messages.
  889. *
  890. * @return string The logout reason string.
  891. */
  892. function getLogoutReasonString()
  893. {
  894. switch (Auth::getLogoutReason()) {
  895. case AUTH_REASON_SESSION:
  896. $text = sprintf(_("Your %s session has expired. Please login again."), $GLOBALS['registry']->get('name'));
  897. break;
  898. case AUTH_REASON_SESSIONIP:
  899. $text = sprintf(_("Your Internet Address has changed since the beginning of your %s session. To protect your security, you must login again."), $GLOBALS['registry']->get('name'));
  900. break;
  901. case AUTH_REASON_BROWSER:
  902. $text = sprintf(_("Your browser appears to have changed since the beginning of your %s session. To protect your security, you must login again."), $GLOBALS['registry']->get('name'));
  903. break;
  904. case AUTH_REASON_LOGOUT:
  905. $text = _("You have been logged out.");
  906. break;
  907. case AUTH_REASON_FAILED:
  908. $text = _("Login failed.");
  909. break;
  910. case AUTH_REASON_BADLOGIN:
  911. $text = _("Login failed because your username or password was entered incorrectly.");
  912. break;
  913. case AUTH_REASON_EXPIRED:
  914. $text = _("Your login has expired.");
  915. break;
  916. case AUTH_REASON_MESSAGE:
  917. if (isset($GLOBALS['__autherror']['msg'])) {
  918. $text = $GLOBALS['__autherror']['msg'];
  919. } else {
  920. $text = Util::getFormData(AUTH_REASON_MSG_PARAM);
  921. }
  922. break;
  923. default:
  924. $text = '';
  925. break;
  926. }
  927. return $text;
  928. }
  929. /**
  930. * Generates the correct parameters to pass to the given logout URL.
  931. *
  932. * If no reason/msg is passed in, use the current global authentication
  933. * error message.
  934. *
  935. * @param string $url The URL to redirect to.
  936. * @param string $reason The reason for logout.
  937. * @param string $msg If reason is AUTH_REASON_MESSAGE, the message to
  938. * display to the user.
  939. * @return string The formatted URL
  940. */
  941. function addLogoutParameters($url, $reason = null, $msg = null)
  942. {
  943. $params = array('horde_logout_token' => Horde::getRequestToken('horde.logout'));
  944. if (isset($GLOBALS['registry'])) {
  945. $params['app'] = $GLOBALS['registry']->getApp();
  946. }
  947. if (is_null($reason)) {
  948. $reason = Auth::getLogoutReason();
  949. }
  950. if ($reason) {
  951. $params[AUTH_REASON_PARAM] = $reason;
  952. if ($reason == AUTH_REASON_MESSAGE) {
  953. if (is_null($msg)) {
  954. $msg = Auth::getLogoutReasonString();
  955. }
  956. $params[AUTH_REASON_MSG_PARAM] = $msg;
  957. }
  958. }
  959. return Util::addParameter($url, $params, null, false);
  960. }
  961. /**
  962. * Reads session data to determine if it contains Horde authentication
  963. * credentials.
  964. *
  965. * @since Horde 3.2
  966. *
  967. * @param string $session_data The session data.
  968. * @param boolean $info Return session information. The following
  969. * information is returned: userid, realm,
  970. * timestamp, remote_addr, browser.
  971. *
  972. * @return array An array of the user's sesion information if
  973. * authenticated or false. The following information is
  974. * returned: userid, realm, timestamp, remote_addr, browser.
  975. */
  976. function readSessionData($session_data)
  977. {
  978. if (empty($session_data)) {
  979. return false;
  980. }
  981. $pos = strpos($session_data, '__auth|');
  982. if ($pos === false) {
  983. return false;
  984. }
  985. $endpos = $pos + 7;
  986. $old_error = error_reporting(0);
  987. while ($endpos !== false) {
  988. $endpos = strpos($session_data, '|', $endpos);
  989. $data = unserialize(substr($session_data, $pos + 7, $endpos));
  990. if (is_array($data)) {
  991. error_reporting($old_error);
  992. if (empty($data['authenticated'])) {
  993. return false;
  994. }
  995. return array(
  996. 'userid' => $data['userId'],
  997. 'realm' => $data['realm'],
  998. 'timestamp' => $data['timestamp'],
  999. 'remote_addr' => $data['remote_addr'],
  1000. 'browser' => $data['browser']
  1001. );
  1002. }
  1003. ++$endpos;
  1004. }
  1005. return false;
  1006. }
  1007. /**
  1008. * Returns the URI of the login screen for this authentication object.
  1009. *
  1010. * @access private
  1011. *
  1012. * @param string $app The application to use.
  1013. * @param string $url The URL to redirect to after login.
  1014. *
  1015. * @return string The login screen URI.
  1016. */
  1017. function _getLoginScreen($app = 'horde', $url = '')
  1018. {
  1019. $login = Horde::url($GLOBALS['registry']->get('webroot', $app) . '/login.php', true);
  1020. if (!empty($url)) {
  1021. $login = Util::addParameter($login, 'url', $url);
  1022. }
  1023. return $login;
  1024. }
  1025. /**
  1026. * Authentication stub.
  1027. *
  1028. * @abstract
  1029. * @access protected
  1030. *
  1031. * @return boolean False.
  1032. */
  1033. function _authenticate()
  1034. {
  1035. return false;
  1036. }
  1037. /**
  1038. * Driver-level admin check stub.
  1039. *
  1040. * @abstract
  1041. * @access protected
  1042. *
  1043. * @return boolean False.
  1044. */
  1045. function _isAdmin($permission = null, $permlevel = null, $user = null)
  1046. {
  1047. return false;
  1048. }
  1049. /**
  1050. * Sets the error message for an invalid authentication.
  1051. *
  1052. * @access private
  1053. *
  1054. * @param string $type The type of error (AUTH_REASON constant).
  1055. * @param string $msg The error message/reason for invalid
  1056. * authentication.
  1057. */
  1058. function _setAuthError($type, $msg = null)
  1059. {
  1060. $GLOBALS['__autherror'] = array();
  1061. $GLOBALS['__autherror']['type'] = $type;
  1062. $GLOBALS['__autherror']['msg'] = $msg;
  1063. }
  1064. /**
  1065. * Returns the error type for an invalid authentication or false on error.
  1066. *
  1067. * @access private
  1068. *
  1069. * @return mixed Error type or false on error
  1070. */
  1071. function _getAuthError()
  1072. {
  1073. if (isset($GLOBALS['__autherror']['type'])) {
  1074. return $GLOBALS['__autherror']['type'];
  1075. }
  1076. return false;
  1077. }
  1078. /**
  1079. * Returns the appropriate authentication driver, if any, selecting by the
  1080. * specified parameter.
  1081. *
  1082. * @access private
  1083. *
  1084. * @param string $name The parameter name.
  1085. * @param array $params The parameter list.
  1086. * @param string $driverparams A list of parameters to pass to the driver.
  1087. *
  1088. * @return mixed Return value or called user func or null if unavailable
  1089. */
  1090. function _getDriverByParam($name, $params, $driverparams = array())
  1091. {
  1092. if (isset($params[$name]) &&
  1093. function_exists($params[$name])) {
  1094. return call_user_func_array($params[$name], $driverparams);
  1095. }
  1096. return null;
  1097. }
  1098. /**
  1099. * Performs check on session to see if IP Address has changed since the
  1100. * last access.
  1101. *
  1102. * @access private
  1103. *
  1104. * @return boolean True if IP Address is the same (or the check is
  1105. * disabled), false if the address has changed.
  1106. */
  1107. function _checkSessionIP()
  1108. {
  1109. return (empty($GLOBALS['conf']['auth']['checkip']) ||
  1110. (isset($_SESSION['__auth']['remote_addr']) && $_SESSION['__auth']['remote_addr'] == $_SERVER['REMOTE_ADDR']));
  1111. }
  1112. /**
  1113. * Performs check on session to see if browser string has changed since
  1114. * the last access.
  1115. *
  1116. * @access private
  1117. *
  1118. * @return boolean True if browser string is the same, false if the
  1119. * string has changed.
  1120. */
  1121. function _checkBrowserString()
  1122. {
  1123. return (empty($GLOBALS['conf']['auth']['checkbrowser']) ||
  1124. $_SESSION['__auth']['browser'] == $GLOBALS['browser']->getAgentString());
  1125. }
  1126. /**
  1127. * Converts to allowed 64 characters for APRMD5 passwords.
  1128. *
  1129. * @access private
  1130. *
  1131. * @param string $value
  1132. * @param integer $count
  1133. *
  1134. * @return string $value converted to the 64 MD5 characters.
  1135. */
  1136. function _toAPRMD5($value, $count)
  1137. {
  1138. /* 64 characters that are valid for APRMD5 passwords. */
  1139. $APRMD5 = './0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
  1140. $aprmd5 = '';
  1141. $count = abs($count);
  1142. while (--$count) {
  1143. $aprmd5 .= $APRMD5[$value & 0x3f];
  1144. $value >>= 6;
  1145. }
  1146. return $aprmd5;
  1147. }
  1148. /**
  1149. * Attempts to return a concrete Auth instance based on $driver.
  1150. *
  1151. * @param mixed $driver The type of concrete Auth subclass to return. This
  1152. * is based on the storage driver ($driver). The code
  1153. * is dynamically included. If $driver is an array,
  1154. * then we will look in $driver[0]/lib/Auth/ for the
  1155. * subclass implementation named $driver[1].php.
  1156. * @param array $params A hash containing any additional configuration or
  1157. * connection parameters a subclass might need.
  1158. *
  1159. * @return Auth The newly created concrete Auth instance, or false on an
  1160. * error.
  1161. */
  1162. function factory($driver, $params = null)
  1163. {
  1164. if (is_array($driver)) {
  1165. $app = $driver[0];
  1166. $driver = $driver[1];
  1167. }
  1168. $driver = basename($driver);
  1169. if (empty($driver) || ($driver == 'none')) {
  1170. return new Auth();
  1171. }
  1172. if (is_null($params)) {
  1173. $params = Horde::getDriverConfig('auth', $driver);
  1174. }
  1175. $class = 'Auth_' . $driver;
  1176. $include_error = '';
  1177. if (!class_exists($class)) {
  1178. $oldTrackErrors = ini_set('track_errors', 1);
  1179. if (!empty($app)) {
  1180. include $GLOBALS['registry']->get('fileroot', $app) . '/lib/Auth/' . $driver . '.php';
  1181. } else {
  1182. include 'Horde/Auth/' . $driver . '.php';
  1183. }
  1184. if (isset($php_errormsg)) {
  1185. $include_error = $php_errormsg;
  1186. }
  1187. ini_set('track_errors', $oldTrackErrors);
  1188. }
  1189. if (class_exists($class)) {
  1190. $auth = new $class($params);
  1191. } else {
  1192. $auth = PEAR::raiseError('Auth Driver (' . $class . ') not found' . ($include_error ? ': ' . $include_error : '') . '.');
  1193. }
  1194. return $auth;
  1195. }
  1196. /**
  1197. * Attempts to return a reference to a concrete Auth instance based on
  1198. * $driver. It will only create a new instance if no Auth instance with
  1199. * the same parameters currently exists.
  1200. *
  1201. * This should be used if multiple authentication sources (and, thus,
  1202. * multiple Auth instances) are required.
  1203. *
  1204. * This method must be invoked as: $var = &Auth::singleton()
  1205. *
  1206. * @param string $driver The type of concrete Auth subclass to return.
  1207. * This is based on the storage driver ($driver).
  1208. * The code is dynamically included.
  1209. * @param array $params A hash containing any additional configuration or
  1210. * connection parameters a subclass might need.
  1211. *
  1212. * @return Auth The concrete Auth reference, or false on an error.
  1213. */
  1214. function &singleton($driver, $params = null)
  1215. {
  1216. static $instances = array();
  1217. if (is_null($params)) {
  1218. $params = Horde::getDriverConfig('auth',
  1219. is_array($driver) ? $driver[1] : $driver);
  1220. }
  1221. $signature = serialize(array($driver, $params));
  1222. if (empty($instances[$signature])) {
  1223. $instances[$signature] = Auth::factory($driver, $params);
  1224. }
  1225. return $instances[$signature];
  1226. }
  1227. }