TYPO3 v15 dev-main snapshot ()
This commit is contained in:
@@ -0,0 +1,232 @@
|
||||
<?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 Psr\Log\LogLevel;
|
||||
use TYPO3\CMS\Core\Crypto\PasswordHashing\InvalidPasswordHashException;
|
||||
use TYPO3\CMS\Core\Crypto\PasswordHashing\PasswordHashFactory;
|
||||
use TYPO3\CMS\Core\Database\ConnectionPool;
|
||||
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 implements MimicServiceInterface
|
||||
{
|
||||
/**
|
||||
* Process the submitted credentials. Returns true, if loginData was processed successfully, else false.
|
||||
* In addition, extensions overwriting this method or implement the context related methods `processLoginDataFE`
|
||||
* or `processLoginDataBE` may also return an int (e.g. >= 200) in order to stop loginData processing of other
|
||||
* services in the authentication service chain.
|
||||
*
|
||||
* @param array $loginData Credentials that are submitted and potentially modified by other services
|
||||
*/
|
||||
public function processLoginData(array &$loginData): bool|int
|
||||
{
|
||||
$loginData = array_map(trim(...), $loginData);
|
||||
$loginData['uident_text'] = $loginData['uident'];
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a user (eg. look up the user record in database when a login is sent)
|
||||
*
|
||||
* @return array<string, mixed>|false User array or FALSE
|
||||
*/
|
||||
public function getUser()
|
||||
{
|
||||
if (LoginType::tryFrom($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, null, 'Login-attempt from ###IP### for username \'%s\' with an empty password!', [
|
||||
$this->login['uname'],
|
||||
]);
|
||||
$this->logger->warning('Login-attempt from {ip}, for username "{username}" with an empty password!', [
|
||||
'ip' => $this->authInfo['REMOTE_ADDR'],
|
||||
'username' => $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, null, 'Login-attempt from ###IP###, username \'%s\' not found!', [$this->login['uname']]);
|
||||
$this->logger->info('Login-attempt from username "{username}" not found!', [
|
||||
'username' => $this->login['uname'],
|
||||
'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.
|
||||
*
|
||||
* 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<string, mixed> $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'];
|
||||
$userDatabaseTable = $this->db_user['table'];
|
||||
|
||||
$isReHashNeeded = false;
|
||||
|
||||
$saltFactory = GeneralUtility::makeInstance(PasswordHashFactory::class);
|
||||
|
||||
// Get a hashed password instance for the hash stored in db of this user
|
||||
try {
|
||||
$hashInstance = $saltFactory->get($passwordHashInDatabase, $this->pObj->loginType);
|
||||
} catch (InvalidPasswordHashException $exception) {
|
||||
// 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, null, $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 (!$isValidPassword) {
|
||||
// Failed login attempt - wrong password
|
||||
$message = 'Login-attempt from ###IP###, username \'%s\', password not accepted!';
|
||||
$this->writeLogMessage($message, $submittedUsername);
|
||||
$this->writelog(SystemLogType::LOGIN, SystemLogLoginAction::ATTEMPT, SystemLogErrorClassification::SECURITY_NOTICE, null, $message, [$submittedUsername]);
|
||||
// Responsible, authentication failed, 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. 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mimics password hashing for invalid authentication requests to mitigate
|
||||
* @link https://cwe.mitre.org/data/definitions/208.html: CWE-208: Observable Timing Discrepancy
|
||||
*/
|
||||
public function mimicAuthUser(): bool
|
||||
{
|
||||
try {
|
||||
$hashFactory = GeneralUtility::makeInstance(PasswordHashFactory::class);
|
||||
$defaultHashInstance = $hashFactory->getDefaultHashInstance($this->pObj->loginType);
|
||||
$defaultHashInstance->getHashedPassword(random_bytes(10));
|
||||
} catch (\Exception) {
|
||||
// no further processing here
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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}', [
|
||||
'table' => $table,
|
||||
'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()
|
||||
* If a marker ###IP### is present in the message, it is automatically replaced with the REMOTE_ADDR
|
||||
*
|
||||
* @param string $message Message to output
|
||||
* @param array<int,string> $params
|
||||
*/
|
||||
protected function writeLogMessage(string $message, ...$params): void
|
||||
{
|
||||
if (!empty($params)) {
|
||||
$message = vsprintf($message, $params);
|
||||
}
|
||||
$message = str_replace('###IP###', (string)($this->authInfo['REMOTE_ADDR'] ?? ''), $message);
|
||||
if ($this->pObj->loginType === 'FE') {
|
||||
$timeTracker = GeneralUtility::makeInstance(TimeTracker::class);
|
||||
$timeTracker->setTSlogMessage($message, LogLevel::INFO);
|
||||
}
|
||||
$this->logger->notice($message);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user