‪TYPO3CMS  10.4
AuthenticationService.php
Go to the documentation of this file.
1 <?php
2 
3 /*
4  * This file is part of the TYPO3 CMS project.
5  *
6  * It is free software; you can redistribute it and/or modify it under
7  * the terms of the GNU General Public License, either version 2
8  * of the License, or any later version.
9  *
10  * For the full copyright and license information, please read the
11  * LICENSE.txt file that was distributed with this source code.
12  *
13  * The TYPO3 project - inspiring people to share!
14  */
15 
17 
23 use ‪TYPO3\CMS\Core\SysLog\Action\Login as SystemLogLoginAction;
24 use ‪TYPO3\CMS\Core\SysLog\Error as SystemLogErrorClassification;
25 use ‪TYPO3\CMS\Core\SysLog\Type as SystemLogType;
28 
33 {
42  public function ‪processLoginData(array &$loginData, $passwordTransmissionStrategy)
43  {
44  $isProcessed = false;
45  if ($passwordTransmissionStrategy === 'normal') {
46  $loginData = array_map('trim', $loginData);
47  $loginData['uident_text'] = $loginData['uident'];
48  $isProcessed = true;
49  }
50  return $isProcessed;
51  }
52 
58  public function ‪getUser()
59  {
60  if ($this->login['status'] !== ‪LoginType::LOGIN) {
61  return false;
62  }
63  if ((string)$this->login['uident_text'] === '') {
64  // Failed Login attempt (no password given)
65  $this->‪writelog(SystemLogType::LOGIN, SystemLogLoginAction::ATTEMPT, SystemLogErrorClassification::SECURITY_NOTICE, 2, 'Login-attempt from ###IP### for username \'%s\' with an empty password!', [
66  $this->login['uname']
67  ]);
68  $this->logger->warning(sprintf('Login-attempt from %s, for username \'%s\' with an empty password!', $this->authInfo['REMOTE_ADDR'], $this->login['uname']));
69  return false;
70  }
71 
72  $user = $this->‪fetchUserRecord($this->login['uname']);
73  if (!is_array($user)) {
74  // Failed login attempt (no username found)
75  $this->‪writelog(SystemLogType::LOGIN, SystemLogLoginAction::ATTEMPT, SystemLogErrorClassification::SECURITY_NOTICE, 2, 'Login-attempt from ###IP###, username \'%s\' not found!!', [$this->login['uname']]);
76  $this->logger->info('Login-attempt from username \'' . $this->login['uname'] . '\' not found!', [
77  'REMOTE_ADDR' => $this->authInfo['REMOTE_ADDR']
78  ]);
79  } else {
80  $this->logger->debug('User found', [
81  $this->db_user['userid_column'] => $user[$this->db_user['userid_column']],
82  $this->db_user['username_column'] => $user[$this->db_user['username_column']]
83  ]);
84  }
85  return $user;
86  }
87 
101  public function authUser(array $user): int
102  {
103  // Early 100 "not responsible, check other services" if username or password is empty
104  if (!isset($this->login['uident_text']) || (string)$this->login['uident_text'] === ''
105  || !isset($this->login['uname']) || (string)$this->login['uname'] === '') {
106  return 100;
107  }
108 
109  if (empty($this->db_user['table'])) {
110  throw new \RuntimeException('User database table not set', 1533159150);
111  }
112 
113  $submittedUsername = (string)$this->login['uname'];
114  $submittedPassword = (string)$this->login['uident_text'];
115  $passwordHashInDatabase = $user['password'];
116  $queriedDomain = $this->authInfo['HTTP_HOST'];
117  $configuredDomainLock = $user['lockToDomain'];
118  $userDatabaseTable = $this->db_user['table'];
119 
120  $isReHashNeeded = false;
121  $isDomainLockMet = false;
122 
123  $saltFactory = GeneralUtility::makeInstance(PasswordHashFactory::class);
124 
125  // Get a hashed password instance for the hash stored in db of this user
126  $invalidPasswordHashException = null;
127  try {
128  $hashInstance = $saltFactory->get($passwordHashInDatabase, $this->pObj->loginType);
129  } catch (InvalidPasswordHashException $invalidPasswordHashException) {
130  // Could not find a responsible hash algorithm for given password. This is unusual since other
131  // authentication services would usually be called before this one with higher priority. We thus log
132  // the failed login but still return '100' to proceed with other services that may follow.
133  $message = 'Login-attempt from ###IP###, username \'%s\', no suitable hash method found!';
134  $this->‪writeLogMessage($message, $submittedUsername);
135  $this->‪writelog(SystemLogType::LOGIN, SystemLogLoginAction::ATTEMPT, SystemLogErrorClassification::SECURITY_NOTICE, 1, $message, [$submittedUsername]);
136  // Not responsible, check other services
137  return 100;
138  }
139 
140  // An instance of the currently configured salted password mechanism
141  // Don't catch InvalidPasswordHashException here: Only install tool should handle those configuration failures
142  $defaultHashInstance = $saltFactory->getDefaultHashInstance($this->pObj->loginType);
143 
144  // We found a hash class that can handle this type of hash
145  $isValidPassword = $hashInstance->checkPassword($submittedPassword, $passwordHashInDatabase);
146  if ($isValidPassword) {
147  if ($hashInstance->isHashUpdateNeeded($passwordHashInDatabase)
148  || $defaultHashInstance != $hashInstance
149  ) {
150  // Lax object comparison intended: Rehash if old and new salt objects are not
151  // instances of the same class.
152  $isReHashNeeded = true;
153  }
154  if (empty($configuredDomainLock)) {
155  // No domain restriction set for user in db. This is ok.
156  $isDomainLockMet = true;
157  } elseif (!strcasecmp($configuredDomainLock, $queriedDomain)) {
158  // Domain restriction set and it matches given host. Ok.
159  $isDomainLockMet = true;
160  }
161  }
162 
163  if (!$isValidPassword) {
164  // Failed login attempt - wrong password
165  $message = 'Login-attempt from ###IP###, username \'%s\', password not accepted!';
166  $this->‪writeLogMessage($message, $submittedUsername);
167  $this->‪writelog(SystemLogType::LOGIN, SystemLogLoginAction::ATTEMPT, SystemLogErrorClassification::SECURITY_NOTICE, 1, $message, [$submittedUsername]);
168  // Responsible, authentication failed, do NOT check other services
169  return 0;
170  }
171 
172  if (!$isDomainLockMet) {
173  // Password ok, but configured domain lock not met
174  $errorMessage = 'Login-attempt from ###IP###, username \'%s\', locked domain \'%s\' did not match \'%s\'!';
175  $this->‪writeLogMessage($errorMessage, $user[$this->db_user['username_column']], $configuredDomainLock, $queriedDomain);
176  $this->‪writelog(SystemLogType::LOGIN, SystemLogLoginAction::ATTEMPT, SystemLogErrorClassification::SECURITY_NOTICE, 1, $errorMessage, [$user[$this->db_user['username_column']], $configuredDomainLock, $queriedDomain]);
177  $this->logger->info(sprintf($errorMessage, $user[$this->db_user['username_column']], $configuredDomainLock, $queriedDomain));
178  // Responsible, authentication ok, but domain lock not ok, do NOT check other services
179  return 0;
180  }
181 
182  if ($isReHashNeeded) {
183  // Given password validated but a re-hash is needed. Do so.
185  $userDatabaseTable,
186  (int)$user['uid'],
187  $defaultHashInstance->getHashedPassword($submittedPassword)
188  );
189  }
190 
191  // Responsible, authentication ok, domain lock ok. Log successful login and return 'auth ok, do NOT check other services'
192  $this->‪writeLogMessage($this->pObj->loginType . ' Authentication successful for username \'%s\'', $submittedUsername);
193  return 200;
194  }
195 
200  public function ‪mimicAuthUser(): bool
201  {
202  try {
203  $hashFactory = GeneralUtility::makeInstance(PasswordHashFactory::class);
204  $defaultHashInstance = $hashFactory->getDefaultHashInstance($this->pObj->loginType);
205  $defaultHashInstance->getHashedPassword(random_bytes(10));
206  } catch (\‪Exception $exception) {
207  // no further processing here
208  }
209  return false;
210  }
211 
219  public function ‪getGroups($user, $knownGroups)
220  {
221  // Attention: $knownGroups is not used within this method, but other services can use it.
222  // This parameter should not be removed!
223  // The FrontendUserAuthentication call getGroups and handover the previous detected groups.
224  $groupDataArr = [];
225  if ($this->mode === 'getGroupsFE') {
226  $groups = [];
227  if ($user[$this->db_user['usergroup_column']] ?? false) {
228  $groupList = $user[$this->db_user['usergroup_column']];
229  $groups = [];
230  $this->‪getSubGroups($groupList, '', $groups);
231  }
232  // ADD group-numbers if the IPmask matches.
233  foreach (‪$GLOBALS['TYPO3_CONF_VARS']['FE']['IPmaskMountGroups'] ?? [] as $IPel) {
234  if ($this->authInfo['REMOTE_ADDR'] && $IPel[0] && GeneralUtility::cmpIP($this->authInfo['REMOTE_ADDR'], $IPel[0])) {
235  $groups[] = (int)$IPel[1];
236  }
237  }
238  $groups = array_unique($groups);
239  if (!empty($groups)) {
240  $this->logger->debug('Get usergroups with id: ' . implode(',', $groups));
241  $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
242  ->getQueryBuilderForTable($this->db_groups['table']);
243  if (!empty($this->authInfo['showHiddenRecords'])) {
244  $queryBuilder->getRestrictions()->removeByType(HiddenRestriction::class);
245  }
246 
247  $res = $queryBuilder->select('*')
248  ->from($this->db_groups['table'])
249  ->where(
250  $queryBuilder->expr()->in(
251  'uid',
252  $queryBuilder->createNamedParameter($groups, Connection::PARAM_INT_ARRAY)
253  ),
254  $queryBuilder->expr()->orX(
255  $queryBuilder->expr()->eq(
256  'lockToDomain',
257  $queryBuilder->createNamedParameter('', \PDO::PARAM_STR)
258  ),
259  $queryBuilder->expr()->isNull('lockToDomain'),
260  $queryBuilder->expr()->eq(
261  'lockToDomain',
262  $queryBuilder->createNamedParameter($this->authInfo['HTTP_HOST'], \PDO::PARAM_STR)
263  )
264  )
265  )
266  ->execute();
267 
268  while ($row = $res->fetch()) {
269  $groupDataArr[$row['uid']] = $row;
270  }
271  } else {
272  $this->logger->debug('No usergroups found.');
273  }
274  }
275  return $groupDataArr;
276  }
277 
288  public function ‪getSubGroups($grList, $idList, &$groups)
289  {
290  // Fetching records of the groups in $grList (which are not blocked by lockedToDomain either):
291  $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('fe_groups');
292  if (!empty($this->authInfo['showHiddenRecords'])) {
293  $queryBuilder->getRestrictions()->removeByType(HiddenRestriction::class);
294  }
295 
296  $res = $queryBuilder
297  ->select('uid', 'subgroup')
298  ->from($this->db_groups['table'])
299  ->where(
300  $queryBuilder->expr()->in(
301  'uid',
302  $queryBuilder->createNamedParameter(
303  ‪GeneralUtility::intExplode(',', $grList, true),
304  Connection::PARAM_INT_ARRAY
305  )
306  ),
307  $queryBuilder->expr()->orX(
308  $queryBuilder->expr()->eq(
309  'lockToDomain',
310  $queryBuilder->createNamedParameter('', \PDO::PARAM_STR)
311  ),
312  $queryBuilder->expr()->isNull('lockToDomain'),
313  $queryBuilder->expr()->eq(
314  'lockToDomain',
315  $queryBuilder->createNamedParameter($this->authInfo['HTTP_HOST'], \PDO::PARAM_STR)
316  )
317  )
318  )
319  ->execute();
320 
321  // Internal group record storage
322  $groupRows = [];
323  // The groups array is filled
324  while ($row = $res->fetch()) {
325  if (!in_array($row['uid'], $groups)) {
326  $groups[] = $row['uid'];
327  }
328  $groupRows[$row['uid']] = $row;
329  }
330  // Traversing records in the correct order
331  $include_staticArr = ‪GeneralUtility::intExplode(',', $grList);
332  // traversing list
333  foreach ($include_staticArr as $uid) {
334  // Get row:
335  $row = $groupRows[$uid];
336  // Must be an array and $uid should not be in the idList, because then it is somewhere previously in the grouplist
337  if (is_array($row) && !GeneralUtility::inList($idList, (string)$uid) && trim($row['subgroup'])) {
338  // Make integer list
339  $theList = implode(',', ‪GeneralUtility::intExplode(',', $row['subgroup']));
340  // Call recursively, pass along list of already processed groups so they are not processed again.
341  $this->‪getSubGroups($theList, $idList . ',' . $uid, $groups);
342  }
343  }
344  }
345 
353  protected function ‪updatePasswordHashInDatabase(string $table, int $uid, string $newPassword): void
354  {
355  $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($table);
356  $connection->update(
357  $table,
358  ['password' => $newPassword],
359  ['uid' => $uid]
360  );
361  $this->logger->notice('Automatic password update for user record in ' . $table . ' with uid ' . $uid);
362  }
363 
374  protected function ‪writeLogMessage(string $message, ...$params): void
375  {
376  if (!empty($params)) {
377  $message = vsprintf($message, $params);
378  }
379  $message = str_replace('###IP###', (string)GeneralUtility::getIndpEnv('REMOTE_ADDR'), $message);
380  if ($this->pObj->loginType === 'FE') {
381  $timeTracker = GeneralUtility::makeInstance(TimeTracker::class);
382  $timeTracker->setTSlogMessage($message);
383  }
384  $this->logger->notice($message);
385  }
386 }
‪TYPO3\CMS\Core\Database\Query\Restriction\HiddenRestriction
Definition: HiddenRestriction.php:27
‪TYPO3\CMS\Core\Crypto\PasswordHashing\PasswordHashFactory
Definition: PasswordHashFactory.php:27
‪TYPO3\CMS\Core\Authentication\MimicServiceInterface
Definition: MimicServiceInterface.php:21
‪TYPO3\CMS\Core\Authentication\AuthenticationService\writeLogMessage
‪writeLogMessage(string $message,... $params)
Definition: AuthenticationService.php:374
‪TYPO3\CMS\Core\Exception
Definition: Exception.php:22
‪TYPO3\CMS\Core\Authentication\AbstractAuthenticationService\fetchUserRecord
‪mixed fetchUserRecord($username, $extraWhere='', $dbUserSetup='')
Definition: AbstractAuthenticationService.php:126
‪TYPO3\CMS\Core\Authentication
Definition: AbstractAuthenticationService.php:16
‪TYPO3\CMS\Core\Crypto\PasswordHashing\InvalidPasswordHashException
Definition: InvalidPasswordHashException.php:26
‪TYPO3\CMS\Core\Authentication\AuthenticationService\mimicAuthUser
‪mimicAuthUser()
Definition: AuthenticationService.php:200
‪TYPO3\CMS\Core\Authentication\AbstractAuthenticationService\writelog
‪writelog($type, $action, $error, $details_nr, $details, $data, $tablename='', $recuid='', $recpid='')
Definition: AbstractAuthenticationService.php:111
‪TYPO3\CMS\Core\Authentication\AuthenticationService\getSubGroups
‪getSubGroups($grList, $idList, &$groups)
Definition: AuthenticationService.php:288
‪TYPO3\CMS\Core\Authentication\AbstractAuthenticationService
Definition: AbstractAuthenticationService.php:29
‪TYPO3\CMS\Core\SysLog\Action\Login
Definition: Login.php:24
‪TYPO3\CMS\Core\SysLog\Error
Definition: Error.php:24
‪TYPO3\CMS\Core\Authentication\AuthenticationService\getGroups
‪mixed getGroups($user, $knownGroups)
Definition: AuthenticationService.php:219
‪TYPO3\CMS\Core\Authentication\AuthenticationService
Definition: AuthenticationService.php:33
‪TYPO3\CMS\Core\Authentication\LoginType\LOGIN
‪const LOGIN
Definition: LoginType.php:30
‪TYPO3\CMS\Core\Authentication\AuthenticationService\getUser
‪mixed getUser()
Definition: AuthenticationService.php:58
‪TYPO3\CMS\Core\Database\Connection
Definition: Connection.php:36
‪TYPO3\CMS\Core\Authentication\AuthenticationService\updatePasswordHashInDatabase
‪updatePasswordHashInDatabase(string $table, int $uid, string $newPassword)
Definition: AuthenticationService.php:353
‪$GLOBALS
‪$GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['adminpanel']['modules']
Definition: ext_localconf.php:5
‪TYPO3\CMS\Core\Utility\GeneralUtility\intExplode
‪static int[] intExplode($delimiter, $string, $removeEmptyValues=false, $limit=0)
Definition: GeneralUtility.php:988
‪TYPO3\CMS\Core\Authentication\AuthenticationService\processLoginData
‪bool processLoginData(array &$loginData, $passwordTransmissionStrategy)
Definition: AuthenticationService.php:42
‪TYPO3\CMS\Core\Database\ConnectionPool
Definition: ConnectionPool.php:46
‪TYPO3\CMS\Core\Utility\GeneralUtility
Definition: GeneralUtility.php:46
‪TYPO3\CMS\Core\TimeTracker\TimeTracker
Definition: TimeTracker.php:30
‪TYPO3\CMS\Core\SysLog\Type
Definition: Type.php:24