1: <?php
2: /**
3: * The Horde_Auth_Base:: class provides a common abstracted interface to
4: * creating various authentication backends.
5: *
6: * Copyright 1999-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, http://www.horde.org/licenses/lgpl21
10: *
11: * @author Chuck Hagenbuch <chuck@horde.org>
12: * @author Michael Slusarz <slusarz@horde.org>
13: * @category Horde
14: * @license http://www.horde.org/licenses/lgpl21 LGPL-2.1
15: * @package Auth
16: */
17: abstract class Horde_Auth_Base
18: {
19: /**
20: * An array of capabilities, so that the driver can report which
21: * operations it supports and which it doesn't.
22: *
23: * @var array
24: */
25: protected $_capabilities = array(
26: 'add' => false,
27: 'authenticate' => true,
28: 'groups' => false,
29: 'list' => false,
30: 'resetpassword' => false,
31: 'remove' => false,
32: 'transparent' => false,
33: 'update' => false,
34: 'badlogincount' => false,
35: 'lock' => false,
36: );
37:
38: /**
39: * Hash containing parameters needed for the drivers.
40: *
41: * @var array
42: */
43: protected $_params = array();
44:
45: /**
46: * The credentials currently being authenticated.
47: *
48: * @var array
49: */
50: protected $_credentials = array(
51: 'change' => false,
52: 'credentials' => array(),
53: 'expire' => null,
54: 'userId' => ''
55: );
56:
57: /**
58: * Logger object.
59: *
60: * @var Horde_Log_Logger
61: */
62: protected $_logger;
63:
64: /**
65: * History object.
66: *
67: * @var Horde_History
68: */
69: protected $_history_api;
70:
71: /**
72: * Lock object.
73: *
74: * @var Horde_Lock
75: */
76: protected $_lock_api;
77:
78: /**
79: * Authentication error information.
80: *
81: * @var array
82: */
83: protected $_error;
84:
85: /**
86: * Constructor.
87: *
88: * @param array $params Optional parameters:
89: * - default_user: (string) The default user.
90: * - logger: (Horde_Log_Logger, optional) A logger object.
91: * - lock_api: (Horde_Lock, optional) A locking object.
92: * - history_api: (Horde_History, optional) A history object.
93: * - login_block_count: (integer, optional) How many failed logins
94: * trigger autoblocking? 0 disables the feature.
95: * - login_block_time: (integer, options) How many minutes should
96: * autoblocking last? 0 means no expiration.
97: */
98: public function __construct(array $params = array())
99: {
100: if (isset($params['logger'])) {
101: $this->_logger = $params['logger'];
102: unset($params['logger']);
103: }
104:
105: if (isset($params['lock_api'])) {
106: $this->_lock_api = $params['lock_api'];
107: $this->_capabilities['lock'] = true;
108: unset($params['lock_api']);
109: }
110:
111: if (isset($params['history_api'])) {
112: $this->_history_api = $params['history_api'];
113: $this->_capabilities['badlogincount'] = true;
114: unset($params['history_api']);
115: }
116:
117: $params = array_merge(array(
118: 'default_user' => ''
119: ), $params);
120:
121: $this->_params = $params;
122: }
123:
124: /**
125: * Finds out if a set of login credentials are valid, and if requested,
126: * mark the user as logged in in the current session.
127: *
128: * @param string $userId The userId to check.
129: * @param array $credentials The credentials to check.
130: * @param boolean $login Whether to log the user in. If false, we'll
131: * only test the credentials and won't modify
132: * the current session. Defaults to true.
133: *
134: * @return boolean Whether or not the credentials are valid.
135: */
136: public function authenticate($userId, $credentials, $login = true)
137: {
138: $userId = trim($userId);
139:
140: try {
141: $this->_credentials['userId'] = $userId;
142: if (($this->hasCapability('lock')) &&
143: $this->isLocked($userId)) {
144: throw new Horde_Auth_Exception('', Horde_Auth::REASON_LOCKED);
145: }
146: $this->_authenticate($userId, $credentials);
147: $this->setCredential('userId', $this->_credentials['userId']);
148: $this->setCredential('credentials', $credentials);
149: if ($this->hasCapability('badlogincount')) {
150: $this->_resetBadLogins($userId);
151: }
152: return true;
153: } catch (Horde_Auth_Exception $e) {
154: if (($code = $e->getCode()) &&
155: $code != Horde_Auth::REASON_MESSAGE) {
156: if (($code == Horde_Auth::REASON_BADLOGIN) &&
157: $this->hasCapability('badlogincount')) {
158: $this->_badLogin($userId);
159: }
160: $this->setError($code);
161: } else {
162: $this->setError(Horde_Auth::REASON_MESSAGE, $e->getMessage());
163: }
164: return false;
165: }
166: }
167:
168: /**
169: * Basic sort implementation.
170: *
171: * If the backend has listUsers and doesn't have a native sorting option,
172: * fall back to this method.
173: *
174: * @param array $users An array of usernames.
175: * @param boolean $sort Whether to sort or not.
176: *
177: * @return array the users, sorted or not
178: *
179: */
180: protected function _sort($users, $sort)
181: {
182: if ($sort) {
183: sort($users);
184: }
185: return $users;
186: }
187:
188: /**
189: * Authentication stub.
190: *
191: * On failure, Horde_Auth_Exception should pass a message string (if any)
192: * in the message field, and the Horde_Auth::REASON_* constant in the code
193: * field (defaults to Horde_Auth::REASON_MESSAGE).
194: *
195: * @param string $userID The userID to check.
196: * @param array $credentials An array of login credentials.
197: *
198: * @throws Horde_Auth_Exception
199: */
200: abstract protected function _authenticate($userId, $credentials);
201:
202: /**
203: * Checks for triggers that may invalidate the current auth.
204: * These triggers are independent of the credentials.
205: *
206: * @return boolean True if the results of authenticate() are still valid.
207: */
208: public function validateAuth()
209: {
210: return true;
211: }
212:
213: /**
214: * Adds a set of authentication credentials.
215: *
216: * @param string $userId The userId to add.
217: * @param array $credentials The credentials to use.
218: *
219: * @throws Horde_Auth_Exception
220: */
221: public function addUser($userId, $credentials)
222: {
223: throw new Horde_Auth_Exception('Unsupported.');
224: }
225:
226: /**
227: * Locks a user indefinitely or for a specified time.
228: *
229: * @since Horde_Auth 1.2.0
230: *
231: * @param string $userId The user to lock.
232: * @param integer $time The duration in minutes, 0 = permanent.
233: *
234: * @throws Horde_Auth_Exception
235: */
236: public function lockUser($userId, $time = 0)
237: {
238: if (!$this->_lock_api) {
239: throw new Horde_Auth_Exception('Unsupported.');
240: }
241:
242: if ($time == 0) {
243: /* Roughly max timestamp32. */
244: $time = pow(2, 32) - time();
245: } else {
246: $time *= 60;
247: }
248:
249: try {
250: if ($this->_lock_api->setLock($userId, 'horde_auth', 'login:' . $userId, $time, Horde_Lock::TYPE_EXCLUSIVE)) {
251: return;
252: }
253: } catch (Horde_Lock_Exception $e) {
254: throw new Horde_Auth_Exception($e);
255: }
256:
257: throw new Horde_Auth_Exception('User is already locked',
258: Horde_Auth::REASON_LOCKED);
259: }
260:
261: /**
262: * Unlocks a user and optionally resets the bad login count.
263: *
264: * @since Horde_Auth 1.2.0
265: *
266: * @param string $userId The user to unlock.
267: * @param boolean $resetBadLogins Reset bad login counter?
268: *
269: * @throws Horde_Auth_Exception
270: */
271: public function unlockUser($userId, $resetBadLogins = false)
272: {
273: if (!$this->_lock_api) {
274: throw new Horde_Auth_Exception('Unsupported.');
275: }
276:
277: try {
278: $locks = $this->_lock_api->getLocks(
279: 'horde_auth', 'login:' . $userId, Horde_Lock::TYPE_EXCLUSIVE);
280: $lock_id = key($locks);
281: if ($lock_id) {
282: $this->_lock_api->clearLock($lock_id);
283: }
284: if ($resetBadLogins) {
285: $this->_resetBadLogins($userId);
286: }
287: } catch (Horde_Lock_Exception $e) {
288: throw new Horde_Auth_Exception($e);
289: }
290: }
291:
292: /**
293: * Returns whether a user is currently locked.
294: *
295: * @since Horde_Auth 1.2.0
296: *
297: * @param string $userId The user to check.
298: * @param boolean $show_details Return timeout too?
299: *
300: * @return boolean|array If $show_details is a true, an array with
301: * 'locked' and 'lock_timeout' values. Whether the
302: * user is locked, otherwise.
303: * @throws Horde_Auth_Exception
304: */
305: public function isLocked($userId, $show_details = false)
306: {
307: if (!$this->_lock_api) {
308: throw new Horde_Auth_Exception('Unsupported.');
309: }
310:
311: try {
312: $locks = $this->_lock_api->getLocks(
313: 'horde_auth', 'login:' . $userId, Horde_Lock::TYPE_EXCLUSIVE);
314: } catch (Horde_Lock_Exception $e) {
315: throw new Horde_Auth_Exception($e);
316: }
317:
318: if ($show_details) {
319: $lock_id = key($locks);
320: return empty($lock_id)
321: ? array('locked' => false, 'lock_timeout' => 0)
322: : array('locked' => true, 'lock_timeout' => $locks[$lock_id]['lock_expiry_timestamp']);
323: }
324:
325: return !empty($locks);
326: }
327:
328: /**
329: * Handles a bad login.
330: *
331: * @since Horde_Auth 1.2.0
332: *
333: * @param string $userId The user with a bad login.
334: *
335: * @throws Horde_Auth_Exception
336: */
337: protected function _badLogin($userId)
338: {
339: if (!$this->_history_api) {
340: throw new Horde_Auth_Exception('Unsupported.');
341: }
342:
343: $history_identifier = $userId . '@logins.failed';
344: try {
345: $this->_history_api->log(
346: $history_identifier,
347: array('action' => 'login_failed', 'who' => $userId));
348: $history_log = $this->_history_api->getHistory($history_identifier);
349: if ($this->_params['login_block_count'] > 0 &&
350: $this->_params['login_block_count'] <= $history_log->count() &&
351: $this->hasCapability('lock')) {
352: $this->lockUser($userId, $this->_params['login_block_time']);
353: }
354: } catch (Horde_History_Exception $e) {
355: throw new Horde_Auth_Exception($e);
356: }
357: }
358:
359: /**
360: * Resets the bad login counter.
361: *
362: * @since Horde_Auth 1.2.0
363: *
364: * @param string $userId The user to reset.
365: *
366: * @throws Horde_Auth_Exception
367: */
368: protected function _resetBadLogins($userId)
369: {
370: if (!$this->_history_api) {
371: throw new Horde_Auth_Exception('Unsupported.');
372: }
373:
374: try {
375: $this->_history_api->removeByNames(array($userId . '@logins.failed'));
376: } catch (Horde_History_Exception $e) {
377: throw new Horde_Auth_Exception($e);
378: }
379: }
380:
381: /**
382: * Updates a set of authentication credentials.
383: *
384: * @param string $oldID The old userId.
385: * @param string $newID The new userId.
386: * @param array $credentials The new credentials
387: *
388: * @throws Horde_Auth_Exception
389: */
390: public function updateUser($oldID, $newID, $credentials)
391: {
392: throw new Horde_Auth_Exception('Unsupported.');
393: }
394:
395: /**
396: * Deletes a set of authentication credentials.
397: *
398: * @param string $userId The userId to delete.
399: *
400: * @throws Horde_Auth_Exception
401: */
402: public function removeUser($userId)
403: {
404: throw new Horde_Auth_Exception('Unsupported.');
405: }
406:
407: /**
408: * Lists all users in the system.
409: *
410: * @return mixed The array of userIds.
411: * @throws Horde_Auth_Exception
412: */
413: public function listUsers($sort = false)
414: {
415: throw new Horde_Auth_Exception('Unsupported.');
416: }
417:
418: /**
419: * Checks if $userId exists in the system.
420: *
421: * @param string $userId User ID for which to check
422: *
423: * @return boolean Whether or not $userId already exists.
424: */
425: public function exists($userId)
426: {
427: try {
428: $users = $this->listUsers();
429: return in_array($userId, $users);
430: } catch (Horde_Auth_Exception $e) {
431: return false;
432: }
433: }
434:
435: /**
436: * Automatic authentication.
437: *
438: * Transparent authentication should set 'userId', 'credentials', or
439: * 'params' in $this->_credentials as needed - these values will be used
440: * to set the credentials in the session.
441: *
442: * Transparent authentication should normally never throw an error - false
443: * should be returned.
444: *
445: * @return boolean Whether transparent login is supported.
446: * @throws Horde_Auth_Exception
447: */
448: public function transparent()
449: {
450: return false;
451: }
452:
453: /**
454: * Reset a user's password. Used for example when the user does not
455: * remember the existing password.
456: *
457: * @param string $userId The user id for which to reset the password.
458: *
459: * @return string The new password on success.
460: * @throws Horde_Auth_Exception
461: */
462: public function resetPassword($userId)
463: {
464: throw new Horde_Auth_Exception('Unsupported.');
465: }
466:
467: /**
468: * Queries the current driver to find out if it supports the given
469: * capability.
470: *
471: * @param string $capability The capability to test for.
472: *
473: * @return boolean Whether or not the capability is supported.
474: */
475: public function hasCapability($capability)
476: {
477: return !empty($this->_capabilities[$capability]);
478: }
479:
480: /**
481: * Returns the named parameter for the current auth driver.
482: *
483: * @param string $param The parameter to fetch.
484: *
485: * @return string The parameter's value, or null if it doesn't exist.
486: */
487: public function getParam($param)
488: {
489: return isset($this->_params[$param])
490: ? $this->_params[$param]
491: : null;
492: }
493:
494: /**
495: * Retrieve internal credential value(s).
496: *
497: * @param mixed $name The credential value to get. If null, will return
498: * the entire credential list. Valid names:
499: * <pre>
500: * 'change' - (boolean) Do credentials need to be changed?
501: * 'credentials' - (array) The credentials needed to authenticate.
502: * 'expire' - (integer) UNIX timestamp of the credential expiration date.
503: * 'userId' - (string) The user ID.
504: * </pre>
505: *
506: * @return mixed Return the credential information, or null if the
507: * credential doesn't exist.
508: */
509: public function getCredential($name = null)
510: {
511: if (is_null($name)) {
512: return $this->_credentials;
513: }
514:
515: return isset($this->_credentials[$name])
516: ? $this->_credentials[$name]
517: : null;
518: }
519:
520: /**
521: * Set internal credential value.
522: *
523: * @param string $name The credential name to set.
524: * @param mixed $value The credential value to set. See getCredential()
525: * for the list of valid credentials/types.
526: */
527: public function setCredential($type, $value)
528: {
529: switch ($type) {
530: case 'change':
531: $this->_credentials['change'] = (bool)$value;
532: break;
533:
534: case 'credentials':
535: $this->_credentials['credentials'] = array_filter(array_merge($this->_credentials['credentials'], $value));
536: break;
537:
538: case 'expire':
539: $this->_credentials['expire'] = intval($value);
540: break;
541:
542: case 'userId':
543: $this->_credentials['userId'] = strval($value);
544: break;
545: }
546: }
547:
548: /**
549: * Sets the error message for an invalid authentication.
550: *
551: * @param string $type The type of error (Horde_Auth::REASON_* constant).
552: * @param string $msg The error message/reason for invalid
553: * authentication.
554: */
555: public function setError($type, $msg = null)
556: {
557: $this->_error = array(
558: 'msg' => $msg,
559: 'type' => $type
560: );
561: }
562:
563: /**
564: * Returns the error type or message for an invalid authentication.
565: *
566: * @param boolean $msg If true, returns the message string (if set).
567: *
568: * @return mixed Error type, error message (if $msg is true) or false
569: * if entry doesn't exist.
570: */
571: public function getError($msg = false)
572: {
573: return isset($this->_error['type'])
574: ? ($msg ? $this->_error['msg'] : $this->_error['type'])
575: : false;
576: }
577:
578: }
579: