1<?php 2namespace TYPO3\CMS\Core\Authentication; 3 4/* 5 * This file is part of the TYPO3 CMS project. 6 * 7 * It is free software; you can redistribute it and/or modify it under 8 * the terms of the GNU General Public License, either version 2 9 * of the License, or any later version. 10 * 11 * For the full copyright and license information, please read the 12 * LICENSE.txt file that was distributed with this source code. 13 * 14 * The TYPO3 project - inspiring people to share! 15 */ 16 17use Psr\Log\LoggerAwareInterface; 18use Psr\Log\LoggerAwareTrait; 19use Symfony\Component\HttpFoundation\Cookie; 20use TYPO3\CMS\Core\Core\Environment; 21use TYPO3\CMS\Core\Crypto\Random; 22use TYPO3\CMS\Core\Database\Connection; 23use TYPO3\CMS\Core\Database\ConnectionPool; 24use TYPO3\CMS\Core\Database\Query\QueryHelper; 25use TYPO3\CMS\Core\Database\Query\Restriction\DefaultRestrictionContainer; 26use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction; 27use TYPO3\CMS\Core\Database\Query\Restriction\EndTimeRestriction; 28use TYPO3\CMS\Core\Database\Query\Restriction\HiddenRestriction; 29use TYPO3\CMS\Core\Database\Query\Restriction\QueryRestrictionContainerInterface; 30use TYPO3\CMS\Core\Database\Query\Restriction\RootLevelRestriction; 31use TYPO3\CMS\Core\Database\Query\Restriction\StartTimeRestriction; 32use TYPO3\CMS\Core\Exception; 33use TYPO3\CMS\Core\Http\CookieHeaderTrait; 34use TYPO3\CMS\Core\Session\Backend\Exception\SessionNotFoundException; 35use TYPO3\CMS\Core\Session\Backend\HashableSessionBackendInterface; 36use TYPO3\CMS\Core\Session\Backend\SessionBackendInterface; 37use TYPO3\CMS\Core\Session\SessionManager; 38use TYPO3\CMS\Core\Utility\GeneralUtility; 39use TYPO3\CMS\Core\Utility\MathUtility; 40 41/** 42 * Authentication of users in TYPO3 43 * 44 * This class is used to authenticate a login user. 45 * The class is used by both the frontend and backend. 46 * In both cases this class is a parent class to BackendUserAuthentication and FrontendUserAuthentication 47 * 48 * See Inside TYPO3 for more information about the API of the class and internal variables. 49 */ 50abstract class AbstractUserAuthentication implements LoggerAwareInterface 51{ 52 use LoggerAwareTrait; 53 use CookieHeaderTrait; 54 55 /** 56 * Session/Cookie name 57 * @var string 58 */ 59 public $name = ''; 60 61 /** 62 * Session/GET-var name 63 * @var string 64 */ 65 public $get_name = ''; 66 67 /** 68 * Table in database with user data 69 * @var string 70 */ 71 public $user_table = ''; 72 73 /** 74 * Table in database with user groups 75 * @var string 76 */ 77 public $usergroup_table = ''; 78 79 /** 80 * Column for login-name 81 * @var string 82 */ 83 public $username_column = ''; 84 85 /** 86 * Column for password 87 * @var string 88 */ 89 public $userident_column = ''; 90 91 /** 92 * Column for user-id 93 * @var string 94 */ 95 public $userid_column = ''; 96 97 /** 98 * Column for user group information 99 * @var string 100 */ 101 public $usergroup_column = ''; 102 103 /** 104 * Column name for last login timestamp 105 * @var string 106 */ 107 public $lastLogin_column = ''; 108 109 /** 110 * Enable field columns of user table 111 * @var array 112 */ 113 public $enablecolumns = [ 114 'rootLevel' => '', 115 // Boolean: If TRUE, 'AND pid=0' will be a part of the query... 116 'disabled' => '', 117 'starttime' => '', 118 'endtime' => '', 119 'deleted' => '', 120 ]; 121 122 /** 123 * @var bool 124 */ 125 public $showHiddenRecords = false; 126 127 /** 128 * Form field with login-name 129 * @var string 130 */ 131 public $formfield_uname = ''; 132 133 /** 134 * Form field with password 135 * @var string 136 */ 137 public $formfield_uident = ''; 138 139 /** 140 * Form field with status: *'login', 'logout'. If empty login is not verified. 141 * @var string 142 */ 143 public $formfield_status = ''; 144 145 /** 146 * Session timeout (on the server) 147 * 148 * If >0: session-timeout in seconds. 149 * If <=0: Instant logout after login. 150 * 151 * @var int 152 */ 153 public $sessionTimeout = 0; 154 155 /** 156 * Name for a field to fetch the server session timeout from. 157 * If not empty this is a field name from the user table where the timeout can be found. 158 * @var string 159 */ 160 public $auth_timeout_field = ''; 161 162 /** 163 * Lifetime for the session-cookie (on the client) 164 * 165 * If >0: permanent cookie with given lifetime 166 * If 0: session-cookie 167 * Session-cookie means the browser will remove it when the browser is closed. 168 * 169 * @var int 170 */ 171 public $lifetime = 0; 172 173 /** 174 * GarbageCollection 175 * Purge all server session data older than $gc_time seconds. 176 * 0 = default to $this->sessionTimeout or use 86400 seconds (1 day) if $this->sessionTimeout == 0 177 * @var int 178 */ 179 public $gc_time = 0; 180 181 /** 182 * Probability for garbage collection to be run (in percent) 183 * @var int 184 */ 185 public $gc_probability = 1; 186 187 /** 188 * Decides if the writelog() function is called at login and logout 189 * @var bool 190 */ 191 public $writeStdLog = false; 192 193 /** 194 * Log failed login attempts 195 * @var bool 196 */ 197 public $writeAttemptLog = false; 198 199 /** 200 * Send no-cache headers 201 * @var bool 202 */ 203 public $sendNoCacheHeaders = true; 204 205 /** 206 * If this is set, authentication is also accepted by $_GET. 207 * Notice that the identification is NOT 128bit MD5 hash but reduced. 208 * This is done in order to minimize the size for mobile-devices, such as WAP-phones 209 * @var bool 210 */ 211 public $getFallBack = false; 212 213 /** 214 * The ident-hash is normally 32 characters and should be! 215 * But if you are making sites for WAP-devices or other low-bandwidth stuff, 216 * you may shorten the length. 217 * Never let this value drop below 6! 218 * A length of 6 would give you more than 16 mio possibilities. 219 * @var int 220 */ 221 public $hash_length = 32; 222 223 /** 224 * Setting this flag TRUE lets user-authentication happen from GET_VARS if 225 * POST_VARS are not set. Thus you may supply username/password with the URL. 226 * @var bool 227 */ 228 public $getMethodEnabled = false; 229 230 /** 231 * If set to 4, the session will be locked to the user's IP address (all four numbers). 232 * Reducing this to 1-3 means that only the given number of parts of the IP address is used. 233 * @var int 234 */ 235 public $lockIP = 4; 236 237 /** 238 * @var string 239 */ 240 public $warningEmail = ''; 241 242 /** 243 * Time span (in seconds) within the number of failed logins are collected 244 * @var int 245 */ 246 public $warningPeriod = 3600; 247 248 /** 249 * The maximum accepted number of warnings before an email to $warningEmail is sent 250 * @var int 251 */ 252 public $warningMax = 3; 253 254 /** 255 * If set, the user-record must be stored at the page defined by $checkPid_value 256 * @var bool 257 */ 258 public $checkPid = true; 259 260 /** 261 * The page id the user record must be stored at 262 * @var int 263 */ 264 public $checkPid_value = 0; 265 266 /** 267 * session_id (MD5-hash) 268 * @var string 269 * @internal 270 */ 271 public $id; 272 273 /** 274 * Indicates if an authentication was started but failed 275 * @var bool 276 */ 277 public $loginFailure = false; 278 279 /** 280 * Will be set to TRUE if the login session is actually written during auth-check. 281 * @var bool 282 */ 283 public $loginSessionStarted = false; 284 285 /** 286 * @var array|null contains user- AND session-data from database (joined tables) 287 * @internal 288 */ 289 public $user; 290 291 /** 292 * Will be added to the url (eg. '&login=ab7ef8d...') 293 * GET-auth-var if getFallBack is TRUE. Should be inserted in links! 294 * @var string 295 * @internal 296 */ 297 public $get_URL_ID = ''; 298 299 /** 300 * Will be set to TRUE if a new session ID was created 301 * @var bool 302 */ 303 public $newSessionID = false; 304 305 /** 306 * Will force the session cookie to be set every time (lifetime must be 0) 307 * @var bool 308 */ 309 public $forceSetCookie = false; 310 311 /** 312 * Will prevent the setting of the session cookie (takes precedence over forceSetCookie) 313 * @var bool 314 */ 315 public $dontSetCookie = false; 316 317 /** 318 * @var bool 319 */ 320 protected $cookieWasSetOnCurrentRequest = false; 321 322 /** 323 * Login type, used for services. 324 * @var string 325 */ 326 public $loginType = ''; 327 328 /** 329 * "auth" services configuration array from $GLOBALS['TYPO3_CONF_VARS']['SVCONF']['auth'] 330 * @var array 331 */ 332 public $svConfig = []; 333 334 /** 335 * @var array 336 */ 337 public $uc; 338 339 /** 340 * @var SessionBackendInterface 341 */ 342 protected $sessionBackend; 343 344 /** 345 * Holds deserialized data from session records. 346 * 'Reserved' keys are: 347 * - 'sys': Reserved for TypoScript standard code. 348 * @var array 349 */ 350 protected $sessionData = []; 351 352 /** 353 * Initialize some important variables 354 */ 355 public function __construct() 356 { 357 // This function has to stay even if it's empty 358 // Implementations of that abstract class might call parent::__construct(); 359 } 360 361 /** 362 * Starts a user session 363 * Typical configurations will: 364 * a) check if session cookie was set and if not, set one, 365 * b) check if a password/username was sent and if so, try to authenticate the user 366 * c) Lookup a session attached to a user and check timeout etc. 367 * d) Garbage collection, setting of no-cache headers. 368 * If a user is authenticated the database record of the user (array) will be set in the ->user internal variable. 369 * 370 * @throws Exception 371 */ 372 public function start() 373 { 374 // Backend or frontend login - used for auth services 375 if (empty($this->loginType)) { 376 throw new Exception('No loginType defined, should be set explicitly by subclass', 1476045345); 377 } 378 $this->logger->debug('## Beginning of auth logging.'); 379 // Init vars. 380 $mode = ''; 381 $this->newSessionID = false; 382 // $id is set to ses_id if cookie is present. Else set to FALSE, which will start a new session 383 $id = $this->getCookie($this->name); 384 $this->svConfig = $GLOBALS['TYPO3_CONF_VARS']['SVCONF']['auth'] ?? []; 385 386 // If fallback to get mode.... 387 if (!$id && $this->getFallBack && $this->get_name) { 388 $id = isset($_GET[$this->get_name]) ? GeneralUtility::_GET($this->get_name) : ''; 389 if (strlen($id) != $this->hash_length) { 390 $id = ''; 391 } 392 $mode = 'get'; 393 } 394 395 // If new session or client tries to fix session... 396 if (!$id || !$this->isExistingSessionRecord($id)) { 397 // New random session-$id is made 398 $id = $this->createSessionId(); 399 // New session 400 $this->newSessionID = true; 401 } 402 // Internal var 'id' is set 403 $this->id = $id; 404 // If fallback to get mode.... 405 if ($mode === 'get' && $this->getFallBack && $this->get_name) { 406 $this->get_URL_ID = '&' . $this->get_name . '=' . $id; 407 } 408 // Make certain that NO user is set initially 409 $this->user = null; 410 // Set all possible headers that could ensure that the script is not cached on the client-side 411 $this->sendHttpHeaders(); 412 // Load user session, check to see if anyone has submitted login-information and if so authenticate 413 // the user with the session. $this->user[uid] may be used to write log... 414 $this->checkAuthentication(); 415 // Setting cookies 416 if (!$this->dontSetCookie) { 417 $this->setSessionCookie(); 418 } 419 // Hook for alternative ways of filling the $this->user array (is used by the "timtaw" extension) 420 foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_userauth.php']['postUserLookUp'] ?? [] as $funcName) { 421 $_params = [ 422 'pObj' => $this, 423 ]; 424 GeneralUtility::callUserFunction($funcName, $_params, $this); 425 } 426 // Set $this->gc_time if not explicitly specified 427 if ($this->gc_time === 0) { 428 // Default to 86400 seconds (1 day) if $this->sessionTimeout is 0 429 $this->gc_time = $this->sessionTimeout === 0 ? 86400 : $this->sessionTimeout; 430 } 431 // If we're lucky we'll get to clean up old sessions 432 if (rand() % 100 <= $this->gc_probability) { 433 $this->gc(); 434 } 435 } 436 437 /** 438 * Set all possible headers that could ensure that the script 439 * is not cached on the client-side. 440 * 441 * Only do this if $this->sendNoCacheHeaders is set. 442 */ 443 protected function sendHttpHeaders() 444 { 445 // skip sending the "no-cache" headers if it's a CLI request or the no-cache headers should not be sent. 446 if (!$this->sendNoCacheHeaders || Environment::isCli()) { 447 return; 448 } 449 $httpHeaders = $this->getHttpHeaders(); 450 foreach ($httpHeaders as $httpHeaderName => $value) { 451 header($httpHeaderName . ': ' . $value); 452 } 453 } 454 455 /** 456 * Get the http headers to be sent if an authenticated user is available, in order to disallow 457 * browsers to store the response on the client side. 458 * 459 * @return array 460 */ 461 protected function getHttpHeaders(): array 462 { 463 $headers = [ 464 'Expires' => 0, 465 'Last-Modified' => gmdate('D, d M Y H:i:s') . ' GMT' 466 ]; 467 $cacheControlHeader = 'no-cache, must-revalidate'; 468 $pragmaHeader = 'no-cache'; 469 // Prevent error message in IE when using a https connection 470 // see http://forge.typo3.org/issues/24125 471 if (strpos(GeneralUtility::getIndpEnv('HTTP_USER_AGENT'), 'MSIE') !== false 472 && GeneralUtility::getIndpEnv('TYPO3_SSL')) { 473 // Some IEs can not handle no-cache 474 // see http://support.microsoft.com/kb/323308/en-us 475 $cacheControlHeader = 'must-revalidate'; 476 // IE needs "Pragma: private" if SSL connection 477 $pragmaHeader = 'private'; 478 } 479 $headers['Cache-Control'] = $cacheControlHeader; 480 $headers['Pragma'] = $pragmaHeader; 481 return $headers; 482 } 483 484 /** 485 * Sets the session cookie for the current disposal. 486 * 487 * @throws Exception 488 */ 489 protected function setSessionCookie() 490 { 491 $isSetSessionCookie = $this->isSetSessionCookie(); 492 $isRefreshTimeBasedCookie = $this->isRefreshTimeBasedCookie(); 493 if ($isSetSessionCookie || $isRefreshTimeBasedCookie) { 494 $settings = $GLOBALS['TYPO3_CONF_VARS']['SYS']; 495 // Get the domain to be used for the cookie (if any): 496 $cookieDomain = $this->getCookieDomain(); 497 // If no cookie domain is set, use the base path: 498 $cookiePath = $cookieDomain ? '/' : GeneralUtility::getIndpEnv('TYPO3_SITE_PATH'); 499 // If the cookie lifetime is set, use it: 500 $cookieExpire = $isRefreshTimeBasedCookie ? $GLOBALS['EXEC_TIME'] + $this->lifetime : 0; 501 // Use the secure option when the current request is served by a secure connection: 502 $cookieSecure = (bool)$settings['cookieSecure'] && GeneralUtility::getIndpEnv('TYPO3_SSL'); 503 // Valid options are "strict", "lax" or "none", whereas "none" only works in HTTPS requests (default & fallback is "strict") 504 $cookieSameSite = $this->sanitizeSameSiteCookieValue( 505 strtolower($GLOBALS['TYPO3_CONF_VARS'][$this->loginType]['cookieSameSite'] ?? Cookie::SAMESITE_STRICT) 506 ); 507 // SameSite "none" needs the secure option (only allowed on HTTPS) 508 if ($cookieSameSite === Cookie::SAMESITE_NONE) { 509 $cookieSecure = true; 510 } 511 // Do not set cookie if cookieSecure is set to "1" (force HTTPS) and no secure channel is used: 512 if ((int)$settings['cookieSecure'] !== 1 || GeneralUtility::getIndpEnv('TYPO3_SSL')) { 513 $cookie = new Cookie( 514 $this->name, 515 $this->id, 516 $cookieExpire, 517 $cookiePath, 518 $cookieDomain, 519 $cookieSecure, 520 true, 521 false, 522 $cookieSameSite 523 ); 524 header('Set-Cookie: ' . $cookie->__toString(), false); 525 $this->cookieWasSetOnCurrentRequest = true; 526 } else { 527 throw new Exception('Cookie was not set since HTTPS was forced in $TYPO3_CONF_VARS[SYS][cookieSecure].', 1254325546); 528 } 529 $this->logger->debug( 530 ($isRefreshTimeBasedCookie ? 'Updated Cookie: ' : 'Set Cookie: ') 531 . sha1($this->id) . ($cookieDomain ? ', ' . $cookieDomain : '') 532 ); 533 } 534 } 535 536 /** 537 * Gets the domain to be used on setting cookies. 538 * The information is taken from the value in $GLOBALS['TYPO3_CONF_VARS']['SYS']['cookieDomain']. 539 * 540 * @return string The domain to be used on setting cookies 541 */ 542 protected function getCookieDomain() 543 { 544 $result = ''; 545 $cookieDomain = $GLOBALS['TYPO3_CONF_VARS']['SYS']['cookieDomain']; 546 // If a specific cookie domain is defined for a given TYPO3_MODE, 547 // use that domain 548 if (!empty($GLOBALS['TYPO3_CONF_VARS'][$this->loginType]['cookieDomain'])) { 549 $cookieDomain = $GLOBALS['TYPO3_CONF_VARS'][$this->loginType]['cookieDomain']; 550 } 551 if ($cookieDomain) { 552 if ($cookieDomain[0] === '/') { 553 $match = []; 554 $matchCnt = @preg_match($cookieDomain, GeneralUtility::getIndpEnv('TYPO3_HOST_ONLY'), $match); 555 if ($matchCnt === false) { 556 $this->logger->critical('The regular expression for the cookie domain (' . $cookieDomain . ') contains errors. The session is not shared across sub-domains.'); 557 } elseif ($matchCnt) { 558 $result = $match[0]; 559 } 560 } else { 561 $result = $cookieDomain; 562 } 563 } 564 return $result; 565 } 566 567 /** 568 * Get the value of a specified cookie. 569 * 570 * @param string $cookieName The cookie ID 571 * @return string The value stored in the cookie 572 */ 573 protected function getCookie($cookieName) 574 { 575 return isset($_COOKIE[$cookieName]) ? stripslashes($_COOKIE[$cookieName]) : ''; 576 } 577 578 /** 579 * Determine whether a session cookie needs to be set (lifetime=0) 580 * 581 * @return bool 582 * @internal 583 */ 584 public function isSetSessionCookie() 585 { 586 return ($this->newSessionID || $this->forceSetCookie) && $this->lifetime == 0; 587 } 588 589 /** 590 * Determine whether a non-session cookie needs to be set (lifetime>0) 591 * 592 * @return bool 593 * @internal 594 */ 595 public function isRefreshTimeBasedCookie() 596 { 597 return $this->lifetime > 0; 598 } 599 600 /** 601 * Checks if a submission of username and password is present or use other authentication by auth services 602 * 603 * @throws \RuntimeException 604 * @internal 605 */ 606 public function checkAuthentication() 607 { 608 // No user for now - will be searched by service below 609 $tempuserArr = []; 610 $tempuser = false; 611 // User is not authenticated by default 612 $authenticated = false; 613 // User want to login with passed login data (name/password) 614 $activeLogin = false; 615 // Indicates if an active authentication failed (not auto login) 616 $this->loginFailure = false; 617 $this->logger->debug('Login type: ' . $this->loginType); 618 // The info array provide additional information for auth services 619 $authInfo = $this->getAuthInfoArray(); 620 // Get Login/Logout data submitted by a form or params 621 $loginData = $this->getLoginFormData(); 622 $this->logger->debug('Login data', $this->removeSensitiveLoginDataForLoggingInfo($loginData)); 623 // Active logout (eg. with "logout" button) 624 if ($loginData['status'] === LoginType::LOGOUT) { 625 if ($this->writeStdLog) { 626 // $type,$action,$error,$details_nr,$details,$data,$tablename,$recuid,$recpid 627 $this->writelog(255, 2, 0, 2, 'User %s logged out', [$this->user['username']], '', 0, 0); 628 } 629 $this->logger->info('User logged out. Id: ' . sha1($this->id)); 630 $this->logoff(); 631 } 632 // Determine whether we need to skip session update. 633 // This is used mainly for checking session timeout in advance without refreshing the current session's timeout. 634 $skipSessionUpdate = (bool)GeneralUtility::_GP('skipSessionUpdate'); 635 $haveSession = false; 636 $anonymousSession = false; 637 if (!$this->newSessionID) { 638 // Read user session 639 $authInfo['userSession'] = $this->fetchUserSession($skipSessionUpdate); 640 $haveSession = is_array($authInfo['userSession']); 641 if ($haveSession && !empty($authInfo['userSession']['ses_anonymous'])) { 642 $anonymousSession = true; 643 } 644 } 645 646 // Active login (eg. with login form). 647 if (!$haveSession && $loginData['status'] === LoginType::LOGIN) { 648 $activeLogin = true; 649 $this->logger->debug('Active login (eg. with login form)'); 650 // check referrer for submitted login values 651 if ($this->formfield_status && $loginData['uident'] && $loginData['uname']) { 652 // Delete old user session if any 653 $this->logoff(); 654 } 655 // Refuse login for _CLI users, if not processing a CLI request type 656 // (although we shouldn't be here in case of a CLI request type) 657 if (stripos($loginData['uname'], '_CLI_') === 0 && !Environment::isCli()) { 658 throw new \RuntimeException('TYPO3 Fatal Error: You have tried to login using a CLI user. Access prohibited!', 1270853931); 659 } 660 } 661 662 // Cause elevation of privilege, make sure regenerateSessionId is called later on 663 if ($anonymousSession && $loginData['status'] === LoginType::LOGIN) { 664 $activeLogin = true; 665 } 666 667 if ($haveSession) { 668 $this->logger->debug('User session found', [ 669 $this->userid_column => $authInfo['userSession'][$this->userid_column] ?? null, 670 $this->username_column => $authInfo['userSession'][$this->username_column] ?? null, 671 ]); 672 } else { 673 $this->logger->debug('No user session found'); 674 } 675 if (is_array($this->svConfig['setup'] ?? false)) { 676 $this->logger->debug('SV setup', $this->svConfig['setup']); 677 } 678 679 // Fetch user if ... 680 if ( 681 $activeLogin || !empty($this->svConfig['setup'][$this->loginType . '_alwaysFetchUser']) 682 || !$haveSession && !empty($this->svConfig['setup'][$this->loginType . '_fetchUserIfNoSession']) 683 ) { 684 // Use 'auth' service to find the user 685 // First found user will be used 686 $subType = 'getUser' . $this->loginType; 687 foreach ($this->getAuthServices($subType, $loginData, $authInfo) as $serviceObj) { 688 if ($row = $serviceObj->getUser()) { 689 $tempuserArr[] = $row; 690 $this->logger->debug('User found', [ 691 $this->userid_column => $row[$this->userid_column], 692 $this->username_column => $row[$this->username_column], 693 ]); 694 // User found, just stop to search for more if not configured to go on 695 if (empty($this->svConfig['setup'][$this->loginType . '_fetchAllUsers'])) { 696 break; 697 } 698 } 699 } 700 701 if (!empty($this->svConfig['setup'][$this->loginType . '_alwaysFetchUser'])) { 702 $this->logger->debug($this->loginType . '_alwaysFetchUser option is enabled'); 703 } 704 if (empty($tempuserArr)) { 705 $this->logger->debug('No user found by services'); 706 } else { 707 $this->logger->debug(count($tempuserArr) . ' user records found by services'); 708 } 709 } 710 711 // If no new user was set we use the already found user session 712 if (empty($tempuserArr) && $haveSession && !$anonymousSession) { 713 $tempuserArr[] = $authInfo['userSession']; 714 $tempuser = $authInfo['userSession']; 715 // User is authenticated because we found a user session 716 $authenticated = true; 717 $this->logger->debug('User session used', [ 718 $this->userid_column => $authInfo['userSession'][$this->userid_column], 719 $this->username_column => $authInfo['userSession'][$this->username_column], 720 ]); 721 } 722 // Re-auth user when 'auth'-service option is set 723 if (!empty($this->svConfig['setup'][$this->loginType . '_alwaysAuthUser'])) { 724 $authenticated = false; 725 $this->logger->debug('alwaysAuthUser option is enabled'); 726 } 727 // Authenticate the user if needed 728 if (!empty($tempuserArr) && !$authenticated) { 729 foreach ($tempuserArr as $tempuser) { 730 // Use 'auth' service to authenticate the user 731 // If one service returns FALSE then authentication failed 732 // a service might return 100 which means there's no reason to stop but the user can't be authenticated by that service 733 $this->logger->debug('Auth user', $this->removeSensitiveLoginDataForLoggingInfo($tempuser, true)); 734 $subType = 'authUser' . $this->loginType; 735 736 foreach ($this->getAuthServices($subType, $loginData, $authInfo) as $serviceObj) { 737 if (($ret = $serviceObj->authUser($tempuser)) > 0) { 738 // If the service returns >=200 then no more checking is needed - useful for IP checking without password 739 if ((int)$ret >= 200) { 740 $authenticated = true; 741 break; 742 } 743 if ((int)$ret >= 100) { 744 } else { 745 $authenticated = true; 746 } 747 } else { 748 $authenticated = false; 749 break; 750 } 751 } 752 753 if ($authenticated) { 754 // Leave foreach() because a user is authenticated 755 break; 756 } 757 } 758 } 759 760 // If user is authenticated a valid user is in $tempuser 761 if ($authenticated) { 762 // Reset failure flag 763 $this->loginFailure = false; 764 // Insert session record if needed: 765 if (!$haveSession || $anonymousSession || $tempuser['ses_id'] != $this->id && $tempuser['uid'] != $authInfo['userSession']['ses_userid']) { 766 $sessionData = $this->createUserSession($tempuser); 767 768 // Preserve session data on login 769 if ($anonymousSession) { 770 $sessionData = $this->getSessionBackend()->update( 771 $this->id, 772 ['ses_data' => $authInfo['userSession']['ses_data']] 773 ); 774 } 775 776 $this->user = array_merge( 777 $tempuser, 778 $sessionData 779 ); 780 // The login session is started. 781 $this->loginSessionStarted = true; 782 if (is_array($this->user)) { 783 $this->logger->debug('User session finally read', [ 784 $this->userid_column => $this->user[$this->userid_column], 785 $this->username_column => $this->user[$this->username_column], 786 ]); 787 } 788 } elseif ($haveSession) { 789 // Validate the session ID and promote it 790 // This check can be removed in TYPO3 v11 791 if ($this->getSessionBackend() instanceof HashableSessionBackendInterface && !empty($authInfo['userSession']['ses_id'] ?? '')) { 792 // The session is stored in plaintext, promote it 793 if ($authInfo['userSession']['ses_id'] === $this->id) { 794 $authInfo['userSession'] = $this->getSessionBackend()->update( 795 $this->id, 796 ['ses_data' => $authInfo['userSession']['ses_data']] 797 ); 798 } 799 } 800 // if we come here the current session is for sure not anonymous as this is a pre-condition for $authenticated = true 801 $this->user = $authInfo['userSession']; 802 } 803 804 if ($activeLogin && !$this->newSessionID) { 805 $this->regenerateSessionId(); 806 } 807 808 // User logged in - write that to the log! 809 if ($this->writeStdLog && $activeLogin) { 810 $this->writelog(255, 1, 0, 1, 'User %s logged in from ###IP###', [$tempuser[$this->username_column]], '', '', ''); 811 } 812 if ($activeLogin) { 813 $this->logger->info('User ' . $tempuser[$this->username_column] . ' logged in from ' . GeneralUtility::getIndpEnv('REMOTE_ADDR')); 814 } 815 if (!$activeLogin) { 816 $this->logger->debug('User ' . $tempuser[$this->username_column] . ' authenticated from ' . GeneralUtility::getIndpEnv('REMOTE_ADDR')); 817 } 818 } else { 819 // User was not authenticated, so we should reuse the existing anonymous session 820 if ($anonymousSession) { 821 $this->user = $authInfo['userSession']; 822 } 823 824 // Mark the current login attempt as failed 825 if ($activeLogin || !empty($tempuserArr)) { 826 $this->loginFailure = true; 827 if (empty($tempuserArr) && $activeLogin) { 828 $logData = [ 829 'loginData' => $this->removeSensitiveLoginDataForLoggingInfo($loginData) 830 ]; 831 $this->logger->debug('Login failed', $logData); 832 } 833 if (!empty($tempuserArr)) { 834 $logData = [ 835 $this->userid_column => $tempuser[$this->userid_column], 836 $this->username_column => $tempuser[$this->username_column], 837 ]; 838 $this->logger->debug('Login failed', $logData); 839 } 840 } 841 } 842 843 // If there were a login failure, check to see if a warning email should be sent: 844 if ($this->loginFailure && $activeLogin) { 845 $this->logger->debug( 846 'Call checkLogFailures', 847 [ 848 'warningEmail' => $this->warningEmail, 849 'warningPeriod' => $this->warningPeriod, 850 'warningMax' => $this->warningMax 851 ] 852 ); 853 854 // Hook to implement login failure tracking methods 855 $_params = []; 856 $sleep = true; 857 foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_userauth.php']['postLoginFailureProcessing'] ?? [] as $_funcRef) { 858 GeneralUtility::callUserFunction($_funcRef, $_params, $this); 859 $sleep = false; 860 } 861 862 if ($sleep) { 863 // No hooks were triggered - default login failure behavior is to sleep 5 seconds 864 sleep(5); 865 } 866 867 $this->checkLogFailures($this->warningEmail, $this->warningPeriod, $this->warningMax); 868 } 869 } 870 871 /** 872 * Creates a new session ID. 873 * 874 * @return string The new session ID 875 */ 876 public function createSessionId() 877 { 878 return GeneralUtility::makeInstance(Random::class)->generateRandomHexString($this->hash_length); 879 } 880 881 /** 882 * Initializes authentication services to be used in a foreach loop 883 * 884 * @param string $subType e.g. getUserFE 885 * @param array $loginData 886 * @param array $authInfo 887 * @return \Traversable A generator of service objects 888 */ 889 protected function getAuthServices(string $subType, array $loginData, array $authInfo): \Traversable 890 { 891 $serviceChain = ''; 892 while (is_object($serviceObj = GeneralUtility::makeInstanceService('auth', $subType, $serviceChain))) { 893 $serviceChain .= ',' . $serviceObj->getServiceKey(); 894 $serviceObj->initAuth($subType, $loginData, $authInfo, $this); 895 yield $serviceObj; 896 } 897 if ($serviceChain) { 898 $this->logger->debug($subType . ' auth services called: ' . $serviceChain); 899 } 900 } 901 902 /** 903 * Regenerate the session ID and transfer the session to new ID 904 * Call this method whenever a user proceeds to a higher authorization level 905 * e.g. when an anonymous session is now authenticated. 906 * 907 * @param array $existingSessionRecord If given, this session record will be used instead of fetching again 908 * @param bool $anonymous If true session will be regenerated as anonymous session 909 */ 910 protected function regenerateSessionId(array $existingSessionRecord = [], bool $anonymous = false) 911 { 912 if (empty($existingSessionRecord)) { 913 $existingSessionRecord = $this->getSessionBackend()->get($this->id); 914 } 915 916 // Update session record with new ID 917 $oldSessionId = $this->id; 918 $this->id = $this->createSessionId(); 919 $updatedSession = $this->getSessionBackend()->set($this->id, $existingSessionRecord); 920 $this->sessionData = unserialize($updatedSession['ses_data']); 921 // Merge new session data into user/session array 922 $this->user = array_merge($this->user ?? [], $updatedSession); 923 $this->getSessionBackend()->remove($oldSessionId); 924 $this->newSessionID = true; 925 } 926 927 /************************* 928 * 929 * User Sessions 930 * 931 *************************/ 932 933 /** 934 * Creates a user session record and returns its values. 935 * 936 * @param array $tempuser User data array 937 * 938 * @return array The session data for the newly created session. 939 */ 940 public function createUserSession($tempuser) 941 { 942 $this->logger->debug('Create session ses_id = ' . sha1($this->id)); 943 // Delete any session entry first 944 $this->getSessionBackend()->remove($this->id); 945 // Re-create session entry 946 $sessionRecord = $this->getNewSessionRecord($tempuser); 947 $sessionRecord = $this->getSessionBackend()->set($this->id, $sessionRecord); 948 // Updating lastLogin_column carrying information about last login. 949 $this->updateLoginTimestamp($tempuser[$this->userid_column]); 950 return $sessionRecord; 951 } 952 953 /** 954 * Updates the last login column in the user with the given id 955 * 956 * @param int $userId 957 */ 958 protected function updateLoginTimestamp(int $userId) 959 { 960 if ($this->lastLogin_column) { 961 $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($this->user_table); 962 $connection->update( 963 $this->user_table, 964 [$this->lastLogin_column => $GLOBALS['EXEC_TIME']], 965 [$this->userid_column => $userId] 966 ); 967 } 968 } 969 970 /** 971 * Returns a new session record for the current user for insertion into the DB. 972 * This function is mainly there as a wrapper for inheriting classes to override it. 973 * 974 * @param array $tempuser 975 * @return array User session record 976 */ 977 public function getNewSessionRecord($tempuser) 978 { 979 $sessionIpLock = '[DISABLED]'; 980 if ($this->lockIP && empty($tempuser['disableIPlock'])) { 981 $sessionIpLock = $this->ipLockClause_remoteIPNumber($this->lockIP); 982 } 983 984 return [ 985 'ses_id' => $this->id, 986 'ses_iplock' => $sessionIpLock, 987 'ses_userid' => $tempuser[$this->userid_column] ?? 0, 988 'ses_tstamp' => $GLOBALS['EXEC_TIME'], 989 'ses_data' => '', 990 ]; 991 } 992 993 /** 994 * Read the user session from db. 995 * 996 * @param bool $skipSessionUpdate 997 * @return array|bool User session data, false if $this->id does not represent valid session 998 */ 999 public function fetchUserSession($skipSessionUpdate = false) 1000 { 1001 $this->logger->debug('Fetch session ses_id = ' . sha1($this->id)); 1002 try { 1003 $sessionRecord = $this->getSessionBackend()->get($this->id); 1004 } catch (SessionNotFoundException $e) { 1005 return false; 1006 } 1007 1008 $this->sessionData = unserialize($sessionRecord['ses_data']); 1009 // Session is anonymous so no need to fetch user 1010 if (!empty($sessionRecord['ses_anonymous'])) { 1011 return $sessionRecord; 1012 } 1013 1014 // Fetch the user from the DB 1015 $userRecord = $this->getRawUserByUid((int)$sessionRecord['ses_userid']); 1016 if ($userRecord) { 1017 $userRecord = array_merge($sessionRecord, $userRecord); 1018 // A user was found 1019 $userRecord['ses_tstamp'] = (int)$userRecord['ses_tstamp']; 1020 $userRecord['is_online'] = (int)$userRecord['ses_tstamp']; 1021 1022 if (!empty($this->auth_timeout_field)) { 1023 // Get timeout-time from usertable 1024 $timeout = (int)$userRecord[$this->auth_timeout_field]; 1025 } else { 1026 $timeout = $this->sessionTimeout; 1027 } 1028 // If timeout > 0 (TRUE) and current time has not exceeded the latest sessions-time plus the timeout in seconds then accept user 1029 // Use a gracetime-value to avoid updating a session-record too often 1030 if ($timeout > 0 && $GLOBALS['EXEC_TIME'] < $userRecord['ses_tstamp'] + $timeout) { 1031 $sessionUpdateGracePeriod = 61; 1032 if (!$skipSessionUpdate && $GLOBALS['EXEC_TIME'] > ($userRecord['ses_tstamp'] + $sessionUpdateGracePeriod)) { 1033 // Update the session timestamp by writing a dummy update. (Backend will update the timestamp) 1034 $updatesSession = $this->getSessionBackend()->update($this->id, []); 1035 $userRecord = array_merge($userRecord, $updatesSession); 1036 } 1037 } else { 1038 // Delete any user set... 1039 $this->logoff(); 1040 $userRecord = false; 1041 } 1042 } 1043 return $userRecord; 1044 } 1045 1046 /** 1047 * Regenerates the session ID and sets the cookie again. 1048 * 1049 * @internal 1050 */ 1051 public function enforceNewSessionId() 1052 { 1053 $this->regenerateSessionId(); 1054 $this->setSessionCookie(); 1055 } 1056 1057 /** 1058 * Log out current user! 1059 * Removes the current session record, sets the internal ->user array to a blank string; 1060 * Thereby the current user (if any) is effectively logged out! 1061 */ 1062 public function logoff() 1063 { 1064 $this->logger->debug('logoff: ses_id = ' . sha1($this->id)); 1065 1066 $_params = []; 1067 foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_userauth.php']['logoff_pre_processing'] ?? [] as $_funcRef) { 1068 if ($_funcRef) { 1069 GeneralUtility::callUserFunction($_funcRef, $_params, $this); 1070 } 1071 } 1072 $this->performLogoff(); 1073 1074 foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_userauth.php']['logoff_post_processing'] ?? [] as $_funcRef) { 1075 if ($_funcRef) { 1076 GeneralUtility::callUserFunction($_funcRef, $_params, $this); 1077 } 1078 } 1079 } 1080 1081 /** 1082 * Perform the logoff action. Called from logoff() as a way to allow subclasses to override 1083 * what happens when a user logs off, without needing to reproduce the hook calls and logging 1084 * that happens in the public logoff() API method. 1085 */ 1086 protected function performLogoff() 1087 { 1088 if ($this->id) { 1089 $this->getSessionBackend()->remove($this->id); 1090 } 1091 $this->sessionData = []; 1092 $this->user = null; 1093 } 1094 1095 /** 1096 * Empty / unset the cookie 1097 * 1098 * @param string $cookieName usually, this is $this->name 1099 */ 1100 public function removeCookie($cookieName) 1101 { 1102 $cookieDomain = $this->getCookieDomain(); 1103 // If no cookie domain is set, use the base path 1104 $cookiePath = $cookieDomain ? '/' : GeneralUtility::getIndpEnv('TYPO3_SITE_PATH'); 1105 setcookie($cookieName, null, -1, $cookiePath, $cookieDomain); 1106 } 1107 1108 /** 1109 * Determine whether there's an according session record to a given session_id. 1110 * Don't care if session record is still valid or not. 1111 * 1112 * @param string $id Claimed Session ID 1113 * @return bool Returns TRUE if a corresponding session was found in the database 1114 */ 1115 public function isExistingSessionRecord($id) 1116 { 1117 try { 1118 $sessionRecord = $this->getSessionBackend()->get($id); 1119 if (empty($sessionRecord)) { 1120 return false; 1121 } 1122 // If the session does not match the current IP lock, it should be treated as invalid 1123 // and a new session should be created. 1124 if ($sessionRecord['ses_iplock'] !== $this->ipLockClause_remoteIPNumber($this->lockIP) && $sessionRecord['ses_iplock'] !== '[DISABLED]') { 1125 return false; 1126 } 1127 return true; 1128 } catch (SessionNotFoundException $e) { 1129 return false; 1130 } 1131 } 1132 1133 /** 1134 * Returns whether this request is going to set a cookie 1135 * or a cookie was already found in the system 1136 * 1137 * @return bool Returns TRUE if a cookie is set 1138 */ 1139 public function isCookieSet() 1140 { 1141 return $this->cookieWasSetOnCurrentRequest || $this->getCookie($this->name); 1142 } 1143 1144 /************************* 1145 * 1146 * SQL Functions 1147 * 1148 *************************/ 1149 /** 1150 * This returns the restrictions needed to select the user respecting 1151 * enable columns and flags like deleted, hidden, starttime, endtime 1152 * and rootLevel 1153 * 1154 * @return QueryRestrictionContainerInterface 1155 * @internal 1156 */ 1157 protected function userConstraints(): QueryRestrictionContainerInterface 1158 { 1159 $restrictionContainer = GeneralUtility::makeInstance(DefaultRestrictionContainer::class); 1160 1161 if (empty($this->enablecolumns['disabled'])) { 1162 $restrictionContainer->removeByType(HiddenRestriction::class); 1163 } 1164 1165 if (empty($this->enablecolumns['deleted'])) { 1166 $restrictionContainer->removeByType(DeletedRestriction::class); 1167 } 1168 1169 if (empty($this->enablecolumns['starttime'])) { 1170 $restrictionContainer->removeByType(StartTimeRestriction::class); 1171 } 1172 1173 if (empty($this->enablecolumns['endtime'])) { 1174 $restrictionContainer->removeByType(EndTimeRestriction::class); 1175 } 1176 1177 if (!empty($this->enablecolumns['rootLevel'])) { 1178 $restrictionContainer->add(GeneralUtility::makeInstance(RootLevelRestriction::class, [$this->user_table])); 1179 } 1180 1181 return $restrictionContainer; 1182 } 1183 1184 /** 1185 * Returns the IP address to lock to. 1186 * The IP address may be partial based on $parts. 1187 * 1188 * @param int $parts 1-4: Indicates how many parts of the IP address to return. 4 means all, 1 means only first number. 1189 * @return string (Partial) IP address for REMOTE_ADDR 1190 */ 1191 protected function ipLockClause_remoteIPNumber($parts) 1192 { 1193 $IP = GeneralUtility::getIndpEnv('REMOTE_ADDR'); 1194 if ($parts >= 4) { 1195 return $IP; 1196 } 1197 $parts = MathUtility::forceIntegerInRange($parts, 1, 3); 1198 $IPparts = explode('.', $IP); 1199 for ($a = 4; $a > $parts; $a--) { 1200 unset($IPparts[$a - 1]); 1201 } 1202 return implode('.', $IPparts); 1203 } 1204 1205 /************************* 1206 * 1207 * Session and Configuration Handling 1208 * 1209 *************************/ 1210 /** 1211 * This writes $variable to the user-record. This is a way of providing session-data. 1212 * You can fetch the data again through $this->uc in this class! 1213 * If $variable is not an array, $this->uc is saved! 1214 * 1215 * @param array|string $variable An array you want to store for the user as session data. If $variable is not supplied (is null), the internal variable, ->uc, is stored by default 1216 */ 1217 public function writeUC($variable = '') 1218 { 1219 if (is_array($this->user) && $this->user[$this->userid_column]) { 1220 if (!is_array($variable)) { 1221 $variable = $this->uc; 1222 } 1223 $this->logger->debug('writeUC: ' . $this->userid_column . '=' . (int)$this->user[$this->userid_column]); 1224 GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($this->user_table)->update( 1225 $this->user_table, 1226 ['uc' => serialize($variable)], 1227 [$this->userid_column => (int)$this->user[$this->userid_column]], 1228 ['uc' => Connection::PARAM_LOB] 1229 ); 1230 } 1231 } 1232 1233 /** 1234 * Sets $theUC as the internal variable ->uc IF $theUC is an array. 1235 * If $theUC is FALSE, the 'uc' content from the ->user array will be unserialized and restored in ->uc 1236 * 1237 * @param mixed $theUC If an array, then set as ->uc, otherwise load from user record 1238 */ 1239 public function unpack_uc($theUC = '') 1240 { 1241 if (!$theUC && isset($this->user['uc'])) { 1242 $theUC = unserialize($this->user['uc'], ['allowed_classes' => false]); 1243 } 1244 if (is_array($theUC)) { 1245 $this->uc = $theUC; 1246 } 1247 } 1248 1249 /** 1250 * Stores data for a module. 1251 * The data is stored with the session id so you can even check upon retrieval 1252 * if the module data is from a previous session or from the current session. 1253 * 1254 * @param string $module Is the name of the module ($MCONF['name']) 1255 * @param mixed $data Is the data you want to store for that module (array, string, ...) 1256 * @param bool|int $noSave If $noSave is set, then the ->uc array (which carries all kinds of user data) is NOT written immediately, but must be written by some subsequent call. 1257 */ 1258 public function pushModuleData($module, $data, $noSave = 0) 1259 { 1260 $sessionHash = GeneralUtility::hmac( 1261 $this->id, 1262 'core-session-hash' 1263 ); 1264 $this->uc['moduleData'][$module] = $data; 1265 $this->uc['moduleSessionID'][$module] = $sessionHash; 1266 if (!$noSave) { 1267 $this->writeUC(); 1268 } 1269 } 1270 1271 /** 1272 * Gets module data for a module (from a loaded ->uc array) 1273 * 1274 * @param string $module Is the name of the module ($MCONF['name']) 1275 * @param string $type If $type = 'ses' then module data is returned only if it was stored in the current session, otherwise data from a previous session will be returned (if available). 1276 * @return mixed The module data if available: $this->uc['moduleData'][$module]; 1277 */ 1278 public function getModuleData($module, $type = '') 1279 { 1280 $sessionHash = GeneralUtility::hmac( 1281 $this->id, 1282 'core-session-hash' 1283 ); 1284 $sessionData = $this->uc['moduleData'][$module] ?? null; 1285 $moduleSessionIdHash = $this->uc['moduleSessionID'][$module] ?? null; 1286 if ($type !== 'ses' 1287 || $sessionData !== null && $moduleSessionIdHash === $sessionHash 1288 // @todo Fallback for non-hashed values in `moduleSessionID`, remove for TYPO3 v11.5 LTS 1289 || $sessionData !== null && $moduleSessionIdHash === $this->id 1290 ) { 1291 return $sessionData; 1292 } 1293 return null; 1294 } 1295 1296 /** 1297 * Returns the session data stored for $key. 1298 * The data will last only for this login session since it is stored in the user session. 1299 * 1300 * @param string $key The key associated with the session data 1301 * @return mixed 1302 */ 1303 public function getSessionData($key) 1304 { 1305 return $this->sessionData[$key] ?? null; 1306 } 1307 1308 /** 1309 * Set session data by key. 1310 * The data will last only for this login session since it is stored in the user session. 1311 * 1312 * @param string $key A non empty string to store the data under 1313 * @param mixed $data Data store store in session 1314 */ 1315 public function setSessionData($key, $data) 1316 { 1317 if (empty($key)) { 1318 throw new \InvalidArgumentException('Argument key must not be empty', 1484311516); 1319 } 1320 $this->sessionData[$key] = $data; 1321 } 1322 1323 /** 1324 * Sets the session data ($data) for $key and writes all session data (from ->user['ses_data']) to the database. 1325 * The data will last only for this login session since it is stored in the session table. 1326 * 1327 * @param string $key Pointer to an associative key in the session data array which is stored serialized in the field "ses_data" of the session table. 1328 * @param mixed $data The data to store in index $key 1329 */ 1330 public function setAndSaveSessionData($key, $data) 1331 { 1332 $this->sessionData[$key] = $data; 1333 $this->user['ses_data'] = serialize($this->sessionData); 1334 $this->logger->debug('setAndSaveSessionData: ses_id = ' . sha1($this->id)); 1335 $updatedSession = $this->getSessionBackend()->update( 1336 $this->id, 1337 ['ses_data' => $this->user['ses_data']] 1338 ); 1339 $this->user = array_merge($this->user ?? [], $updatedSession); 1340 } 1341 1342 /************************* 1343 * 1344 * Misc 1345 * 1346 *************************/ 1347 /** 1348 * Returns an info array with Login/Logout data submitted by a form or params 1349 * 1350 * @return array 1351 * @internal 1352 */ 1353 public function getLoginFormData() 1354 { 1355 $loginData = []; 1356 $loginData['status'] = GeneralUtility::_GP($this->formfield_status); 1357 if ($this->getMethodEnabled) { 1358 $loginData['uname'] = GeneralUtility::_GP($this->formfield_uname); 1359 $loginData['uident'] = GeneralUtility::_GP($this->formfield_uident); 1360 } else { 1361 $loginData['uname'] = GeneralUtility::_POST($this->formfield_uname); 1362 $loginData['uident'] = GeneralUtility::_POST($this->formfield_uident); 1363 } 1364 // Only process the login data if a login is requested 1365 if ($loginData['status'] === LoginType::LOGIN) { 1366 $loginData = $this->processLoginData($loginData); 1367 } 1368 $loginData = array_map('trim', $loginData); 1369 return $loginData; 1370 } 1371 1372 /** 1373 * Processes Login data submitted by a form or params depending on the 1374 * passwordTransmissionStrategy 1375 * 1376 * @param array $loginData Login data array 1377 * @param string $passwordTransmissionStrategy Alternative passwordTransmissionStrategy. Used when authentication services wants to override the default. 1378 * @return array 1379 * @internal 1380 */ 1381 public function processLoginData($loginData, $passwordTransmissionStrategy = '') 1382 { 1383 $loginSecurityLevel = trim($GLOBALS['TYPO3_CONF_VARS'][$this->loginType]['loginSecurityLevel']) ?: 'normal'; 1384 $passwordTransmissionStrategy = $passwordTransmissionStrategy ?: $loginSecurityLevel; 1385 $this->logger->debug('Login data before processing', $loginData); 1386 $serviceChain = ''; 1387 $subType = 'processLoginData' . $this->loginType; 1388 $authInfo = $this->getAuthInfoArray(); 1389 $isLoginDataProcessed = false; 1390 $processedLoginData = $loginData; 1391 while (is_object($serviceObject = GeneralUtility::makeInstanceService('auth', $subType, $serviceChain))) { 1392 $serviceChain .= ',' . $serviceObject->getServiceKey(); 1393 $serviceObject->initAuth($subType, $loginData, $authInfo, $this); 1394 $serviceResult = $serviceObject->processLoginData($processedLoginData, $passwordTransmissionStrategy); 1395 if (!empty($serviceResult)) { 1396 $isLoginDataProcessed = true; 1397 // If the service returns >=200 then no more processing is needed 1398 if ((int)$serviceResult >= 200) { 1399 unset($serviceObject); 1400 break; 1401 } 1402 } 1403 unset($serviceObject); 1404 } 1405 if ($isLoginDataProcessed) { 1406 $loginData = $processedLoginData; 1407 $this->logger->debug('Processed login data', $this->removeSensitiveLoginDataForLoggingInfo($processedLoginData)); 1408 } 1409 return $loginData; 1410 } 1411 1412 /** 1413 * Removes any sensitive data from the incoming data (either from loginData, processedLogin data 1414 * or the user record from the DB). 1415 * 1416 * No type hinting is added because it might be possible that the incoming data is of any other type. 1417 * 1418 * @param mixed|array $data 1419 * @param bool $isUserRecord 1420 * @return mixed 1421 */ 1422 protected function removeSensitiveLoginDataForLoggingInfo($data, bool $isUserRecord = false) 1423 { 1424 if ($isUserRecord && is_array($data)) { 1425 $fieldNames = ['uid', 'pid', 'tstamp', 'crdate', 'cruser_id', 'deleted', 'disabled', 'starttime', 'endtime', 'username', 'admin', 'usergroup', 'db_mountpoints', 'file_mountpoints', 'file_permissions', 'workspace_perms', 'lastlogin', 'workspace_id', 'category_perms']; 1426 $data = array_intersect_key($data, array_combine($fieldNames, $fieldNames)); 1427 } 1428 if (isset($data['uident'])) { 1429 $data['uident'] = '********'; 1430 } 1431 if (isset($data['uident_text'])) { 1432 $data['uident_text'] = '********'; 1433 } 1434 if (isset($data['password'])) { 1435 $data['password'] = '********'; 1436 } 1437 return $data; 1438 } 1439 1440 /** 1441 * Returns an info array which provides additional information for auth services 1442 * 1443 * @return array 1444 * @internal 1445 */ 1446 public function getAuthInfoArray() 1447 { 1448 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($this->user_table); 1449 $expressionBuilder = $queryBuilder->expr(); 1450 $authInfo = []; 1451 $authInfo['loginType'] = $this->loginType; 1452 $authInfo['refInfo'] = parse_url(GeneralUtility::getIndpEnv('HTTP_REFERER')); 1453 $authInfo['HTTP_HOST'] = GeneralUtility::getIndpEnv('HTTP_HOST'); 1454 $authInfo['REMOTE_ADDR'] = GeneralUtility::getIndpEnv('REMOTE_ADDR'); 1455 $authInfo['REMOTE_HOST'] = GeneralUtility::getIndpEnv('REMOTE_HOST'); 1456 $authInfo['showHiddenRecords'] = $this->showHiddenRecords; 1457 // Can be overidden in localconf by SVCONF: 1458 $authInfo['db_user']['table'] = $this->user_table; 1459 $authInfo['db_user']['userid_column'] = $this->userid_column; 1460 $authInfo['db_user']['username_column'] = $this->username_column; 1461 $authInfo['db_user']['userident_column'] = $this->userident_column; 1462 $authInfo['db_user']['usergroup_column'] = $this->usergroup_column; 1463 $authInfo['db_user']['enable_clause'] = $this->userConstraints()->buildExpression( 1464 [$this->user_table => $this->user_table], 1465 $expressionBuilder 1466 ); 1467 if ($this->checkPid && $this->checkPid_value !== null) { 1468 $authInfo['db_user']['checkPidList'] = $this->checkPid_value; 1469 $authInfo['db_user']['check_pid_clause'] = $expressionBuilder->in( 1470 'pid', 1471 GeneralUtility::intExplode(',', $this->checkPid_value) 1472 ); 1473 } else { 1474 $authInfo['db_user']['checkPidList'] = ''; 1475 $authInfo['db_user']['check_pid_clause'] = ''; 1476 } 1477 $authInfo['db_groups']['table'] = $this->usergroup_table; 1478 return $authInfo; 1479 } 1480 1481 /** 1482 * Check the login data with the user record data for builtin login methods 1483 * 1484 * @param array $user User data array 1485 * @param array $loginData Login data array 1486 * @param string $passwordCompareStrategy Alternative passwordCompareStrategy. Used when authentication services wants to override the default. 1487 * @return bool TRUE if login data matched 1488 * @deprecated since TYPO3 v9, will be removed in TYPO3 v10.0. 1489 */ 1490 public function compareUident($user, $loginData, $passwordCompareStrategy = '') 1491 { 1492 trigger_error('This method will be removed in TYPO3 v10.0.', E_USER_DEPRECATED); 1493 return (string)$loginData['uident_text'] !== '' && (string)$loginData['uident_text'] === (string)$user[$this->userident_column]; 1494 } 1495 1496 /** 1497 * Garbage collector, removing old expired sessions. 1498 * 1499 * @internal 1500 */ 1501 public function gc() 1502 { 1503 $this->getSessionBackend()->collectGarbage($this->gc_time); 1504 } 1505 1506 /** 1507 * DUMMY: Writes to log database table (in some extension classes) 1508 * 1509 * @param int $type denotes which module that has submitted the entry. This is the current list: 1=tce_db; 2=tce_file; 3=system (eg. sys_history save); 4=modules; 254=Personal settings changed; 255=login / out action: 1=login, 2=logout, 3=failed login (+ errorcode 3), 4=failure_warning_email sent 1510 * @param int $action denotes which specific operation that wrote the entry (eg. 'delete', 'upload', 'update' and so on...). Specific for each $type. Also used to trigger update of the interface. (see the log-module for the meaning of each number !!) 1511 * @param int $error flag. 0 = message, 1 = error (user problem), 2 = System Error (which should not happen), 3 = security notice (admin) 1512 * @param int $details_nr The message number. Specific for each $type and $action. in the future this will make it possible to translate errormessages to other languages 1513 * @param string $details Default text that follows the message 1514 * @param array $data Data that follows the log. Might be used to carry special information. If an array the first 5 entries (0-4) will be sprintf'ed the details-text... 1515 * @param string $tablename Special field used by tce_main.php. These ($tablename, $recuid, $recpid) holds the reference to the record which the log-entry is about. (Was used in attic status.php to update the interface.) 1516 * @param int $recuid Special field used by tce_main.php. These ($tablename, $recuid, $recpid) holds the reference to the record which the log-entry is about. (Was used in attic status.php to update the interface.) 1517 * @param int $recpid Special field used by tce_main.php. These ($tablename, $recuid, $recpid) holds the reference to the record which the log-entry is about. (Was used in attic status.php to update the interface.) 1518 */ 1519 public function writelog($type, $action, $error, $details_nr, $details, $data, $tablename, $recuid, $recpid) 1520 { 1521 } 1522 1523 /** 1524 * DUMMY: Check login failures (in some extension classes) 1525 * 1526 * @param string $email Email address 1527 * @param int $secondsBack Number of sections back in time to check. This is a kind of limit for how many failures an hour for instance 1528 * @param int $maxFailures Max allowed failures before a warning mail is sent 1529 * @ignore 1530 */ 1531 public function checkLogFailures($email, $secondsBack, $maxFailures) 1532 { 1533 } 1534 1535 /** 1536 * Raw initialization of the be_user with uid=$uid 1537 * This will circumvent all login procedures and select a be_users record from the 1538 * database and set the content of ->user to the record selected. 1539 * Thus the BE_USER object will appear like if a user was authenticated - however without 1540 * a session id and the fields from the session table of course. 1541 * Will check the users for disabled, start/endtime, etc. ($this->user_where_clause()) 1542 * 1543 * @param int $uid The UID of the backend user to set in ->user 1544 * @internal 1545 */ 1546 public function setBeUserByUid($uid) 1547 { 1548 $this->user = $this->getRawUserByUid($uid); 1549 } 1550 1551 /** 1552 * Raw initialization of the be_user with username=$name 1553 * 1554 * @param string $name The username to look up. 1555 * @see \TYPO3\CMS\Core\Authentication\AbstractUserAuthentication::setBeUserByUid() 1556 * @internal 1557 */ 1558 public function setBeUserByName($name) 1559 { 1560 $this->user = $this->getRawUserByName($name); 1561 } 1562 1563 /** 1564 * Fetching raw user record with uid=$uid 1565 * 1566 * @param int $uid The UID of the backend user to set in ->user 1567 * @return array user record or FALSE 1568 * @internal 1569 */ 1570 public function getRawUserByUid($uid) 1571 { 1572 $query = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($this->user_table); 1573 $query->setRestrictions($this->userConstraints()); 1574 $query->select('*') 1575 ->from($this->user_table) 1576 ->where($query->expr()->eq('uid', $query->createNamedParameter($uid, \PDO::PARAM_INT))); 1577 1578 return $query->execute()->fetch(); 1579 } 1580 1581 /** 1582 * Fetching raw user record with username=$name 1583 * 1584 * @param string $name The username to look up. 1585 * @return array user record or FALSE 1586 * @see \TYPO3\CMS\Core\Authentication\AbstractUserAuthentication::getUserByUid() 1587 * @internal 1588 */ 1589 public function getRawUserByName($name) 1590 { 1591 $query = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($this->user_table); 1592 $query->setRestrictions($this->userConstraints()); 1593 $query->select('*') 1594 ->from($this->user_table) 1595 ->where($query->expr()->eq('username', $query->createNamedParameter($name, \PDO::PARAM_STR))); 1596 1597 return $query->execute()->fetch(); 1598 } 1599 1600 /** 1601 * Get a user from DB by username 1602 * provided for usage from services 1603 * 1604 * @param array $dbUser User db table definition: $this->db_user 1605 * @param string $username user name 1606 * @param string $extraWhere Additional WHERE clause: " AND ... 1607 * @return mixed User array or FALSE 1608 * @deprecated since TYPO3 v9, will be removed in TYPO3 v10.0 1609 */ 1610 public function fetchUserRecord($dbUser, $username, $extraWhere = '') 1611 { 1612 trigger_error('This method will be removed in TYPO3 v10.0.', E_USER_DEPRECATED); 1613 $user = false; 1614 if ($username || $extraWhere) { 1615 $query = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($dbUser['table']); 1616 $query->getRestrictions()->removeAll() 1617 ->add(GeneralUtility::makeInstance(DeletedRestriction::class)); 1618 1619 $constraints = array_filter([ 1620 QueryHelper::stripLogicalOperatorPrefix($dbUser['check_pid_clause']), 1621 QueryHelper::stripLogicalOperatorPrefix($dbUser['enable_clause']), 1622 QueryHelper::stripLogicalOperatorPrefix($extraWhere), 1623 ]); 1624 1625 if (!empty($username)) { 1626 array_unshift( 1627 $constraints, 1628 $query->expr()->eq( 1629 $dbUser['username_column'], 1630 $query->createNamedParameter($username, \PDO::PARAM_STR) 1631 ) 1632 ); 1633 } 1634 1635 $user = $query->select('*') 1636 ->from($dbUser['table']) 1637 ->where(...$constraints) 1638 ->execute() 1639 ->fetch(); 1640 } 1641 return $user; 1642 } 1643 1644 /** 1645 * @internal 1646 * @return string 1647 */ 1648 public function getSessionId(): string 1649 { 1650 return $this->id; 1651 } 1652 1653 /** 1654 * @internal 1655 * @return string 1656 */ 1657 public function getLoginType(): string 1658 { 1659 return $this->loginType; 1660 } 1661 1662 /** 1663 * Returns initialized session backend. Returns same session backend if called multiple times 1664 * 1665 * @return SessionBackendInterface 1666 */ 1667 protected function getSessionBackend() 1668 { 1669 if (!isset($this->sessionBackend)) { 1670 $this->sessionBackend = GeneralUtility::makeInstance(SessionManager::class)->getSessionBackend($this->loginType); 1671 } 1672 return $this->sessionBackend; 1673 } 1674} 1675