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