‪TYPO3CMS  11.5
RecoveryCodesProvider.php
Go to the documentation of this file.
1 <?php
2 
3 /*
4  * This file is part of the TYPO3 CMS project.
5  *
6  * It is free software; you can redistribute it and/or modify it under
7  * the terms of the GNU General Public License, either version 2
8  * of the License, or any later version.
9  *
10  * For the full copyright and license information, please read the
11  * LICENSE.txt file that was distributed with this source code.
12  *
13  * The TYPO3 project - inspiring people to share!
14  */
15 
16 declare(strict_types=1);
17 
19 
20 use Psr\Http\Message\ResponseInterface;
21 use Psr\Http\Message\ServerRequestInterface;
36 
43 {
48 
49  public function ‪__construct(
54  ) {
55  $this->mfaProviderRegistry = ‪$mfaProviderRegistry;
56  $this->context = ‪$context;
57  $this->uriBuilder = ‪$uriBuilder;
58  $this->flashMessageService = ‪$flashMessageService;
59  }
60 
61  private const ‪MAX_ATTEMPTS = 3;
62 
69  public function ‪canProcess(ServerRequestInterface $request): bool
70  {
71  return $this->‪getRecoveryCode($request) !== '';
72  }
73 
83  public function ‪isActive(‪MfaProviderPropertyManager $propertyManager): bool
84  {
85  return (bool)$propertyManager->‪getProperty('active')
86  && $this->‪activeProvidersExist($propertyManager);
87  }
88 
97  public function ‪isLocked(‪MfaProviderPropertyManager $propertyManager): bool
98  {
99  $attempts = (int)$propertyManager->‪getProperty('attempts', 0);
100  $codes = (array)$propertyManager->‪getProperty('codes', []);
101 
102  // Assume the provider is locked in case either the maximum attempts are exceeded or no codes
103  // are available. A provider however can only be locked if set up - an entry exists in database.
104  return $propertyManager->‪hasProviderEntry() && ($attempts >= self::MAX_ATTEMPTS || $codes === []);
105  }
106 
116  public function ‪verify(ServerRequestInterface $request, ‪MfaProviderPropertyManager $propertyManager): bool
117  {
118  if (!$this->‪isActive($propertyManager) || $this->‪isLocked($propertyManager)) {
119  // Can not verify an inactive or locked provider
120  return false;
121  }
122 
123  $recoveryCode = $this->‪getRecoveryCode($request);
124  $codes = $propertyManager->‪getProperty('codes', []);
125  $recoveryCodes = GeneralUtility::makeInstance(RecoveryCodes::class, $this->‪getMode($propertyManager));
126  if (!$recoveryCodes->verifyRecoveryCode($recoveryCode, $codes)) {
127  $attempts = $propertyManager->‪getProperty('attempts', 0);
128  $propertyManager->‪updateProperties(['attempts' => ++$attempts]);
129  return false;
130  }
131 
132  // Since the codes were passed by reference to the verify method, the matching code was
133  // unset so we simply need to write the array back. However, if the update fails, we must
134  // return FALSE even if the authentication was successful to prevent data inconsistency.
135  return $propertyManager->‪updateProperties([
136  'codes' => $codes,
137  'attempts' => 0,
138  'lastUsed' => $this->context->getPropertyFromAspect('date', 'timestamp'),
139  ]);
140  }
141 
151  public function ‪handleRequest(
152  ServerRequestInterface $request,
153  ‪MfaProviderPropertyManager $propertyManager,
154  string $type
155  ): ResponseInterface {
156  $view = GeneralUtility::makeInstance(StandaloneView::class);
157  $view->setTemplateRootPaths(['EXT:core/Resources/Private/Templates/Authentication/MfaProvider/RecoveryCodes']);
158  switch ($type) {
160  if (!$this->‪activeProvidersExist($propertyManager)) {
161  // If no active providers are present for the current user, add a flash message and redirect
162  $lang = $this->‪getLanguageService();
163  $this->‪addFlashMessage(
164  $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_mfa_provider.xlf:setup.recoveryCodes.noActiveProviders.message'),
165  $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_mfa_provider.xlf:setup.recoveryCodes.noActiveProviders.title'),
167  );
168  if (($normalizedParams = $request->getAttribute('normalizedParams'))) {
169  $returnUrl = $normalizedParams->getHttpReferer();
170  } else {
171  // @todo this will not work for FE - make this more generic!
172  $returnUrl = $this->uriBuilder->buildUriFromRoute('mfa');
173  }
174  throw new ‪PropagateResponseException(new ‪RedirectResponse($returnUrl, 303), 1612883326);
175  }
176  $codes = GeneralUtility::makeInstance(RecoveryCodes::class, $this->‪getMode($propertyManager))->generatePlainRecoveryCodes();
177  $view->setTemplate('Setup');
178  $view->assignMultiple([
179  'recoveryCodes' => implode(PHP_EOL, $codes),
180  // Generate hmac of the recovery codes to prevent them from being changed in the setup from
181  'checksum' => GeneralUtility::hmac(json_encode($codes) ?: '', 'recovery-codes-setup'),
182  ]);
183  break;
185  $view->setTemplate('Edit');
186  $view->assignMultiple([
187  'name' => $propertyManager->‪getProperty('name'),
188  'amountOfCodesLeft' => count($propertyManager->‪getProperty('codes', [])),
189  'lastUsed' => $this->getDateTime($propertyManager->‪getProperty('lastUsed', 0)),
190  'updated' => $this->getDateTime($propertyManager->‪getProperty('updated', 0)),
191  ]);
192  break;
194  $view->setTemplate('Auth');
195  $view->assign('isLocked', $this->‪isLocked($propertyManager));
196  break;
197  }
198  return new ‪HtmlResponse($view->assign('providerIdentifier', $propertyManager->‪getIdentifier())->render());
199  }
200 
208  public function ‪activate(ServerRequestInterface $request, ‪MfaProviderPropertyManager $propertyManager): bool
209  {
210  if ($this->‪isActive($propertyManager)) {
211  // Can not activate an active provider
212  return false;
213  }
214 
215  if (!$this->‪activeProvidersExist($propertyManager)) {
216  // Can not activate since no other provider is activated yet
217  return false;
218  }
219 
220  $recoveryCodes = ‪GeneralUtility::trimExplode(PHP_EOL, (string)($request->getParsedBody()['recoveryCodes'] ?? ''));
221  $checksum = (string)($request->getParsedBody()['checksum'] ?? '');
222  if ($recoveryCodes === []
223  || !hash_equals(GeneralUtility::hmac(json_encode($recoveryCodes) ?: '', 'recovery-codes-setup'), $checksum)
224  ) {
225  // Return since the request does not contain the initially created recovery codes
226  return false;
227  }
228 
229  // Hash given plain recovery codes and prepare the properties array with active state and custom name
230  $hashedCodes = GeneralUtility::makeInstance(RecoveryCodes::class, $this->‪getMode($propertyManager))->generatedHashedRecoveryCodes($recoveryCodes);
231  $properties = ['codes' => $hashedCodes, 'active' => true];
232  if (($name = (string)($request->getParsedBody()['name'] ?? '')) !== '') {
233  $properties['name'] = $name;
234  }
235 
236  // Usually there should be no entry if the provider is not activated, but to prevent the
237  // provider from being unable to activate again, we update the existing entry in such case.
238  return $propertyManager->‪hasProviderEntry()
239  ? $propertyManager->‪updateProperties($properties)
240  : $propertyManager->‪createProviderEntry($properties);
241  }
242 
250  public function ‪deactivate(ServerRequestInterface $request, ‪MfaProviderPropertyManager $propertyManager): bool
251  {
252  // Only check for the active property here to enable bulk deactivation,
253  // e.g. in FormEngine. Otherwise it would not be possible to deactivate
254  // this provider if the last "fully" provider was deactivated before.
255  if (!(bool)$propertyManager->‪getProperty('active')) {
256  // Can not deactivate an inactive provider
257  return false;
258  }
259 
260  // Delete the provider entry
261  return $propertyManager->‪deleteProviderEntry();
262  }
263 
272  public function ‪unlock(ServerRequestInterface $request, ‪MfaProviderPropertyManager $propertyManager): bool
273  {
274  if (!$this->‪isActive($propertyManager) || !$this->‪isLocked($propertyManager)) {
275  // Can not unlock an inactive or not locked provider
276  return false;
277  }
278 
279  // Reset attempts
280  if ((int)$propertyManager->‪getProperty('attempts', 0) !== 0
281  && !$propertyManager->‪updateProperties(['attempts' => 0])
282  ) {
283  // Could not reset the attempts, so we can not unlock the provider
284  return false;
285  }
286 
287  // Regenerate codes
288  if ($propertyManager->‪getProperty('codes', []) === []) {
289  // Generate new codes and store the hashed ones
290  $recoveryCodes = GeneralUtility::makeInstance(RecoveryCodes::class, $this->‪getMode($propertyManager))->generateRecoveryCodes();
291  if (!$propertyManager->‪updateProperties(['codes' => array_values($recoveryCodes)])) {
292  // Codes could not be stored, so we can not unlock the provider
293  return false;
294  }
295  // Add the newly generated codes to a flash message so the user can copy them
296  $lang = $this->‪getLanguageService();
297  $this->‪addFlashMessage(
298  sprintf(
299  $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_mfa_provider.xlf:unlock.recoveryCodes.message'),
300  implode(' ', array_keys($recoveryCodes))
301  ),
302  $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_mfa_provider.xlf:unlock.recoveryCodes.title'),
304  );
305  }
306 
307  return true;
308  }
309 
310  public function ‪update(ServerRequestInterface $request, ‪MfaProviderPropertyManager $propertyManager): bool
311  {
312  if (!$this->‪isActive($propertyManager) || $this->‪isLocked($propertyManager)) {
313  // Can not update an inactive or locked provider
314  return false;
315  }
316 
317  $name = (string)($request->getParsedBody()['name'] ?? '');
318  if ($name !== '' && !$propertyManager->‪updateProperties(['name' => $name])) {
319  return false;
320  }
321 
322  if ((bool)($request->getParsedBody()['regenerateCodes'] ?? false)) {
323  // Generate new codes and store the hashed ones
324  $recoveryCodes = GeneralUtility::makeInstance(RecoveryCodes::class, $this->‪getMode($propertyManager))->generateRecoveryCodes();
325  if (!$propertyManager->‪updateProperties(['codes' => array_values($recoveryCodes)])) {
326  // Codes could not be stored, so we can not update the provider
327  return false;
328  }
329  // Add the newly generated codes to a flash message so the user can copy them
330  $lang = $this->‪getLanguageService();
331  $this->‪addFlashMessage(
332  sprintf(
333  $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_mfa_provider.xlf:update.recoveryCodes.message'),
334  implode(' ', array_keys($recoveryCodes))
335  ),
336  $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_mfa_provider.xlf:update.recoveryCodes.title'),
338  );
339  }
340 
341  // Provider properties successfully updated
342  return true;
343  }
344 
351  protected function ‪activeProvidersExist(‪MfaProviderPropertyManager $currentPropertyManager): bool
352  {
353  $user = $currentPropertyManager->‪getUser();
354  foreach ($this->mfaProviderRegistry->getProviders() as $identifier => $provider) {
355  $propertyManager = ‪MfaProviderPropertyManager::create($provider, $user);
356  if ($identifier !== $currentPropertyManager->‪getIdentifier() && $provider->isActive($propertyManager)) {
357  return true;
358  }
359  }
360  return false;
361  }
362 
369  protected function ‪getRecoveryCode(ServerRequestInterface $request): string
370  {
371  return trim((string)($request->getQueryParams()['rc'] ?? $request->getParsedBody()['rc'] ?? ''));
372  }
373 
380  protected function ‪getMode(‪MfaProviderPropertyManager $propertyManager): string
381  {
382  return $propertyManager->‪getUser()->loginType;
383  }
384 
393  protected function ‪addFlashMessage(string $message, string $title = '', int $severity = ‪FlashMessage::INFO): void
394  {
395  $this->flashMessageService->getMessageQueueByIdentifier()->enqueue(
396  GeneralUtility::makeInstance(FlashMessage::class, $message, $title, $severity, true)
397  );
398  }
399 
406  protected function ‪getDateTime(int $timestamp): string
407  {
408  if ($timestamp === 0) {
409  return '';
410  }
411 
412  return date(
413  ‪$GLOBALS['TYPO3_CONF_VARS']['SYS']['ddmmyy'] . ' ' . ‪$GLOBALS['TYPO3_CONF_VARS']['SYS']['hhmm'],
414  $timestamp
415  ) ?: '';
416  }
417 
419  {
420  return ‪$GLOBALS['LANG'];
421  }
422 }
‪TYPO3\CMS\Core\Utility\GeneralUtility\trimExplode
‪static list< string > trimExplode($delim, $string, $removeEmptyValues=false, $limit=0)
Definition: GeneralUtility.php:999
‪TYPO3\CMS\Core\Authentication\Mfa\MfaViewType\EDIT
‪const EDIT
Definition: MfaViewType.php:28
‪TYPO3\CMS\Core\Authentication\Mfa\Provider
Definition: RecoveryCodes.php:18
‪TYPO3\CMS\Core\Authentication\Mfa\MfaViewType\AUTH
‪const AUTH
Definition: MfaViewType.php:29
‪TYPO3\CMS\Core\Authentication\Mfa\Provider\RecoveryCodesProvider\$context
‪Context $context
Definition: RecoveryCodesProvider.php:45
‪TYPO3\CMS\Core\Authentication\Mfa\Provider\RecoveryCodesProvider\getLanguageService
‪getLanguageService()
Definition: RecoveryCodesProvider.php:418
‪TYPO3\CMS\Core\Authentication\Mfa\Provider\RecoveryCodesProvider\$flashMessageService
‪FlashMessageService $flashMessageService
Definition: RecoveryCodesProvider.php:47
‪TYPO3\CMS\Core\Authentication\Mfa\MfaViewType\SETUP
‪const SETUP
Definition: MfaViewType.php:27
‪TYPO3\CMS\Core\Authentication\Mfa\Provider\RecoveryCodesProvider\activate
‪bool activate(ServerRequestInterface $request, MfaProviderPropertyManager $propertyManager)
Definition: RecoveryCodesProvider.php:208
‪TYPO3\CMS\Core\Authentication\Mfa\MfaProviderPropertyManager\getProperty
‪mixed null getProperty(string $key, $default=null)
Definition: MfaProviderPropertyManager.php:79
‪TYPO3\CMS\Core\Authentication\Mfa\MfaProviderPropertyManager\hasProviderEntry
‪bool hasProviderEntry()
Definition: MfaProviderPropertyManager.php:55
‪TYPO3\CMS\Core\Authentication\Mfa\MfaProviderPropertyManager\deleteProviderEntry
‪bool deleteProviderEntry()
Definition: MfaProviderPropertyManager.php:158
‪TYPO3\CMS\Core\Context\Context
Definition: Context.php:53
‪TYPO3\CMS\Core\Authentication\Mfa\Provider\RecoveryCodesProvider\__construct
‪__construct(MfaProviderRegistry $mfaProviderRegistry, Context $context, UriBuilder $uriBuilder, FlashMessageService $flashMessageService)
Definition: RecoveryCodesProvider.php:49
‪TYPO3\CMS\Core\Authentication\Mfa\Provider\RecoveryCodesProvider\activeProvidersExist
‪bool activeProvidersExist(MfaProviderPropertyManager $currentPropertyManager)
Definition: RecoveryCodesProvider.php:351
‪TYPO3\CMS\Core\Authentication\Mfa\Provider\RecoveryCodesProvider\verify
‪bool verify(ServerRequestInterface $request, MfaProviderPropertyManager $propertyManager)
Definition: RecoveryCodesProvider.php:116
‪TYPO3\CMS\Core\Authentication\Mfa\Provider\RecoveryCodesProvider\getMode
‪string getMode(MfaProviderPropertyManager $propertyManager)
Definition: RecoveryCodesProvider.php:380
‪TYPO3\CMS\Core\Authentication\Mfa\Provider\RecoveryCodesProvider\unlock
‪bool unlock(ServerRequestInterface $request, MfaProviderPropertyManager $propertyManager)
Definition: RecoveryCodesProvider.php:272
‪TYPO3\CMS\Core\Authentication\Mfa\Provider\RecoveryCodesProvider
Definition: RecoveryCodesProvider.php:43
‪TYPO3\CMS\Core\Messaging\AbstractMessage\WARNING
‪const WARNING
Definition: AbstractMessage.php:30
‪TYPO3\CMS\Core\Authentication\Mfa\MfaProviderPropertyManager\updateProperties
‪bool updateProperties(array $properties)
Definition: MfaProviderPropertyManager.php:102
‪TYPO3\CMS\Core\Authentication\Mfa\Provider\RecoveryCodesProvider\handleRequest
‪ResponseInterface handleRequest(ServerRequestInterface $request, MfaProviderPropertyManager $propertyManager, string $type)
Definition: RecoveryCodesProvider.php:151
‪TYPO3\CMS\Core\Authentication\Mfa\MfaProviderInterface
Definition: MfaProviderInterface.php:27
‪TYPO3\CMS\Core\Authentication\Mfa\MfaProviderPropertyManager\getUser
‪AbstractUserAuthentication getUser()
Definition: MfaProviderPropertyManager.php:202
‪TYPO3\CMS\Backend\Routing\UriBuilder
Definition: UriBuilder.php:40
‪TYPO3\CMS\Core\Authentication\Mfa\Provider\RecoveryCodesProvider\MAX_ATTEMPTS
‪const MAX_ATTEMPTS
Definition: RecoveryCodesProvider.php:61
‪TYPO3\CMS\Core\Authentication\Mfa\Provider\RecoveryCodesProvider\canProcess
‪bool canProcess(ServerRequestInterface $request)
Definition: RecoveryCodesProvider.php:69
‪TYPO3\CMS\Core\Authentication\Mfa\Provider\RecoveryCodesProvider\update
‪update(ServerRequestInterface $request, MfaProviderPropertyManager $propertyManager)
Definition: RecoveryCodesProvider.php:310
‪TYPO3\CMS\Core\Authentication\Mfa\Provider\RecoveryCodesProvider\$mfaProviderRegistry
‪MfaProviderRegistry $mfaProviderRegistry
Definition: RecoveryCodesProvider.php:44
‪TYPO3\CMS\Core\Authentication\Mfa\Provider\RecoveryCodesProvider\$uriBuilder
‪UriBuilder $uriBuilder
Definition: RecoveryCodesProvider.php:46
‪TYPO3\CMS\Core\Authentication\Mfa\Provider\RecoveryCodesProvider\deactivate
‪bool deactivate(ServerRequestInterface $request, MfaProviderPropertyManager $propertyManager)
Definition: RecoveryCodesProvider.php:250
‪TYPO3\CMS\Core\Authentication\Mfa\Provider\RecoveryCodesProvider\getRecoveryCode
‪string getRecoveryCode(ServerRequestInterface $request)
Definition: RecoveryCodesProvider.php:369
‪TYPO3\CMS\Core\Authentication\Mfa\MfaProviderPropertyManager
Definition: MfaProviderPropertyManager.php:33
‪TYPO3\CMS\Core\Http\PropagateResponseException
Definition: PropagateResponseException.php:47
‪TYPO3\CMS\Core\Http\RedirectResponse
Definition: RedirectResponse.php:28
‪TYPO3\CMS\Core\Authentication\Mfa\Provider\RecoveryCodesProvider\isLocked
‪bool isLocked(MfaProviderPropertyManager $propertyManager)
Definition: RecoveryCodesProvider.php:97
‪TYPO3\CMS\Core\Messaging\AbstractMessage\OK
‪const OK
Definition: AbstractMessage.php:29
‪TYPO3\CMS\Core\Messaging\AbstractMessage\INFO
‪const INFO
Definition: AbstractMessage.php:28
‪TYPO3\CMS\Core\Messaging\FlashMessage
Definition: FlashMessage.php:26
‪TYPO3\CMS\Fluid\View\StandaloneView
Definition: StandaloneView.php:31
‪$GLOBALS
‪$GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['adminpanel']['modules']
Definition: ext_localconf.php:25
‪TYPO3\CMS\Core\Authentication\Mfa\MfaViewType
Definition: MfaViewType.php:26
‪TYPO3\CMS\Core\Authentication\Mfa\MfaProviderPropertyManager\create
‪static MfaProviderPropertyManager create(MfaProviderManifestInterface $provider, AbstractUserAuthentication $user)
Definition: MfaProviderPropertyManager.php:224
‪TYPO3\CMS\Core\Localization\LanguageService
Definition: LanguageService.php:42
‪TYPO3\CMS\Core\Authentication\Mfa\Provider\RecoveryCodesProvider\getDateTime
‪string getDateTime(int $timestamp)
Definition: RecoveryCodesProvider.php:406
‪TYPO3\CMS\Core\Utility\GeneralUtility
Definition: GeneralUtility.php:50
‪TYPO3\CMS\Core\Authentication\Mfa\Provider\RecoveryCodesProvider\isActive
‪bool isActive(MfaProviderPropertyManager $propertyManager)
Definition: RecoveryCodesProvider.php:83
‪TYPO3\CMS\Core\Authentication\Mfa\MfaProviderPropertyManager\createProviderEntry
‪bool createProviderEntry(array $properties)
Definition: MfaProviderPropertyManager.php:129
‪TYPO3\CMS\Core\Authentication\Mfa\Provider\RecoveryCodesProvider\addFlashMessage
‪addFlashMessage(string $message, string $title='', int $severity=FlashMessage::INFO)
Definition: RecoveryCodesProvider.php:393
‪TYPO3\CMS\Core\Authentication\Mfa\MfaProviderPropertyManager\getIdentifier
‪string getIdentifier()
Definition: MfaProviderPropertyManager.php:212
‪TYPO3\CMS\Core\Messaging\FlashMessageService
Definition: FlashMessageService.php:27
‪TYPO3\CMS\Core\Http\HtmlResponse
Definition: HtmlResponse.php:26
‪TYPO3\CMS\Core\Authentication\Mfa\MfaProviderRegistry
Definition: MfaProviderRegistry.php:28