-
Notifications
You must be signed in to change notification settings - Fork 638
/
AuthenticationService.php
370 lines (344 loc) · 17.2 KB
/
AuthenticationService.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
<?php
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Core\Authentication;
use TYPO3\CMS\Core\Crypto\PasswordHashing\InvalidPasswordHashException;
use TYPO3\CMS\Core\Crypto\PasswordHashing\PasswordHashFactory;
use TYPO3\CMS\Core\Database\Connection;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Database\Query\Restriction\HiddenRestriction;
use TYPO3\CMS\Core\SysLog\Action\Login as SystemLogLoginAction;
use TYPO3\CMS\Core\SysLog\Error as SystemLogErrorClassification;
use TYPO3\CMS\Core\SysLog\Type as SystemLogType;
use TYPO3\CMS\Core\TimeTracker\TimeTracker;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Authentication services class
*/
class AuthenticationService extends AbstractAuthenticationService
{
/**
* Process the submitted credentials.
* In this case hash the clear text password if it has been submitted.
*
* @param array $loginData Credentials that are submitted and potentially modified by other services
* @param string $passwordTransmissionStrategy Keyword of how the password has been hashed or encrypted before submission
* @return bool
*/
public function processLoginData(array &$loginData, $passwordTransmissionStrategy)
{
$isProcessed = false;
if ($passwordTransmissionStrategy === 'normal') {
$loginData = array_map('trim', $loginData);
$loginData['uident_text'] = $loginData['uident'];
$isProcessed = true;
}
return $isProcessed;
}
/**
* Find a user (eg. look up the user record in database when a login is sent)
*
* @return mixed User array or FALSE
*/
public function getUser()
{
if ($this->login['status'] !== LoginType::LOGIN) {
return false;
}
if ((string)$this->login['uident_text'] === '') {
// Failed Login attempt (no password given)
$this->writelog(SystemLogType::LOGIN, SystemLogLoginAction::ATTEMPT, SystemLogErrorClassification::SECURITY_NOTICE, 2, 'Login-attempt from ###IP### for username \'%s\' with an empty password!', [
$this->login['uname']
]);
$this->logger->warning(sprintf('Login-attempt from %s, for username \'%s\' with an empty password!', $this->authInfo['REMOTE_ADDR'], $this->login['uname']));
return false;
}
$user = $this->fetchUserRecord($this->login['uname']);
if (!is_array($user)) {
// Failed login attempt (no username found)
$this->writelog(SystemLogType::LOGIN, SystemLogLoginAction::ATTEMPT, SystemLogErrorClassification::SECURITY_NOTICE, 2, 'Login-attempt from ###IP###, username \'%s\' not found!!', [$this->login['uname']]);
$this->logger->info('Login-attempt from username \'' . $this->login['uname'] . '\' not found!', [
'REMOTE_ADDR' => $this->authInfo['REMOTE_ADDR']
]);
} else {
$this->logger->debug('User found', [
$this->db_user['userid_column'] => $user[$this->db_user['userid_column']],
$this->db_user['username_column'] => $user[$this->db_user['username_column']]
]);
}
return $user;
}
/**
* Authenticate a user: Check submitted user credentials against stored hashed password,
* check domain lock if configured.
*
* Returns one of the following status codes:
* >= 200: User authenticated successfully. No more checking is needed by other auth services.
* >= 100: User not authenticated; this service is not responsible. Other auth services will be asked.
* > 0: User authenticated successfully. Other auth services will still be asked.
* <= 0: Authentication failed, no more checking needed by other auth services.
*
* @param array $user User data
* @return int Authentication status code, one of 0, 100, 200
*/
public function authUser(array $user): int
{
// Early 100 "not responsible, check other services" if username or password is empty
if (!isset($this->login['uident_text']) || (string)$this->login['uident_text'] === ''
|| !isset($this->login['uname']) || (string)$this->login['uname'] === '') {
return 100;
}
if (empty($this->db_user['table'])) {
throw new \RuntimeException('User database table not set', 1533159150);
}
$submittedUsername = (string)$this->login['uname'];
$submittedPassword = (string)$this->login['uident_text'];
$passwordHashInDatabase = $user['password'];
$queriedDomain = $this->authInfo['HTTP_HOST'];
$configuredDomainLock = $user['lockToDomain'];
$userDatabaseTable = $this->db_user['table'];
$isReHashNeeded = false;
$isDomainLockMet = false;
$saltFactory = GeneralUtility::makeInstance(PasswordHashFactory::class);
// Get a hashed password instance for the hash stored in db of this user
$invalidPasswordHashException = null;
try {
$hashInstance = $saltFactory->get($passwordHashInDatabase, $this->pObj->loginType);
} catch (InvalidPasswordHashException $invalidPasswordHashException) {
// Could not find a responsible hash algorithm for given password. This is unusual since other
// authentication services would usually be called before this one with higher priority. We thus log
// the failed login but still return '100' to proceed with other services that may follow.
$message = 'Login-attempt from ###IP###, username \'%s\', no suitable hash method found!';
$this->writeLogMessage($message, $submittedUsername);
$this->writelog(SystemLogType::LOGIN, SystemLogLoginAction::ATTEMPT, SystemLogErrorClassification::SECURITY_NOTICE, 1, $message, [$submittedUsername]);
$this->logger->info(sprintf($message, $submittedUsername));
// Not responsible, check other services
return 100;
}
// An instance of the currently configured salted password mechanism
// Don't catch InvalidPasswordHashException here: Only install tool should handle those configuration failures
$defaultHashInstance = $saltFactory->getDefaultHashInstance($this->pObj->loginType);
// We found a hash class that can handle this type of hash
$isValidPassword = $hashInstance->checkPassword($submittedPassword, $passwordHashInDatabase);
if ($isValidPassword) {
if ($hashInstance->isHashUpdateNeeded($passwordHashInDatabase)
|| $defaultHashInstance != $hashInstance
) {
// Lax object comparison intended: Rehash if old and new salt objects are not
// instances of the same class.
$isReHashNeeded = true;
}
if (empty($configuredDomainLock)) {
// No domain restriction set for user in db. This is ok.
$isDomainLockMet = true;
} elseif (!strcasecmp($configuredDomainLock, $queriedDomain)) {
// Domain restriction set and it matches given host. Ok.
$isDomainLockMet = true;
}
}
if (!$isValidPassword) {
// Failed login attempt - wrong password
$this->writeLogMessage($this->pObj->loginType . ' Authentication failed - wrong password for username \'%s\'', $submittedUsername);
$message = 'Login-attempt from ###IP###, username \'%s\', password not accepted!';
$this->writelog(SystemLogType::LOGIN, SystemLogLoginAction::ATTEMPT, SystemLogErrorClassification::SECURITY_NOTICE, 1, $message, [$submittedUsername]);
$this->logger->info(sprintf($message, $submittedUsername));
// Responsible, authentication failed, do NOT check other services
return 0;
}
if (!$isDomainLockMet) {
// Password ok, but configured domain lock not met
$errorMessage = 'Login-attempt from ###IP###, username \'%s\', locked domain \'%s\' did not match \'%s\'!';
$this->writeLogMessage($errorMessage, $user[$this->db_user['username_column']], $configuredDomainLock, $queriedDomain);
$this->writelog(SystemLogType::LOGIN, SystemLogLoginAction::ATTEMPT, SystemLogErrorClassification::SECURITY_NOTICE, 1, $errorMessage, [$user[$this->db_user['username_column']], $configuredDomainLock, $queriedDomain]);
$this->logger->info(sprintf($errorMessage, $user[$this->db_user['username_column']], $configuredDomainLock, $queriedDomain));
// Responsible, authentication ok, but domain lock not ok, do NOT check other services
return 0;
}
if ($isReHashNeeded) {
// Given password validated but a re-hash is needed. Do so.
$this->updatePasswordHashInDatabase(
$userDatabaseTable,
(int)$user['uid'],
$defaultHashInstance->getHashedPassword($submittedPassword)
);
}
// Responsible, authentication ok, domain lock ok. Log successful login and return 'auth ok, do NOT check other services'
$this->writeLogMessage($this->pObj->loginType . ' Authentication successful for username \'%s\'', $submittedUsername);
return 200;
}
/**
* Find usergroup records, currently only for frontend
*
* @param array $user Data of user.
* @param array $knownGroups Group data array of already known groups. This is handy if you want select other related groups. Keys in this array are unique IDs of those groups.
* @return mixed Groups array, keys = uid which must be unique
*/
public function getGroups($user, $knownGroups)
{
// Attention: $knownGroups is not used within this method, but other services can use it.
// This parameter should not be removed!
// The FrontendUserAuthentication call getGroups and handover the previous detected groups.
$groupDataArr = [];
if ($this->mode === 'getGroupsFE') {
$groups = [];
if ($user[$this->db_user['usergroup_column']] ?? false) {
$groupList = $user[$this->db_user['usergroup_column']];
$groups = [];
$this->getSubGroups($groupList, '', $groups);
}
// ADD group-numbers if the IPmask matches.
foreach ($GLOBALS['TYPO3_CONF_VARS']['FE']['IPmaskMountGroups'] ?? [] as $IPel) {
if ($this->authInfo['REMOTE_ADDR'] && $IPel[0] && GeneralUtility::cmpIP($this->authInfo['REMOTE_ADDR'], $IPel[0])) {
$groups[] = (int)$IPel[1];
}
}
$groups = array_unique($groups);
if (!empty($groups)) {
$this->logger->debug('Get usergroups with id: ' . implode(',', $groups));
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
->getQueryBuilderForTable($this->db_groups['table']);
if (!empty($this->authInfo['showHiddenRecords'])) {
$queryBuilder->getRestrictions()->removeByType(HiddenRestriction::class);
}
$res = $queryBuilder->select('*')
->from($this->db_groups['table'])
->where(
$queryBuilder->expr()->in(
'uid',
$queryBuilder->createNamedParameter($groups, Connection::PARAM_INT_ARRAY)
),
$queryBuilder->expr()->orX(
$queryBuilder->expr()->eq(
'lockToDomain',
$queryBuilder->createNamedParameter('', \PDO::PARAM_STR)
),
$queryBuilder->expr()->isNull('lockToDomain'),
$queryBuilder->expr()->eq(
'lockToDomain',
$queryBuilder->createNamedParameter($this->authInfo['HTTP_HOST'], \PDO::PARAM_STR)
)
)
)
->execute();
while ($row = $res->fetch()) {
$groupDataArr[$row['uid']] = $row;
}
} else {
$this->logger->debug('No usergroups found.');
}
}
return $groupDataArr;
}
/**
* Fetches subgroups of groups. Function is called recursively for each subgroup.
* Function was previously copied from
* \TYPO3\CMS\Core\Authentication\BackendUserAuthentication->fetchGroups and has been slightly modified.
*
* @param string $grList Commalist of fe_groups uid numbers
* @param string $idList List of already processed fe_groups-uids so the function will not fall into an eternal recursion.
* @param array $groups
* @internal
*/
public function getSubGroups($grList, $idList, &$groups)
{
// Fetching records of the groups in $grList (which are not blocked by lockedToDomain either):
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('fe_groups');
if (!empty($this->authInfo['showHiddenRecords'])) {
$queryBuilder->getRestrictions()->removeByType(HiddenRestriction::class);
}
$res = $queryBuilder
->select('uid', 'subgroup')
->from($this->db_groups['table'])
->where(
$queryBuilder->expr()->in(
'uid',
$queryBuilder->createNamedParameter(
GeneralUtility::intExplode(',', $grList, true),
Connection::PARAM_INT_ARRAY
)
),
$queryBuilder->expr()->orX(
$queryBuilder->expr()->eq(
'lockToDomain',
$queryBuilder->createNamedParameter('', \PDO::PARAM_STR)
),
$queryBuilder->expr()->isNull('lockToDomain'),
$queryBuilder->expr()->eq(
'lockToDomain',
$queryBuilder->createNamedParameter($this->authInfo['HTTP_HOST'], \PDO::PARAM_STR)
)
)
)
->execute();
// Internal group record storage
$groupRows = [];
// The groups array is filled
while ($row = $res->fetch()) {
if (!in_array($row['uid'], $groups)) {
$groups[] = $row['uid'];
}
$groupRows[$row['uid']] = $row;
}
// Traversing records in the correct order
$include_staticArr = GeneralUtility::intExplode(',', $grList);
// traversing list
foreach ($include_staticArr as $uid) {
// Get row:
$row = $groupRows[$uid];
// Must be an array and $uid should not be in the idList, because then it is somewhere previously in the grouplist
if (is_array($row) && !GeneralUtility::inList($idList, (string)$uid) && trim($row['subgroup'])) {
// Make integer list
$theList = implode(',', GeneralUtility::intExplode(',', $row['subgroup']));
// Call recursively, pass along list of already processed groups so they are not processed again.
$this->getSubGroups($theList, $idList . ',' . $uid, $groups);
}
}
}
/**
* Method updates a FE/BE user record - in this case a new password string will be set.
*
* @param string $table Database table of this user, usually 'be_users' or 'fe_users'
* @param int $uid uid of user record that will be updated
* @param string $newPassword Field values as key=>value pairs to be updated in database
*/
protected function updatePasswordHashInDatabase(string $table, int $uid, string $newPassword): void
{
$connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($table);
$connection->update(
$table,
['password' => $newPassword],
['uid' => $uid]
);
$this->logger->notice('Automatic password update for user record in ' . $table . ' with uid ' . $uid);
}
/**
* Writes log message. Destination log depends on the current system mode.
*
* This function accepts variable number of arguments and can format
* parameters. The syntax is the same as for sprintf()
*
* @param string $message Message to output
* @param array<int,mixed> $params
*/
protected function writeLogMessage(string $message, ...$params): void
{
if (!empty($params)) {
$message = vsprintf($message, $params);
}
if ($this->pObj->loginType === 'FE') {
$timeTracker = GeneralUtility::makeInstance(TimeTracker::class);
$timeTracker->setTSlogMessage($message);
}
$this->logger->notice($message);
}
}