1<?php 2 3require_once(INCLUDE_DIR.'class.2fa.php'); 4 5interface AuthenticatedUser { 6 // Get basic information 7 function getId(); 8 function getUsername(); 9 function getUserType(); 10 11 12 // Get password reset timestamp 13 function getPasswdResetTimestamp(); 14 15 //Backend used to authenticate the user 16 function getAuthBackend(); 17 18 // Get 2FA Backend 19 function get2FABackend(); 20 21 //Authentication key 22 function setAuthKey($key); 23 24 function getAuthKey(); 25 26 // logOut the user 27 function logOut(); 28 29 // Signal method to allow performing extra things when a user is logged 30 // into the sysem 31 function onLogin($bk); 32} 33 34abstract class BaseAuthenticatedUser 35implements AuthenticatedUser { 36 //Authorization key returned by the backend used to authorize the user 37 private $authkey; 38 39 // Get basic information 40 abstract function getId(); 41 abstract function getUsername(); 42 abstract function getUserType(); 43 44 // Get password reset timestamp 45 function getPasswdResetTimestamp() { 46 return null; 47 } 48 49 //Backend used to authenticate the user 50 abstract function getAuthBackend(); 51 52 // Get 2FA Backend 53 abstract function get2FABackend(); 54 55 //Authentication key 56 function setAuthKey($key) { 57 $this->authkey = $key; 58 } 59 60 function getAuthKey() { 61 return $this->authkey; 62 } 63 64 // logOut the user 65 function logOut() { 66 67 if ($bk = $this->getAuthBackend()) 68 return $bk->signOut($this); 69 70 return false; 71 } 72 73 // Signal method to allow performing extra things when a user is logged 74 // into the sysem 75 function onLogin($bk) {} 76} 77 78require_once(INCLUDE_DIR.'class.ostsession.php'); 79require_once(INCLUDE_DIR.'class.usersession.php'); 80 81interface AuthDirectorySearch { 82 /** 83 * Indicates if the backend can be used to search for user information. 84 * Lookup is performed to find user information based on a unique 85 * identifier. 86 */ 87 function lookup($id); 88 89 /** 90 * Indicates if the backend supports searching for usernames. This is 91 * distinct from information lookup in that lookup is intended to lookup 92 * information based on a unique identifier 93 */ 94 function search($query); 95} 96 97/** 98 * Class: ClientCreateRequest 99 * 100 * Simple container to represent a remote authentication success for a 101 * client which should be imported into the local database. The class will 102 * provide access to the backend that authenticated the user, the username 103 * that the user entered when logging in, and any other information about 104 * the user that the backend was able to lookup. Generally, this extra 105 * information would be the same information retrieved from calling the 106 * AuthDirectorySearch::lookup() method. 107 */ 108class ClientCreateRequest { 109 110 var $backend; 111 var $username; 112 var $info; 113 114 function __construct($backend, $username, $info=array()) { 115 $this->backend = $backend; 116 $this->username = $username; 117 $this->info = $info; 118 } 119 120 function getBackend() { 121 return $this->backend; 122 } 123 function setBackend($what) { 124 $this->backend = $what; 125 } 126 127 function getUsername() { 128 return $this->username; 129 } 130 function getInfo() { 131 return $this->info; 132 } 133 134 function attemptAutoRegister() { 135 global $cfg; 136 137 if (!$cfg || !$cfg->isClientRegistrationEnabled()) 138 return false; 139 140 // Attempt to automatically register 141 $this_form = UserForm::getUserForm()->getForm($this->getInfo()); 142 $bk = $this->getBackend(); 143 $defaults = array( 144 'timezone' => $cfg->getDefaultTimezone(), 145 'username' => $this->getUsername(), 146 ); 147 if ($bk->supportsInteractiveAuthentication()) 148 // User can only be authenticated against this backend 149 $defaults['backend'] = $bk::$id; 150 if ($this_form->isValid(function($f) { return !$f->isVisibleToUsers(); }) 151 && ($U = User::fromVars($this_form->getClean())) 152 && ($acct = ClientAccount::createForUser($U, $defaults)) 153 // Confirm and save the account 154 && $acct->confirm() 155 // Login, since `tickets.php` will not attempt SSO 156 && ($cl = new ClientSession(new EndUser($U))) 157 && ($bk->login($cl, $bk))) 158 return $cl; 159 } 160} 161 162/** 163 * Authentication backend 164 * 165 * Authentication provides the basis of abstracting the link between the 166 * login page with a username and password and the staff member, 167 * administrator, or client using the system. 168 * 169 * The system works by allowing the AUTH_BACKENDS setting from 170 * ost-config.php to determine the list of authentication backends or 171 * providers and also specify the order they should be evaluated in. 172 * 173 * The authentication backend should define a authenticate() method which 174 * receives a username and optional password. If the authentication 175 * succeeds, an instance deriving from <User> should be returned. 176 */ 177abstract class AuthenticationBackend { 178 static protected $registry = array(); 179 static $name; 180 static $id; 181 182 183 /* static */ 184 static function register($class) { 185 if (is_string($class) && class_exists($class)) 186 $class = new $class(); 187 188 if (!is_object($class) 189 || !($class instanceof AuthenticationBackend)) 190 return false; 191 192 return static::_register($class); 193 } 194 195 static function _register($class) { 196 // XXX: Raise error if $class::id is already in the registry 197 static::$registry[$class::$id] = $class; 198 } 199 200 static function allRegistered() { 201 return static::$registry; 202 } 203 204 static function getBackend($id) { 205 206 if ($id 207 && ($backends = static::allRegistered()) 208 && isset($backends[$id])) 209 return $backends[$id]; 210 } 211 212 static function getSearchDirectoryBackend($id) { 213 214 if ($id 215 && ($backends = static::getSearchDirectories()) 216 && isset($backends[$id])) 217 return $backends[$id]; 218 } 219 220 /* 221 * Allow the backend to do login audit depending on the result 222 * This is mainly used to track failed login attempts 223 */ 224 static function authAudit($result, $credentials=null) { 225 226 if (!$result) return; 227 228 foreach (static::allRegistered() as $bk) 229 $bk->audit($result, $credentials); 230 } 231 232 static function process($username, $password=null, &$errors=array()) { 233 234 if (!$username) 235 return false; 236 237 $backends = static::getAllowedBackends($username); 238 foreach (static::allRegistered() as $bk) { 239 if ($backends //Allowed backends 240 && $bk->supportsInteractiveAuthentication() 241 && !in_array($bk::$id, $backends)) 242 // User cannot be authenticated against this backend 243 continue; 244 245 // All backends are queried here, even if they don't support 246 // authentication so that extensions like lockouts and audits 247 // can be supported. 248 try { 249 $result = $bk->authenticate($username, $password); 250 if ($result instanceof AuthenticatedUser 251 && ($bk->login($result, $bk))) 252 return $result; 253 elseif ($result instanceof ClientCreateRequest 254 && $bk instanceof UserAuthenticationBackend) 255 return $result; 256 elseif ($result instanceof AccessDenied) { 257 break; 258 } 259 } 260 catch (AccessDenied $e) { 261 $result = $e; 262 break; 263 } 264 } 265 266 if (!$result) 267 $result = new AccessDenied(__('Access denied')); 268 269 if ($result && $result instanceof AccessDenied) 270 $errors['err'] = $result->reason; 271 272 $info = array('username' => $username, 'password' => $password); 273 Signal::send('auth.login.failed', null, $info); 274 self::authAudit($result, $info); 275 } 276 277 /* 278 * Attempt to process non-interactive sign-on e.g HTTP-Passthrough 279 * 280 * $forcedAuth - indicate if authentication is required. 281 * 282 */ 283 function processSignOn(&$errors, $forcedAuth=true) { 284 285 foreach (static::allRegistered() as $bk) { 286 // All backends are queried here, even if they don't support 287 // authentication so that extensions like lockouts and audits 288 // can be supported. 289 try { 290 $result = $bk->signOn(); 291 if ($result instanceof AuthenticatedUser) { 292 //Perform further Object specific checks and the actual login 293 if (!$bk->login($result, $bk)) 294 continue; 295 296 return $result; 297 } 298 elseif ($result instanceof ClientCreateRequest 299 && $bk instanceof UserAuthenticationBackend) 300 return $result; 301 elseif ($result instanceof AccessDenied) { 302 break; 303 } 304 } 305 catch (AccessDenied $e) { 306 $result = $e; 307 break; 308 } 309 } 310 311 if (!$result && $forcedAuth) 312 $result = new AccessDenied(__('Unknown user')); 313 314 if ($result && $result instanceof AccessDenied) 315 $errors['err'] = $result->reason; 316 317 self::authAudit($result); 318 } 319 320 static function getSearchDirectories() { 321 $backends = array(); 322 foreach (StaffAuthenticationBackend::allRegistered() as $bk) 323 if ($bk instanceof AuthDirectorySearch) 324 $backends[$bk::$id] = $bk; 325 326 foreach (UserAuthenticationBackend::allRegistered() as $bk) 327 if ($bk instanceof AuthDirectorySearch) 328 $backends[$bk::$id] = $bk; 329 330 return array_unique($backends); 331 } 332 333 static function searchUsers($query) { 334 $users = array(); 335 foreach (static::getSearchDirectories() as $bk) 336 $users = array_merge($users, $bk->search($query)); 337 338 return $users; 339 } 340 341 /** 342 * Fetches the friendly name of the backend 343 */ 344 function getName() { 345 return static::$name; 346 } 347 348 /** 349 * Indicates if the backed supports authentication. Useful if the 350 * backend is used for logging or lockout only 351 */ 352 function supportsInteractiveAuthentication() { 353 return true; 354 } 355 356 /** 357 * Indicates if the backend supports changing a user's password. This 358 * would be done in two fashions. Either the currently-logged in user 359 * want to change its own password or a user requests to have their 360 * password reset. This requires an administrative privilege which this 361 * backend might not possess, so it's defined in supportsPasswordReset() 362 */ 363 function supportsPasswordChange() { 364 return false; 365 } 366 367 368 /** 369 * Get supported password policies for the backend. 370 * 371 */ 372 function getPasswordPolicies($user=null) { 373 return PasswordPolicy::allActivePolicies(); 374 } 375 376 /** 377 * Request the backend to update the password for a user. This method is 378 * the main entry for password updates so that password policies can be 379 * applied to the new password before passing the new password to the 380 * backend for updating. 381 * 382 * Throws: 383 * BadPassword — if password does not meet policy requirement 384 * PasswordUpdateFailed — if backend failed to update the password 385 */ 386 function setPassword($user, $password, $current=false) { 387 foreach ($this->getPasswordPolicies($user) as $P) 388 $P->onSet($password, $current); 389 390 $rv = $this->syncPassword($user, $password); 391 if ($rv) { 392 $info = array('password' => $password, 'current' => $current); 393 Signal::send('auth.pwchange', $user, $info); 394 } 395 return $rv; 396 } 397 398 /* 399 * Request the backend to check the policies for a just logged 400 * in user. 401 * Throws: BadPassword & ExpiredPassword - for password related failures 402 */ 403 function checkPolicies($user, $password) { 404 // Password policies 405 foreach ($this->getPasswordPolicies($user) as $P) 406 $P->onLogin($user, $password); 407 } 408 409 /** 410 * Request the backend to update the user's password with the password 411 * given. This method should only be used if the backend advertises 412 * supported password updates with the supportsPasswordChange() method. 413 * 414 * Returns: 415 * true if the password was successfully updated and false otherwise. 416 */ 417 protected function syncPassword($user, $password) { 418 return false; 419 } 420 421 function supportsPasswordReset() { 422 return false; 423 } 424 425 function signOn() { 426 return null; 427 } 428 429 protected function validate($auth) { 430 return null; 431 } 432 433 protected function audit($result, $credentials) { 434 return null; 435 } 436 437 abstract function authenticate($username, $password); 438 abstract function login($user, $bk); 439 abstract static function getUser(); //Validates authenticated users. 440 abstract function getAllowedBackends($userid); 441 abstract protected function getAuthKey($user); 442 abstract static function signOut($user); 443} 444 445/** 446 * ExternalAuthenticationBackend 447 * 448 * External authentication backends are backends such as Google+ which 449 * require a redirect to a remote site and a redirect back to osTicket in 450 * order for a user to be authenticated. For such backends, neither the 451 * username and password fields nor single sign on alone can be used to 452 * authenticate the user. 453 */ 454interface ExternalAuthentication { 455 456 /** 457 * Requests the backend to render an external link box. When the user 458 * clicks this box, the backend will be prompted to redirect the user to 459 * the remote site for authentication there. 460 */ 461 function renderExternalLink(); 462 463 /** 464 * Function: getServiceName 465 * 466 * Called to get the service name displayed on login page. 467 */ 468 function getServiceName(); 469 470 /** 471 * Function: triggerAuth 472 * 473 * Called when a user clicks the button rendered in the 474 * ::renderExternalLink() function. This method should initiate the 475 * remote authentication mechanism. 476 */ 477 function triggerAuth(); 478} 479 480abstract class StaffAuthenticationBackend extends AuthenticationBackend { 481 482 static private $_registry = array(); 483 484 static function _register($class) { 485 static::$_registry[$class::$id] = $class; 486 } 487 488 static function allRegistered() { 489 return array_merge(self::$_registry, parent::allRegistered()); 490 } 491 492 function isBackendAllowed($staff, $bk) { 493 494 if (!($backends=self::getAllowedBackends($staff->getId()))) 495 return true; //No restrictions 496 497 return in_array($bk::$id, array_map('strtolower', $backends)); 498 } 499 500 function getPasswordPolicies($user=null) { 501 global $cfg; 502 $policies = PasswordPolicy::allActivePolicies(); 503 if ($cfg && ($policy = $cfg->getStaffPasswordPolicy())) { 504 foreach ($policies as $P) 505 if ($policy == $P::$id) 506 return array($P); 507 } 508 509 return $policies; 510 } 511 512 function getAllowedBackends($userid) { 513 514 $backends =array(); 515 //XXX: Only one backend can be specified at the moment. 516 $sql = 'SELECT backend FROM '.STAFF_TABLE 517 .' WHERE backend IS NOT NULL '; 518 if (is_numeric($userid)) 519 $sql.= ' AND staff_id='.db_input($userid); 520 else { 521 $sql.= ' AND (username='.db_input($userid) .' OR email='.db_input($userid).')'; 522 } 523 524 if (($res=db_query($sql, false)) && db_num_rows($res)) 525 $backends[] = db_result($res); 526 527 return array_filter($backends); 528 } 529 530 function login($staff, $bk) { 531 global $ost; 532 533 if (!$bk || !($staff instanceof Staff)) 534 return false; 535 536 // Ensure staff is allowed for realz to be authenticated via the backend. 537 if (!static::isBackendAllowed($staff, $bk) 538 || !($authkey=$bk->getAuthKey($staff))) 539 return false; 540 541 //Log debug info. 542 $ost->logDebug(_S('Agent Login'), 543 sprintf(_S("%s logged in [%s], via %s"), $staff->getUserName(), 544 $_SERVER['REMOTE_ADDR'], get_class($bk))); //Debug. 545 546 $agent = Staff::lookup($staff->getId()); 547 $type = array('type' => 'login'); 548 Signal::send('person.login', $agent, $type); 549 550 // Check if the agent has 2fa enabled 551 $auth2fa = null; 552 if (($_2fa = $staff->get2FABackend()) 553 && ($token=$_2fa->send($staff))) { 554 $auth2fa = sprintf('%s:%s:%s', 555 $_2fa->getId(), md5($token.$staff->getId()), time()); 556 } 557 558 // Tag the authkey. 559 $authkey = $bk::$id.':'.$authkey; 560 // Now set session crap and lets roll baby! 561 $authsession = &$_SESSION['_auth']['staff']; 562 $authsession = array(); //clear. 563 $authsession['id'] = $staff->getId(); 564 $authsession['key'] = $authkey; 565 $authsession['2fa'] = $auth2fa; 566 567 $staff->setAuthKey($authkey); 568 $staff->refreshSession(true); //set the hash. 569 Signal::send('auth.login.succeeded', $staff); 570 571 if ($bk->supportsInteractiveAuthentication()) 572 $staff->cancelResetTokens(); 573 574 575 // Update last-used language, login time, etc 576 $staff->onLogin($bk); 577 578 return true; 579 } 580 581 /* Base signOut 582 * 583 * Backend should extend the signout and perform any additional signout 584 * it requires. 585 */ 586 587 static function signOut($staff) { 588 global $ost; 589 590 $_SESSION['_auth']['staff'] = array(); 591 unset($_SESSION[':token']['staff']); 592 $ost->logDebug(_S('Agent logout'), 593 sprintf(_S("%s logged out [%s]"), 594 $staff->getUserName(), 595 $_SERVER['REMOTE_ADDR'])); //Debug. 596 597 $agent = Staff::lookup($staff->getId()); 598 $type = array('type' => 'logout'); 599 Signal::send('person.logout', $agent, $type); 600 Signal::send('auth.logout', $staff); 601 } 602 603 // Called to get authenticated user (if any) 604 static function getUser() { 605 606 if (!isset($_SESSION['_auth']['staff']) 607 || !$_SESSION['_auth']['staff']['key']) 608 return null; 609 610 list($id, $auth) = explode(':', $_SESSION['_auth']['staff']['key']); 611 612 if (!($bk=static::getBackend($id)) //get the backend 613 || !($staff = $bk->validate($auth)) //Get AuthicatedUser 614 || !($staff instanceof Staff) 615 || $staff->getId() != $_SESSION['_auth']['staff']['id'] // check ID 616 ) 617 return null; 618 619 $staff->setAuthKey($_SESSION['_auth']['staff']['key']); 620 621 return $staff; 622 } 623 624 function authenticate($username, $password) { 625 return false; 626 } 627 628 // Generic authentication key for staff's backend is the username 629 protected function getAuthKey($staff) { 630 631 if(!($staff instanceof Staff)) 632 return null; 633 634 return $staff->getUsername(); 635 } 636 637 protected function validate($authkey) { 638 639 if (($staff = StaffSession::lookup($authkey)) && $staff->getId()) 640 return $staff; 641 } 642} 643 644abstract class ExternalStaffAuthenticationBackend 645 extends StaffAuthenticationBackend 646 implements ExternalAuthentication { 647 648 static $fa_icon = "signin"; 649 static $sign_in_image_url = false; 650 static $service_name = "External"; 651 652 function getServiceName() { 653 return __(static::$service_name); 654 } 655 656 function renderExternalLink() { 657 $service = sprintf('%s %s', 658 __('Sign in with'), 659 $this->getServiceName()); 660 ?> 661 <a class="external-sign-in" title="<?php echo $service; ?>" 662 href="login.php?do=ext&bk=<?php echo urlencode(static::$id); ?>"> 663<?php if (static::$sign_in_image_url) { ?> 664 <img class="sign-in-image" src="<?php echo static::$sign_in_image_url; 665 ?>" alt="<?php echo $service; ?>"/> 666<?php } else { ?> 667 <div class="external-auth-box"> 668 <span class="external-auth-icon"> 669 <i class="icon-<?php echo static::$fa_icon; ?> icon-large icon-fixed-with"></i> 670 </span> 671 <span class="external-auth-name"> 672 <?php echo $service; ?> 673 </span> 674 </div> 675<?php } ?> 676 </a><?php 677 } 678 679 function triggerAuth() { 680 $_SESSION['ext:bk:class'] = get_class($this); 681 } 682} 683Signal::connect('api', function($dispatcher) { 684 $dispatcher->append( 685 url('^/auth/ext$', function() { 686 if ($class = $_SESSION['ext:bk:class']) { 687 $bk = StaffAuthenticationBackend::getBackend($class::$id) 688 ?: UserAuthenticationBackend::getBackend($class::$id); 689 if ($bk instanceof ExternalAuthentication) 690 $bk->triggerAuth(); 691 } 692 }) 693 ); 694}); 695 696abstract class UserAuthenticationBackend extends AuthenticationBackend { 697 698 static private $_registry = array(); 699 700 static function _register($class) { 701 static::$_registry[$class::$id] = $class; 702 } 703 704 static function allRegistered() { 705 return array_merge(self::$_registry, parent::allRegistered()); 706 } 707 708 709 function getPasswordPolicies($user=null) { 710 global $cfg; 711 $policies = PasswordPolicy::allActivePolicies(); 712 if ($cfg && ($policy = $cfg->getClientPasswordPolicy())) { 713 foreach ($policies as $P) 714 if ($policy == $P::$id) 715 return array($P); 716 } 717 718 return $policies; 719 } 720 721 function getAllowedBackends($userid) { 722 $backends = array(); 723 $sql = 'SELECT A1.backend FROM '.USER_ACCOUNT_TABLE 724 .' A1 INNER JOIN '.USER_EMAIL_TABLE.' A2 ON (A2.user_id = A1.user_id)' 725 .' WHERE backend IS NOT NULL ' 726 .' AND (A1.username='.db_input($userid) 727 .' OR A2.`address`='.db_input($userid).')'; 728 729 if (!($res=db_query($sql, false))) 730 return $backends; 731 732 while (list($bk) = db_fetch_row($res)) 733 $backends[] = $bk; 734 735 return array_filter($backends); 736 } 737 738 function login($user, $bk) { 739 global $ost; 740 741 if (!$user || !$bk 742 || !$bk::$id //Must have ID 743 || !($authkey = $bk->getAuthKey($user))) 744 return false; 745 746 $acct = $user->getAccount(); 747 748 if ($acct) { 749 if (!$acct->isConfirmed()) 750 throw new AccessDenied(__('Account confirmation required')); 751 elseif ($acct->isLocked()) 752 throw new AccessDenied(__('Account is administratively locked')); 753 } 754 755 // Tag the user and associated ticket in the SESSION 756 $this->setAuthKey($user, $bk, $authkey); 757 758 //The backend used decides the format of the auth key. 759 // XXX: encrypt to hide the bk?? 760 $user->setAuthKey($authkey); 761 762 $user->refreshSession(true); //set the hash. 763 764 //Log login info... 765 $msg=sprintf(_S('%1$s (%2$s) logged in [%3$s]' 766 /* Tokens are <username>, <id>, and <ip> */), 767 $user->getUserName(), $user->getId(), $_SERVER['REMOTE_ADDR']); 768 $ost->logDebug(_S('User login'), $msg); 769 770 $u = $user->getSessionUser()->getUser(); 771 $type = array('type' => 'login'); 772 Signal::send('person.login', $u, $type); 773 774 if ($bk->supportsInteractiveAuthentication() && ($acct=$user->getAccount())) 775 $acct->cancelResetTokens(); 776 777 // Update last-used language, login time, etc 778 $user->onLogin($bk); 779 780 return true; 781 } 782 783 function setAuthKey($user, $bk, $key=false) { 784 $authkey = $key ?: $bk->getAuthKey($user); 785 786 //Tag the authkey. 787 $authkey = $bk::$id.':'.$authkey; 788 789 //Set the session goodies 790 $authsession = &$_SESSION['_auth']['user']; 791 792 $authsession = array(); //clear. 793 $authsession['id'] = $user->getId(); 794 $authsession['key'] = $authkey; 795 } 796 797 function authenticate($username, $password) { 798 return false; 799 } 800 801 static function signOut($user) { 802 global $ost; 803 804 $_SESSION['_auth']['user'] = array(); 805 unset($_SESSION[':token']['client']); 806 $ost->logDebug(_S('User logout'), 807 sprintf(_S("%s logged out [%s]" /* Tokens are <username> and <ip> */), 808 $user->getUserName(), $_SERVER['REMOTE_ADDR'])); 809 810 $u = $user->getSessionUser()->getUser(); 811 $type = array('type' => 'logout'); 812 Signal::send('person.logout', $u, $type); 813 } 814 815 protected function getAuthKey($user) { 816 return $user->getId(); 817 } 818 819 static function getUser() { 820 821 if (!isset($_SESSION['_auth']['user']) 822 || !$_SESSION['_auth']['user']['key']) 823 return null; 824 825 list($id, $auth) = explode(':', $_SESSION['_auth']['user']['key']); 826 827 if (!($bk=static::getBackend($id)) //get the backend 828 || !($user=$bk->validate($auth)) //Get AuthicatedUser 829 || !($user instanceof AuthenticatedUser) // Make sure it user 830 || $user->getId() != $_SESSION['_auth']['user']['id'] // check ID 831 ) 832 return null; 833 834 $user->setAuthKey($_SESSION['_auth']['user']['key']); 835 836 return $user; 837 } 838 839 protected function validate($userid) { 840 if (!($user = User::lookup($userid))) 841 return false; 842 elseif (!$user->getAccount()) 843 return false; 844 845 return new ClientSession(new EndUser($user)); 846 } 847} 848 849abstract class ExternalUserAuthenticationBackend 850 extends UserAuthenticationBackend 851 implements ExternalAuthentication { 852 853 static $fa_icon = "signin"; 854 static $sign_in_image_url = false; 855 static $service_name = "External"; 856 857 function getServiceName() { 858 return __(static::$service_name); 859 } 860 861 function renderExternalLink() { 862 $service = sprintf('%s %s', 863 __('Sign in with'), 864 $this->getServiceName()); 865 866 ?> 867 <a class="external-sign-in" title="<?php echo $service; ?>" 868 href="login.php?do=ext&bk=<?php echo urlencode(static::$id); ?>"> 869<?php if (static::$sign_in_image_url) { ?> 870 <img class="sign-in-image" src="<?php echo static::$sign_in_image_url; 871 ?>" alt="<?php $service; ?>"/> 872<?php } else { ?> 873 <div class="external-auth-box"> 874 <span class="external-auth-icon"> 875 <i class="icon-<?php echo static::$fa_icon; ?> icon-large icon-fixed-with"></i> 876 </span> 877 <span class="external-auth-name"> 878 <?php echo $service; ?> 879 </span> 880 </div> 881<?php } ?> 882 </a><?php 883 } 884 885 function triggerAuth() { 886 $_SESSION['ext:bk:class'] = get_class($this); 887 } 888} 889 890/** 891 * This will be an exception in later versions of PHP 892 */ 893class AccessDenied extends Exception { 894 function __construct($reason) { 895 $this->reason = $reason; 896 parent::__construct($reason); 897 } 898} 899 900/** 901 * Simple authentication backend which will lock the login form after a 902 * configurable number of attempts 903 */ 904abstract class AuthStrikeBackend extends AuthenticationBackend { 905 906 function authenticate($username, $password=null) { 907 return static::authTimeout(); 908 } 909 910 function signOn() { 911 return static::authTimeout(); 912 } 913 914 static function signOut($user) { 915 return false; 916 } 917 918 919 function login($user, $bk) { 920 return false; 921 } 922 923 static function getUser() { 924 return null; 925 } 926 927 function supportsInteractiveAuthentication() { 928 return false; 929 } 930 931 function getAllowedBackends($userid) { 932 return array(); 933 } 934 935 function getAuthKey($user) { 936 return null; 937 } 938 939 //Provides audit facility for logins attempts 940 function audit($result, $credentials) { 941 942 //Count failed login attempts as a strike. 943 if ($result instanceof AccessDenied) 944 return static::authStrike($credentials); 945 946 } 947 948 abstract function authStrike($credentials); 949 abstract function authTimeout(); 950} 951 952/* 953 * Backend to monitor staff's failed login attempts 954 */ 955class StaffAuthStrikeBackend extends AuthStrikeBackend { 956 957 function authTimeout() { 958 global $ost; 959 960 $cfg = $ost->getConfig(); 961 962 $authsession = &$_SESSION['_auth']['staff']; 963 if (!$authsession['laststrike']) 964 return; 965 966 //Veto login due to excessive login attempts. 967 if((time()-$authsession['laststrike'])<$cfg->getStaffLoginTimeout()) { 968 $authsession['laststrike'] = time(); //reset timer. 969 return new AccessDenied(__('Maximum failed login attempts reached')); 970 } 971 972 //Timeout is over. 973 //Reset the counter for next round of attempts after the timeout. 974 $authsession['laststrike']=null; 975 $authsession['strikes']=0; 976 } 977 978 function authstrike($credentials) { 979 global $ost; 980 981 $cfg = $ost->getConfig(); 982 983 $authsession = &$_SESSION['_auth']['staff']; 984 985 $username = $credentials['username']; 986 987 $authsession['strikes']+=1; 988 if($authsession['strikes']>$cfg->getStaffMaxLogins()) { 989 $authsession['laststrike']=time(); 990 $timeout = $cfg->getStaffLoginTimeout()/60; 991 $alert=_S('Excessive login attempts by an agent?')."\n" 992 ._S('Username').": $username\n" 993 ._S('IP').": {$_SERVER['REMOTE_ADDR']}\n" 994 ._S('Time').": ".date('M j, Y, g:i a T')."\n\n" 995 ._S('Attempts').": {$authsession['strikes']}\n" 996 ._S('Timeout').": ".sprintf(_N('%d minute', '%d minutes', $timeout), $timeout)."\n\n"; 997 $admin_alert = ($cfg->alertONLoginError() == 1) ? TRUE : FALSE; 998 $ost->logWarning(sprintf(_S('Excessive login attempts (%s)'),$username), 999 $alert, $admin_alert); 1000 1001 if ($username) { 1002 $agent = Staff::lookup($username); 1003 $type = array('type' => 'login', 'msg' => sprintf('Excessive login attempts (%s)', $authsession['strikes'])); 1004 Signal::send('person.login', $agent, $type); 1005 } 1006 1007 return new AccessDenied(__('Forgot your login info? Contact Admin.')); 1008 //Log every other third failed login attempt as a warning. 1009 } elseif($authsession['strikes']%3==0) { 1010 $alert=_S('Username').": {$username}\n" 1011 ._S('IP').": {$_SERVER['REMOTE_ADDR']}\n" 1012 ._S('Time').": ".date('M j, Y, g:i a T')."\n\n" 1013 ._S('Attempts').": {$authsession['strikes']}"; 1014 $ost->logWarning(sprintf(_S('Failed agent login attempt (%s)'),$username), 1015 $alert, false); 1016 } 1017 } 1018} 1019StaffAuthenticationBackend::register('StaffAuthStrikeBackend'); 1020 1021/* 1022 * Backend to monitor user's failed login attempts 1023 */ 1024class UserAuthStrikeBackend extends AuthStrikeBackend { 1025 1026 function authTimeout() { 1027 global $ost; 1028 1029 $cfg = $ost->getConfig(); 1030 1031 $authsession = &$_SESSION['_auth']['user']; 1032 if (!$authsession['laststrike']) 1033 return; 1034 1035 //Veto login due to excessive login attempts. 1036 if ((time()-$authsession['laststrike']) < $cfg->getStaffLoginTimeout()) { 1037 $authsession['laststrike'] = time(); //reset timer. 1038 return new AccessDenied(__("You've reached maximum failed login attempts allowed.")); 1039 } 1040 1041 //Timeout is over. 1042 //Reset the counter for next round of attempts after the timeout. 1043 $authsession['laststrike']=null; 1044 $authsession['strikes']=0; 1045 } 1046 1047 function authstrike($credentials) { 1048 global $ost; 1049 1050 $cfg = $ost->getConfig(); 1051 1052 $authsession = &$_SESSION['_auth']['user']; 1053 1054 $username = $credentials['username']; 1055 $password = $credentials['password']; 1056 1057 $authsession['strikes']+=1; 1058 if($authsession['strikes']>$cfg->getClientMaxLogins()) { 1059 $authsession['laststrike'] = time(); 1060 $alert=_S('Excessive login attempts by a user.')."\n". 1061 _S('Username').": {$username}\n". 1062 _S('IP').": {$_SERVER['REMOTE_ADDR']}\n". 1063 _S('Time').": ".date('M j, Y, g:i a T')."\n\n". 1064 _S('Attempts').": {$authsession['strikes']}"; 1065 $admin_alert = ($cfg->alertONLoginError() == 1 ? TRUE : FALSE); 1066 $ost->logError(_S('Excessive login attempts (user)'), $alert, $admin_alert); 1067 1068 if ($username) { 1069 $account = UserAccount::lookupByUsername($username); 1070 $id = UserEmailModel::getIdByEmail($username); 1071 if ($account) 1072 $user = User::lookup($account->user_id); 1073 elseif ($id) 1074 $user = User::lookup($id); 1075 1076 if ($user) { 1077 $type = array('type' => 'login', 'msg' => sprintf('Excessive login attempts (%s)', $authsession['strikes'])); 1078 Signal::send('person.login', $user, $type); 1079 } 1080 } 1081 1082 return new AccessDenied(__('Access denied')); 1083 } elseif($authsession['strikes']%3==0) { //Log every third failed login attempt as a warning. 1084 $alert=_S('Username').": {$username}\n". 1085 _S('IP').": {$_SERVER['REMOTE_ADDR']}\n". 1086 _S('Time').": ".date('M j, Y, g:i a T')."\n\n". 1087 _S('Attempts').": {$authsession['strikes']}"; 1088 $ost->logWarning(_S('Failed login attempt (user)'), $alert, false); 1089 } 1090 1091 } 1092} 1093UserAuthenticationBackend::register('UserAuthStrikeBackend'); 1094 1095 1096class osTicketStaffAuthentication extends StaffAuthenticationBackend { 1097 static $name = "Local Authentication"; 1098 static $id = "local"; 1099 1100 function authenticate($username, $password) { 1101 if (($user = StaffSession::lookup($username)) && $user->getId() && 1102 $user->check_passwd($password)) { 1103 try { 1104 $this->checkPolicies($user, $password); 1105 } catch (BadPassword | ExpiredPassword $ex) { 1106 $user->change_passwd = 1; 1107 } 1108 return $user; 1109 } 1110 } 1111 1112 function supportsPasswordChange() { 1113 return true; 1114 } 1115 1116 function syncPassword($staff, $password) { 1117 $staff->passwd = Passwd::hash($password); 1118 } 1119 1120 static function checkPassword($new, $current) { 1121 PasswordPolicy::checkPassword($new, $current, new self()); 1122 } 1123 1124} 1125StaffAuthenticationBackend::register('osTicketStaffAuthentication'); 1126 1127class PasswordResetTokenBackend extends StaffAuthenticationBackend { 1128 static $id = "pwreset.staff"; 1129 1130 function supportsInteractiveAuthentication() { 1131 return false; 1132 } 1133 1134 function signOn($errors=array()) { 1135 global $ost; 1136 1137 if (!isset($_POST['userid']) || !isset($_POST['token'])) 1138 return false; 1139 elseif (!($_config = new Config('pwreset'))) 1140 return false; 1141 1142 $staff = StaffSession::lookup($_POST['userid']); 1143 if (!$staff || !$staff->getId()) 1144 $errors['msg'] = __('Invalid user-id given'); 1145 elseif (!($id = $_config->get($_POST['token'])) 1146 || $id != $staff->getId()) 1147 $errors['msg'] = __('Invalid reset token'); 1148 elseif (!($ts = $_config->lastModified($_POST['token'])) 1149 && ($ost->getConfig()->getPwResetWindow() < (time() - strtotime($ts)))) 1150 $errors['msg'] = __('Invalid reset token'); 1151 elseif (!$staff->forcePasswdRest()) 1152 $errors['msg'] = __('Unable to reset password'); 1153 else 1154 return $staff; 1155 } 1156 1157 function login($staff, $bk) { 1158 $_SESSION['_staff']['reset-token'] = $_POST['token']; 1159 Signal::send('auth.pwreset.login', $staff); 1160 return parent::login($staff, $bk); 1161 } 1162} 1163StaffAuthenticationBackend::register('PasswordResetTokenBackend'); 1164 1165/* 1166 * AuthToken Authentication Backend 1167 * 1168 * Provides auto-login facility for end users with valid link 1169 * 1170 * Ticket used to loggin is tracked durring the session this is 1171 * important in the future when auto-logins will be 1172 * limited to single ticket view. 1173 */ 1174class AuthTokenAuthentication extends UserAuthenticationBackend { 1175 static $name = "Auth Token Authentication"; 1176 static $id = "authtoken"; 1177 1178 1179 function signOn() { 1180 global $cfg; 1181 1182 1183 if (!$cfg || !$cfg->isAuthTokenEnabled()) 1184 return null; 1185 1186 $user = null; 1187 if ($_GET['auth']) { 1188 if (($u = TicketUser::lookupByToken($_GET['auth']))) 1189 $user = new ClientSession($u); 1190 } 1191 // Support old ticket based tokens. 1192 elseif ($_GET['t'] && $_GET['e'] && $_GET['a']) { 1193 if (($ticket = Ticket::lookupByNumber($_GET['t'], $_GET['e'])) 1194 // Using old ticket auth code algo - hardcoded here because it 1195 // will be removed in ticket class in the upcoming rewrite 1196 && strcasecmp((string) $_GET['a'], md5($ticket->getId() 1197 . strtolower($_GET['e']) . SECRET_SALT)) === 0 1198 && ($owner = $ticket->getOwner())) 1199 $user = new ClientSession($owner); 1200 } 1201 1202 return $user; 1203 } 1204 1205 function supportsInteractiveAuthentication() { 1206 return false; 1207 } 1208 1209 protected function getAuthKey($user) { 1210 1211 if (!$user) 1212 return null; 1213 1214 //Generate authkey based the type of ticket user 1215 // It's required to validate users going forward. 1216 $authkey = sprintf('%s%dt%dh%s', //XXX: Placeholder 1217 ($user->isOwner() ? 'o':'c'), 1218 $user->getId(), 1219 $user->getTicketId(), 1220 md5($user->getId().$this->id)); 1221 1222 return $authkey; 1223 } 1224 1225 protected function validate($authkey) { 1226 1227 $regex = '/^(?P<type>\w{1})(?P<id>\d+)t(?P<tid>\d+)h(?P<hash>.*)$/i'; 1228 $matches = array(); 1229 if (!preg_match($regex, $authkey, $matches)) 1230 return false; 1231 1232 $user = null; 1233 switch ($matches['type']) { 1234 case 'c': //Collaborator 1235 $criteria = array( 1236 'user_id' => $matches['id'], 1237 'thread__ticket__ticket_id' => $matches['tid'] 1238 ); 1239 if (($c = Collaborator::lookup($criteria)) 1240 && ($c->getTicketId() == $matches['tid'])) 1241 $user = new ClientSession($c); 1242 break; 1243 case 'o': //Ticket owner 1244 if (($ticket = Ticket::lookup($matches['tid'])) 1245 && ($o = $ticket->getOwner()) 1246 && ($o->getId() == $matches['id'])) 1247 $user = new ClientSession($o); 1248 break; 1249 } 1250 1251 //Make sure the authkey matches. 1252 if (!$user || strcmp($this->getAuthKey($user), $authkey)) 1253 return null; 1254 1255 $user->flagGuest(); 1256 1257 return $user; 1258 } 1259 1260} 1261 1262UserAuthenticationBackend::register('AuthTokenAuthentication'); 1263 1264//Simple ticket lookup backend used to recover ticket access link. 1265// We're using authentication backend so we can guard aganist brute force 1266// attempts (which doesn't buy much since the link is emailed) 1267class AccessLinkAuthentication extends UserAuthenticationBackend { 1268 static $name = "Ticket Access Link Authentication"; 1269 static $id = "authlink"; 1270 1271 function authenticate($email, $number) { 1272 1273 if (!($ticket = Ticket::lookupByNumber($number)) 1274 || !($user=User::lookup(array('emails__address' => $email)))) 1275 return false; 1276 1277 if (!($user = $this->_getTicketUser($ticket, $user))) 1278 return false; 1279 1280 $_SESSION['_auth']['user-ticket'] = $number; 1281 return new ClientSession($user); 1282 } 1283 1284 function _getTicketUser($ticket, $user) { 1285 // Ticket owner? 1286 if ($ticket->getUserId() == $user->getId()) 1287 $user = $ticket->getOwner(); 1288 // Collaborator? 1289 elseif (!($user = Collaborator::lookup(array( 1290 'user_id' => $user->getId(), 1291 'thread__ticket__ticket_id' => $ticket->getId()) 1292 ))) 1293 return false; //Bro, we don't know you! 1294 1295 return $user; 1296 } 1297 1298 // We are not actually logging in the user.... 1299 function login($user, $bk) { 1300 global $cfg; 1301 1302 if (!$cfg->isClientEmailVerificationRequired()) { 1303 return parent::login($user, $bk); 1304 } 1305 return true; 1306 } 1307 1308 protected function validate($userid) { 1309 $number = $_SESSION['_auth']['user-ticket']; 1310 1311 if (!($ticket = Ticket::lookupByNumber($number))) 1312 return false; 1313 1314 if (!($user = User::lookup($userid))) 1315 return false; 1316 1317 if (!($user = $this->_getTicketUser($ticket, $user))) 1318 return false; 1319 1320 $user = new ClientSession($user); 1321 $user->flagGuest(); 1322 return $user; 1323 } 1324 1325 function supportsInteractiveAuthentication() { 1326 return false; 1327 } 1328} 1329UserAuthenticationBackend::register('AccessLinkAuthentication'); 1330 1331class osTicketClientAuthentication extends UserAuthenticationBackend { 1332 static $name = "Local Client Authentication"; 1333 static $id = "client"; 1334 1335 function authenticate($username, $password) { 1336 if (!($acct = ClientAccount::lookupByUsername($username))) 1337 return; 1338 1339 if (($client = new ClientSession(new EndUser($acct->getUser()))) 1340 && !$client->getId()) 1341 return false; 1342 elseif (!$acct->check_passwd($password)) 1343 return false; 1344 else 1345 return $client; 1346 } 1347 1348 static function checkPassword($new, $current) { 1349 PasswordPolicy::checkPassword($new, $current, new self()); 1350 } 1351} 1352UserAuthenticationBackend::register('osTicketClientAuthentication'); 1353 1354class ClientPasswordResetTokenBackend extends UserAuthenticationBackend { 1355 static $id = "pwreset.client"; 1356 1357 function supportsInteractiveAuthentication() { 1358 return false; 1359 } 1360 1361 function signOn($errors=array()) { 1362 global $ost; 1363 1364 if (!isset($_POST['userid']) || !isset($_POST['token'])) 1365 return false; 1366 elseif (!($_config = new Config('pwreset'))) 1367 return false; 1368 elseif (!($acct = ClientAccount::lookupByUsername($_POST['userid'])) 1369 || !$acct->getId() 1370 || !($client = new ClientSession(new EndUser($acct->getUser())))) 1371 $errors['msg'] = __('Invalid user-id given'); 1372 elseif (!($id = $_config->get($_POST['token'])) 1373 || $id != 'c'.$client->getId()) 1374 $errors['msg'] = __('Invalid reset token'); 1375 elseif (!($ts = $_config->lastModified($_POST['token'])) 1376 && ($ost->getConfig()->getPwResetWindow() < (time() - strtotime($ts)))) 1377 $errors['msg'] = __('Invalid reset token'); 1378 elseif (!$acct->forcePasswdReset()) 1379 $errors['msg'] = __('Unable to reset password'); 1380 else 1381 return $client; 1382 } 1383 1384 function login($client, $bk) { 1385 $_SESSION['_client']['reset-token'] = $_POST['token']; 1386 Signal::send('auth.pwreset.login', $client); 1387 return parent::login($client, $bk); 1388 } 1389} 1390UserAuthenticationBackend::register('ClientPasswordResetTokenBackend'); 1391 1392class ClientAcctConfirmationTokenBackend extends UserAuthenticationBackend { 1393 static $id = "confirm.client"; 1394 1395 function supportsInteractiveAuthentication() { 1396 return false; 1397 } 1398 1399 function signOn($errors=array()) { 1400 global $ost; 1401 1402 if (!isset($_GET['token'])) 1403 return false; 1404 elseif (!($_config = new Config('pwreset'))) 1405 return false; 1406 elseif (!($id = $_config->get($_GET['token']))) 1407 return false; 1408 elseif (!($acct = ClientAccount::lookup(array('user_id'=>substr($id,1)))) 1409 || !$acct->getId() 1410 || $id != 'c'.$acct->getUserId() 1411 || !($client = new ClientSession(new EndUser($acct->getUser())))) 1412 return false; 1413 else 1414 return $client; 1415 } 1416} 1417UserAuthenticationBackend::register('ClientAcctConfirmationTokenBackend'); 1418 1419// ----- Password Policy -------------------------------------- 1420 1421class BadPassword extends Exception {} 1422class ExpiredPassword extends Exception {} 1423class PasswordUpdateFailed extends Exception {} 1424 1425abstract class PasswordPolicy { 1426 static protected $registry = array(); 1427 1428 static $id; 1429 static $name; 1430 1431 /** 1432 * Check a password and throw BadPassword with a meaningful message if 1433 * the password cannot be accepted. 1434 */ 1435 abstract function onset($new, $current); 1436 1437 /* 1438 * Called on login to enforce policies & check for expired passwords 1439 */ 1440 abstract function onLogin($user, $password); 1441 1442 /* 1443 * get friendly name of the policy 1444 */ 1445 function getName() { 1446 return static::$name; 1447 } 1448 1449 /* 1450 * Check a password aganist all available policies 1451 */ 1452 static function checkPassword($new, $current, $bk=null) { 1453 if ($bk && is_a($bk, 'AuthenticationBackend')) 1454 $policies = $bk->getPasswordPolicies(); 1455 else 1456 $policies = self::allActivePolicies(); 1457 1458 foreach ($policies as $P) 1459 $P->onSet($new, $current); 1460 } 1461 1462 static function allActivePolicies() { 1463 $policies = array(); 1464 foreach (array_reverse(static::$registry) as $P) { 1465 if (is_string($P) && class_exists($P)) 1466 $P = new $P(); 1467 if ($P instanceof PasswordPolicy) 1468 $policies[] = $P; 1469 } 1470 return $policies; 1471 } 1472 1473 static function register($policy) { 1474 static::$registry[] = $policy; 1475 } 1476 1477 static function cleanSessions($model, $user=null) { 1478 $criteria = array(); 1479 1480 switch (true) { 1481 case ($model instanceof Staff): 1482 $criteria['user_id'] = $model->getId(); 1483 1484 if ($user && ($model->getId() == $user->getId())) 1485 array_push($criteria, 1486 Q::not(array('session_id' => $user->session->session_id))); 1487 break; 1488 case ($model instanceof User): 1489 $regexp = '_auth\|.*"user";[a-z]+:[0-9]+:\{[a-z]+:[0-9]+:"id";[a-z]+:'.$model->getId(); 1490 $criteria['user_id'] = 0; 1491 $criteria['session_data__regex'] = $regexp; 1492 1493 if ($user) 1494 array_push($criteria, 1495 Q::not(array('session_id' => $user->session->session_id))); 1496 break; 1497 default: 1498 return false; 1499 } 1500 1501 return SessionData::objects()->filter($criteria)->delete(); 1502 } 1503} 1504Signal::connect('auth.clean', array('PasswordPolicy', 'cleanSessions')); 1505 1506/* 1507 * Basic default password policy that ships with osTicket. 1508 * 1509 */ 1510class osTicketPasswordPolicy 1511extends PasswordPolicy { 1512 static $id = "basic"; 1513 static $name = /* @trans */ "Default Basic Policy"; 1514 1515 function onLogin($user, $password) { 1516 global $cfg; 1517 1518 // Check for possible password expiration 1519 // Check is only here for legacy reasons - password management 1520 // policies are now done via plugins. 1521 if ($cfg && $user 1522 && ($period=$cfg->getPasswdResetPeriod()) 1523 && ($time=$user->getPasswdResetTimestamp()) 1524 && $time < time()-($period*2629800)) 1525 throw new ExpiredPassword(__('Expired password')); 1526 } 1527 1528 function onSet($passwd, $current) { 1529 if (strlen($passwd) < 6) { 1530 throw new BadPassword( 1531 __('Password must be at least 6 characters')); 1532 } 1533 // XXX: Changing case is technicall changing the password 1534 if (0 === strcasecmp($passwd, $current)) { 1535 throw new BadPassword( 1536 __('New password MUST be different from the current password!')); 1537 } 1538 } 1539} 1540PasswordPolicy::register('osTicketPasswordPolicy'); 1541?> 1542