‪TYPO3CMS  ‪main
RecoveryCodesProvider.php
Go to the documentation of this file.
1 <?php
2 
3 declare(strict_types=1);
4 
5 /*
6  * This file is part of the TYPO3 CMS project.
7  *
8  * It is free software; you can redistribute it and/or modify it under
9  * the terms of the GNU General Public License, either version 2
10  * of the License, or any later version.
11  *
12  * For the full copyright and license information, please read the
13  * LICENSE.txt file that was distributed with this source code.
14  *
15  * The TYPO3 project - inspiring people to share!
16  */
17 
19 
20 use Psr\Http\Message\ResponseInterface;
21 use Psr\Http\Message\ServerRequestInterface;
38 
45 {
46  public function ‪__construct(
47  protected readonly ‪MfaProviderRegistry $mfaProviderRegistry,
48  protected readonly ‪Context $context,
49  protected readonly ‪UriBuilder $uriBuilder,
50  protected readonly ‪FlashMessageService $flashMessageService,
51  protected readonly ‪HashService $hashService,
52  ) {}
53 
54  private const ‪MAX_ATTEMPTS = 3;
55 
59  public function ‪canProcess(ServerRequestInterface $request): bool
60  {
61  return $this->‪getRecoveryCode($request) !== '';
62  }
63 
70  public function ‪isActive(‪MfaProviderPropertyManager $propertyManager): bool
71  {
72  return (bool)$propertyManager->‪getProperty('active')
73  && $this->‪activeProvidersExist($propertyManager);
74  }
75 
81  public function ‪isLocked(‪MfaProviderPropertyManager $propertyManager): bool
82  {
83  $attempts = (int)$propertyManager->‪getProperty('attempts', 0);
84  $codes = (array)$propertyManager->‪getProperty('codes', []);
85 
86  // Assume the provider is locked in case either the maximum attempts are exceeded or no codes
87  // are available. A provider however can only be locked if set up - an entry exists in database.
88  return $propertyManager->‪hasProviderEntry() && ($attempts >= self::MAX_ATTEMPTS || $codes === []);
89  }
90 
95  public function ‪verify(ServerRequestInterface $request, ‪MfaProviderPropertyManager $propertyManager): bool
96  {
97  if (!$this->‪isActive($propertyManager) || $this->‪isLocked($propertyManager)) {
98  // Can not verify an inactive or locked provider
99  return false;
100  }
101 
102  $recoveryCode = $this->‪getRecoveryCode($request);
103  $codes = $propertyManager->‪getProperty('codes', []);
104  $recoveryCodes = GeneralUtility::makeInstance(RecoveryCodes::class, $this->‪getMode($propertyManager));
105  if (!$recoveryCodes->verifyRecoveryCode($recoveryCode, $codes)) {
106  $attempts = $propertyManager->‪getProperty('attempts', 0);
107  $propertyManager->‪updateProperties(['attempts' => ++$attempts]);
108  return false;
109  }
110 
111  // Since the codes were passed by reference to the verify method, the matching code was
112  // unset so we simply need to write the array back. However, if the update fails, we must
113  // return FALSE even if the authentication was successful to prevent data inconsistency.
114  return $propertyManager->‪updateProperties([
115  'codes' => $codes,
116  'attempts' => 0,
117  'lastUsed' => $this->context->getPropertyFromAspect('date', 'timestamp'),
118  ]);
119  }
120 
126  public function ‪handleRequest(
127  ServerRequestInterface $request,
128  ‪MfaProviderPropertyManager $propertyManager,
129  ‪MfaViewType $type
130  ): ResponseInterface {
131  $view = GeneralUtility::makeInstance(StandaloneView::class);
132  $view->setTemplateRootPaths(['EXT:core/Resources/Private/Templates/Authentication/MfaProvider/RecoveryCodes']);
133  switch ($type) {
134  case MfaViewType::SETUP:
135  if (!$this->‪activeProvidersExist($propertyManager)) {
136  // If no active providers are present for the current user, add a flash message and redirect
137  $lang = $this->‪getLanguageService();
138  $this->‪addFlashMessage(
139  $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_mfa_provider.xlf:setup.recoveryCodes.noActiveProviders.message'),
140  $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_mfa_provider.xlf:setup.recoveryCodes.noActiveProviders.title'),
141  ContextualFeedbackSeverity::WARNING
142  );
143  if (($normalizedParams = $request->getAttribute('normalizedParams'))) {
144  $returnUrl = $normalizedParams->getHttpReferer();
145  } else {
146  // @todo this will not work for FE - make this more generic!
147  $returnUrl = $this->uriBuilder->buildUriFromRoute('mfa');
148  }
149  throw new ‪PropagateResponseException(new ‪RedirectResponse($returnUrl, 303), 1612883326);
150  }
151  $codes = GeneralUtility::makeInstance(RecoveryCodes::class, $this->‪getMode($propertyManager))->generatePlainRecoveryCodes();
152  $view->setTemplate('Setup');
153  $view->assignMultiple([
154  'recoveryCodes' => implode(PHP_EOL, $codes),
155  // Generate hmac of the recovery codes to prevent them from being changed in the setup from
156  'checksum' => $this->hashService->hmac(json_encode($codes) ?: '', 'recovery-codes-setup'),
157  ]);
158  break;
159  case MfaViewType::EDIT:
160  $view->setTemplate('Edit');
161  $view->assignMultiple([
162  'name' => $propertyManager->‪getProperty('name'),
163  'amountOfCodesLeft' => count($propertyManager->‪getProperty('codes', [])),
164  'lastUsed' => $this->getDateTime($propertyManager->‪getProperty('lastUsed', 0)),
165  'updated' => $this->getDateTime($propertyManager->‪getProperty('updated', 0)),
166  ]);
167  break;
169  $view->setTemplate('Auth');
170  $view->assign('isLocked', $this->‪isLocked($propertyManager));
171  break;
172  }
173  return new ‪HtmlResponse($view->assign('providerIdentifier', $propertyManager->‪getIdentifier())->render());
174  }
175 
179  public function ‪activate(ServerRequestInterface $request, ‪MfaProviderPropertyManager $propertyManager): bool
180  {
181  if ($this->‪isActive($propertyManager)) {
182  // Can not activate an active provider
183  return false;
184  }
185 
186  if (!$this->‪activeProvidersExist($propertyManager)) {
187  // Can not activate since no other provider is activated yet
188  return false;
189  }
190 
191  $recoveryCodes = ‪GeneralUtility::trimExplode(PHP_EOL, (string)($request->getParsedBody()['recoveryCodes'] ?? ''));
192  $checksum = (string)($request->getParsedBody()['checksum'] ?? '');
193  if ($recoveryCodes === []
194  || !hash_equals($this->hashService->hmac(json_encode($recoveryCodes) ?: '', 'recovery-codes-setup'), $checksum)
195  ) {
196  // Return since the request does not contain the initially created recovery codes
197  return false;
198  }
199 
200  // Hash given plain recovery codes and prepare the properties array with active state and custom name
201  $hashedCodes = GeneralUtility::makeInstance(RecoveryCodes::class, $this->‪getMode($propertyManager))->generatedHashedRecoveryCodes($recoveryCodes);
202  $properties = ['codes' => $hashedCodes, 'active' => true];
203  if (($name = (string)($request->getParsedBody()['name'] ?? '')) !== '') {
204  $properties['name'] = $name;
205  }
206 
207  // Usually there should be no entry if the provider is not activated, but to prevent the
208  // provider from being unable to activate again, we update the existing entry in such case.
209  return $propertyManager->‪hasProviderEntry()
210  ? $propertyManager->‪updateProperties($properties)
211  : $propertyManager->‪createProviderEntry($properties);
212  }
213 
217  public function ‪deactivate(ServerRequestInterface $request, ‪MfaProviderPropertyManager $propertyManager): bool
218  {
219  // Only check for the active property here to enable bulk deactivation,
220  // e.g. in FormEngine. Otherwise it would not be possible to deactivate
221  // this provider if the last "fully" provider was deactivated before.
222  if (!(bool)$propertyManager->‪getProperty('active')) {
223  // Can not deactivate an inactive provider
224  return false;
225  }
226 
227  // Delete the provider entry
228  return $propertyManager->‪deleteProviderEntry();
229  }
230 
235  public function ‪unlock(ServerRequestInterface $request, ‪MfaProviderPropertyManager $propertyManager): bool
236  {
237  if (!$this->‪isActive($propertyManager) || !$this->‪isLocked($propertyManager)) {
238  // Can not unlock an inactive or not locked provider
239  return false;
240  }
241 
242  // Reset attempts
243  if ((int)$propertyManager->‪getProperty('attempts', 0) !== 0
244  && !$propertyManager->‪updateProperties(['attempts' => 0])
245  ) {
246  // Could not reset the attempts, so we can not unlock the provider
247  return false;
248  }
249 
250  // Regenerate codes
251  if ($propertyManager->‪getProperty('codes', []) === []) {
252  // Generate new codes and store the hashed ones
253  $recoveryCodes = GeneralUtility::makeInstance(RecoveryCodes::class, $this->‪getMode($propertyManager))->generateRecoveryCodes();
254  if (!$propertyManager->‪updateProperties(['codes' => array_values($recoveryCodes)])) {
255  // Codes could not be stored, so we can not unlock the provider
256  return false;
257  }
258  // Add the newly generated codes to a flash message so the user can copy them
259  $lang = $this->‪getLanguageService();
260  $this->‪addFlashMessage(
261  sprintf(
262  $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_mfa_provider.xlf:unlock.recoveryCodes.message'),
263  implode(' ', array_keys($recoveryCodes))
264  ),
265  $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_mfa_provider.xlf:unlock.recoveryCodes.title'),
266  ContextualFeedbackSeverity::WARNING
267  );
268  }
269 
270  return true;
271  }
272 
273  public function ‪update(ServerRequestInterface $request, ‪MfaProviderPropertyManager $propertyManager): bool
274  {
275  if (!$this->‪isActive($propertyManager) || $this->‪isLocked($propertyManager)) {
276  // Can not update an inactive or locked provider
277  return false;
278  }
279 
280  $name = (string)($request->getParsedBody()['name'] ?? '');
281  if ($name !== '' && !$propertyManager->‪updateProperties(['name' => $name])) {
282  return false;
283  }
284 
285  if ((bool)($request->getParsedBody()['regenerateCodes'] ?? false)) {
286  // Generate new codes and store the hashed ones
287  $recoveryCodes = GeneralUtility::makeInstance(RecoveryCodes::class, $this->‪getMode($propertyManager))->generateRecoveryCodes();
288  if (!$propertyManager->‪updateProperties(['codes' => array_values($recoveryCodes)])) {
289  // Codes could not be stored, so we can not update the provider
290  return false;
291  }
292  // Add the newly generated codes to a flash message so the user can copy them
293  $lang = $this->‪getLanguageService();
294  $this->‪addFlashMessage(
295  sprintf(
296  $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_mfa_provider.xlf:update.recoveryCodes.message'),
297  implode(' ', array_keys($recoveryCodes))
298  ),
299  $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_mfa_provider.xlf:update.recoveryCodes.title'),
300  ContextualFeedbackSeverity::OK
301  );
302  }
303 
304  // Provider properties successfully updated
305  return true;
306  }
307 
311  protected function ‪activeProvidersExist(‪MfaProviderPropertyManager $currentPropertyManager): bool
312  {
313  $user = $currentPropertyManager->‪getUser();
314  foreach ($this->mfaProviderRegistry->getProviders() as ‪$identifier => $provider) {
315  $propertyManager = ‪MfaProviderPropertyManager::create($provider, $user);
316  if (‪$identifier !== $currentPropertyManager->‪getIdentifier() && $provider->isActive($propertyManager)) {
317  return true;
318  }
319  }
320  return false;
321  }
322 
326  protected function ‪getRecoveryCode(ServerRequestInterface $request): string
327  {
328  return trim((string)($request->getQueryParams()['rc'] ?? $request->getParsedBody()['rc'] ?? ''));
329  }
330 
334  protected function ‪getMode(‪MfaProviderPropertyManager $propertyManager): string
335  {
336  return $propertyManager->‪getUser()->loginType;
337  }
338 
343  protected function ‪addFlashMessage(string $message, string $title = '', ‪ContextualFeedbackSeverity $severity = ContextualFeedbackSeverity::INFO): void
344  {
345  $this->flashMessageService->getMessageQueueByIdentifier()->enqueue(
346  GeneralUtility::makeInstance(FlashMessage::class, $message, $title, $severity, true)
347  );
348  }
349 
353  protected function ‪getDateTime(int $timestamp): string
354  {
355  if ($timestamp === 0) {
356  return '';
357  }
358 
359  return date(
360  ‪$GLOBALS['TYPO3_CONF_VARS']['SYS']['ddmmyy'] . ' ' . ‪$GLOBALS['TYPO3_CONF_VARS']['SYS']['hhmm'],
361  $timestamp
362  ) ?: '';
363  }
364 
366  {
367  return ‪$GLOBALS['LANG'];
368  }
369 }
‪TYPO3\CMS\Core\Authentication\Mfa\Provider
Definition: RecoveryCodes.php:18
‪TYPO3\CMS\Core\Authentication\Mfa\Provider\RecoveryCodesProvider\getLanguageService
‪getLanguageService()
Definition: RecoveryCodesProvider.php:365
‪TYPO3\CMS\Core\Authentication\Mfa\Provider\RecoveryCodesProvider\activeProvidersExist
‪activeProvidersExist(MfaProviderPropertyManager $currentPropertyManager)
Definition: RecoveryCodesProvider.php:311
‪TYPO3\CMS\Core\Authentication\Mfa\Provider\RecoveryCodesProvider\unlock
‪unlock(ServerRequestInterface $request, MfaProviderPropertyManager $propertyManager)
Definition: RecoveryCodesProvider.php:235
‪TYPO3\CMS\Core\Authentication\Mfa\Provider\RecoveryCodesProvider\isActive
‪isActive(MfaProviderPropertyManager $propertyManager)
Definition: RecoveryCodesProvider.php:70
‪TYPO3\CMS\Core\Authentication\Mfa\Provider\RecoveryCodesProvider\isLocked
‪isLocked(MfaProviderPropertyManager $propertyManager)
Definition: RecoveryCodesProvider.php:81
‪TYPO3\CMS\Core\Authentication\Mfa\MfaProviderPropertyManager\getProperty
‪getProperty(string $key, mixed $default=null)
Definition: MfaProviderPropertyManager.php:66
‪TYPO3\CMS\Core\Context\Context
Definition: Context.php:54
‪TYPO3\CMS\Core\Authentication\Mfa\AUTH
‪@ AUTH
Definition: MfaViewType.php:27
‪TYPO3\CMS\Core\Type\ContextualFeedbackSeverity
‪ContextualFeedbackSeverity
Definition: ContextualFeedbackSeverity.php:25
‪TYPO3\CMS\Core\Authentication\Mfa\Provider\RecoveryCodesProvider
Definition: RecoveryCodesProvider.php:45
‪TYPO3\CMS\Core\Authentication\Mfa\Provider\RecoveryCodesProvider\handleRequest
‪handleRequest(ServerRequestInterface $request, MfaProviderPropertyManager $propertyManager, MfaViewType $type)
Definition: RecoveryCodesProvider.php:126
‪TYPO3\CMS\Core\Authentication\Mfa\MfaProviderPropertyManager\getIdentifier
‪getIdentifier()
Definition: MfaProviderPropertyManager.php:185
‪TYPO3\CMS\Core\Authentication\Mfa\Provider\RecoveryCodesProvider\canProcess
‪canProcess(ServerRequestInterface $request)
Definition: RecoveryCodesProvider.php:59
‪TYPO3\CMS\Core\Authentication\Mfa\MfaProviderPropertyManager\deleteProviderEntry
‪deleteProviderEntry()
Definition: MfaProviderPropertyManager.php:136
‪TYPO3\CMS\Core\Authentication\Mfa\MfaProviderInterface
Definition: MfaProviderInterface.php:27
‪TYPO3\CMS\Core\Authentication\Mfa\Provider\RecoveryCodesProvider\getMode
‪getMode(MfaProviderPropertyManager $propertyManager)
Definition: RecoveryCodesProvider.php:334
‪TYPO3\CMS\Core\Authentication\Mfa\MfaProviderPropertyManager\getUser
‪getUser()
Definition: MfaProviderPropertyManager.php:177
‪TYPO3\CMS\Core\Authentication\Mfa\MfaProviderPropertyManager\create
‪static create(MfaProviderManifestInterface $provider, AbstractUserAuthentication $user)
Definition: MfaProviderPropertyManager.php:193
‪TYPO3\CMS\Backend\Routing\UriBuilder
Definition: UriBuilder.php:44
‪TYPO3\CMS\Core\Authentication\Mfa\MfaProviderPropertyManager\createProviderEntry
‪createProviderEntry(array $properties)
Definition: MfaProviderPropertyManager.php:108
‪TYPO3\CMS\Core\Authentication\Mfa\Provider\RecoveryCodesProvider\getRecoveryCode
‪getRecoveryCode(ServerRequestInterface $request)
Definition: RecoveryCodesProvider.php:326
‪TYPO3\CMS\Core\Authentication\Mfa\Provider\RecoveryCodesProvider\MAX_ATTEMPTS
‪const MAX_ATTEMPTS
Definition: RecoveryCodesProvider.php:54
‪TYPO3\CMS\Core\Authentication\Mfa\Provider\RecoveryCodesProvider\activate
‪activate(ServerRequestInterface $request, MfaProviderPropertyManager $propertyManager)
Definition: RecoveryCodesProvider.php:179
‪TYPO3\CMS\Core\Authentication\Mfa\Provider\RecoveryCodesProvider\deactivate
‪deactivate(ServerRequestInterface $request, MfaProviderPropertyManager $propertyManager)
Definition: RecoveryCodesProvider.php:217
‪TYPO3\CMS\Core\Authentication\Mfa\Provider\RecoveryCodesProvider\update
‪update(ServerRequestInterface $request, MfaProviderPropertyManager $propertyManager)
Definition: RecoveryCodesProvider.php:273
‪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:30
‪TYPO3\CMS\Core\Authentication\Mfa\MfaProviderPropertyManager\hasProviderEntry
‪hasProviderEntry()
Definition: MfaProviderPropertyManager.php:49
‪TYPO3\CMS\Core\Messaging\FlashMessage
Definition: FlashMessage.php:27
‪TYPO3\CMS\Fluid\View\StandaloneView
Definition: StandaloneView.php:30
‪$GLOBALS
‪$GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['adminpanel']['modules']
Definition: ext_localconf.php:25
‪TYPO3\CMS\Core\Authentication\Mfa\Provider\RecoveryCodesProvider\__construct
‪__construct(protected readonly MfaProviderRegistry $mfaProviderRegistry, protected readonly Context $context, protected readonly UriBuilder $uriBuilder, protected readonly FlashMessageService $flashMessageService, protected readonly HashService $hashService,)
Definition: RecoveryCodesProvider.php:46
‪TYPO3\CMS\Core\Authentication\Mfa\Provider\RecoveryCodesProvider\addFlashMessage
‪addFlashMessage(string $message, string $title='', ContextualFeedbackSeverity $severity=ContextualFeedbackSeverity::INFO)
Definition: RecoveryCodesProvider.php:343
‪TYPO3\CMS\Core\Authentication\Mfa\MfaProviderPropertyManager\updateProperties
‪updateProperties(array $properties)
Definition: MfaProviderPropertyManager.php:84
‪TYPO3\CMS\Core\Authentication\Mfa\Provider\RecoveryCodesProvider\verify
‪verify(ServerRequestInterface $request, MfaProviderPropertyManager $propertyManager)
Definition: RecoveryCodesProvider.php:95
‪TYPO3\CMS\Core\Localization\LanguageService
Definition: LanguageService.php:46
‪TYPO3\CMS\Core\Utility\GeneralUtility
Definition: GeneralUtility.php:52
‪TYPO3\CMS\Core\Authentication\Mfa\MfaViewType
‪MfaViewType
Definition: MfaViewType.php:24
‪TYPO3\CMS\Core\Crypto\HashService
Definition: HashService.php:27
‪TYPO3\CMS\Core\Messaging\FlashMessageService
Definition: FlashMessageService.php:27
‪TYPO3\CMS\Core\Utility\GeneralUtility\trimExplode
‪static list< string > trimExplode(string $delim, string $string, bool $removeEmptyValues=false, int $limit=0)
Definition: GeneralUtility.php:822
‪TYPO3\CMS\Webhooks\Message\$identifier
‪identifier readonly string $identifier
Definition: FileAddedMessage.php:37
‪TYPO3\CMS\Core\Http\HtmlResponse
Definition: HtmlResponse.php:28
‪TYPO3\CMS\Core\Authentication\Mfa\MfaProviderRegistry
Definition: MfaProviderRegistry.php:28
‪TYPO3\CMS\Core\Authentication\Mfa\Provider\RecoveryCodesProvider\getDateTime
‪getDateTime(int $timestamp)
Definition: RecoveryCodesProvider.php:353