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