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