‪TYPO3CMS  ‪main
MfaConfigurationControllerTest.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 PHPUnit\Framework\Attributes\DataProvider;
21 use PHPUnit\Framework\Attributes\Test;
38 use TYPO3\TestingFramework\Core\Functional\FunctionalTestCase;
39 
40 final class ‪MfaConfigurationControllerTest extends FunctionalTestCase
41 {
46 
48  'BE' => [
49  'recommendedMfaProvider' => 'totp',
50  'requireMfa' => 1,
51  ],
52  ];
53 
54  protected function ‪setUp(): void
55  {
56  parent::setUp();
57  $this->importCSVDataSet(__DIR__ . '/../Fixtures/be_users.csv');
58  $backendUser = $this->setUpBackendUser(1);
59  ‪$GLOBALS['LANG'] = $this->get(LanguageServiceFactory::class)->createFromUserPreferences($backendUser);
60 
61  $this->subject = new ‪MfaConfigurationController(
62  $this->get(IconFactory::class),
63  $this->get(UriBuilder::class),
64  $this->get(ModuleTemplateFactory::class),
65  );
66  $this->subject->injectMfaProviderRegistry($this->get(MfaProviderRegistry::class));
67  $this->hashService = new ‪HashService();
68  $this->request = (new ‪ServerRequest('https://example.com/typo3/'))
69  ->withAttribute('applicationType', ‪SystemEnvironmentBuilder::REQUESTTYPE_BE)
70  ->withAttribute('route', new ‪Route('path', ['packageName' => 'typo3/cms-backend']));
71  $this->normalizedParams = new ‪NormalizedParams([], [], '', '');
72  }
73 
74  #[Test]
76  {
77  $queryParams = [
78  'action' => 'unknown',
79  ];
80 
81  ‪$request = $this->request
82  ->‪withAttribute('normalizedParams', $this->normalizedParams)
83  ->withQueryParams($queryParams);
84  ‪$GLOBALS['TYPO3_REQUEST'] = ‪$request;
85  $response = $this->subject->handleRequest(‪$request);
86 
87  self::assertEquals(400, $response->getStatusCode());
88  self::assertEquals('Action not allowed', $response->getBody()->getContents());
89  }
90 
91  #[Test]
93  {
94  ‪$request = $this->request->‪withAttribute('normalizedParams', $this->normalizedParams);
95  ‪$GLOBALS['TYPO3_REQUEST'] = ‪$request;
96  $response = $this->subject->handleRequest(‪$request);
97 
98  self::assertEquals(200, $response->getStatusCode());
99  $response->getBody()->rewind();
100  self::assertStringContainsString('Multi-factor Authentication Overview', $response->getBody()->getContents());
101  }
102 
103  #[Test]
105  {
106  ‪$request = $this->request->‪withAttribute('normalizedParams', $this->normalizedParams);
107  ‪$GLOBALS['TYPO3_REQUEST'] = ‪$request;
108  $response = $this->subject->handleRequest(‪$request);
109 
110  self::assertEquals(200, $response->getStatusCode());
111 
112  $response->getBody()->rewind();
113  $responseContent = $response->getBody()->getContents();
114  foreach (GeneralUtility::makeInstance(MfaProviderRegistry::class)->getProviders() as $provider) {
115  self::assertStringContainsString('id="' . $provider->getIdentifier() . '-provider"', $responseContent);
116  }
117  }
118 
119  #[Test]
121  {
122  ‪$request = $this->request->‪withAttribute('normalizedParams', $this->normalizedParams);
123  ‪$GLOBALS['TYPO3_REQUEST'] = ‪$request;
124  $response = $this->subject->handleRequest(‪$request);
125 
126  self::assertEquals(200, $response->getStatusCode());
127  $response->getBody()->rewind();
128  $responseContent = $response->getBody()->getContents();
129  self::assertStringContainsString('Multi-factor authentication required', $responseContent);
130  self::assertMatchesRegularExpression('/<div.*class="card card-size-fixed-small card-success".*id="totp-provider"/s', $responseContent);
131  }
132 
133  #[Test]
135  {
136  ‪$GLOBALS['BE_USER']->user['mfa'] = json_encode(['totp' => ['active' => true, 'secret' => 'KRMVATZTJFZUC53FONXW2ZJB']]);
137  ‪$GLOBALS['BE_USER']->uc['mfa']['defaultProvider'] = 'totp';
138 
139  ‪$request = $this->request->‪withAttribute('normalizedParams', $this->normalizedParams);
140  ‪$GLOBALS['TYPO3_REQUEST'] = ‪$request;
141  $response = $this->subject->handleRequest(‪$request);
142 
143  self::assertEquals(200, $response->getStatusCode());
144  $response->getBody()->rewind();
145  self::assertMatchesRegularExpression('/<span.*title="Default provider">/s', $response->getBody()->getContents());
146  }
147 
148  #[Test]
150  {
151  $returnUrl = ‪Environment::getPublicPath() . '/typo3/some/module?token=123';
152 
153  $queryParams = [
154  'action' => 'overview',
155  'returnUrl' => $returnUrl,
156  ];
157 
158  ‪$request = $this->request
159  ->‪withAttribute('normalizedParams', $this->normalizedParams)
160  ->withQueryParams($queryParams);
161  ‪$GLOBALS['TYPO3_REQUEST'] = ‪$request;
162  $response = $this->subject->handleRequest(‪$request);
163 
164  self::assertEquals(200, $response->getStatusCode());
165  $response->getBody()->rewind();
166  self::assertStringContainsString('href="' . $returnUrl . '" class="btn btn-sm btn-default " title="Go back"', $response->getBody()->getContents());
167  }
168 
169  #[DataProvider('handleRequestRedirectsToOverviewOnActionProviderMismatchTestDataProvider')]
170  #[Test]
172  string $action,
173  string $provider,
174  bool $providerActive,
175  string $flashMessage
176  ): void {
177  $queryParams = [
178  'action' => $action,
179  'identifier' => $provider,
180  ];
181 
182  if ($providerActive) {
183  ‪$GLOBALS['BE_USER']->user['mfa'] = json_encode(['totp' => ['active' => true, 'secret' => 'KRMVATZTJFZUC53FONXW2ZJB']]);
184  }
185 
186  ‪$request = $this->request
187  ->‪withAttribute('normalizedParams', $this->normalizedParams)
188  ->withQueryParams($queryParams);
189  ‪$GLOBALS['TYPO3_REQUEST'] = ‪$request;
190  $response = $this->subject->handleRequest(‪$request);
191 
192  $redirect = parse_url($response->getHeaderLine('location'));
193  $query = [];
194  parse_str($redirect['query'] ?? '', $query);
195  $message = GeneralUtility::makeInstance(FlashMessageService::class)->getMessageQueueByIdentifier()->getAllMessages()[0];
196 
197  self::assertEquals(302, $response->getStatusCode());
198  self::assertEquals('/typo3/mfa', $redirect['path']);
199  self::assertEquals('overview', $query['action']);
200  self::assertEquals($flashMessage, $message->getMessage());
201  }
202 
204  {
205  yield 'Empty provider' => [
206  'setup',
207  '',
208  false,
209  'Selected MFA provider was not found!',
210  ];
211  yield 'Invalid provider' => [
212  'setup',
213  'unknown',
214  false,
215  'Selected MFA provider was not found!',
216  ];
217  yield 'Inactive provider on edit' => [
218  'edit',
219  'totp',
220  false,
221  'Selected MFA provider has to be active to perform this action!',
222  ];
223  yield 'Inactive provider on update' => [
224  'save',
225  'totp',
226  false,
227  'Selected MFA provider has to be active to perform this action!',
228  ];
229  yield 'Inactive provider on deactivate' => [
230  'deactivate',
231  'totp',
232  false,
233  'Selected MFA provider has to be active to perform this action!',
234  ];
235  yield 'Inactive provider on unlock' => [
236  'unlock',
237  'totp',
238  false,
239  'Selected MFA provider has to be active to perform this action!',
240  ];
241  yield 'Active provider on setup' => [
242  'setup',
243  'totp',
244  true,
245  'Selected MFA provider has to be inactive to perform this action!',
246  ];
247  yield 'Active provider on activate' => [
248  'activate',
249  'totp',
250  true,
251  'Selected MFA provider has to be inactive to perform this action!',
252  ];
253  }
254 
255  #[DataProvider('handleRequestForwardsToCorrectActionTestDataProvider')]
256  #[Test]
258  string $action,
259  string $provider,
260  bool $providerActive,
261  bool $redirect,
262  string $searchString
263  ): void {
264  $parsedBody = [];
265  $queryParams = [
266  'action' => $action,
267  'identifier' => $provider,
268  ];
269 
270  if ($providerActive) {
271  ‪$GLOBALS['BE_USER']->user['mfa'] = json_encode([
272  'totp' => [
273  'active' => true,
274  'secret' => 'KRMVATZTJFZUC53FONXW2ZJB',
275  'attempts' => ($action === 'unlock' ? 3 : 0),
276  ],
277  ]);
278  }
279 
280  if ($action === 'activate') {
281  $timestamp = GeneralUtility::makeInstance(Context::class)->getPropertyFromAspect('date', 'timestamp');
282  $parsedBody['totp'] = GeneralUtility::makeInstance(
283  Totp::class,
284  'KRMVATZTJFZUC53FONXW2ZJB'
285  )->generateTotp((int)floor($timestamp / 30));
286  $parsedBody['secret'] = 'KRMVATZTJFZUC53FONXW2ZJB';
287  $parsedBody['checksum'] = $this->hashService->hmac('KRMVATZTJFZUC53FONXW2ZJB', 'totp-setup');
288  }
289 
290  ‪$request = $this->request
291  ->‪withAttribute('normalizedParams', $this->normalizedParams)
292  ->withQueryParams($queryParams)
293  ->withParsedBody($parsedBody);
294  ‪$GLOBALS['TYPO3_REQUEST'] = ‪$request;
295  $response = $this->subject->handleRequest(‪$request);
296 
297  if ($redirect) {
298  self::assertEquals(302, $response->getStatusCode());
299  $messages = GeneralUtility::makeInstance(FlashMessageService::class)->getMessageQueueByIdentifier()->getAllMessages()[0];
300  self::assertEquals($searchString, $messages->getMessage());
301  } else {
302  self::assertEquals(200, $response->getStatusCode());
303  $response->getBody()->rewind();
304  self::assertStringContainsString($searchString, $response->getBody()->getContents());
305  }
306  }
307 
308  public static function ‪handleRequestForwardsToCorrectActionTestDataProvider(): \Generator
309  {
310  yield 'Edit provider' => [
311  'edit',
312  'totp',
313  true,
314  false,
315  'Edit Time-based one-time password',
316  ];
317  yield 'Save provider' => [
318  'save',
319  'totp',
320  true,
321  true,
322  'Successfully updated MFA provider Time-based one-time password.',
323  ];
324  yield 'Deactivate provider' => [
325  'deactivate',
326  'totp',
327  true,
328  true,
329  'Successfully deactivated MFA provider Time-based one-time password.',
330  ];
331  yield 'Unlock provider' => [
332  'unlock',
333  'totp',
334  true,
335  true,
336  'Successfully unlocked MFA provider Time-based one-time password.',
337  ];
338  yield 'Setup provider' => [
339  'setup',
340  'totp',
341  false,
342  false,
343  'Set up Time-based one-time password',
344  ];
345  yield 'Activate provider' => [
346  'activate',
347  'totp',
348  false,
349  true,
350  'Successfully activated MFA provider Time-based one-time password.',
351  ];
352  }
353 
354  #[DataProvider('handleRequestAddsFormOnInteractionViewsTestTestDataProvider')]
355  #[Test]
357  string $action,
358  bool $providerActive,
359  string $providerContent
360  ): void {
361  $queryParams = [
362  'action' => $action,
363  'identifier' => 'totp',
364  ];
365 
366  if ($providerActive) {
367  ‪$GLOBALS['BE_USER']->user['mfa'] = json_encode(['totp' => ['active' => true, 'secret' => 'KRMVATZTJFZUC53FONXW2ZJB']]);
368  }
369 
370  ‪$request = $this->request
371  ->‪withAttribute('normalizedParams', $this->normalizedParams)
372  ->withQueryParams($queryParams);
373  ‪$GLOBALS['TYPO3_REQUEST'] = ‪$request;
374  $response = $this->subject->handleRequest(‪$request);
375 
376  $response->‪getBody()->rewind();
377  $responseContent = $response->getBody()->getContents();
378 
379  self::assertEquals(200, $response->getStatusCode());
380  self::assertMatchesRegularExpression('/<a.*href="\/typo3\/mfa.*title="Close">/s', $responseContent);
381  self::assertMatchesRegularExpression('/<button.*name="save".*form="mfaConfigurationController">/s', $responseContent);
382  self::assertMatchesRegularExpression('/<form.*name="' . $action . '".*id="mfaConfigurationController">/s', $responseContent);
383 
384  // Ensure provider specific content is added as well
385  self::assertMatchesRegularExpression($providerContent, $responseContent);
386  }
387 
389  {
390  yield 'Edit provider' => ['edit', true, '/<input.*id="name"/s'];
391  yield 'Setup provider' => ['setup', false, '/<input.*id="totp"/s'];
392  }
393 }
‪TYPO3\CMS\Core\Localization\LanguageServiceFactory
Definition: LanguageServiceFactory.php:25
‪TYPO3\CMS\Core\Http\Message\getBody
‪StreamInterface getBody()
Definition: Message.php:287
‪TYPO3\CMS\Backend\Tests\Functional\Controller\MfaConfigurationControllerTest
Definition: MfaConfigurationControllerTest.php:41
‪TYPO3\CMS\Backend\Tests\Functional\Controller\MfaConfigurationControllerTest\setUp
‪setUp()
Definition: MfaConfigurationControllerTest.php:54
‪TYPO3\CMS\Backend\Tests\Functional\Controller\MfaConfigurationControllerTest\handleRequestAddsInformationAboutMfaBeingRequiredAndRecommendedTest
‪handleRequestAddsInformationAboutMfaBeingRequiredAndRecommendedTest()
Definition: MfaConfigurationControllerTest.php:120
‪TYPO3\CMS\Backend\Tests\Functional\Controller\MfaConfigurationControllerTest\handleRequestRedirectsToOverviewOnActionProviderMismatchTest
‪handleRequestRedirectsToOverviewOnActionProviderMismatchTest(string $action, string $provider, bool $providerActive, string $flashMessage)
Definition: MfaConfigurationControllerTest.php:171
‪TYPO3\CMS\Backend\Template\ModuleTemplateFactory
Definition: ModuleTemplateFactory.php:33
‪TYPO3\CMS\Core\Core\SystemEnvironmentBuilder
Definition: SystemEnvironmentBuilder.php:41
‪TYPO3\CMS\Backend\Tests\Functional\Controller\MfaConfigurationControllerTest\$normalizedParams
‪NormalizedParams $normalizedParams
Definition: MfaConfigurationControllerTest.php:45
‪TYPO3\CMS\Core\Core\Environment\getPublicPath
‪static getPublicPath()
Definition: Environment.php:187
‪TYPO3\CMS\Backend\Tests\Functional\Controller\MfaConfigurationControllerTest\handleRequestShowsAllRegisteredProvidersTest
‪handleRequestShowsAllRegisteredProvidersTest()
Definition: MfaConfigurationControllerTest.php:104
‪TYPO3\CMS\Backend\Tests\Functional\Controller\MfaConfigurationControllerTest\handleRequestReturnsBadRequestForInvalidActionTest
‪handleRequestReturnsBadRequestForInvalidActionTest()
Definition: MfaConfigurationControllerTest.php:75
‪TYPO3\CMS\Core\Core\SystemEnvironmentBuilder\REQUESTTYPE_BE
‪const REQUESTTYPE_BE
Definition: SystemEnvironmentBuilder.php:45
‪TYPO3\CMS\Backend\Tests\Functional\Controller\MfaConfigurationControllerTest\$request
‪ServerRequest $request
Definition: MfaConfigurationControllerTest.php:43
‪TYPO3\CMS\Core\Imaging\IconFactory
Definition: IconFactory.php:34
‪TYPO3\CMS\Core\Authentication\Mfa\Provider\Totp
Definition: Totp.php:30
‪TYPO3\CMS\Backend\Routing\Route
Definition: Route.php:24
‪TYPO3\CMS\Core\Context\Context
Definition: Context.php:54
‪TYPO3\CMS\Backend\Tests\Functional\Controller\MfaConfigurationControllerTest\handleRequestRespectsReturnUrlTest
‪handleRequestRespectsReturnUrlTest()
Definition: MfaConfigurationControllerTest.php:149
‪TYPO3\CMS\Backend\Tests\Functional\Controller\MfaConfigurationControllerTest\handleRequestAddsFormOnInteractionViewsTest
‪handleRequestAddsFormOnInteractionViewsTest(string $action, bool $providerActive, string $providerContent)
Definition: MfaConfigurationControllerTest.php:356
‪TYPO3\CMS\Backend\Tests\Functional\Controller\MfaConfigurationControllerTest\handleRequestForwardsToCorrectActionTestDataProvider
‪static handleRequestForwardsToCorrectActionTestDataProvider()
Definition: MfaConfigurationControllerTest.php:308
‪TYPO3\CMS\Backend\Tests\Functional\Controller\MfaConfigurationControllerTest\handleRequestAddsFormOnInteractionViewsTestTestDataProvider
‪static handleRequestAddsFormOnInteractionViewsTestTestDataProvider()
Definition: MfaConfigurationControllerTest.php:388
‪TYPO3\CMS\Backend\Tests\Functional\Controller\MfaConfigurationControllerTest\handleRequestRedirectsToOverviewOnActionProviderMismatchTestDataProvider
‪static handleRequestRedirectsToOverviewOnActionProviderMismatchTestDataProvider()
Definition: MfaConfigurationControllerTest.php:203
‪TYPO3\CMS\Backend\Routing\UriBuilder
Definition: UriBuilder.php:44
‪TYPO3\CMS\Core\Http\ServerRequest
Definition: ServerRequest.php:39
‪TYPO3\CMS\Backend\Tests\Functional\Controller\MfaConfigurationControllerTest\handleRequestForwardsToCorrectActionTest
‪handleRequestForwardsToCorrectActionTest(string $action, string $provider, bool $providerActive, bool $redirect, string $searchString)
Definition: MfaConfigurationControllerTest.php:257
‪$GLOBALS
‪$GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['adminpanel']['modules']
Definition: ext_localconf.php:25
‪TYPO3\CMS\Core\Core\Environment
Definition: Environment.php:41
‪TYPO3\CMS\Backend\Tests\Functional\Controller\MfaConfigurationControllerTest\handleRequestFallsBackToOverviewActionIfNoActionGivenTest
‪handleRequestFallsBackToOverviewActionIfNoActionGivenTest()
Definition: MfaConfigurationControllerTest.php:92
‪TYPO3\CMS\Backend\Tests\Functional\Controller\MfaConfigurationControllerTest\$configurationToUseInTestInstance
‪array $configurationToUseInTestInstance
Definition: MfaConfigurationControllerTest.php:47
‪TYPO3\CMS\Core\Utility\GeneralUtility
Definition: GeneralUtility.php:52
‪TYPO3\CMS\Backend\Tests\Functional\Controller\MfaConfigurationControllerTest\$hashService
‪HashService $hashService
Definition: MfaConfigurationControllerTest.php:44
‪TYPO3\CMS\Core\Crypto\HashService
Definition: HashService.php:27
‪TYPO3\CMS\Backend\Controller\MfaConfigurationController
Definition: MfaConfigurationController.php:48
‪TYPO3\CMS\Core\Messaging\FlashMessageService
Definition: FlashMessageService.php:27
‪TYPO3\CMS\Backend\Tests\Functional\Controller
Definition: BackendControllerTest.php:18
‪TYPO3\CMS\Backend\Tests\Functional\Controller\MfaConfigurationControllerTest\handleRequestIndicatesDefaultProviderTest
‪handleRequestIndicatesDefaultProviderTest()
Definition: MfaConfigurationControllerTest.php:134
‪TYPO3\CMS\Core\Http\NormalizedParams
Definition: NormalizedParams.php:38
‪TYPO3\CMS\Core\Http\ServerRequest\withAttribute
‪static withAttribute(string $name, $value)
Definition: ServerRequest.php:310
‪TYPO3\CMS\Backend\Tests\Functional\Controller\MfaConfigurationControllerTest\$subject
‪MfaConfigurationController $subject
Definition: MfaConfigurationControllerTest.php:42
‪TYPO3\CMS\Core\Authentication\Mfa\MfaProviderRegistry
Definition: MfaProviderRegistry.php:28