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