2 namespace TYPO3\CMS\Core\Authentication
;
5 * This file is part of the TYPO3 CMS project.
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.
11 * For the full copyright and license information, please read the
12 * LICENSE.txt file that was distributed with this source code.
14 * The TYPO3 project - inspiring people to share!
17 use Psr\Log\LoggerAwareInterface
;
18 use Psr\Log\LoggerAwareTrait
;
19 use TYPO3\CMS\Core\Core\Environment
;
20 use TYPO3\CMS\Core\Crypto\Random
;
21 use TYPO3\CMS\Core\Database\Connection
;
22 use TYPO3\CMS\Core\Database\ConnectionPool
;
23 use TYPO3\CMS\Core\Database\Query\Restriction\DefaultRestrictionContainer
;
24 use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction
;
25 use TYPO3\CMS\Core\Database\Query\Restriction\EndTimeRestriction
;
26 use TYPO3\CMS\Core\Database\Query\Restriction\HiddenRestriction
;
27 use TYPO3\CMS\Core\Database\Query\Restriction\QueryRestrictionContainerInterface
;
28 use TYPO3\CMS\Core\Database\Query\Restriction\RootLevelRestriction
;
29 use TYPO3\CMS\Core\Database\Query\Restriction\StartTimeRestriction
;
30 use TYPO3\CMS\Core\Exception
;
31 use TYPO3\CMS\Core\Session\Backend\Exception\SessionNotFoundException
;
32 use TYPO3\CMS\Core\Session\Backend\SessionBackendInterface
;
33 use TYPO3\CMS\Core\Session\SessionManager
;
34 use TYPO3\CMS\Core\Utility\GeneralUtility
;
35 use TYPO3\CMS\Core\Utility\MathUtility
;
38 * Authentication of users in TYPO3
40 * This class is used to authenticate a login user.
41 * The class is used by both the frontend and backend.
42 * In both cases this class is a parent class to BackendUserAuthentication and FrontendUserAuthentication
44 * See Inside TYPO3 for more information about the API of the class and internal variables.
46 abstract class AbstractUserAuthentication
implements LoggerAwareInterface
57 * Session/GET-var name
60 public $get_name = '';
63 * Table in database with user data
66 public $user_table = '';
69 * Table in database with user groups
72 public $usergroup_table = '';
75 * Column for login-name
78 public $username_column = '';
84 public $userident_column = '';
90 public $userid_column = '';
93 * Column for user group information
96 public $usergroup_column = '';
99 * Column name for last login timestamp
102 public $lastLogin_column = '';
105 * Enable field columns of user table
108 public $enablecolumns = [
110 // Boolean: If TRUE, 'AND pid=0' will be a part of the query...
120 public $showHiddenRecords = false;
123 * Form field with login-name
126 public $formfield_uname = '';
129 * Form field with password
132 public $formfield_uident = '';
135 * Form field with status: *'login', 'logout'. If empty login is not verified.
138 public $formfield_status = '';
141 * Session timeout (on the server)
143 * If >0: session-timeout in seconds.
144 * If <=0: Instant logout after login.
148 public $sessionTimeout = 0;
151 * Name for a field to fetch the server session timeout from.
152 * If not empty this is a field name from the user table where the timeout can be found.
155 public $auth_timeout_field = '';
158 * Lifetime for the session-cookie (on the client)
160 * If >0: permanent cookie with given lifetime
161 * If 0: session-cookie
162 * Session-cookie means the browser will remove it when the browser is closed.
166 public $lifetime = 0;
170 * Purge all server session data older than $gc_time seconds.
171 * 0 = default to $this->sessionTimeout or use 86400 seconds (1 day) if $this->sessionTimeout == 0
177 * Probability for garbage collection to be run (in percent)
180 public $gc_probability = 1;
183 * Decides if the writelog() function is called at login and logout
186 public $writeStdLog = false;
189 * Log failed login attempts
192 public $writeAttemptLog = false;
195 * Send no-cache headers
198 public $sendNoCacheHeaders = true;
201 * If this is set, authentication is also accepted by $_GET.
202 * Notice that the identification is NOT 128bit MD5 hash but reduced.
203 * This is done in order to minimize the size for mobile-devices, such as WAP-phones
206 public $getFallBack = false;
209 * The ident-hash is normally 32 characters and should be!
210 * But if you are making sites for WAP-devices or other low-bandwidth stuff,
211 * you may shorten the length.
212 * Never let this value drop below 6!
213 * A length of 6 would give you more than 16 mio possibilities.
216 public $hash_length = 32;
219 * Setting this flag TRUE lets user-authentication happen from GET_VARS if
220 * POST_VARS are not set. Thus you may supply username/password with the URL.
223 public $getMethodEnabled = false;
226 * If set to 4, the session will be locked to the user's IP address (all four numbers).
227 * Reducing this to 1-3 means that only the given number of parts of the IP address is used.
235 public $warningEmail = '';
238 * Time span (in seconds) within the number of failed logins are collected
241 public $warningPeriod = 3600;
244 * The maximum accepted number of warnings before an email to $warningEmail is sent
247 public $warningMax = 3;
250 * If set, the user-record must be stored at the page defined by $checkPid_value
253 public $checkPid = true;
256 * The page id the user record must be stored at
259 public $checkPid_value = 0;
262 * session_id (MD5-hash)
269 * Indicates if an authentication was started but failed
272 public $loginFailure = false;
275 * Will be set to TRUE if the login session is actually written during auth-check.
278 public $loginSessionStarted = false;
281 * @var array|null contains user- AND session-data from database (joined tables)
287 * Will be added to the url (eg. '&login=ab7ef8d...')
288 * GET-auth-var if getFallBack is TRUE. Should be inserted in links!
292 public $get_URL_ID = '';
295 * Will be set to TRUE if a new session ID was created
298 public $newSessionID = false;
301 * Will force the session cookie to be set every time (lifetime must be 0)
304 public $forceSetCookie = false;
307 * Will prevent the setting of the session cookie (takes precedence over forceSetCookie)
310 public $dontSetCookie = false;
315 protected $cookieWasSetOnCurrentRequest = false;
318 * Login type, used for services.
321 public $loginType = '';
324 * "auth" services configuration array from $GLOBALS['TYPO3_CONF_VARS']['SVCONF']['auth']
327 public $svConfig = [];
335 * @var SessionBackendInterface
337 protected $sessionBackend;
340 * Holds deserialized data from session records.
341 * 'Reserved' keys are:
342 * - 'sys': Reserved for TypoScript standard code.
345 protected $sessionData = [];
348 * Initialize some important variables
350 public function __construct()
352 // This function has to stay even if it's empty
353 // Implementations of that abstract class might call parent::__construct();
357 * Starts a user session
358 * Typical configurations will:
359 * a) check if session cookie was set and if not, set one,
360 * b) check if a password/username was sent and if so, try to authenticate the user
361 * c) Lookup a session attached to a user and check timeout etc.
362 * d) Garbage collection, setting of no-cache headers.
363 * If a user is authenticated the database record of the user (array) will be set in the ->user internal variable.
367 public function start()
369 // Backend or frontend login - used for auth services
370 if (empty($this->loginType
)) {
371 throw new Exception('No loginType defined, should be set explicitly by subclass', 1476045345);
373 $this->logger
->debug('## Beginning of auth logging.');
376 $this->newSessionID
= false;
377 // $id is set to ses_id if cookie is present. Else set to FALSE, which will start a new session
378 $id = $this->getCookie($this->name
);
379 $this->svConfig
= $GLOBALS['TYPO3_CONF_VARS']['SVCONF']['auth'] ??
[];
381 // If fallback to get mode....
382 if (!$id && $this->getFallBack
&& $this->get_name
) {
383 $id = isset($_GET[$this->get_name
]) ? GeneralUtility
::_GET($this->get_name
) : '';
384 if (strlen($id) != $this->hash_length
) {
390 // If new session or client tries to fix session...
391 if (!$id ||
!$this->isExistingSessionRecord($id)) {
392 // New random session-$id is made
393 $id = $this->createSessionId();
395 $this->newSessionID
= true;
397 // Internal var 'id' is set
399 // If fallback to get mode....
400 if ($mode === 'get' && $this->getFallBack
&& $this->get_name
) {
401 $this->get_URL_ID
= '&' . $this->get_name
. '=' . $id;
403 // Make certain that NO user is set initially
405 // Set all possible headers that could ensure that the script is not cached on the client-side
406 $this->sendHttpHeaders();
407 // Load user session, check to see if anyone has submitted login-information and if so authenticate
408 // the user with the session. $this->user[uid] may be used to write log...
409 $this->checkAuthentication();
411 if (!$this->dontSetCookie
) {
412 $this->setSessionCookie();
414 // Hook for alternative ways of filling the $this->user array (is used by the "timtaw" extension)
415 foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_userauth.php']['postUserLookUp'] ??
[] as $funcName) {
419 GeneralUtility
::callUserFunction($funcName, $_params, $this);
421 // Set $this->gc_time if not explicitly specified
422 if ($this->gc_time
=== 0) {
423 // Default to 86400 seconds (1 day) if $this->sessionTimeout is 0
424 $this->gc_time
= $this->sessionTimeout
=== 0 ?
86400 : $this->sessionTimeout
;
426 // If we're lucky we'll get to clean up old sessions
427 if (rand() %
100 <= $this->gc_probability
) {
433 * Set all possible headers that could ensure that the script
434 * is not cached on the client-side.
436 * Only do this if $this->sendNoCacheHeaders is set.
438 protected function sendHttpHeaders()
440 // skip sending the "no-cache" headers if it's a CLI request or the no-cache headers should not be sent.
441 if (!$this->sendNoCacheHeaders || Environment
::isCli()) {
444 $httpHeaders = $this->getHttpHeaders();
445 foreach ($httpHeaders as $httpHeaderName => $value) {
446 header($httpHeaderName . ': ' . $value);
451 * Get the http headers to be sent if an authenticated user is available, in order to disallow
452 * browsers to store the response on the client side.
456 protected function getHttpHeaders(): array
460 'Last-Modified' => gmdate('D, d M Y H:i:s') . ' GMT'
462 $cacheControlHeader = 'no-cache, must-revalidate';
463 $pragmaHeader = 'no-cache';
464 // Prevent error message in IE when using a https connection
465 // see http://forge.typo3.org/issues/24125
466 if (strpos(GeneralUtility
::getIndpEnv('HTTP_USER_AGENT'), 'MSIE') !== false
467 && GeneralUtility
::getIndpEnv('TYPO3_SSL')) {
468 // Some IEs can not handle no-cache
469 // see http://support.microsoft.com/kb/323308/en-us
470 $cacheControlHeader = 'must-revalidate';
471 // IE needs "Pragma: private" if SSL connection
472 $pragmaHeader = 'private';
474 $headers['Cache-Control'] = $cacheControlHeader;
475 $headers['Pragma'] = $pragmaHeader;
480 * Sets the session cookie for the current disposal.
484 protected function setSessionCookie()
486 $isSetSessionCookie = $this->isSetSessionCookie();
487 $isRefreshTimeBasedCookie = $this->isRefreshTimeBasedCookie();
488 if ($isSetSessionCookie ||
$isRefreshTimeBasedCookie) {
489 $settings = $GLOBALS['TYPO3_CONF_VARS']['SYS'];
490 // Get the domain to be used for the cookie (if any):
491 $cookieDomain = $this->getCookieDomain();
492 // If no cookie domain is set, use the base path:
493 $cookiePath = $cookieDomain ?
'/' : GeneralUtility
::getIndpEnv('TYPO3_SITE_PATH');
494 // If the cookie lifetime is set, use it:
495 $cookieExpire = $isRefreshTimeBasedCookie ?
$GLOBALS['EXEC_TIME'] +
$this->lifetime
: 0;
496 // Use the secure option when the current request is served by a secure connection:
497 $cookieSecure = (bool)$settings['cookieSecure'] && GeneralUtility
::getIndpEnv('TYPO3_SSL');
498 // Do not set cookie if cookieSecure is set to "1" (force HTTPS) and no secure channel is used:
499 if ((int)$settings['cookieSecure'] !== 1 || GeneralUtility
::getIndpEnv('TYPO3_SSL')) {
500 setcookie($this->name
, $this->id
, $cookieExpire, $cookiePath, $cookieDomain, $cookieSecure, true);
501 $this->cookieWasSetOnCurrentRequest
= true;
503 throw new Exception('Cookie was not set since HTTPS was forced in $TYPO3_CONF_VARS[SYS][cookieSecure].', 1254325546);
505 $this->logger
->debug(
506 ($isRefreshTimeBasedCookie ?
'Updated Cookie: ' : 'Set Cookie: ')
507 . $this->id
. ($cookieDomain ?
', ' . $cookieDomain : '')
513 * Gets the domain to be used on setting cookies.
514 * The information is taken from the value in $GLOBALS['TYPO3_CONF_VARS']['SYS']['cookieDomain'].
516 * @return string The domain to be used on setting cookies
518 protected function getCookieDomain()
521 $cookieDomain = $GLOBALS['TYPO3_CONF_VARS']['SYS']['cookieDomain'];
522 // If a specific cookie domain is defined for a given TYPO3_MODE,
524 if (!empty($GLOBALS['TYPO3_CONF_VARS'][$this->loginType
]['cookieDomain'])) {
525 $cookieDomain = $GLOBALS['TYPO3_CONF_VARS'][$this->loginType
]['cookieDomain'];
528 if ($cookieDomain[0] === '/') {
530 $matchCnt = @preg_match
($cookieDomain, GeneralUtility
::getIndpEnv('TYPO3_HOST_ONLY'), $match);
531 if ($matchCnt === false) {
532 $this->logger
->critical('The regular expression for the cookie domain (' . $cookieDomain . ') contains errors. The session is not shared across sub-domains.');
533 } elseif ($matchCnt) {
537 $result = $cookieDomain;
544 * Get the value of a specified cookie.
546 * @param string $cookieName The cookie ID
547 * @return string The value stored in the cookie
549 protected function getCookie($cookieName)
551 return isset($_COOKIE[$cookieName]) ?
stripslashes($_COOKIE[$cookieName]) : '';
555 * Determine whether a session cookie needs to be set (lifetime=0)
560 public function isSetSessionCookie()
562 return ($this->newSessionID ||
$this->forceSetCookie
) && $this->lifetime
== 0;
566 * Determine whether a non-session cookie needs to be set (lifetime>0)
571 public function isRefreshTimeBasedCookie()
573 return $this->lifetime
> 0;
577 * Checks if a submission of username and password is present or use other authentication by auth services
579 * @throws \RuntimeException
582 public function checkAuthentication()
584 // No user for now - will be searched by service below
587 // User is not authenticated by default
588 $authenticated = false;
589 // User want to login with passed login data (name/password)
590 $activeLogin = false;
591 // Indicates if an active authentication failed (not auto login)
592 $this->loginFailure
= false;
593 $this->logger
->debug('Login type: ' . $this->loginType
);
594 // The info array provide additional information for auth services
595 $authInfo = $this->getAuthInfoArray();
596 // Get Login/Logout data submitted by a form or params
597 $loginData = $this->getLoginFormData();
598 $this->logger
->debug('Login data', $loginData);
599 // Active logout (eg. with "logout" button)
600 if ($loginData['status'] === LoginType
::LOGOUT
) {
601 if ($this->writeStdLog
) {
602 // $type,$action,$error,$details_nr,$details,$data,$tablename,$recuid,$recpid
603 $this->writelog(255, 2, 0, 2, 'User %s logged out', [$this->user
['username']], '', 0, 0);
605 $this->logger
->info('User logged out. Id: ' . $this->id
);
608 // Determine whether we need to skip session update.
609 // This is used mainly for checking session timeout in advance without refreshing the current session's timeout.
610 $skipSessionUpdate = (bool)GeneralUtility
::_GP('skipSessionUpdate');
611 $haveSession = false;
612 $anonymousSession = false;
613 if (!$this->newSessionID
) {
615 $authInfo['userSession'] = $this->fetchUserSession($skipSessionUpdate);
616 $haveSession = is_array($authInfo['userSession']);
617 if ($haveSession && !empty($authInfo['userSession']['ses_anonymous'])) {
618 $anonymousSession = true;
622 // Active login (eg. with login form).
623 if (!$haveSession && $loginData['status'] === LoginType
::LOGIN
) {
625 $this->logger
->debug('Active login (eg. with login form)');
626 // check referrer for submitted login values
627 if ($this->formfield_status
&& $loginData['uident'] && $loginData['uname']) {
628 // Delete old user session if any
631 // Refuse login for _CLI users, if not processing a CLI request type
632 // (although we shouldn't be here in case of a CLI request type)
633 if (stripos($loginData['uname'], '_CLI_') === 0 && !Environment
::isCli()) {
634 throw new \
RuntimeException('TYPO3 Fatal Error: You have tried to login using a CLI user. Access prohibited!', 1270853931);
638 // Cause elevation of privilege, make sure regenerateSessionId is called later on
639 if ($anonymousSession && $loginData['status'] === LoginType
::LOGIN
) {
644 $this->logger
->debug('User session found', [
645 $this->userid_column
=> $authInfo['userSession'][$this->userid_column
] ??
null,
646 $this->username_column
=> $authInfo['userSession'][$this->username_column
] ??
null,
649 $this->logger
->debug('No user session found');
651 if (is_array($this->svConfig
['setup'] ??
false)) {
652 $this->logger
->debug('SV setup', $this->svConfig
['setup']);
657 $activeLogin ||
!empty($this->svConfig
['setup'][$this->loginType
. '_alwaysFetchUser'])
658 ||
!$haveSession && !empty($this->svConfig
['setup'][$this->loginType
. '_fetchUserIfNoSession'])
660 // Use 'auth' service to find the user
661 // First found user will be used
662 $subType = 'getUser' . $this->loginType
;
663 foreach ($this->getAuthServices($subType, $loginData, $authInfo) as $serviceObj) {
664 if ($row = $serviceObj->getUser()) {
665 $tempuserArr[] = $row;
666 $this->logger
->debug('User found', [
667 $this->userid_column
=> $row[$this->userid_column
],
668 $this->username_column
=> $row[$this->username_column
],
670 // User found, just stop to search for more if not configured to go on
671 if (empty($this->svConfig
['setup'][$this->loginType
. '_fetchAllUsers'])) {
677 if (!empty($this->svConfig
['setup'][$this->loginType
. '_alwaysFetchUser'])) {
678 $this->logger
->debug($this->loginType
. '_alwaysFetchUser option is enabled');
680 if (empty($tempuserArr)) {
681 $this->logger
->debug('No user found by services');
683 $this->logger
->debug(count($tempuserArr) . ' user records found by services');
687 // If no new user was set we use the already found user session
688 if (empty($tempuserArr) && $haveSession && !$anonymousSession) {
689 $tempuserArr[] = $authInfo['userSession'];
690 $tempuser = $authInfo['userSession'];
691 // User is authenticated because we found a user session
692 $authenticated = true;
693 $this->logger
->debug('User session used', [
694 $this->userid_column
=> $authInfo['userSession'][$this->userid_column
],
695 $this->username_column
=> $authInfo['userSession'][$this->username_column
],
698 // Re-auth user when 'auth'-service option is set
699 if (!empty($this->svConfig
['setup'][$this->loginType
. '_alwaysAuthUser'])) {
700 $authenticated = false;
701 $this->logger
->debug('alwaysAuthUser option is enabled');
703 // Authenticate the user if needed
704 if (!empty($tempuserArr) && !$authenticated) {
705 foreach ($tempuserArr as $tempuser) {
706 // Use 'auth' service to authenticate the user
707 // If one service returns FALSE then authentication failed
708 // a service might return 100 which means there's no reason to stop but the user can't be authenticated by that service
709 $this->logger
->debug('Auth user', $tempuser);
710 $subType = 'authUser' . $this->loginType
;
712 foreach ($this->getAuthServices($subType, $loginData, $authInfo) as $serviceObj) {
713 if (($ret = $serviceObj->authUser($tempuser)) > 0) {
714 // If the service returns >=200 then no more checking is needed - useful for IP checking without password
715 if ((int)$ret >= 200) {
716 $authenticated = true;
719 if ((int)$ret >= 100) {
721 $authenticated = true;
724 $authenticated = false;
729 if ($authenticated) {
730 // Leave foreach() because a user is authenticated
736 // If user is authenticated a valid user is in $tempuser
737 if ($authenticated) {
738 // Reset failure flag
739 $this->loginFailure
= false;
740 // Insert session record if needed:
741 if (!$haveSession ||
$anonymousSession ||
$tempuser['ses_id'] != $this->id
&& $tempuser['uid'] != $authInfo['userSession']['ses_userid']) {
742 $sessionData = $this->createUserSession($tempuser);
744 // Preserve session data on login
745 if ($anonymousSession) {
746 $sessionData = $this->getSessionBackend()->update(
748 ['ses_data' => $authInfo['userSession']['ses_data']]
752 $this->user
= array_merge(
756 // The login session is started.
757 $this->loginSessionStarted
= true;
758 if (is_array($this->user
)) {
759 $this->logger
->debug('User session finally read', [
760 $this->userid_column
=> $this->user
[$this->userid_column
],
761 $this->username_column
=> $this->user
[$this->username_column
],
764 } elseif ($haveSession) {
765 // if we come here the current session is for sure not anonymous as this is a pre-condition for $authenticated = true
766 $this->user
= $authInfo['userSession'];
769 if ($activeLogin && !$this->newSessionID
) {
770 $this->regenerateSessionId();
773 // User logged in - write that to the log!
774 if ($this->writeStdLog
&& $activeLogin) {
775 $this->writelog(255, 1, 0, 1, 'User %s logged in from ###IP###', [$tempuser[$this->username_column
]], '', '', '');
778 $this->logger
->info('User ' . $tempuser[$this->username_column
] . ' logged in from ' . GeneralUtility
::getIndpEnv('REMOTE_ADDR'));
781 $this->logger
->debug('User ' . $tempuser[$this->username_column
] . ' authenticated from ' . GeneralUtility
::getIndpEnv('REMOTE_ADDR'));
784 // User was not authenticated, so we should reuse the existing anonymous session
785 if ($anonymousSession) {
786 $this->user
= $authInfo['userSession'];
789 // Mark the current login attempt as failed
790 if ($activeLogin ||
!empty($tempuserArr)) {
791 $this->loginFailure
= true;
792 if (empty($tempuserArr) && $activeLogin) {
794 'loginData' => $loginData
796 $this->logger
->warning('Login failed', $logData);
798 if (!empty($tempuserArr)) {
800 $this->userid_column
=> $tempuser[$this->userid_column
],
801 $this->username_column
=> $tempuser[$this->username_column
],
803 $this->logger
->warning('Login failed', $logData);
808 // If there were a login failure, check to see if a warning email should be sent:
809 if ($this->loginFailure
&& $activeLogin) {
810 $this->logger
->debug(
811 'Call checkLogFailures',
813 'warningEmail' => $this->warningEmail
,
814 'warningPeriod' => $this->warningPeriod
,
815 'warningMax' => $this->warningMax
819 // Hook to implement login failure tracking methods
822 foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_userauth.php']['postLoginFailureProcessing'] ??
[] as $_funcRef) {
823 GeneralUtility
::callUserFunction($_funcRef, $_params, $this);
828 // No hooks were triggered - default login failure behavior is to sleep 5 seconds
832 $this->checkLogFailures($this->warningEmail
, $this->warningPeriod
, $this->warningMax
);
837 * Creates a new session ID.
839 * @return string The new session ID
841 public function createSessionId()
843 return GeneralUtility
::makeInstance(Random
::class)->generateRandomHexString($this->hash_length
);
847 * Initializes authentication services to be used in a foreach loop
849 * @param string $subType e.g. getUserFE
850 * @param array $loginData
851 * @param array $authInfo
852 * @return \Traversable A generator of service objects
854 protected function getAuthServices(string $subType, array $loginData, array $authInfo): \Traversable
857 while (is_object($serviceObj = GeneralUtility
::makeInstanceService('auth', $subType, $serviceChain))) {
858 $serviceChain .= ',' . $serviceObj->getServiceKey();
859 $serviceObj->initAuth($subType, $loginData, $authInfo, $this);
863 $this->logger
->debug($subType . ' auth services called: ' . $serviceChain);
868 * Regenerate the session ID and transfer the session to new ID
869 * Call this method whenever a user proceeds to a higher authorization level
870 * e.g. when an anonymous session is now authenticated.
872 * @param array $existingSessionRecord If given, this session record will be used instead of fetching again
873 * @param bool $anonymous If true session will be regenerated as anonymous session
875 protected function regenerateSessionId(array $existingSessionRecord = [], bool $anonymous = false)
877 if (empty($existingSessionRecord)) {
878 $existingSessionRecord = $this->getSessionBackend()->get($this->id
);
881 // Update session record with new ID
882 $oldSessionId = $this->id
;
883 $this->id
= $this->createSessionId();
884 $updatedSession = $this->getSessionBackend()->set($this->id
, $existingSessionRecord);
885 $this->sessionData
= unserialize($updatedSession['ses_data']);
886 // Merge new session data into user/session array
887 $this->user
= array_merge($this->user ??
[], $updatedSession);
888 $this->getSessionBackend()->remove($oldSessionId);
889 $this->newSessionID
= true;
892 /*************************
896 *************************/
899 * Creates a user session record and returns its values.
901 * @param array $tempuser User data array
903 * @return array The session data for the newly created session.
905 public function createUserSession($tempuser)
907 $this->logger
->debug('Create session ses_id = ' . $this->id
);
908 // Delete any session entry first
909 $this->getSessionBackend()->remove($this->id
);
910 // Re-create session entry
911 $sessionRecord = $this->getNewSessionRecord($tempuser);
912 $sessionRecord = $this->getSessionBackend()->set($this->id
, $sessionRecord);
913 // Updating lastLogin_column carrying information about last login.
914 $this->updateLoginTimestamp($tempuser[$this->userid_column
]);
915 return $sessionRecord;
919 * Updates the last login column in the user with the given id
923 protected function updateLoginTimestamp(int $userId)
925 if ($this->lastLogin_column
) {
926 $connection = GeneralUtility
::makeInstance(ConnectionPool
::class)->getConnectionForTable($this->user_table
);
929 [$this->lastLogin_column
=> $GLOBALS['EXEC_TIME']],
930 [$this->userid_column
=> $userId]
936 * Returns a new session record for the current user for insertion into the DB.
937 * This function is mainly there as a wrapper for inheriting classes to override it.
939 * @param array $tempuser
940 * @return array User session record
942 public function getNewSessionRecord($tempuser)
944 $sessionIpLock = '[DISABLED]';
945 if ($this->lockIP
&& empty($tempuser['disableIPlock'])) {
946 $sessionIpLock = $this->ipLockClause_remoteIPNumber($this->lockIP
);
950 'ses_id' => $this->id
,
951 'ses_iplock' => $sessionIpLock,
952 'ses_userid' => $tempuser[$this->userid_column
] ??
0,
953 'ses_tstamp' => $GLOBALS['EXEC_TIME'],
959 * Read the user session from db.
961 * @param bool $skipSessionUpdate
962 * @return array|bool User session data, false if $this->id does not represent valid session
964 public function fetchUserSession($skipSessionUpdate = false)
966 $this->logger
->debug('Fetch session ses_id = ' . $this->id
);
968 $sessionRecord = $this->getSessionBackend()->get($this->id
);
969 } catch (SessionNotFoundException
$e) {
973 $this->sessionData
= unserialize($sessionRecord['ses_data']);
974 // Session is anonymous so no need to fetch user
975 if (!empty($sessionRecord['ses_anonymous'])) {
976 return $sessionRecord;
979 // Fetch the user from the DB
980 $userRecord = $this->getRawUserByUid((int)$sessionRecord['ses_userid']);
982 $userRecord = array_merge($sessionRecord, $userRecord);
984 $userRecord['ses_tstamp'] = (int)$userRecord['ses_tstamp'];
985 $userRecord['is_online'] = (int)$userRecord['ses_tstamp'];
987 if (!empty($this->auth_timeout_field
)) {
988 // Get timeout-time from usertable
989 $timeout = (int)$userRecord[$this->auth_timeout_field
];
991 $timeout = $this->sessionTimeout
;
993 // If timeout > 0 (TRUE) and current time has not exceeded the latest sessions-time plus the timeout in seconds then accept user
994 // Use a gracetime-value to avoid updating a session-record too often
995 if ($timeout > 0 && $GLOBALS['EXEC_TIME'] < $userRecord['ses_tstamp'] +
$timeout) {
996 $sessionUpdateGracePeriod = 61;
997 if (!$skipSessionUpdate && $GLOBALS['EXEC_TIME'] > ($userRecord['ses_tstamp'] +
$sessionUpdateGracePeriod)) {
998 // Update the session timestamp by writing a dummy update. (Backend will update the timestamp)
999 $updatesSession = $this->getSessionBackend()->update($this->id
, []);
1000 $userRecord = array_merge($userRecord, $updatesSession);
1003 // Delete any user set...
1005 $userRecord = false;
1012 * Log out current user!
1013 * Removes the current session record, sets the internal ->user array to a blank string;
1014 * Thereby the current user (if any) is effectively logged out!
1016 public function logoff()
1018 $this->logger
->debug('logoff: ses_id = ' . $this->id
);
1021 foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_userauth.php']['logoff_pre_processing'] ??
[] as $_funcRef) {
1023 GeneralUtility
::callUserFunction($_funcRef, $_params, $this);
1026 $this->performLogoff();
1028 foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_userauth.php']['logoff_post_processing'] ??
[] as $_funcRef) {
1030 GeneralUtility
::callUserFunction($_funcRef, $_params, $this);
1036 * Perform the logoff action. Called from logoff() as a way to allow subclasses to override
1037 * what happens when a user logs off, without needing to reproduce the hook calls and logging
1038 * that happens in the public logoff() API method.
1040 protected function performLogoff()
1043 $this->getSessionBackend()->remove($this->id
);
1049 * Empty / unset the cookie
1051 * @param string $cookieName usually, this is $this->name
1053 public function removeCookie($cookieName)
1055 $cookieDomain = $this->getCookieDomain();
1056 // If no cookie domain is set, use the base path
1057 $cookiePath = $cookieDomain ?
'/' : GeneralUtility
::getIndpEnv('TYPO3_SITE_PATH');
1058 setcookie($cookieName, null, -1, $cookiePath, $cookieDomain);
1062 * Determine whether there's an according session record to a given session_id.
1063 * Don't care if session record is still valid or not.
1065 * @param string $id Claimed Session ID
1066 * @return bool Returns TRUE if a corresponding session was found in the database
1068 public function isExistingSessionRecord($id)
1071 $sessionRecord = $this->getSessionBackend()->get($id);
1072 if (empty($sessionRecord)) {
1075 // If the session does not match the current IP lock, it should be treated as invalid
1076 // and a new session should be created.
1077 if ($sessionRecord['ses_iplock'] !== $this->ipLockClause_remoteIPNumber($this->lockIP
) && $sessionRecord['ses_iplock'] !== '[DISABLED]') {
1081 } catch (SessionNotFoundException
$e) {
1087 * Returns whether this request is going to set a cookie
1088 * or a cookie was already found in the system
1090 * @return bool Returns TRUE if a cookie is set
1092 public function isCookieSet()
1094 return $this->cookieWasSetOnCurrentRequest ||
$this->getCookie($this->name
);
1097 /*************************
1101 *************************/
1103 * This returns the restrictions needed to select the user respecting
1104 * enable columns and flags like deleted, hidden, starttime, endtime
1107 * @return QueryRestrictionContainerInterface
1110 protected function userConstraints(): QueryRestrictionContainerInterface
1112 $restrictionContainer = GeneralUtility
::makeInstance(DefaultRestrictionContainer
::class);
1114 if (empty($this->enablecolumns
['disabled'])) {
1115 $restrictionContainer->removeByType(HiddenRestriction
::class);
1118 if (empty($this->enablecolumns
['deleted'])) {
1119 $restrictionContainer->removeByType(DeletedRestriction
::class);
1122 if (empty($this->enablecolumns
['starttime'])) {
1123 $restrictionContainer->removeByType(StartTimeRestriction
::class);
1126 if (empty($this->enablecolumns
['endtime'])) {
1127 $restrictionContainer->removeByType(EndTimeRestriction
::class);
1130 if (!empty($this->enablecolumns
['rootLevel'])) {
1131 $restrictionContainer->add(GeneralUtility
::makeInstance(RootLevelRestriction
::class, [$this->user_table
]));
1134 return $restrictionContainer;
1138 * Returns the IP address to lock to.
1139 * The IP address may be partial based on $parts.
1141 * @param int $parts 1-4: Indicates how many parts of the IP address to return. 4 means all, 1 means only first number.
1142 * @return string (Partial) IP address for REMOTE_ADDR
1144 protected function ipLockClause_remoteIPNumber($parts)
1146 $IP = GeneralUtility
::getIndpEnv('REMOTE_ADDR');
1150 $parts = MathUtility
::forceIntegerInRange($parts, 1, 3);
1151 $IPparts = explode('.', $IP);
1152 for ($a = 4; $a > $parts; $a--) {
1153 unset($IPparts[$a - 1]);
1155 return implode('.', $IPparts);
1158 /*************************
1160 * Session and Configuration Handling
1162 *************************/
1164 * This writes $variable to the user-record. This is a way of providing session-data.
1165 * You can fetch the data again through $this->uc in this class!
1166 * If $variable is not an array, $this->uc is saved!
1168 * @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
1170 public function writeUC($variable = '')
1172 if (is_array($this->user
) && $this->user
[$this->userid_column
]) {
1173 if (!is_array($variable)) {
1174 $variable = $this->uc
;
1176 $this->logger
->debug('writeUC: ' . $this->userid_column
. '=' . (int)$this->user
[$this->userid_column
]);
1177 GeneralUtility
::makeInstance(ConnectionPool
::class)->getConnectionForTable($this->user_table
)->update(
1179 ['uc' => serialize($variable)],
1180 [$this->userid_column
=> (int)$this->user
[$this->userid_column
]],
1181 ['uc' => Connection
::PARAM_LOB
]
1187 * Sets $theUC as the internal variable ->uc IF $theUC is an array.
1188 * If $theUC is FALSE, the 'uc' content from the ->user array will be unserialized and restored in ->uc
1190 * @param mixed $theUC If an array, then set as ->uc, otherwise load from user record
1192 public function unpack_uc($theUC = '')
1194 if (!$theUC && isset($this->user
['uc'])) {
1195 $theUC = unserialize($this->user
['uc']);
1197 if (is_array($theUC)) {
1203 * Stores data for a module.
1204 * The data is stored with the session id so you can even check upon retrieval
1205 * if the module data is from a previous session or from the current session.
1207 * @param string $module Is the name of the module ($MCONF['name'])
1208 * @param mixed $data Is the data you want to store for that module (array, string, ...)
1209 * @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.
1211 public function pushModuleData($module, $data, $noSave = 0)
1213 $this->uc
['moduleData'][$module] = $data;
1214 $this->uc
['moduleSessionID'][$module] = $this->id
;
1221 * Gets module data for a module (from a loaded ->uc array)
1223 * @param string $module Is the name of the module ($MCONF['name'])
1224 * @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).
1225 * @return mixed The module data if available: $this->uc['moduleData'][$module];
1227 public function getModuleData($module, $type = '')
1229 if ($type !== 'ses' ||
(isset($this->uc
['moduleSessionID'][$module]) && $this->uc
['moduleSessionID'][$module] == $this->id
)) {
1230 return $this->uc
['moduleData'][$module];
1236 * Returns the session data stored for $key.
1237 * The data will last only for this login session since it is stored in the user session.
1239 * @param string $key The key associated with the session data
1242 public function getSessionData($key)
1244 return $this->sessionData
[$key] ??
null;
1248 * Set session data by key.
1249 * The data will last only for this login session since it is stored in the user session.
1251 * @param string $key A non empty string to store the data under
1252 * @param mixed $data Data store store in session
1254 public function setSessionData($key, $data)
1257 throw new \
InvalidArgumentException('Argument key must not be empty', 1484311516);
1259 $this->sessionData
[$key] = $data;
1263 * Sets the session data ($data) for $key and writes all session data (from ->user['ses_data']) to the database.
1264 * The data will last only for this login session since it is stored in the session table.
1266 * @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.
1267 * @param mixed $data The data to store in index $key
1269 public function setAndSaveSessionData($key, $data)
1271 $this->sessionData
[$key] = $data;
1272 $this->user
['ses_data'] = serialize($this->sessionData
);
1273 $this->logger
->debug('setAndSaveSessionData: ses_id = ' . $this->id
);
1274 $updatedSession = $this->getSessionBackend()->update(
1276 ['ses_data' => $this->user
['ses_data']]
1278 $this->user
= array_merge($this->user ??
[], $updatedSession);
1281 /*************************
1285 *************************/
1287 * Returns an info array with Login/Logout data submitted by a form or params
1292 public function getLoginFormData()
1295 $loginData['status'] = GeneralUtility
::_GP($this->formfield_status
);
1296 if ($this->getMethodEnabled
) {
1297 $loginData['uname'] = GeneralUtility
::_GP($this->formfield_uname
);
1298 $loginData['uident'] = GeneralUtility
::_GP($this->formfield_uident
);
1300 $loginData['uname'] = GeneralUtility
::_POST($this->formfield_uname
);
1301 $loginData['uident'] = GeneralUtility
::_POST($this->formfield_uident
);
1303 // Only process the login data if a login is requested
1304 if ($loginData['status'] === LoginType
::LOGIN
) {
1305 $loginData = $this->processLoginData($loginData);
1307 $loginData = array_map('trim', $loginData);
1312 * Processes Login data submitted by a form or params depending on the
1313 * passwordTransmissionStrategy
1315 * @param array $loginData Login data array
1316 * @param string $passwordTransmissionStrategy Alternative passwordTransmissionStrategy. Used when authentication services wants to override the default.
1320 public function processLoginData($loginData, $passwordTransmissionStrategy = '')
1322 $loginSecurityLevel = trim($GLOBALS['TYPO3_CONF_VARS'][$this->loginType
]['loginSecurityLevel']) ?
: 'normal';
1323 $passwordTransmissionStrategy = $passwordTransmissionStrategy ?
: $loginSecurityLevel;
1324 $this->logger
->debug('Login data before processing', $loginData);
1326 $subType = 'processLoginData' . $this->loginType
;
1327 $authInfo = $this->getAuthInfoArray();
1328 $isLoginDataProcessed = false;
1329 $processedLoginData = $loginData;
1330 while (is_object($serviceObject = GeneralUtility
::makeInstanceService('auth', $subType, $serviceChain))) {
1331 $serviceChain .= ',' . $serviceObject->getServiceKey();
1332 $serviceObject->initAuth($subType, $loginData, $authInfo, $this);
1333 $serviceResult = $serviceObject->processLoginData($processedLoginData, $passwordTransmissionStrategy);
1334 if (!empty($serviceResult)) {
1335 $isLoginDataProcessed = true;
1336 // If the service returns >=200 then no more processing is needed
1337 if ((int)$serviceResult >= 200) {
1338 unset($serviceObject);
1342 unset($serviceObject);
1344 if ($isLoginDataProcessed) {
1345 $loginData = $processedLoginData;
1346 $this->logger
->debug('Processed login data', $processedLoginData);
1352 * Returns an info array which provides additional information for auth services
1357 public function getAuthInfoArray()
1359 $queryBuilder = GeneralUtility
::makeInstance(ConnectionPool
::class)->getQueryBuilderForTable($this->user_table
);
1360 $expressionBuilder = $queryBuilder->expr();
1362 $authInfo['loginType'] = $this->loginType
;
1363 $authInfo['refInfo'] = parse_url(GeneralUtility
::getIndpEnv('HTTP_REFERER'));
1364 $authInfo['HTTP_HOST'] = GeneralUtility
::getIndpEnv('HTTP_HOST');
1365 $authInfo['REMOTE_ADDR'] = GeneralUtility
::getIndpEnv('REMOTE_ADDR');
1366 $authInfo['REMOTE_HOST'] = GeneralUtility
::getIndpEnv('REMOTE_HOST');
1367 $authInfo['showHiddenRecords'] = $this->showHiddenRecords
;
1368 // Can be overidden in localconf by SVCONF:
1369 $authInfo['db_user']['table'] = $this->user_table
;
1370 $authInfo['db_user']['userid_column'] = $this->userid_column
;
1371 $authInfo['db_user']['username_column'] = $this->username_column
;
1372 $authInfo['db_user']['userident_column'] = $this->userident_column
;
1373 $authInfo['db_user']['usergroup_column'] = $this->usergroup_column
;
1374 $authInfo['db_user']['enable_clause'] = $this->userConstraints()->buildExpression(
1375 [$this->user_table
=> $this->user_table
],
1378 if ($this->checkPid
&& $this->checkPid_value
!== null) {
1379 $authInfo['db_user']['checkPidList'] = $this->checkPid_value
;
1380 $authInfo['db_user']['check_pid_clause'] = $expressionBuilder->in(
1382 GeneralUtility
::intExplode(',', $this->checkPid_value
)
1385 $authInfo['db_user']['checkPidList'] = '';
1386 $authInfo['db_user']['check_pid_clause'] = '';
1388 $authInfo['db_groups']['table'] = $this->usergroup_table
;
1393 * Garbage collector, removing old expired sessions.
1397 public function gc()
1399 $this->getSessionBackend()->collectGarbage($this->gc_time
);
1403 * DUMMY: Writes to log database table (in some extension classes)
1405 * @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
1406 * @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 !!)
1407 * @param int $error flag. 0 = message, 1 = error (user problem), 2 = System Error (which should not happen), 3 = security notice (admin)
1408 * @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
1409 * @param string $details Default text that follows the message
1410 * @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...
1411 * @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.)
1412 * @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.)
1413 * @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.)
1415 public function writelog($type, $action, $error, $details_nr, $details, $data, $tablename, $recuid, $recpid)
1420 * DUMMY: Check login failures (in some extension classes)
1422 * @param string $email Email address
1423 * @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
1424 * @param int $maxFailures Max allowed failures before a warning mail is sent
1427 public function checkLogFailures($email, $secondsBack, $maxFailures)
1432 * Raw initialization of the be_user with uid=$uid
1433 * This will circumvent all login procedures and select a be_users record from the
1434 * database and set the content of ->user to the record selected.
1435 * Thus the BE_USER object will appear like if a user was authenticated - however without
1436 * a session id and the fields from the session table of course.
1437 * Will check the users for disabled, start/endtime, etc. ($this->user_where_clause())
1439 * @param int $uid The UID of the backend user to set in ->user
1442 public function setBeUserByUid($uid)
1444 $this->user
= $this->getRawUserByUid($uid);
1448 * Raw initialization of the be_user with username=$name
1450 * @param string $name The username to look up.
1451 * @see \TYPO3\CMS\Core\Authentication\AbstractUserAuthentication::setBeUserByUid()
1454 public function setBeUserByName($name)
1456 $this->user
= $this->getRawUserByName($name);
1460 * Fetching raw user record with uid=$uid
1462 * @param int $uid The UID of the backend user to set in ->user
1463 * @return array user record or FALSE
1466 public function getRawUserByUid($uid)
1468 $query = GeneralUtility
::makeInstance(ConnectionPool
::class)->getQueryBuilderForTable($this->user_table
);
1469 $query->setRestrictions($this->userConstraints());
1471 ->from($this->user_table
)
1472 ->where($query->expr()->eq('uid', $query->createNamedParameter($uid, \PDO
::PARAM_INT
)));
1474 return $query->execute()->fetch();
1478 * Fetching raw user record with username=$name
1480 * @param string $name The username to look up.
1481 * @return array user record or FALSE
1482 * @see \TYPO3\CMS\Core\Authentication\AbstractUserAuthentication::getUserByUid()
1485 public function getRawUserByName($name)
1487 $query = GeneralUtility
::makeInstance(ConnectionPool
::class)->getQueryBuilderForTable($this->user_table
);
1488 $query->setRestrictions($this->userConstraints());
1490 ->from($this->user_table
)
1491 ->where($query->expr()->eq('username', $query->createNamedParameter($name, \PDO
::PARAM_STR
)));
1493 return $query->execute()->fetch();
1500 public function getSessionId(): string
1509 public function getLoginType(): string
1511 return $this->loginType
;
1515 * Returns initialized session backend. Returns same session backend if called multiple times
1517 * @return SessionBackendInterface
1519 protected function getSessionBackend()
1521 if (!isset($this->sessionBackend
)) {
1522 $this->sessionBackend
= GeneralUtility
::makeInstance(SessionManager
::class)->getSessionBackend($this->loginType
);
1524 return $this->sessionBackend
;