‪TYPO3CMS  ‪main
TotpProvider.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 BaconQrCode\Renderer\Image\SvgImageBackEnd;
21 use BaconQrCode\Renderer\ImageRenderer;
22 use BaconQrCode\Renderer\RendererStyle\RendererStyle;
23 use BaconQrCode\Writer;
24 use Psr\Http\Message\ResponseInterface;
25 use Psr\Http\Message\ServerRequestInterface;
34 
41 {
42  private const ‪MAX_ATTEMPTS = 3;
43 
44  public function ‪__construct(protected readonly ‪Context $context, protected readonly ‪HashService $hashService) {}
45 
49  public function ‪canProcess(ServerRequestInterface $request): bool
50  {
51  return $this->‪getTotp($request) !== '';
52  }
53 
58  public function ‪isActive(‪MfaProviderPropertyManager $propertyManager): bool
59  {
60  return (bool)$propertyManager->‪getProperty('active')
61  && $propertyManager->‪getProperty('secret', '') !== '';
62  }
63 
68  public function ‪isLocked(‪MfaProviderPropertyManager $propertyManager): bool
69  {
70  $attempts = (int)$propertyManager->‪getProperty('attempts', 0);
71 
72  // Assume the provider is locked in case the maximum attempts are exceeded.
73  // A provider however can only be locked if set up - an entry exists in database.
74  return $propertyManager->‪hasProviderEntry() && $attempts >= ‪self::MAX_ATTEMPTS;
75  }
76 
80  public function ‪verify(ServerRequestInterface $request, ‪MfaProviderPropertyManager $propertyManager): bool
81  {
82  if (!$this->‪isActive($propertyManager) || $this->‪isLocked($propertyManager)) {
83  // Can not verify an inactive or locked provider
84  return false;
85  }
86 
87  $totp = $this->‪getTotp($request);
88  $secret = $propertyManager->‪getProperty('secret', '');
89  $verified = GeneralUtility::makeInstance(Totp::class, $secret)->verifyTotp($totp, 2);
90  if (!$verified) {
91  $attempts = $propertyManager->‪getProperty('attempts', 0);
92  $propertyManager->‪updateProperties(['attempts' => ++$attempts]);
93  return false;
94  }
95  $propertyManager->‪updateProperties([
96  'attempts' => 0,
97  'lastUsed' => $this->context->getPropertyFromAspect('date', 'timestamp'),
98  ]);
99  return true;
100  }
101 
106  public function ‪activate(ServerRequestInterface $request, ‪MfaProviderPropertyManager $propertyManager): bool
107  {
108  if ($this->‪isActive($propertyManager)) {
109  // Can not activate an active provider
110  return false;
111  }
112 
113  if (!$this->‪canProcess($request)) {
114  // Return since the request can not be processed by this provider
115  return false;
116  }
117 
118  $secret = (string)($request->getParsedBody()['secret'] ?? '');
119  $checksum = (string)($request->getParsedBody()['checksum'] ?? '');
120  if ($secret === '' || !hash_equals($this->hashService->hmac($secret, 'totp-setup'), $checksum)) {
121  // Return since the request does not contain the initially created secret
122  return false;
123  }
124 
125  $totpInstance = GeneralUtility::makeInstance(Totp::class, $secret);
126  if (!$totpInstance->verifyTotp($this->getTotp($request), 2)) {
127  // Return since the given TOTP could not be verified
128  return false;
129  }
130 
131  // If valid, prepare the provider properties to be stored
132  $properties = ['secret' => $secret, 'active' => true];
133  if (($name = (string)($request->getParsedBody()['name'] ?? '')) !== '') {
134  $properties['name'] = $name;
135  }
136 
137  // Usually there should be no entry if the provider is not activated, but to prevent the
138  // provider from being unable to activate again, we update the existing entry in such case.
139  return $propertyManager->‪hasProviderEntry()
140  ? $propertyManager->‪updateProperties($properties)
141  : $propertyManager->‪createProviderEntry($properties);
142  }
143 
147  public function ‪update(ServerRequestInterface $request, ‪MfaProviderPropertyManager $propertyManager): bool
148  {
149  if (!$this->‪isActive($propertyManager) || $this->‪isLocked($propertyManager)) {
150  // Can not update an inactive or locked provider
151  return false;
152  }
153 
154  $name = (string)($request->getParsedBody()['name'] ?? '');
155  if ($name !== '') {
156  return $propertyManager->‪updateProperties(['name' => $name]);
157  }
158 
159  // Provider properties successfully updated
160  return true;
161  }
162 
166  public function ‪unlock(ServerRequestInterface $request, ‪MfaProviderPropertyManager $propertyManager): bool
167  {
168  if (!$this->‪isActive($propertyManager) || !$this->‪isLocked($propertyManager)) {
169  // Can not unlock an inactive or not locked provider
170  return false;
171  }
172 
173  // Reset the attempts
174  return $propertyManager->‪updateProperties(['attempts' => 0]);
175  }
176 
182  public function ‪deactivate(ServerRequestInterface $request, ‪MfaProviderPropertyManager $propertyManager): bool
183  {
184  if (!$this->‪isActive($propertyManager)) {
185  // Can not deactivate an inactive provider
186  return false;
187  }
188 
189  // Delete the provider entry
190  return $propertyManager->‪deleteProviderEntry();
191  }
192 
197  public function ‪handleRequest(
198  ServerRequestInterface $request,
199  ‪MfaProviderPropertyManager $propertyManager,
200  ‪MfaViewType $type
201  ): ResponseInterface {
202  $view = GeneralUtility::makeInstance(StandaloneView::class);
203  $view->setTemplateRootPaths(['EXT:core/Resources/Private/Templates/Authentication/MfaProvider/Totp']);
204  switch ($type) {
205  case MfaViewType::SETUP:
206  $this->‪prepareSetupView($view, $propertyManager);
207  break;
208  case MfaViewType::EDIT:
209  $this->‪prepareEditView($view, $propertyManager);
210  break;
212  $this->‪prepareAuthView($view, $propertyManager);
213  break;
214  }
215  return new ‪HtmlResponse($view->assign('providerIdentifier', $propertyManager->‪getIdentifier())->render());
216  }
217 
223  protected function ‪prepareSetupView(‪StandaloneView $view, ‪MfaProviderPropertyManager $propertyManager): void
224  {
225  $userData = $propertyManager->‪getUser()->user ?? [];
226  $secret = ‪Totp::generateEncodedSecret([(string)($userData['uid'] ?? ''), (string)($userData['username'] ?? '')]);
227  $totpInstance = GeneralUtility::makeInstance(Totp::class, $secret);
228  $totpAuthUrl = $totpInstance->getTotpAuthUrl(
229  (string)(‪$GLOBALS['TYPO3_CONF_VARS']['SYS']['sitename'] ?? 'TYPO3'),
230  (string)($userData['email'] ?? '') ?: (string)($userData['username'] ?? '')
231  );
232  $view->‪setTemplate('Setup');
233  $view->assignMultiple([
234  'secret' => $secret,
235  'totpAuthUrl' => $totpAuthUrl,
236  'qrCode' => $this->‪getSvgQrCode($totpAuthUrl),
237  // Generate hmac of the secret to prevent it from being changed in the setup from
238  'checksum' => $this->hashService->hmac($secret, 'totp-setup'),
239  ]);
240  }
241 
245  protected function ‪prepareEditView(‪StandaloneView $view, ‪MfaProviderPropertyManager $propertyManager): void
246  {
247  $view->‪setTemplate('Edit');
248  $view->assignMultiple([
249  'name' => $propertyManager->‪getProperty('name'),
250  'lastUsed' => $this->getDateTime($propertyManager->‪getProperty('lastUsed', 0)),
251  'updated' => $this->getDateTime($propertyManager->‪getProperty('updated', 0)),
252  ]);
253  }
254 
258  protected function ‪prepareAuthView(‪StandaloneView $view, ‪MfaProviderPropertyManager $propertyManager): void
259  {
260  $view->‪setTemplate('Auth');
261  $view->assign('isLocked', $this->‪isLocked($propertyManager));
262  }
263 
267  protected function ‪getTotp(ServerRequestInterface $request): string
268  {
269  return trim((string)($request->getQueryParams()['totp'] ?? $request->getParsedBody()['totp'] ?? ''));
270  }
271 
275  protected function ‪getSvgQrCode(string $content): string
276  {
277  $qrCodeRenderer = new ImageRenderer(
278  new RendererStyle(225, 4),
279  new SvgImageBackEnd()
280  );
281 
282  return (new Writer($qrCodeRenderer))->writeString($content);
283  }
284 
288  protected function ‪getDateTime(int $timestamp): string
289  {
290  if ($timestamp === 0) {
291  return '';
292  }
293 
294  return date(
295  ‪$GLOBALS['TYPO3_CONF_VARS']['SYS']['ddmmyy'] . ' ' . ‪$GLOBALS['TYPO3_CONF_VARS']['SYS']['hhmm'],
296  $timestamp
297  ) ?: '';
298  }
299 }
‪TYPO3\CMS\Core\Authentication\Mfa\Provider\TotpProvider\getTotp
‪getTotp(ServerRequestInterface $request)
Definition: TotpProvider.php:267
‪TYPO3\CMS\Core\Authentication\Mfa\Provider\TotpProvider\prepareAuthView
‪prepareAuthView(StandaloneView $view, MfaProviderPropertyManager $propertyManager)
Definition: TotpProvider.php:258
‪TYPO3\CMS\Core\Authentication\Mfa\Provider\TotpProvider\getDateTime
‪getDateTime(int $timestamp)
Definition: TotpProvider.php:288
‪TYPO3\CMS\Core\Authentication\Mfa\Provider
Definition: RecoveryCodes.php:18
‪TYPO3\CMS\Core\Authentication\Mfa\Provider\TotpProvider\isLocked
‪isLocked(MfaProviderPropertyManager $propertyManager)
Definition: TotpProvider.php:68
‪TYPO3\CMS\Core\Authentication\Mfa\Provider\TotpProvider\prepareSetupView
‪prepareSetupView(StandaloneView $view, MfaProviderPropertyManager $propertyManager)
Definition: TotpProvider.php:223
‪TYPO3\CMS\Core\Authentication\Mfa\Provider\TotpProvider\verify
‪verify(ServerRequestInterface $request, MfaProviderPropertyManager $propertyManager)
Definition: TotpProvider.php:80
‪TYPO3\CMS\Core\Authentication\Mfa\Provider\TotpProvider\prepareEditView
‪prepareEditView(StandaloneView $view, MfaProviderPropertyManager $propertyManager)
Definition: TotpProvider.php:245
‪TYPO3\CMS\Core\Authentication\Mfa\Provider\TotpProvider\update
‪update(ServerRequestInterface $request, MfaProviderPropertyManager $propertyManager)
Definition: TotpProvider.php:147
‪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\Provider\TotpProvider
Definition: TotpProvider.php:41
‪TYPO3\CMS\Core\Authentication\Mfa\AUTH
‪@ AUTH
Definition: MfaViewType.php:27
‪TYPO3\CMS\Core\Authentication\Mfa\Provider\TotpProvider\handleRequest
‪handleRequest(ServerRequestInterface $request, MfaProviderPropertyManager $propertyManager, MfaViewType $type)
Definition: TotpProvider.php:197
‪TYPO3\CMS\Core\Authentication\Mfa\Provider\TotpProvider\deactivate
‪deactivate(ServerRequestInterface $request, MfaProviderPropertyManager $propertyManager)
Definition: TotpProvider.php:182
‪TYPO3\CMS\Core\Authentication\Mfa\Provider\TotpProvider\unlock
‪unlock(ServerRequestInterface $request, MfaProviderPropertyManager $propertyManager)
Definition: TotpProvider.php:166
‪TYPO3\CMS\Core\Authentication\Mfa\MfaProviderPropertyManager\getIdentifier
‪getIdentifier()
Definition: MfaProviderPropertyManager.php:185
‪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\MfaProviderPropertyManager\getUser
‪getUser()
Definition: MfaProviderPropertyManager.php:177
‪TYPO3\CMS\Fluid\View\AbstractTemplateView\setTemplate
‪setTemplate(string $templateName)
Definition: AbstractTemplateView.php:45
‪TYPO3\CMS\Core\Authentication\Mfa\MfaProviderPropertyManager\createProviderEntry
‪createProviderEntry(array $properties)
Definition: MfaProviderPropertyManager.php:108
‪TYPO3\CMS\Core\Authentication\Mfa\MfaProviderPropertyManager
Definition: MfaProviderPropertyManager.php:33
‪TYPO3\CMS\Core\Authentication\Mfa\Provider\Totp\generateEncodedSecret
‪static generateEncodedSecret(array $additionalAuthFactors=[])
Definition: Totp.php:177
‪TYPO3\CMS\Core\Authentication\Mfa\MfaProviderPropertyManager\hasProviderEntry
‪hasProviderEntry()
Definition: MfaProviderPropertyManager.php:49
‪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\TotpProvider\MAX_ATTEMPTS
‪const MAX_ATTEMPTS
Definition: TotpProvider.php:42
‪TYPO3\CMS\Core\Authentication\Mfa\MfaProviderPropertyManager\updateProperties
‪updateProperties(array $properties)
Definition: MfaProviderPropertyManager.php:84
‪TYPO3\CMS\Core\Authentication\Mfa\Provider\TotpProvider\activate
‪activate(ServerRequestInterface $request, MfaProviderPropertyManager $propertyManager)
Definition: TotpProvider.php:106
‪TYPO3\CMS\Core\Authentication\Mfa\Provider\TotpProvider\__construct
‪__construct(protected readonly Context $context, protected readonly HashService $hashService)
Definition: TotpProvider.php:44
‪TYPO3\CMS\Core\Authentication\Mfa\Provider\TotpProvider\canProcess
‪canProcess(ServerRequestInterface $request)
Definition: TotpProvider.php:49
‪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\Authentication\Mfa\Provider\TotpProvider\getSvgQrCode
‪getSvgQrCode(string $content)
Definition: TotpProvider.php:275
‪TYPO3\CMS\Core\Http\HtmlResponse
Definition: HtmlResponse.php:28
‪TYPO3\CMS\Core\Authentication\Mfa\Provider\TotpProvider\isActive
‪isActive(MfaProviderPropertyManager $propertyManager)
Definition: TotpProvider.php:58