= 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|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 $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 $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); } }