'', // Boolean: If TRUE, 'AND pid=0' will be a part of the query... 'disabled' => '', 'starttime' => '', 'endtime' => '', 'deleted' => '', ]; /** * Form field with login-name * @var string * @internal */ protected $formfield_uname = ''; /** * Form field with password * @var string * @internal */ protected $formfield_uident = ''; /** * Form field with status: *'login', 'logout'. If empty login is not verified. * @var string * @internal */ protected $formfield_status = ''; /** * Decides if the writelog() function is called at login and logout * @var bool */ public $writeStdLog = false; /** * Log failed login attempts * @var bool */ public $writeAttemptLog = false; /** * If set, the user-record must be stored at the page defined by $checkPid_value * @var bool */ public $checkPid = true; /** * The page id the user record must be stored at, can also hold a comma separated list of pids * @var int|string|null */ public $checkPid_value = 0; /** * Will be set to TRUE if the login session is actually written during auth-check. * @var bool * @internal */ protected $loginSessionStarted = false; /** * @var array|null contains user- AND session-data from database (joined tables) * @internal */ public $user; /** * This array will hold the groups that the user is a member of */ public array $userGroups = []; /** * Will prevent the setting of the session cookie * @var bool * @internal */ protected $dontSetCookie = false; /** * Login type, used for services. * @var string */ public $loginType = ''; /** * User Settings (= preferences) */ public array $uc = []; protected ?UserSession $userSession = null; protected UserSessionManager $userSessionManager; /** * If set, this cookie will be set to the response. */ protected SetCookieBehavior $setCookie = SetCookieBehavior::None; /** * Initialize some important variables * * @throws Exception */ public function __construct() { // Backend or frontend login - used for auth services if (empty($this->loginType)) { throw new Exception('No loginType defined, must be set explicitly by subclass', 1476045345); } } /** * Currently needed for various unit tests, until start() and checkAuthentication() methods * are smaller and extracted from this class. * * @internal */ public function initializeUserSessionManager(?UserSessionManager $userSessionManager = null): void { $this->userSessionManager = $userSessionManager ?? UserSessionManager::create($this->loginType); $this->createAnonymousSession(); } /** * Creates an anonymous user session. * This method should be avoided, as it is only a workaround due to the ugly setup of this class * and the authentication / logout behavior. */ public function createAnonymousSession(): void { if (!empty($this->userSessionManager)) { $this->userSession = $this->userSessionManager->createAnonymousSession(); } } /** * Starts a user session * Typical configurations will: * a) check if session cookie was set and if not, set one, * b) check if a password/username was sent and if so, try to authenticate the user * c) Lookup a session attached to a user and check timeout etc. * d) Garbage collection, setting of no-cache headers. * If a user is authenticated the database record of the user (array) will be set in the ->user internal variable. */ public function start(ServerRequestInterface $request) { $this->logger->debug('## Beginning of auth logging.'); // Make certain that NO user is set initially $this->user = null; if (!isset($this->userSessionManager)) { $this->initializeUserSessionManager(); } $this->userSession = $this->userSessionManager->createFromRequestOrAnonymous($request, $this->name); // Load user session, check to see if anyone has submitted login-information and if so authenticate // the user with the session. $this->user[uid] may be used to write log... try { $this->checkAuthentication($request); } catch (MfaRequiredException $mfaRequiredException) { // Ensure the cookie is still set to keep the user session available if ($this->shallSetSessionCookie()) { $this->setSessionCookie(); } throw $mfaRequiredException; } if ($this->shallSetSessionCookie()) { $this->setSessionCookie(); } // Hook for alternative ways of filling the $this->user array foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_userauth.php']['postUserLookUp'] ?? [] as $funcName) { $_params = [ 'pObj' => $this, ]; GeneralUtility::callUserFunction($funcName, $_params, $this); } } /** * Used to apply a cookie to a PSR-7 Response. * * @todo: should go into a middleware? * @internal */ public function appendCookieToResponse(ResponseInterface $response, ?NormalizedParams $normalizedParams = null): ResponseInterface { if ($this->setCookie === SetCookieBehavior::None) { return $response; } if ($normalizedParams === null) { $normalizedParams = NormalizedParams::createFromRequest($GLOBALS['TYPO3_REQUEST']); } $setCookieService = SetCookieService::create($this->name, $this->loginType); if ($this->setCookie === SetCookieBehavior::Send) { $cookieObject = $setCookieService->setSessionCookie($this->userSession, $normalizedParams); if ($cookieObject) { $response = $response->withAddedHeader('Set-Cookie', $cookieObject->__toString()); } } if ($this->setCookie === SetCookieBehavior::Remove) { $cookieObject = $setCookieService->removeCookie($normalizedParams); $response = $response->withAddedHeader('Set-Cookie', $cookieObject->__toString()); } return $response; } /** * Sets the setCookie directive to "Send", which will then result in appending * a new cookie to the PSR-7 response, see appendCookieToResponse(). * In case this method is called, the cookie needs to be set later. */ protected function setSessionCookie() { $this->setCookie = SetCookieBehavior::Send; } /** * Determines whether setting the session cookie is generally enabled, * or the current session is a non-session cookie (FE permalogin). */ protected function shallSetSessionCookie(): bool { return !$this->dontSetCookie || SetCookieService::create($this->name, $this->loginType)->isRefreshTimeBasedCookie($this->userSession); } /** * Determine whether a session cookie needs to be set (lifetime=0) * * @return bool * @internal */ protected function isSetSessionCookie() { return SetCookieService::create($this->name, $this->loginType)->isSetSessionCookie($this->userSession); } /** * Determine whether a non-session cookie needs to be set (lifetime>0) * * @return bool * @internal */ protected function isRefreshTimeBasedCookie() { return SetCookieService::create($this->name, $this->loginType)->isRefreshTimeBasedCookie($this->userSession); } /** * "auth" services configuration array from $GLOBALS['TYPO3_CONF_VARS']['SVCONF']['auth'] */ protected function getAuthServiceConfiguration(): array { if (is_array($GLOBALS['TYPO3_CONF_VARS']['SVCONF']['auth']['setup'] ?? null)) { return $GLOBALS['TYPO3_CONF_VARS']['SVCONF']['auth']['setup']; } return []; } /** * Checks if a submission of username and password is present or use other authentication by auth services * * @throws MfaRequiredException * @internal */ public function checkAuthentication(ServerRequestInterface $request) { $authConfiguration = $this->getAuthServiceConfiguration(); if (!empty($authConfiguration)) { $this->logger->debug('Authentication Service Configuration found.', ['auth_configuration' => $authConfiguration]); } $userRecordCandidate = false; // User is not authenticated by default $authenticated = false; // User want to login with passed login data (name/password) $activeLogin = false; $this->logger->debug('Login type: {type}', ['type' => $this->loginType]); // Get Login/Logout data submitted by a form or params $loginData = $this->getLoginFormData($request); $this->logger->debug('Login data', $this->removeSensitiveLoginDataForLoggingInfo($loginData)); $type = LoginType::tryFrom($loginData['status'] ?? ''); // Active logout (eg. with "logout" button) if ($type === LoginType::LOGOUT) { if ($this->writeStdLog) { $this->writelog(SystemLogType::LOGIN, SystemLogLoginAction::LOGOUT, SystemLogErrorClassification::MESSAGE, null, 'User %s logged out', [$this->user['username']], '', 0); } $this->logger->info('User logged out. Id: {session}', ['session' => sha1($this->userSession->getIdentifier())]); $this->logoff(); } // Determine whether we need to skip session update. // This is used mainly for checking session timeout in advance without refreshing the current session's timeout. $skipSessionUpdate = (bool)($request->getQueryParams()['skipSessionUpdate'] ?? false); $isExistingSession = false; $anonymousSession = false; $authenticatedUserFromSession = null; if (!$this->userSession->isNew()) { // Read user data if this is bound to a user // However, if the user data is not valid, or the session has timed out we'll recreate a new anonymous session if ($this->userSession->getUserId() > 0) { $authenticatedUserFromSession = $this->fetchValidUserFromSessionOrDestroySession($skipSessionUpdate); } $isExistingSession = !$this->userSession->isNew(); $anonymousSession = $isExistingSession && $this->userSession->isAnonymous(); } // Active login (eg. with login form). if ($type === LoginType::LOGIN) { if (!$isExistingSession) { $activeLogin = true; $this->logger->debug('Active login (eg. with login form)'); // check referrer for submitted login values if ($this->formfield_status && $loginData['uident'] && $loginData['uname']) { // Delete old user session if any $this->logoff(); } // Refuse login for _CLI users, if not processing a CLI request type // (although we shouldn't be here in case of a CLI request type) if (stripos($loginData['uname'], '_CLI_') === 0 && !Environment::isCli()) { throw new \RuntimeException('TYPO3 Fatal Error: You have tried to login using a CLI user. Access prohibited!', 1270853931); } } // Cause elevation of privilege, make sure regenerateSessionId is called later on // Note for further research: $anonymousSession actually implies having $isExistingSession = true // allowing to further simplify this concern. if ($anonymousSession) { $activeLogin = true; } } if ($isExistingSession && $authenticatedUserFromSession !== null) { $this->logger->debug('User found in session', [ $this->userid_column => $authenticatedUserFromSession[$this->userid_column] ?? null, $this->username_column => $authenticatedUserFromSession[$this->username_column] ?? null, ]); } else { $this->logger->debug('No user session found'); } if ($activeLogin) { $context = GeneralUtility::makeInstance(Context::class); $securityAspect = SecurityAspect::provideIn($context); $requestToken = $securityAspect->getReceivedRequestToken(); $event = new BeforeRequestTokenProcessedEvent($this, $request, $requestToken); GeneralUtility::makeInstance(EventDispatcherInterface::class)->dispatch($event); $requestToken = $event->getRequestToken(); $requestTokenScopeMatches = ($requestToken->scope ?? null) === 'core/user-auth/' . strtolower($this->loginType); if (!$requestTokenScopeMatches) { $this->logger->debug('Missing or invalid request token during login', ['requestToken' => $requestToken]); // important: disable `$activeLogin` state $activeLogin = false; } elseif ($requestToken instanceof RequestToken && $requestToken->getSigningSecretIdentifier() !== null) { $securityAspect->getSigningSecretResolver()->revokeIdentifier( $requestToken->getSigningSecretIdentifier() ); } } // Fetch users from the database (or somewhere else) $possibleUsers = $this->fetchPossibleUsers($loginData, $activeLogin, $isExistingSession, $authenticatedUserFromSession, $request); // If no new user was set we use the already found user session if (empty($possibleUsers) && $isExistingSession && !$anonymousSession) { // Check if the previous services returned a proper user if (is_array($authenticatedUserFromSession)) { $possibleUsers[] = $authenticatedUserFromSession; $userRecordCandidate = $authenticatedUserFromSession; // User is authenticated because we found a user session $authenticated = true; $this->logger->debug('User session used', [ $this->userid_column => $authenticatedUserFromSession[$this->userid_column] ?? '', $this->username_column => $authenticatedUserFromSession[$this->username_column] ?? '', ]); } } // Re-auth user when 'auth'-service option is set if (!empty($authConfiguration[$this->loginType . '_alwaysAuthUser'])) { $authenticated = false; $this->logger->debug('alwaysAuthUser option is enabled'); } // Authenticate the user if needed if (!empty($possibleUsers) && !$authenticated) { foreach ($possibleUsers as $userRecordCandidate) { // Use 'auth' service to authenticate the user // If one service returns FALSE then authentication failed // a service might return 100 which means there's no reason to stop but the user can't be authenticated by that service $this->logger->debug('Auth user', $this->removeSensitiveLoginDataForLoggingInfo($userRecordCandidate, true)); $subType = 'authUser' . $this->loginType; /** @var AuthenticationService $serviceObj */ foreach ($this->getAuthServices($subType, $loginData, $authenticatedUserFromSession, $request) as $serviceObj) { if (($ret = (int)$serviceObj->authUser($userRecordCandidate)) > 0) { // If the service returns >=200 then no more checking is needed - useful for IP checking without password if ($ret >= 200) { $authenticated = true; break; } if ($ret < 100) { $authenticated = true; } // $ret is between 100 and 199 which means "I'm not responsible, ask others" } else { // $ret is < 0 $authenticated = false; break; } } if ($authenticated) { // Leave foreach() because a user is authenticated break; } } // mimic user authentication to mitigate observable timing discrepancies // @link https://cwe.mitre.org/data/definitions/208.html } elseif ($activeLogin) { $subType = 'authUser' . $this->loginType; foreach ($this->getAuthServices($subType, $loginData, $authenticatedUserFromSession, $request) as $serviceObj) { if ($serviceObj instanceof MimicServiceInterface && $serviceObj->mimicAuthUser() === false) { break; } } } // If user is authenticated, then a valid user is found in $userRecordCandidate if ($authenticated) { // Insert session record if needed if (!$isExistingSession || $anonymousSession || (int)($userRecordCandidate[$this->userid_column] ?? 0) !== $this->userSession->getUserId() ) { $sessionData = $this->userSession->getData(); // Create a new session with a fixated user $this->userSession = $this->createUserSession($userRecordCandidate); // Preserve session data on login if ($anonymousSession || $isExistingSession) { $this->userSession->overrideData($sessionData); } $this->user = array_merge($userRecordCandidate, $this->user ?? []); // The login session is started. $this->loginSessionStarted = true; $this->logger->debug('User session finally read', [ $this->userid_column => $this->getUserId(), $this->username_column => $this->getUserName(), ]); } else { // if we come here the current session is for sure not anonymous as this is a pre-condition for $authenticated = true $this->user = $authenticatedUserFromSession; } if ($activeLogin && !$this->userSession->isNew()) { $this->regenerateSessionId(); } // Since the user is not fully authenticated we need to unpack UC here to be // able to retrieve a possible defined default (preferred) MFA provider. $this->unpack_uc(); if ($activeLogin) { // User logged in - write that to the log! if ($this->writeStdLog) { $this->writelog(SystemLogType::LOGIN, SystemLogLoginAction::LOGIN, SystemLogErrorClassification::MESSAGE, null, 'User %s logged in from ###IP###', [$userRecordCandidate[$this->username_column]], '', ''); } $this->logger->info('User {username} logged in from {ip}', [ 'username' => $userRecordCandidate[$this->username_column], 'ip' => $request->getAttribute('normalizedParams')->getRemoteAddress(), ]); } else { $this->logger->debug('User {username} authenticated from {ip}', [ 'username' => $userRecordCandidate[$this->username_column], 'ip' => $request->getAttribute('normalizedParams')->getRemoteAddress(), ]); } // Check if multi-factor authentication is required $this->evaluateMfaRequirements(); } else { // Mark the current login attempt as failed if (empty($possibleUsers) && $activeLogin) { $this->logger->debug('Login failed', [ 'loginData' => $this->removeSensitiveLoginDataForLoggingInfo($loginData), ]); } elseif (!empty($possibleUsers)) { $this->logger->debug('Login failed', [ $this->userid_column => $userRecordCandidate[$this->userid_column], $this->username_column => $userRecordCandidate[$this->username_column], ]); } // If there were a login failure, check to see if a warning email should be sent if ($activeLogin) { GeneralUtility::makeInstance(EventDispatcherInterface::class)->dispatch( new LoginAttemptFailedEvent($this, $request, $this->removeSensitiveLoginDataForLoggingInfo($loginData)) ); } } } /** * Loads users from various sources (= authentication services) as an array of arrays. * * @param array|null $authenticatedUserFromSession if we have a user from an existing session, this is set here, otherwise null */ protected function fetchPossibleUsers(array $loginData, bool $activeLogin, bool $isExistingSession, ?array $authenticatedUserFromSession, ServerRequestInterface $request): array { $possibleUsers = []; $authConfiguration = $this->getAuthServiceConfiguration(); $alwaysFetchUsers = !empty($authConfiguration[$this->loginType . '_alwaysFetchUser']); $fetchUsersIfNoSessionIsGiven = !empty($authConfiguration[$this->loginType . '_fetchUserIfNoSession']); if ( $activeLogin || $alwaysFetchUsers || (!$isExistingSession && $fetchUsersIfNoSessionIsGiven) ) { // Use 'auth' service to find the user // First found user will be used $subType = 'getUser' . $this->loginType; /** @var AuthenticationService $serviceObj */ foreach ($this->getAuthServices($subType, $loginData, $authenticatedUserFromSession, $request) as $serviceObj) { $row = $serviceObj->getUser(); if (is_array($row)) { $possibleUsers[] = $row; $this->logger->debug('User found', [ $this->userid_column => $row[$this->userid_column], $this->username_column => $row[$this->username_column], ]); // User found, just stop to search for more if not configured to go on if (empty($authConfiguration[$this->loginType . '_fetchAllUsers'])) { break; } } } if ($alwaysFetchUsers) { $this->logger->debug($this->loginType . '_alwaysFetchUser option is enabled'); } if (empty($possibleUsers)) { $this->logger->debug('No user found by services'); } else { $this->logger->debug('{count} user records found by services', ['count' => count($possibleUsers)]); } } return $possibleUsers; } /** * This method checks if the user is authenticated but has not succeeded in * passing his MFA challenge. This method can therefore only be used if a user * has been authenticated against his first authentication method (username+password * or any other authentication token). * * @throws MfaRequiredException * @internal */ protected function evaluateMfaRequirements(): void { // MFA has been validated already, nothing to do if ($this->getSessionData('mfa')) { return; } // If the user session does not contain the 'mfa' key - indicating that MFA is already // passed - get the first provider for authentication, which is either the default provider // or the first active provider (based on the providers configured ordering). $provider = GeneralUtility::makeInstance(MfaProviderRegistry::class)->getFirstAuthenticationAwareProvider($this); // Throw an exception (hopefully caught in a middleware) when an active provider for the user exists if ($provider !== null) { throw new MfaRequiredException($provider, 1613687097); } } /** * Whether the user is required to set up MFA * * @internal */ public function isMfaSetupRequired(): bool { return false; } /** * Initializes authentication services to be used in a foreach loop * * @param string $subType e.g. getUserFE * @param array|null $authenticatedUserFromSession the user which was loaded from the session, or null if none was found * @return \Traversable A generator of service objects */ protected function getAuthServices(string $subType, array $loginData, ?array $authenticatedUserFromSession, ServerRequestInterface $request): \Traversable { $serviceChain = []; // The info array provide additional information for auth services $authInfo = $this->getAuthInfoArray($request); if ($authenticatedUserFromSession !== null) { $authInfo['user'] = $authenticatedUserFromSession; } while (is_object($serviceObj = GeneralUtility::makeInstanceService('auth', $subType, $serviceChain))) { $serviceChain[] = $serviceObj->getServiceKey(); $serviceObj->initAuth($subType, $loginData, $authInfo, $this); yield $serviceObj; } if (!empty($serviceChain)) { $this->logger->debug('{subtype} auth services called: {chain}', [ 'subtype' => $subType, 'chain' => implode(',', $serviceChain), ]); } } /** * Regenerate the session ID and transfer the session to new ID * Call this method whenever a user proceeds to a higher authorization level * e.g. when an anonymous session is now authenticated. */ protected function regenerateSessionId() { $this->userSession = $this->userSessionManager->regenerateSession($this->userSession->getIdentifier()); } /************************* * * User Sessions * *************************/ /** * Creates a user session record and returns its values. * * @param array $userRecordCandidate User data array * @return UserSession The session data for the newly created session. */ public function createUserSession(array $userRecordCandidate): UserSession { // Needed for testing framework if (!isset($this->userSessionManager)) { $this->initializeUserSessionManager(); } $userRecordCandidateId = (int)($userRecordCandidate[$this->userid_column] ?? 0); $session = $this->userSessionManager->elevateToFixatedUserSession($this->userSession, $userRecordCandidateId); // Updating lastLogin_column carrying information about last login. $this->updateLoginTimestamp($userRecordCandidateId); return $session; } /** * Updates the last login column in the user with the given id */ protected function updateLoginTimestamp(int $userId) { if ($this->lastLogin_column) { $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($this->user_table); $connection->update( $this->user_table, [$this->lastLogin_column => $GLOBALS['EXEC_TIME']], [$this->userid_column => $userId] ); $this->user[$this->lastLogin_column] = $GLOBALS['EXEC_TIME']; } } /** * If the session is bound to a user, this method fetches the user record, and returns it. * If the session has a timeout, the session date is extended if needed. Also the ìs_online * flag is updated for the user. * * However, if the session has expired the session is removed and the request is treated as an anonymous session. * * @param bool $skipSessionUpdate */ protected function fetchValidUserFromSessionOrDestroySession(bool $skipSessionUpdate = false): ?array { if ($this->userSession->isAnonymous()) { return null; } // Fetch the user from the DB $userRecord = $this->getRawUserByUid($this->userSession->getUserId() ?? 0); if ($userRecord) { // A user was found $userRecord['is_online'] = $this->userSession->getLastUpdated(); if (!$this->userSessionManager->hasExpired($this->userSession)) { if (!$skipSessionUpdate) { $this->userSession = $this->userSessionManager->updateSessionTimestamp($this->userSession); } } else { // Delete any user set... $this->logoff(); $userRecord = false; $this->createAnonymousSession(); } } return is_array($userRecord) ? $userRecord : null; } /** * Regenerates the session ID and sets the cookie again. * * @internal */ public function enforceNewSessionId() { $this->regenerateSessionId(); $this->setSessionCookie(); } /** * Log out current user! * Removes the current session record, sets the internal ->user array to a blank string; * Thereby the current user (if any) is effectively logged out! */ public function logoff() { $this->logger->debug('logoff: ses_id = {session}', ['session' => sha1($this->userSession->getIdentifier())]); $dispatcher = GeneralUtility::makeInstance(EventDispatcherInterface::class); $event = new BeforeUserLogoutEvent($this, $this->userSession); $event = $dispatcher->dispatch($event); if ($event->shouldLogout()) { $this->performLogoff(); } $dispatcher->dispatch(new AfterUserLoggedOutEvent($this)); } /** * Perform the logoff action. Called from logoff() as a way to allow subclasses to override * what happens when a user logs off, without needing to reproduce the hook calls and logging * that happens in the public logoff() API method. */ protected function performLogoff() { if ($this->userSession) { $this->userSessionManager->removeSession($this->userSession); } $this->createAnonymousSession(); $this->user = null; if ($this->isCookieSet()) { $this->removeCookie(); } } /** * Empty / unset the cookie * * @param string|null $cookieName usually, this is $this->name * @internal */ public function removeCookie($cookieName = null) { $this->setCookie = SetCookieBehavior::Remove; } /** * Returns whether this request is going to set a cookie * or a cookie was already found in the system * * @return bool Returns TRUE if a cookie is set * @internal */ protected function isCookieSet() { return SetCookieService::create($this->name, $this->loginType)->isCookieSet( $GLOBALS['TYPO3_REQUEST'] ?? null, $this->userSession ); } /************************* * * SQL Functions * *************************/ /** * This returns the restrictions needed to select the user respecting * enable columns and flags like deleted, hidden, starttime, endtime * and rootLevel * * @internal */ protected function userConstraints(): QueryRestrictionContainerInterface { $restrictionContainer = GeneralUtility::makeInstance(DefaultRestrictionContainer::class); if (empty($this->enablecolumns['disabled'])) { $restrictionContainer->removeByType(HiddenRestriction::class); } if (empty($this->enablecolumns['deleted'])) { $restrictionContainer->removeByType(DeletedRestriction::class); } if (empty($this->enablecolumns['starttime'])) { $restrictionContainer->removeByType(StartTimeRestriction::class); } if (empty($this->enablecolumns['endtime'])) { $restrictionContainer->removeByType(EndTimeRestriction::class); } if (!empty($this->enablecolumns['rootLevel'])) { $restrictionContainer->add(GeneralUtility::makeInstance(RootLevelRestriction::class, [$this->user_table])); } if ($this->checkPid && $this->checkPid_value !== null && $this->checkPid_value !== '') { $restrictionContainer->add( GeneralUtility::makeInstance( PageIdListRestriction::class, [$this->user_table], GeneralUtility::intExplode(',', (string)$this->checkPid_value, true) ) ); } return $restrictionContainer; } /************************* * * Session and Configuration Handling * *************************/ /** * This writes $this->>uc to the user-record. This is a way of providing session-data. * You can fetch the data again through $this->uc in this class! */ public function writeUC() { $userId = $this->getUserId(); if ($userId) { $this->logger->debug('writeUC: {userid_column}={value}', [ 'userid_column' => $this->userid_column, 'value' => $userId, ]); GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($this->user_table)->update( $this->user_table, ['uc' => serialize($this->uc)], [$this->userid_column => $userId], ['uc' => Connection::PARAM_LOB] ); } } /** * Unserializes the user configuration from the user record into $this->>uc * @internal */ protected function unpack_uc() { if (isset($this->user['uc'])) { $theUC = unserialize($this->user['uc'], ['allowed_classes' => false]); if (is_array($theUC)) { $this->uc = $theUC; } } } /** * Stores data for a module. * The data is stored with the session ID, so you can even check upon retrieval * if the module data is from a previous session or from the current session. * * @param string $module Is the identifier of the module, e.g. "content_status" * @param mixed $data Is the data you want to store for that module (array, string, ...) * @param bool $dontPersistImmediately If set, then the ->uc array (which carries all kinds of user data) is NOT written immediately, but must be written by some subsequent call. */ public function pushModuleData(string $module, mixed $data, bool $dontPersistImmediately = false): void { $hashService = GeneralUtility::makeInstance(HashService::class); $sessionHash = $hashService->hmac( $this->userSession->getIdentifier(), 'core-session-hash' ); $this->uc['moduleData'][$module] = $data; $this->uc['moduleSessionID'][$module] = $sessionHash; if ($dontPersistImmediately === false) { $this->writeUC(); } } /** * Gets module data for a module (from a loaded ->uc array) * * @param string $module Is the identifier of the module, e.g. "content_status" * @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). * @return mixed The module data if available: $this->uc['moduleData'][$module]; */ public function getModuleData(string $module, string $type = ''): mixed { $hashService = GeneralUtility::makeInstance(HashService::class); $sessionHash = $hashService->hmac( $this->userSession->getIdentifier(), 'core-session-hash' ); $sessionData = $this->uc['moduleData'][$module] ?? null; $moduleSessionIdHash = $this->uc['moduleSessionID'][$module] ?? null; if ($type !== 'ses' || ($sessionData !== null && $moduleSessionIdHash === $sessionHash)) { return $sessionData; } return null; } /** * Returns the session data stored for $key. * The data will last only for this login session since it is stored in the user session. * * @param string $key The key associated with the session data * @return mixed */ public function getSessionData($key) { return $this->userSession ? $this->userSession->get($key) : ''; } /** * Set session data by key. * The data will last only for this login session since it is stored in the user session. * * @param string $key A non empty string to store the data under * @param mixed $data Data store store in session */ public function setSessionData($key, $data) { $this->userSession->set($key, $data); } /** * Sets the session data ($data) for $key and writes all session data (from ->user['ses_data']) to the database. * The data will last only for this login session since it is stored in the session table. * * @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. * @param mixed $data The data to store in index $key */ public function setAndSaveSessionData($key, $data) { $this->userSession->set($key, $data); $this->logger->debug('setAndSaveSessionData: ses_id = {session}', ['session' => sha1($this->userSession->getIdentifier())]); $this->userSession = $this->userSessionManager->updateSession($this->userSession); } /************************* * * Misc * *************************/ /** * Returns an info array with Login/Logout data submitted by a form or params * * @return array * @internal */ public function getLoginFormData(ServerRequestInterface $request) { $parsedBody = $request->getParsedBody(); $queryParams = $request->getQueryParams(); $loginData = [ 'status' => StringUtility::filter($parsedBody[$this->formfield_status] ?? $queryParams[$this->formfield_status] ?? null), 'uname' => StringUtility::filter($parsedBody[$this->formfield_uname] ?? '', ''), 'uident' => StringUtility::filter($parsedBody[$this->formfield_uident] ?? '', ''), ]; // Only process the login data if a login is requested if (LoginType::tryFrom($loginData['status'] ?? '') === LoginType::LOGIN) { $loginData = $this->processLoginData($loginData, $request); } return $loginData; } public function isActiveLogin(ServerRequestInterface $request): bool { $status = $request->getParsedBody()[$this->formfield_status] ?? $request->getQueryParams()[$this->formfield_status] ?? ''; return LoginType::tryFrom($status) === LoginType::LOGIN; } /** * Processes Login data submitted by a form or params * * @param array $loginData Login data array * @param ServerRequestInterface $request * @return array * @internal */ public function processLoginData(array $loginData, ServerRequestInterface $request): array { $this->logger->debug('Login data before processing', $this->removeSensitiveLoginDataForLoggingInfo($loginData)); $subType = 'processLoginData' . $this->loginType; $isLoginDataProcessed = false; $processedLoginData = $loginData; /** @var AuthenticationService $serviceObject */ foreach ($this->getAuthServices($subType, $loginData, null, $request) as $serviceObject) { $serviceResult = $serviceObject->processLoginData($processedLoginData); if (!empty($serviceResult)) { $isLoginDataProcessed = true; // If the service returns >=200 then no more processing is needed if ((int)$serviceResult >= 200) { break; } } } if ($isLoginDataProcessed) { $loginData = $processedLoginData; $this->logger->debug('Processed login data', $this->removeSensitiveLoginDataForLoggingInfo($processedLoginData)); } return $loginData; } /** * Removes any sensitive data from the incoming data (either from loginData, processedLogin data * or the user record from the DB). * * No type hinting is added because it might be possible that the incoming data is of any other type. * * @param mixed|array $data * @param bool $isUserRecord * @return mixed */ protected function removeSensitiveLoginDataForLoggingInfo($data, bool $isUserRecord = false) { if ($isUserRecord && is_array($data)) { $fieldNames = ['uid', 'pid', 'tstamp', 'crdate', 'deleted', 'disabled', 'starttime', 'endtime', 'username', 'admin', 'usergroup', 'db_mountpoints', 'file_mountpoints', 'file_permissions', 'workspace_perms', 'lastlogin', 'workspace_id', 'category_perms']; $data = array_intersect_key($data, array_combine($fieldNames, $fieldNames)); } if (isset($data['uident'])) { $data['uident'] = '********'; } if (isset($data['uident_text'])) { $data['uident_text'] = '********'; } if (isset($data['password'])) { $data['password'] = '********'; } return $data; } /** * Returns an info array which provides additional information for auth services * * @return array * @internal */ public function getAuthInfoArray(ServerRequestInterface $request) { $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($this->user_table); $expressionBuilder = $queryBuilder->expr(); $authInfo = []; $authInfo['loginType'] = $this->loginType; $authInfo['request'] = $request; $normalizedParams = $request->getAttribute('normalizedParams'); $authInfo['refInfo'] = $normalizedParams ? parse_url($normalizedParams->getHttpReferer()) : null; $authInfo['HTTP_HOST'] = $normalizedParams?->getHttpHost(); $authInfo['REMOTE_ADDR'] = $normalizedParams?->getRemoteAddress(); $authInfo['REMOTE_HOST'] = $normalizedParams?->getRemoteHost(); // Can be overridden in localconf by SVCONF: $authInfo['db_user']['table'] = $this->user_table; $authInfo['db_user']['userid_column'] = $this->userid_column; $authInfo['db_user']['username_column'] = $this->username_column; $authInfo['db_user']['userident_column'] = $this->userident_column; $authInfo['db_user']['enable_clause'] = $this->userConstraints()->buildExpression( [$this->user_table => $this->user_table], $expressionBuilder ); return $authInfo; } /** * DUMMY: Writes to log database table (in some extension classes) * * @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 * @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 !!) * @param int $error flag. 0 = message, 1 = error (user problem), 2 = System Error (which should not happen), 3 = security notice (admin) * @param null $_ unused * @param string $details Default text that follows the message * @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... * @param string $tablename Special field used by tce_main.php. These ($tablename, $recuid) hold the reference to the record which the log-entry is about. * @param int|string $recuid Special field used by tce_main.php. These ($tablename, $recuid) hold the reference to the record which the log-entry is about. */ public function writelog($type, $action, $error, $_, $details, $data, $tablename, $recuid) {} /** * Raw initialization of the be_user with uid=$uid * This will circumvent all login procedures and select a be_users record from the * database and set the content of ->user to the record selected. * Thus the BE_USER object will appear like if a user was authenticated - however without * a session id and the fields from the session table of course. * Will check the users for disabled, start/endtime, etc. ($this->user_where_clause()) * * @param int $uid The UID of the backend user to set in ->user * @internal */ public function setBeUserByUid($uid) { $this->user = $this->getRawUserByUid($uid); } /** * Raw initialization of the be_user with username=$name * * @param string $name The username to look up. * @see \TYPO3\CMS\Core\Authentication\AbstractUserAuthentication::setBeUserByUid() * @internal */ public function setBeUserByName($name) { $this->user = $this->getRawUserByName($name) ?: null; } /** * Fetching raw user record with uid=$uid * * @param int $uid The UID of the backend user to set in ->user * @return array user record or FALSE * @internal */ public function getRawUserByUid($uid) { $query = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($this->user_table); $query->setRestrictions($this->userConstraints()); $query->select('*') ->from($this->user_table) ->where($query->expr()->eq($this->userid_column, $query->createNamedParameter($uid, Connection::PARAM_INT))); return $query->executeQuery()->fetchAssociative(); } /** * Fetching raw user record with username=$name * * @param string $name The username to look up. * @return array user record or FALSE * @see \TYPO3\CMS\Core\Authentication\AbstractUserAuthentication::getUserByUid() * @internal */ public function getRawUserByName($name) { $query = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($this->user_table); $query->setRestrictions($this->userConstraints()); $query->select('*') ->from($this->user_table) ->where($query->expr()->eq($this->username_column, $query->createNamedParameter($name))); return $query->executeQuery()->fetchAssociative(); } public function getUserId(): ?int { if (isset($this->user[$this->userid_column])) { return (int)$this->user[$this->userid_column]; } return null; } public function getUserName(): ?string { if (isset($this->user[$this->username_column])) { return (string)$this->user[$this->username_column]; } return null; } public function getSession(): UserSession { return $this->userSession; } }