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