‪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;
37 
44 {
45  public function ‪__construct(
46  protected readonly ‪MfaProviderRegistry $mfaProviderRegistry,
47  protected readonly ‪Context $context,
48  protected readonly ‪UriBuilder $uriBuilder,
49  protected readonly ‪FlashMessageService $flashMessageService
50  ) {}
51 
52  private const ‪MAX_ATTEMPTS = 3;
53 
57  public function ‪canProcess(ServerRequestInterface $request): bool
58  {
59  return $this->‪getRecoveryCode($request) !== '';
60  }
61 
68  public function ‪isActive(‪MfaProviderPropertyManager $propertyManager): bool
69  {
70  return (bool)$propertyManager->‪getProperty('active')
71  && $this->‪activeProvidersExist($propertyManager);
72  }
73 
79  public function ‪isLocked(‪MfaProviderPropertyManager $propertyManager): bool
80  {
81  $attempts = (int)$propertyManager->‪getProperty('attempts', 0);
82  $codes = (array)$propertyManager->‪getProperty('codes', []);
83 
84  // Assume the provider is locked in case either the maximum attempts are exceeded or no codes
85  // are available. A provider however can only be locked if set up - an entry exists in database.
86  return $propertyManager->‪hasProviderEntry() && ($attempts >= self::MAX_ATTEMPTS || $codes === []);
87  }
88 
93  public function ‪verify(ServerRequestInterface $request, ‪MfaProviderPropertyManager $propertyManager): bool
94  {
95  if (!$this->‪isActive($propertyManager) || $this->‪isLocked($propertyManager)) {
96  // Can not verify an inactive or locked provider
97  return false;
98  }
99 
100  $recoveryCode = $this->‪getRecoveryCode($request);
101  $codes = $propertyManager->‪getProperty('codes', []);
102  $recoveryCodes = GeneralUtility::makeInstance(RecoveryCodes::class, $this->‪getMode($propertyManager));
103  if (!$recoveryCodes->verifyRecoveryCode($recoveryCode, $codes)) {
104  $attempts = $propertyManager->‪getProperty('attempts', 0);
105  $propertyManager->‪updateProperties(['attempts' => ++$attempts]);
106  return false;
107  }
108 
109  // Since the codes were passed by reference to the verify method, the matching code was
110  // unset so we simply need to write the array back. However, if the update fails, we must
111  // return FALSE even if the authentication was successful to prevent data inconsistency.
112  return $propertyManager->‪updateProperties([
113  'codes' => $codes,
114  'attempts' => 0,
115  'lastUsed' => $this->context->getPropertyFromAspect('date', 'timestamp'),
116  ]);
117  }
118 
124  public function ‪handleRequest(
125  ServerRequestInterface $request,
126  ‪MfaProviderPropertyManager $propertyManager,
127  ‪MfaViewType $type
128  ): ResponseInterface {
129  $view = GeneralUtility::makeInstance(StandaloneView::class);
130  $view->setTemplateRootPaths(['EXT:core/Resources/Private/Templates/Authentication/MfaProvider/RecoveryCodes']);
131  switch ($type) {
132  case MfaViewType::SETUP:
133  if (!$this->‪activeProvidersExist($propertyManager)) {
134  // If no active providers are present for the current user, add a flash message and redirect
135  $lang = $this->‪getLanguageService();
136  $this->‪addFlashMessage(
137  $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_mfa_provider.xlf:setup.recoveryCodes.noActiveProviders.message'),
138  $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_mfa_provider.xlf:setup.recoveryCodes.noActiveProviders.title'),
139  ContextualFeedbackSeverity::WARNING
140  );
141  if (($normalizedParams = $request->getAttribute('normalizedParams'))) {
142  $returnUrl = $normalizedParams->getHttpReferer();
143  } else {
144  // @todo this will not work for FE - make this more generic!
145  $returnUrl = $this->uriBuilder->buildUriFromRoute('mfa');
146  }
147  throw new ‪PropagateResponseException(new ‪RedirectResponse($returnUrl, 303), 1612883326);
148  }
149  $codes = GeneralUtility::makeInstance(RecoveryCodes::class, $this->‪getMode($propertyManager))->generatePlainRecoveryCodes();
150  $view->setTemplate('Setup');
151  $view->assignMultiple([
152  'recoveryCodes' => implode(PHP_EOL, $codes),
153  // Generate hmac of the recovery codes to prevent them from being changed in the setup from
154  'checksum' => ‪GeneralUtility::hmac(json_encode($codes) ?: '', 'recovery-codes-setup'),
155  ]);
156  break;
157  case MfaViewType::EDIT:
158  $view->setTemplate('Edit');
159  $view->assignMultiple([
160  'name' => $propertyManager->‪getProperty('name'),
161  'amountOfCodesLeft' => count($propertyManager->‪getProperty('codes', [])),
162  'lastUsed' => $this->getDateTime($propertyManager->‪getProperty('lastUsed', 0)),
163  'updated' => $this->getDateTime($propertyManager->‪getProperty('updated', 0)),
164  ]);
165  break;
167  $view->setTemplate('Auth');
168  $view->assign('isLocked', $this->‪isLocked($propertyManager));
169  break;
170  }
171  return new ‪HtmlResponse($view->assign('providerIdentifier', $propertyManager->‪getIdentifier())->render());
172  }
173 
177  public function ‪activate(ServerRequestInterface $request, ‪MfaProviderPropertyManager $propertyManager): bool
178  {
179  if ($this->‪isActive($propertyManager)) {
180  // Can not activate an active provider
181  return false;
182  }
183 
184  if (!$this->‪activeProvidersExist($propertyManager)) {
185  // Can not activate since no other provider is activated yet
186  return false;
187  }
188 
189  $recoveryCodes = ‪GeneralUtility::trimExplode(PHP_EOL, (string)($request->getParsedBody()['recoveryCodes'] ?? ''));
190  $checksum = (string)($request->getParsedBody()['checksum'] ?? '');
191  if ($recoveryCodes === []
192  || !hash_equals(‪GeneralUtility::hmac(json_encode($recoveryCodes) ?: '', 'recovery-codes-setup'), $checksum)
193  ) {
194  // Return since the request does not contain the initially created recovery codes
195  return false;
196  }
197 
198  // Hash given plain recovery codes and prepare the properties array with active state and custom name
199  $hashedCodes = GeneralUtility::makeInstance(RecoveryCodes::class, $this->‪getMode($propertyManager))->generatedHashedRecoveryCodes($recoveryCodes);
200  $properties = ['codes' => $hashedCodes, 'active' => true];
201  if (($name = (string)($request->getParsedBody()['name'] ?? '')) !== '') {
202  $properties['name'] = $name;
203  }
204 
205  // Usually there should be no entry if the provider is not activated, but to prevent the
206  // provider from being unable to activate again, we update the existing entry in such case.
207  return $propertyManager->‪hasProviderEntry()
208  ? $propertyManager->‪updateProperties($properties)
209  : $propertyManager->‪createProviderEntry($properties);
210  }
211 
215  public function ‪deactivate(ServerRequestInterface $request, ‪MfaProviderPropertyManager $propertyManager): bool
216  {
217  // Only check for the active property here to enable bulk deactivation,
218  // e.g. in FormEngine. Otherwise it would not be possible to deactivate
219  // this provider if the last "fully" provider was deactivated before.
220  if (!(bool)$propertyManager->‪getProperty('active')) {
221  // Can not deactivate an inactive provider
222  return false;
223  }
224 
225  // Delete the provider entry
226  return $propertyManager->‪deleteProviderEntry();
227  }
228 
233  public function ‪unlock(ServerRequestInterface $request, ‪MfaProviderPropertyManager $propertyManager): bool
234  {
235  if (!$this->‪isActive($propertyManager) || !$this->‪isLocked($propertyManager)) {
236  // Can not unlock an inactive or not locked provider
237  return false;
238  }
239 
240  // Reset attempts
241  if ((int)$propertyManager->‪getProperty('attempts', 0) !== 0
242  && !$propertyManager->‪updateProperties(['attempts' => 0])
243  ) {
244  // Could not reset the attempts, so we can not unlock the provider
245  return false;
246  }
247 
248  // Regenerate codes
249  if ($propertyManager->‪getProperty('codes', []) === []) {
250  // Generate new codes and store the hashed ones
251  $recoveryCodes = GeneralUtility::makeInstance(RecoveryCodes::class, $this->‪getMode($propertyManager))->generateRecoveryCodes();
252  if (!$propertyManager->‪updateProperties(['codes' => array_values($recoveryCodes)])) {
253  // Codes could not be stored, so we can not unlock the provider
254  return false;
255  }
256  // Add the newly generated codes to a flash message so the user can copy them
257  $lang = $this->‪getLanguageService();
258  $this->‪addFlashMessage(
259  sprintf(
260  $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_mfa_provider.xlf:unlock.recoveryCodes.message'),
261  implode(' ', array_keys($recoveryCodes))
262  ),
263  $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_mfa_provider.xlf:unlock.recoveryCodes.title'),
264  ContextualFeedbackSeverity::WARNING
265  );
266  }
267 
268  return true;
269  }
270 
271  public function ‪update(ServerRequestInterface $request, ‪MfaProviderPropertyManager $propertyManager): bool
272  {
273  if (!$this->‪isActive($propertyManager) || $this->‪isLocked($propertyManager)) {
274  // Can not update an inactive or locked provider
275  return false;
276  }
277 
278  $name = (string)($request->getParsedBody()['name'] ?? '');
279  if ($name !== '' && !$propertyManager->‪updateProperties(['name' => $name])) {
280  return false;
281  }
282 
283  if ((bool)($request->getParsedBody()['regenerateCodes'] ?? false)) {
284  // Generate new codes and store the hashed ones
285  $recoveryCodes = GeneralUtility::makeInstance(RecoveryCodes::class, $this->‪getMode($propertyManager))->generateRecoveryCodes();
286  if (!$propertyManager->‪updateProperties(['codes' => array_values($recoveryCodes)])) {
287  // Codes could not be stored, so we can not update the provider
288  return false;
289  }
290  // Add the newly generated codes to a flash message so the user can copy them
291  $lang = $this->‪getLanguageService();
292  $this->‪addFlashMessage(
293  sprintf(
294  $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_mfa_provider.xlf:update.recoveryCodes.message'),
295  implode(' ', array_keys($recoveryCodes))
296  ),
297  $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_mfa_provider.xlf:update.recoveryCodes.title'),
298  ContextualFeedbackSeverity::OK
299  );
300  }
301 
302  // Provider properties successfully updated
303  return true;
304  }
305 
309  protected function ‪activeProvidersExist(‪MfaProviderPropertyManager $currentPropertyManager): bool
310  {
311  $user = $currentPropertyManager->‪getUser();
312  foreach ($this->mfaProviderRegistry->getProviders() as ‪$identifier => $provider) {
313  $propertyManager = ‪MfaProviderPropertyManager::create($provider, $user);
314  if (‪$identifier !== $currentPropertyManager->‪getIdentifier() && $provider->isActive($propertyManager)) {
315  return true;
316  }
317  }
318  return false;
319  }
320 
324  protected function ‪getRecoveryCode(ServerRequestInterface $request): string
325  {
326  return trim((string)($request->getQueryParams()['rc'] ?? $request->getParsedBody()['rc'] ?? ''));
327  }
328 
332  protected function ‪getMode(‪MfaProviderPropertyManager $propertyManager): string
333  {
334  return $propertyManager->‪getUser()->loginType;
335  }
336 
341  protected function ‪addFlashMessage(string $message, string $title = '', ‪ContextualFeedbackSeverity $severity = ContextualFeedbackSeverity::INFO): void
342  {
343  $this->flashMessageService->getMessageQueueByIdentifier()->enqueue(
344  GeneralUtility::makeInstance(FlashMessage::class, $message, $title, $severity, true)
345  );
346  }
347 
351  protected function ‪getDateTime(int $timestamp): string
352  {
353  if ($timestamp === 0) {
354  return '';
355  }
356 
357  return date(
358  ‪$GLOBALS['TYPO3_CONF_VARS']['SYS']['ddmmyy'] . ' ' . ‪$GLOBALS['TYPO3_CONF_VARS']['SYS']['hhmm'],
359  $timestamp
360  ) ?: '';
361  }
362 
364  {
365  return ‪$GLOBALS['LANG'];
366  }
367 }
‪TYPO3\CMS\Core\Authentication\Mfa\Provider
Definition: RecoveryCodes.php:18
‪TYPO3\CMS\Core\Authentication\Mfa\Provider\RecoveryCodesProvider\getLanguageService
‪getLanguageService()
Definition: RecoveryCodesProvider.php:363
‪TYPO3\CMS\Core\Authentication\Mfa\Provider\RecoveryCodesProvider\activeProvidersExist
‪activeProvidersExist(MfaProviderPropertyManager $currentPropertyManager)
Definition: RecoveryCodesProvider.php:309
‪TYPO3\CMS\Core\Authentication\Mfa\Provider\RecoveryCodesProvider\unlock
‪unlock(ServerRequestInterface $request, MfaProviderPropertyManager $propertyManager)
Definition: RecoveryCodesProvider.php:233
‪TYPO3\CMS\Core\Authentication\Mfa\Provider\RecoveryCodesProvider\isActive
‪isActive(MfaProviderPropertyManager $propertyManager)
Definition: RecoveryCodesProvider.php:68
‪TYPO3\CMS\Core\Authentication\Mfa\Provider\RecoveryCodesProvider\isLocked
‪isLocked(MfaProviderPropertyManager $propertyManager)
Definition: RecoveryCodesProvider.php:79
‪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:44
‪TYPO3\CMS\Core\Authentication\Mfa\Provider\RecoveryCodesProvider\handleRequest
‪handleRequest(ServerRequestInterface $request, MfaProviderPropertyManager $propertyManager, MfaViewType $type)
Definition: RecoveryCodesProvider.php:124
‪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:57
‪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:332
‪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:324
‪TYPO3\CMS\Core\Authentication\Mfa\Provider\RecoveryCodesProvider\MAX_ATTEMPTS
‪const MAX_ATTEMPTS
Definition: RecoveryCodesProvider.php:52
‪TYPO3\CMS\Core\Authentication\Mfa\Provider\RecoveryCodesProvider\activate
‪activate(ServerRequestInterface $request, MfaProviderPropertyManager $propertyManager)
Definition: RecoveryCodesProvider.php:177
‪TYPO3\CMS\Core\Utility\GeneralUtility\hmac
‪static string hmac($input, $additionalSecret='')
Definition: GeneralUtility.php:474
‪TYPO3\CMS\Core\Authentication\Mfa\Provider\RecoveryCodesProvider\deactivate
‪deactivate(ServerRequestInterface $request, MfaProviderPropertyManager $propertyManager)
Definition: RecoveryCodesProvider.php:215
‪TYPO3\CMS\Core\Authentication\Mfa\Provider\RecoveryCodesProvider\update
‪update(ServerRequestInterface $request, MfaProviderPropertyManager $propertyManager)
Definition: RecoveryCodesProvider.php:271
‪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\Authentication\Mfa\Provider\RecoveryCodesProvider\__construct
‪__construct(protected readonly MfaProviderRegistry $mfaProviderRegistry, protected readonly Context $context, protected readonly UriBuilder $uriBuilder, protected readonly FlashMessageService $flashMessageService)
Definition: RecoveryCodesProvider.php:45
‪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\addFlashMessage
‪addFlashMessage(string $message, string $title='', ContextualFeedbackSeverity $severity=ContextualFeedbackSeverity::INFO)
Definition: RecoveryCodesProvider.php:341
‪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:93
‪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\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:817
‪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:351