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