[TASK] Drop salted passwords configuration options
[Packages/TYPO3.CMS.git] / typo3 / sysext / saltedpasswords / Classes / SaltedPasswordService.php
1 <?php
2 namespace TYPO3\CMS\Saltedpasswords;
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
17 use TYPO3\CMS\Core\Authentication\AbstractAuthenticationService;
18 use TYPO3\CMS\Core\Database\ConnectionPool;
19 use TYPO3\CMS\Core\TimeTracker\TimeTracker;
20 use TYPO3\CMS\Core\Utility\GeneralUtility;
21 use TYPO3\CMS\Saltedpasswords\Salt\SaltFactory;
22
23 /**
24 * Class implements salted-password hashes authentication service.
25 * Contains authentication service class for salted hashed passwords.
26 */
27 class SaltedPasswordService extends AbstractAuthenticationService
28 {
29 /**
30 * Keeps class name.
31 *
32 * @var string
33 */
34 public $prefixId = 'tx_saltedpasswords_sv1';
35
36 /**
37 * Keeps extension key.
38 *
39 * @var string
40 */
41 public $extKey = 'saltedpasswords';
42
43 /**
44 * Keeps extension configuration.
45 *
46 * @var mixed
47 */
48 protected $extConf;
49
50 /**
51 * An instance of the salted hashing method.
52 * This member is set in the getSaltingInstance() function.
53 *
54 * @var \TYPO3\CMS\Saltedpasswords\Salt\SaltInterface
55 */
56 protected $objInstanceSaltedPW;
57
58 /**
59 * Indicates whether the salted password authentication has failed.
60 *
61 * Prevents authentication bypass. See vulnerability report:
62 * { @link http://forge.typo3.org/issues/22030 }
63 *
64 * @var bool
65 */
66 protected $authenticationFailed = false;
67
68 /**
69 * Set salted passwords extension configuration to $this->extConf
70 *
71 * @return bool TRUE
72 */
73 public function init()
74 {
75 $this->extConf = Utility\SaltedPasswordsUtility::returnExtConf();
76 parent::init();
77 return true;
78 }
79
80 /**
81 * Checks the login data with the user record data for builtin login method.
82 *
83 * @param array $user User data array
84 * @param array $loginData Login data array
85 * @param string $passwordCompareStrategy Password compare strategy
86 * @return bool TRUE if login data matched
87 */
88 public function compareUident(array $user, array $loginData, $passwordCompareStrategy = '')
89 {
90 $validPasswd = false;
91 $password = $loginData['uident_text'];
92 // Determine method used for given salted hashed password
93 $this->objInstanceSaltedPW = SaltFactory::getSaltingInstance($user['password']);
94 // Existing record is in format of Salted Hash password
95 if (is_object($this->objInstanceSaltedPW)) {
96 $validPasswd = $this->objInstanceSaltedPW->checkPassword($password, $user['password']);
97 // Record is in format of Salted Hash password but authentication failed
98 // skip further authentication methods
99 if (!$validPasswd) {
100 $this->authenticationFailed = true;
101 }
102 $defaultHashingClassName = Utility\SaltedPasswordsUtility::getDefaultSaltingHashingMethod();
103 $skip = false;
104 // Test for wrong salted hashing method (only if current method is not related to default method)
105 if ($validPasswd && get_class($this->objInstanceSaltedPW) !== $defaultHashingClassName && !is_subclass_of($this->objInstanceSaltedPW, $defaultHashingClassName)) {
106 // Instantiate default method class
107 $this->objInstanceSaltedPW = SaltFactory::getSaltingInstance(null);
108 $this->updatePassword((int)$user['uid'], ['password' => $this->objInstanceSaltedPW->getHashedPassword($password)]);
109 }
110 if ($validPasswd && !$skip && $this->objInstanceSaltedPW->isHashUpdateNeeded($user['password'])) {
111 $this->updatePassword((int)$user['uid'], ['password' => $this->objInstanceSaltedPW->getHashedPassword($password)]);
112 }
113 } else {
114 // Stored password is in deprecated salted hashing method
115 $hashingMethod = substr($user['password'], 0, 2);
116 if ($hashingMethod === 'M$') {
117 // Instantiate default method class
118 $this->objInstanceSaltedPW = SaltFactory::getSaltingInstance(substr($user['password'], 1));
119 // md5 passwords that have been upgraded to salted passwords using old scheduler task
120 // @todo: The entire 'else' should be dropped in v10, admins had to upgrade users to salted passwords with v8 latest since the
121 // @todo: scheduler task has been dropped with v9, users should have had logged in in v9 era, this fallback is obsolete with v10.
122 $validPasswd = $this->objInstanceSaltedPW->checkPassword(md5($password), substr($user['password'], 1));
123
124 // Skip further authentication methods
125 if (!$validPasswd) {
126 $this->authenticationFailed = true;
127 }
128 }
129 // Upgrade to a sane salt mechanism if password was correct
130 if ($validPasswd) {
131 // Instantiate default method class
132 $this->objInstanceSaltedPW = SaltFactory::getSaltingInstance(null);
133 $this->updatePassword((int)$user['uid'], ['password' => $this->objInstanceSaltedPW->getHashedPassword($password)]);
134 }
135 }
136 return $validPasswd;
137 }
138
139 /**
140 * Method adds a further authUser method.
141 *
142 * Will return one of following authentication status codes:
143 * - 0 - authentication failure
144 * - 100 - just go on. User is not authenticated but there is still no reason to stop
145 * - 200 - the service was able to authenticate the user
146 *
147 * @param array $user Array containing FE user data of the logged user.
148 * @return int Authentication statuscode, one of 0,100 and 200
149 */
150 public function authUser(array $user)
151 {
152 $OK = 100;
153 // The salted password service can only work correctly, if a non empty username along with a non empty password is provided.
154 // Otherwise a different service is allowed to check for other login credentials
155 if ((string)$this->login['uident_text'] !== '' && (string)$this->login['uname'] !== '') {
156 $validPasswd = $this->compareUident($user, $this->login);
157 if (!$validPasswd) {
158 // Failed login attempt (wrong password)
159 $errorMessage = 'Login-attempt from ###IP### (%s), username \'%s\', password not accepted!';
160 // No delegation to further services
161 if ($this->authenticationFailed) {
162 $this->writeLogMessage(TYPO3_MODE . ' Authentication failed - wrong password for username \'%s\'', $this->login['uname']);
163 $OK = 0;
164 } else {
165 $this->writeLogMessage($errorMessage, $this->authInfo['REMOTE_ADDR'], $this->authInfo['REMOTE_HOST'], $this->login['uname']);
166 }
167 $this->writelog(255, 3, 3, 1, $errorMessage, [
168 $this->authInfo['REMOTE_HOST'],
169 $this->login['uname']
170 ]);
171 $this->logger->info(sprintf($errorMessage, $this->authInfo['REMOTE_ADDR'], $this->authInfo['REMOTE_HOST'], $this->login['uname']));
172 } elseif ($validPasswd && $user['lockToDomain'] && strcasecmp($user['lockToDomain'], $this->authInfo['HTTP_HOST'])) {
173 // Lock domain didn't match, so error:
174 $errorMessage = 'Login-attempt from ###IP### (%s), username \'%s\', locked domain \'%s\' did not match \'%s\'!';
175 $this->writeLogMessage($errorMessage, $this->authInfo['REMOTE_ADDR'], $this->authInfo['REMOTE_HOST'], $user[$this->db_user['username_column']], $user['lockToDomain'], $this->authInfo['HTTP_HOST']);
176 $this->writelog(255, 3, 3, 1, $errorMessage, [
177 $this->authInfo['REMOTE_HOST'],
178 $user[$this->db_user['username_column']],
179 $user['lockToDomain'],
180 $this->authInfo['HTTP_HOST']
181 ]);
182 $this->logger->info(sprintf($errorMessage, $this->authInfo['REMOTE_ADDR'], $this->authInfo['REMOTE_HOST'], $user[$this->db_user['username_column']], $user['lockToDomain'], $this->authInfo['HTTP_HOST']));
183 $OK = 0;
184 } elseif ($validPasswd) {
185 $this->writeLogMessage(TYPO3_MODE . ' Authentication successful for username \'%s\'', $this->login['uname']);
186 $OK = 200;
187 }
188 }
189 return $OK;
190 }
191
192 /**
193 * Method updates a FE/BE user record - in this case a new password string will be set.
194 *
195 * @param int $uid uid of user record that will be updated
196 * @param mixed $updateFields Field values as key=>value pairs to be updated in database
197 */
198 protected function updatePassword($uid, $updateFields)
199 {
200 $connection = GeneralUtility::makeInstance(ConnectionPool::class)
201 ->getConnectionForTable($this->pObj->user_table);
202
203 $connection->update(
204 $this->pObj->user_table,
205 $updateFields,
206 ['uid' => (int)$uid]
207 );
208
209 $this->logger->notice('Automatic password update for user record in ' . $this->pObj->user_table . ' with uid ' . $uid);
210 }
211
212 /**
213 * Writes log message. Destination log depends on the current system mode.
214 * For FE the function writes to the admin panel log. For BE messages are
215 * sent to the system log. If developer log is enabled, messages are also
216 * sent there.
217 *
218 * This function accepts variable number of arguments and can format
219 * parameters. The syntax is the same as for sprintf()
220 *
221 * @param string $message Message to output
222 * @param array<int, mixed> $params
223 */
224 public function writeLogMessage($message, ...$params)
225 {
226 if (!empty($params)) {
227 $message = vsprintf($message, $params);
228 }
229 if (TYPO3_MODE === 'FE') {
230 /** @var TimeTracker $timeTracker */
231 $timeTracker = GeneralUtility::makeInstance(TimeTracker::class);
232 $timeTracker->setTSlogMessage($message);
233 }
234 $this->logger->notice($message);
235 }
236 }