1: <?php
2: /**
3: * The Horde_Core_Auth_Application class provides application-specific
4: * authentication built on top of the horde/Auth API.
5: *
6: * Copyright 2002-2012 Horde LLC (http://www.horde.org/)
7: *
8: * See the enclosed file COPYING for license information (LGPL). If you did
9: * not receive this file, see http://opensource.org/licenses/lgpl-2.1.php
10: *
11: * @author Chuck Hagenbuch <chuck@horde.org>
12: * @author Michael Slusarz <slusarz@horde.org>
13: * @category Horde
14: * @license http://opensource.org/licenses/lgpl-2.1.php LGPL
15: * @package Core
16: */
17: class Horde_Core_Auth_Application extends Horde_Auth_Base
18: {
19: /**
20: * Authentication failure reasons (additions to Horde_Auth:: reasons).
21: *
22: * <pre>
23: * REASON_BROWSER - A browser change was detected
24: * REASON_SESSIONIP - Logout due to change of IP address during session
25: * </pre>
26: */
27: const REASON_BROWSER = 100;
28: const REASON_SESSIONIP = 101;
29:
30: /**
31: * Application for authentication.
32: *
33: * @var string
34: */
35: protected $_app = 'horde';
36:
37: /**
38: * The list of application capabilities.
39: *
40: * @var array
41: */
42: protected $_appCapabilities;
43:
44: /**
45: * The base auth driver, used for Horde authentication.
46: *
47: * @var Horde_Auth_Base
48: */
49: protected $_base;
50:
51: /**
52: * The view mode, used to determine if we show dynamic, mobile, traditional
53: * views.
54: *
55: * @var string
56: */
57: protected $_mode = 'auto';
58:
59: /**
60: * Available capabilities.
61: *
62: * @var array
63: */
64: protected $_capabilities = array(
65: 'add',
66: 'authenticate',
67: 'exists',
68: 'list',
69: 'remove',
70: 'resetpassword',
71: 'transparent',
72: 'update',
73: 'validate'
74: );
75:
76: /**
77: * Constructor.
78: *
79: * @param array $params Required parameters:
80: * <pre>
81: * 'app' - (string) The application which is providing authentication.
82: * 'base' - (Horde_Auth_Base) The base Horde_Auth driver. Only needed if
83: * 'app' is 'horde'.
84: * </pre>
85: *
86: * @throws InvalidArgumentException
87: */
88: public function __construct(array $params = array())
89: {
90: if (!isset($params['app'])) {
91: throw new InvalidArgumentException('Missing app parameter.');
92: }
93: $this->_app = $params['app'];
94: unset($params['app']);
95:
96: if ($this->_app == 'horde') {
97: if (!isset($params['base'])) {
98: throw new InvalidArgumentException('Missing base parameter.');
99: }
100:
101: $this->_base = $params['base'];
102: unset($params['base']);
103: }
104:
105: parent::__construct($params);
106: }
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 user ID 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: *
118: * @return boolean Whether or not the credentials are valid.
119: */
120: public function authenticate($userId, $credentials, $login = true)
121: {
122: try {
123: list($userId, $credentials) = $this->runHook(trim($userId), $credentials, 'preauthenticate', 'authenticate');
124: } catch (Horde_Auth_Exception $e) {
125: return false;
126: }
127:
128: if ($this->_base) {
129: if (!$this->_base->authenticate($userId, $credentials, $login)) {
130: return false;
131: }
132: } elseif (!parent::authenticate($userId, $credentials, $login)) {
133: return false;
134: }
135:
136: /* Remember the user's mode choice, if applicable */
137: if (!empty($credentials['mode'])) {
138: $this->_mode = $credentials['mode'];
139: }
140:
141: return $this->_setAuth();
142: }
143:
144: /**
145: * Find out if a set of login credentials are valid.
146: *
147: * @param string $userId The user ID to check.
148: * @param array $credentials The credentials to use. This object will
149: * always be available in the 'auth_ob' key.
150: *
151: * @throws Horde_Auth_Exception
152: */
153: protected function _authenticate($userId, $credentials)
154: {
155: if (!$this->hasCapability('authenticate')) {
156: throw new Horde_Auth_Exception($this->_app . ' does not provide an authenticate() method.');
157: }
158:
159: $credentials['auth_ob'] = $this;
160:
161: $GLOBALS['registry']->callAppMethod($this->_app, 'authAuthenticate', array('args' => array($userId, $credentials), 'noperms' => true));
162: }
163:
164: /**
165: * Checks for triggers that may invalidate the current auth.
166: * These triggers are independent of the credentials.
167: *
168: * @return boolean True if the results of authenticate() are still valid.
169: */
170: public function validateAuth()
171: {
172: if ($this->_base) {
173: return $this->_base->validateAuth();
174: }
175:
176: return $this->hasCapability('validate')
177: ? $GLOBALS['registry']->callAppMethod($this->_app, 'authValidate', array('noperms' => true))
178: : parent::validateAuth();
179: }
180:
181: /**
182: * Add a set of authentication credentials.
183: *
184: * @param string $userId The user ID to add.
185: * @param array $credentials The credentials to use.
186: *
187: * @throws Horde_Auth_Exception
188: */
189: public function addUser($userId, $credentials)
190: {
191: if ($this->_base) {
192: $this->_base->addUser($userId, $credentials);
193: return;
194: }
195:
196: if ($this->hasCapability('add')) {
197: $GLOBALS['registry']->callAppMethod($this->_app, 'authAddUser', array('args' => array($userId, $credentials)));
198: } else {
199: parent::addUser($userId, $credentials);
200: }
201: }
202: /**
203: * Locks a user indefinitely or for a specified time
204: *
205: * @param string $userId The userId to lock.
206: * @param integer $time The duration in seconds, 0 = permanent
207: *
208: * @throws Horde_Auth_Exception
209: */
210: public function lockUser($userId, $time = 0)
211: {
212: if ($this->_base) {
213: $this->_base->lockUser($userId, $time);
214: return;
215: }
216:
217: if ($this->hasCapability('lock')) {
218: $GLOBALS['registry']->callAppMethod($this->_app, 'authLockUser', array('args' => array($userId, $time)));
219: } else {
220: parent::lockUser($userId, $time);
221: }
222: }
223:
224: /**
225: * Unlocks a user and optionally resets bad login count
226: *
227: * @param string $userId The userId to unlock.
228: * @param boolean $resetBadLogins Reset bad login counter, default no.
229: *
230: * @throws Horde_Auth_Exception
231: */
232: public function unlockUser($userId, $resetBadLogins = false)
233: {
234: if ($this->_base) {
235: $this->_base->unlockUser($userId, $resetBadLogins);
236: return;
237: }
238:
239: if ($this->hasCapability('lock')) {
240: $GLOBALS['registry']->callAppMethod($this->_app, 'authUnlockUser', array('args' => array($userId, $resetBadLogins)));
241: } else {
242: parent::unlockUser($userId, $resetBadLogins);
243: }
244: }
245:
246: /**
247: * Checks if $userId is currently locked.
248: *
249: * @param string $userId The userId to check.
250: * @param boolean $show_details Toggle array format with timeout.
251: *
252: * @throws Horde_Auth_Exception
253: */
254: public function isLocked($userId, $show_details = false)
255: {
256: if ($this->_base) {
257: return $this->_base->isLocked($userId, $show_details);
258: }
259:
260: if ($this->hasCapability('lock')) {
261: return $GLOBALS['registry']->callAppMethod($this->_app, 'authIsLocked', array('args' => array($userId, $show_details)));
262: } else {
263: return parent::isLocked($userId, $show_details);
264: }
265: }
266: /**
267: * Update a set of authentication credentials.
268: *
269: * @param string $oldID The old user ID.
270: * @param string $newID The new user ID.
271: * @param array $credentials The new credentials
272: *
273: * @throws Horde_Auth_Exception
274: */
275: public function updateUser($oldID, $newID, $credentials)
276: {
277: if ($this->_base) {
278: $this->_base->updateUser($oldID, $newID, $credentials);
279: return;
280: }
281:
282: if ($this->hasCapability('update')) {
283: $GLOBALS['registry']->callAppMethod($this->_app, 'authUpdateUser', array('args' => array($oldID, $newID, $credentials)));
284: } else {
285: parent::updateUser($oldID, $newID, $credentials);
286: }
287: }
288:
289: /**
290: * Delete a set of authentication credentials.
291: *
292: * @param string $userId The user ID to delete.
293: *
294: * @throws Horde_Auth_Exception
295: */
296: public function removeUser($userId)
297: {
298: if ($this->_base) {
299: $this->_base->removeUser($userId);
300: } else {
301: if ($this->hasCapability('remove')) {
302: $GLOBALS['registry']->callAppMethod($this->_app, 'authRemoveUser', array('args' => array($userId)));
303: } else {
304: parent::removeUser($userId);
305: }
306: }
307: }
308:
309: /**
310: * List all users in the system.
311: *
312: * @return array The array of user IDs.
313: * @throws Horde_Auth_Exception
314: */
315: public function listUsers()
316: {
317: if ($this->_base) {
318: return $this->_base->listUsers();
319: }
320:
321: return $this->hasCapability('list')
322: ? $GLOBALS['registry']->callAppMethod($this->_app, 'authUserList')
323: : parent::listUsers();
324: }
325:
326: /**
327: * Checks if a user ID exists in the system.
328: *
329: * @param string $userId User ID to check.
330: *
331: * @return boolean Whether or not the user ID already exists.
332: */
333: public function exists($userId)
334: {
335: if ($this->_base) {
336: return $this->_base->exists($userId);
337: }
338:
339: return $this->hasCapability('exists')
340: ? $GLOBALS['registry']->callAppMethod($this->_app, 'authUserExists', array('args' => array($userId)))
341: : parent::exists($userId);
342: }
343:
344: /**
345: * Automatic authentication.
346: *
347: * @return boolean Whether or not the client is allowed.
348: * @throws Horde_Auth_Exception
349: */
350: public function transparent()
351: {
352: global $registry;
353:
354: if (!($userId = $this->getCredential('userId'))) {
355: $userId = $registry->getAuth();
356: }
357: if (!($credentials = $this->getCredential('credentials'))) {
358: $credentials = $registry->getAuthCredential();
359: }
360:
361: list($userId, $credentials) = $this->runHook($userId, $credentials, 'preauthenticate', 'transparent');
362:
363: $this->setCredential('userId', $userId);
364: $this->setCredential('credentials', $credentials);
365:
366: if ($this->_base) {
367: $result = $this->_base->transparent();
368: } elseif ($this->hasCapability('transparent')) {
369: $result = $registry->callAppMethod($this->_app, 'authTransparent', array('args' => array($this), 'noperms' => true));
370: } else {
371: /* If this application contains neither transparent nor
372: * authenticate capabilities, it does not require any
373: * authentication if already authenticated to Horde. */
374: $result = ($registry->getAuth() && !$this->hasCapability('authenticate'));
375: }
376:
377: return $result && $this->_setAuth();
378: }
379:
380: /**
381: * Reset a user's password. Used for example when the user does not
382: * remember the existing password.
383: *
384: * @param string $userId The user ID for which to reset the password.
385: *
386: * @return string The new password on success.
387: * @throws Horde_Auth_Exception
388: */
389: public function resetPassword($userId)
390: {
391: if ($this->_base) {
392: return $this->_base->resetPassword($userId);
393: }
394:
395: return $this->hasCapability('resetpassword')
396: ? $GLOBALS['registry']->callAppMethod($this->_app, 'authResetPassword', array('args' => array($userId)))
397: : parent::resetPassword();
398: }
399:
400: /**
401: * Queries the current driver to find out if it supports the given
402: * capability.
403: *
404: * @param string $capability The capability to test for.
405: *
406: * @return boolean Whether or not the capability is supported.
407: */
408: public function hasCapability($capability)
409: {
410: if ($this->_base) {
411: return $this->_base->hasCapability($capability);
412: }
413:
414: if (!isset($this->_appCapabilities)) {
415: $this->_appCapabilities = $GLOBALS['registry']->getApiInstance($this->_app, 'application')->auth;
416: }
417:
418: return in_array(strtolower($capability), $this->_appCapabilities);
419: }
420:
421: /**
422: * Returns the named parameter for the current auth driver.
423: *
424: * @param string $param The parameter to fetch.
425: *
426: * @return string The parameter's value, or null if it doesn't exist.
427: */
428: public function getParam($param)
429: {
430: return $this->_base
431: ? $this->_base->getParam($param)
432: : parent::getParam($param);
433: }
434:
435: /**
436: * Retrieve internal credential value(s).
437: *
438: * @param mixed $name The credential value to get. If null, will return
439: * the entire credential list. Valid names:
440: * <pre>
441: * 'change' - (boolean) Do credentials need to be changed?
442: * 'credentials' - (array) The credentials needed to authenticate.
443: * 'expire' - (integer) UNIX timestamp of the credential expiration date.
444: * 'userId' - (string) The user ID.
445: * </pre>
446: *
447: * @return mixed Return the credential information, or null if the
448: * credential doesn't exist.
449: */
450: public function getCredential($name = null)
451: {
452: return $this->_base
453: ? $this->_base->getCredential($name)
454: : parent::getCredential($name);
455: }
456:
457: /**
458: * Set internal credential value.
459: *
460: * @param string $name The credential name to set.
461: * @param mixed $value The credential value to set. See getCredential()
462: * for the list of valid credentials/types.
463: */
464: public function setCredential($type, $value)
465: {
466: if ($this->_base) {
467: $this->_base->setCredential($type, $value);
468: } else {
469: parent::setCredential($type, $value);
470: }
471: }
472:
473: /**
474: * Sets the error message for an invalid authentication.
475: *
476: * @param string $type The type of error (Horde_Auth::REASON_* constant).
477: * @param string $msg The error message/reason for invalid
478: * authentication.
479: */
480: public function setError($type, $msg = null)
481: {
482: if ($this->_base) {
483: $this->_base->setError($type, $msg);
484: } else {
485: parent::setError($type, $msg);
486: }
487: }
488:
489: /**
490: * Returns the error type or message for an invalid authentication.
491: *
492: * @param boolean $msg If true, returns the message string (if set).
493: *
494: * @return mixed Error type, error message (if $msg is true) or false
495: * if entry doesn't exist.
496: */
497: public function getError($msg = false)
498: {
499: return $this->_base
500: ? $this->_base->getError($msg)
501: : parent::getError($msg);
502: }
503:
504: /**
505: * Returns information on what login parameters to display on the login
506: * screen.
507: *
508: * @return array An array with the following keys:
509: * <pre>
510: * 'js_code' - (array) A list of javascript statements to be included via
511: * Horde::addInlineScript().
512: * 'js_files' - (array) A list of javascript files to be included via
513: * Horde::addScriptFile().
514: * 'params' - (array) A list of parameters to display on the login screen.
515: * Each entry is an array with the following entries:
516: * 'label' - (string) The label of the entry.
517: * 'type' - (string) 'select', 'text', or 'password'.
518: * 'value' - (mixed) If type is 'text' or 'password', the
519: * text to insert into the field by default. If type
520: * is 'select', an array with they keys as the
521: * option values and an array with the following keys:
522: * 'hidden' - (boolean) If true, the option will be
523: * hidden.
524: * 'name' - (string) The option label.
525: * 'selected' - (boolean) If true, will be selected
526: * by default.
527: * </pre>
528: *
529: * @throws Horde_Exception
530: */
531: public function getLoginParams()
532: {
533: return ($this->_base && method_exists($this->_base, 'getLoginParams'))
534: ? $this->_base->getLoginParams()
535: : $GLOBALS['registry']->callAppMethod($this->_app, 'authLoginParams', array('noperms' => true));
536: }
537:
538: /**
539: * Indicate whether the application requires authentication.
540: *
541: * @return boolean True if application requires authentication.
542: */
543: public function requireAuth()
544: {
545: return !$this->_base &&
546: ($this->hasCapability('authenticate') ||
547: $this->hasCapability('transparent'));
548: }
549:
550: /**
551: * Runs the pre/post-authenticate hook and parses the result.
552: *
553: * @param string $userId The userId who has been authorized.
554: * @param array $credentials The credentials of the user.
555: * @param string $type Either 'preauthenticate' or
556: * 'postauthenticate'.
557: * @param string $method The triggering method (preauthenticate only).
558: * Either 'authenticate' or 'transparent'.
559: *
560: * @return array Two element array, $userId and $credentials.
561: * @throws Horde_Auth_Exception
562: */
563: public function runHook($userId, $credentials, $type, $method = null)
564: {
565: if (!is_array($credentials)) {
566: $credentials = empty($credentials)
567: ? array()
568: : array($credentials);
569: }
570:
571: $ret_array = array($userId, $credentials);
572:
573: if ($type == 'preauthenticate') {
574: $credentials['authMethod'] = $method;
575: }
576:
577: try {
578: $result = Horde::callHook($type, array($userId, $credentials), $this->_app);
579: } catch (Horde_Exception $e) {
580: throw new Horde_Auth_Exception($e);
581: } catch (Horde_Exception_HookNotSet $e) {
582: return $ret_array;
583: }
584:
585: unset($credentials['authMethod']);
586:
587: if ($result === false) {
588: if ($this->getError() != Horde_Auth::REASON_MESSAGE) {
589: $this->setError(Horde_Auth::REASON_FAILED);
590: }
591: throw new Horde_Auth_Exception($type . ' hook failed');
592: }
593:
594: if (is_array($result)) {
595: if ($type == 'postauthenticate') {
596: $ret_array[1] = $result;
597: } else {
598: if (isset($result['userId'])) {
599: $ret_array[0] = $result['userId'];
600: }
601:
602: if (isset($result['credentials'])) {
603: $ret_array[1] = $result['credentials'];
604: }
605: }
606: }
607:
608: return $ret_array;
609: }
610:
611: /**
612: * Set authentication credentials in the Horde session.
613: *
614: * @return boolean True on success, false on failure.
615: */
616: protected function _setAuth()
617: {
618: global $registry;
619:
620: if ($registry->isAuthenticated(array('app' => $this->_app, 'notransparent' => true))) {
621: return true;
622: }
623:
624: /* Grab the current language before we destroy the session. */
625: $language = $registry->preferredLang();
626:
627: /* Destroy any existing session on login and make sure to use a
628: * new session ID, to avoid session fixation issues. */
629: if (($userId = $registry->getAuth()) === false) {
630: $registry->getCleanSession();
631: $userId = $this->getCredential('userId');
632: }
633:
634: $credentials = $this->getCredential('credentials');
635:
636: try {
637: list(,$credentials) = $this->runHook($userId, $credentials, 'postauthenticate');
638: } catch (Horde_Auth_Exception $e) {
639: return false;
640: }
641:
642: $registry->setAuth($userId, $credentials, array(
643: 'app' => $this->_app,
644: 'change' => $this->getCredential('change'),
645: 'language' => $language
646: ));
647:
648: /* Only set the view mode on initial authentication */
649: if (!$GLOBALS['session']->get('horde', 'mode')) {
650: $this->_setMode();
651: }
652: if ($this->_base &&
653: isset($GLOBALS['notification']) &&
654: ($expire = $this->_base->getCredential('expire'))) {
655: $toexpire = ($expire - time()) / 86400;
656: $GLOBALS['notification']->push(sprintf(Horde_Core_Translation::ngettext("%d day until your password expires.", "%d days until your password expires.", $toexpire), $toexpire), 'horde.warning');
657: }
658:
659: $registry->callAppMethod($this->_app, 'authAuthenticateCallback', array('noperms' => true));
660:
661: return true;
662: }
663:
664: /**
665: * Sets the default global view mode in the horde session. This can be
666: * checked by applications, and overridden if desired. Also sets a cookie
667: * to remember the last view selection if applicable.
668: */
669: protected function _setMode()
670: {
671: global $conf, $browser, $prefs, $registry;
672:
673: if (empty($conf['user']['force_view'])) {
674: if (empty($conf['user']['select_view'])) {
675: // No value from login form, try to detect.
676: // THIS IS A HACK. DO PROPER SMARTPHONE DETECTION.
677: if ($browser->isMobile()) {
678: $this->_mode = $browser->getBrowser() == 'webkit' ? 'smartmobile' : 'mobile';
679: }
680: } else {
681: setcookie('default_horde_view', $this->_mode, time() + 30 * 86400, $conf['cookie']['path'], $conf['cookie']['domain']);
682: if ($browser->isMobile() && $this->_mode == 'auto') {
683: $this->_mode = $browser->getBrowser() == 'webkit' ? 'smartmobile' : 'mobile';
684: }
685: }
686: } else {
687: // Forcing mode as per config.
688: $this->_mode = $conf['user']['force_view'];
689: }
690:
691: // Set it in the session.
692: $GLOBALS['session']->set('horde', 'mode', $this->_mode);
693: }
694:
695: }
696:
697: