‪TYPO3CMS  ‪main
MfaSetupControllerTest.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\Test;
21 use Psr\Log\NullLogger;
37 use TYPO3\TestingFramework\Core\Functional\FunctionalTestCase;
38 
39 final class ‪MfaSetupControllerTest extends FunctionalTestCase
40 {
44 
54  'SYS' => [
55  'systemMaintainers' => [],
56  ],
57  ];
58 
59  protected function ‪setUp(): void
60  {
61  parent::setUp();
62  $this->importCSVDataSet(__DIR__ . '/../Fixtures/be_users.csv');
63  ‪$GLOBALS['TYPO3_CONF_VARS']['BE']['requireMfa'] = 1;
64  $backendUser = $this->setUpBackendUser(1);
65  ‪$GLOBALS['LANG'] = $this->get(LanguageServiceFactory::class)->createFromUserPreferences($backendUser);
66 
67  $this->subject = new ‪MfaSetupController(
68  $this->get(UriBuilder::class),
69  $this->get(AuthenticationStyleInformation::class),
70  $this->get(PageRenderer::class),
71  $this->get(ExtensionConfiguration::class),
72  new NullLogger(),
73  $this->get(BackendViewFactory::class),
74  );
75  $this->subject->injectMfaProviderRegistry($this->get(MfaProviderRegistry::class));
76  $this->hashService = new ‪HashService();
77  $this->request = (new ‪ServerRequest('https://example.com/typo3/'))
78  ->withAttribute('applicationType', ‪SystemEnvironmentBuilder::REQUESTTYPE_BE)
79  ->withAttribute('route', new ‪Route('path', ['packageName' => 'typo3/cms-backend']));
80  }
81 
82  #[Test]
84  {
85  ‪$GLOBALS['BE_USER']->setAndSaveSessionData('mfa', true);
86 
87  $this->expectException(\InvalidArgumentException::class);
88  $this->expectExceptionCode(1632154036);
89 
91  ‪$GLOBALS['TYPO3_REQUEST'] = ‪$request;
92  $this->subject->handleRequest(‪$request);
93  }
94 
95  #[Test]
97  {
98  ‪$GLOBALS['BE_USER']->setAndSaveSessionData('backuserid', 123);
99 
100  $this->expectException(\InvalidArgumentException::class);
101  $this->expectExceptionCode(1632154036);
102 
104  ‪$GLOBALS['TYPO3_REQUEST'] = ‪$request;
105  $this->subject->handleRequest(‪$request);
106  }
107 
108  #[Test]
110  {
111  ‪$GLOBALS['TYPO3_CONF_VARS']['BE']['requireMfa'] = 0;
112 
113  $this->expectException(\InvalidArgumentException::class);
114  $this->expectExceptionCode(1632154036);
115 
117  ‪$GLOBALS['TYPO3_REQUEST'] = ‪$request;
118  $this->subject->handleRequest(‪$request);
119  }
120 
121  #[Test]
123  {
124  ‪$GLOBALS['BE_USER']->user['mfa'] = json_encode(['totp' => ['active' => true, 'secret' => 'KRMVATZTJFZUC53FONXW2ZJB']]);
125 
126  $this->expectException(\InvalidArgumentException::class);
127  $this->expectExceptionCode(1632154036);
128 
130  ‪$GLOBALS['TYPO3_REQUEST'] = ‪$request;
131  $this->subject->handleRequest(‪$request);
132  }
133 
134  #[Test]
136  {
137  ‪$request = $this->request->‪withQueryParams(['action' => 'unknown']);
138  ‪$GLOBALS['TYPO3_REQUEST'] = ‪$request;
139  $response = $this->subject->handleRequest(‪$request);
140 
141  self::assertEquals(404, $response->getStatusCode());
142  }
143 
144  #[Test]
146  {
147  ‪$request = $this->request->‪withQueryParams(['action' => 'activate']);
148  ‪$GLOBALS['TYPO3_REQUEST'] = ‪$request;
149  $response = $this->subject->handleRequest(‪$request);
150 
151  self::assertEquals(404, $response->getStatusCode());
152  }
153 
154  #[Test]
156  {
158  ‪$GLOBALS['TYPO3_REQUEST'] = ‪$request;
159  $response = $this->subject->handleRequest(‪$request);
160 
161  self::assertEquals(200, $response->getStatusCode());
162 
163  $responseContent = $response->getBody()->__toString();
164 
165  // Selection view is renderer
166  self::assertStringContainsString('Set up MFA', $responseContent);
167 
168  // Allowed default provider is rendered
169  self::assertMatchesRegularExpression('/<a.*class="list-group-item.*title="Set up Time-based one-time password".*>/s', $responseContent);
170 
171  // Non allowed default provider is not rendered
172  self::assertDoesNotMatchRegularExpression('/<a.*class="list-group-item.*title="Set up Recovery codes".*>/s', $responseContent);
173  }
174 
175  #[Test]
177  {
178  $queryParams = [
179  'action' => 'setup',
180  'identifier' => 'totp',
181  'redirect' => 'my_module',
182  'redirectParams' => 'some=param',
183  ];
184 
185  ‪$request = $this->request->‪withQueryParams($queryParams);
186  ‪$GLOBALS['TYPO3_REQUEST'] = ‪$request;
187  $response = $this->subject->handleRequest(‪$request);
188 
189  self::assertEquals(200, $response->getStatusCode());
190 
191  $responseContent = $response->getBody()->__toString();
192 
193  // Redirect params are kept
194  self::assertMatchesRegularExpression('/<form.*action="\/typo3\/setup\/mfa.*&amp;action=activate&amp;redirect=my_module&amp;redirectParams=some%3Dparam".*>/s', $responseContent);
195  self::assertMatchesRegularExpression('/<a.*title="Cancel".*href="\/typo3\/setup\/mfa.*&amp;redirect=my_module&amp;redirectParams=some%3Dparam".*>/s', $responseContent);
196  }
197 
198  #[Test]
199  public function ‪handleRequestReturnsSetupView(): void
200  {
201  $queryParams = [
202  'action' => 'setup',
203  'identifier' => 'totp',
204  ];
205 
206  ‪$request = $this->request->‪withQueryParams($queryParams);
207  ‪$GLOBALS['TYPO3_REQUEST'] = ‪$request;
208  $response = $this->subject->handleRequest(‪$request);
209 
210  self::assertEquals(200, $response->getStatusCode());
211 
212  $responseContent = $response->getBody()->__toString();
213 
214  // Auth view for provider is renderer
215  self::assertStringContainsString('Set up Time-based one-time password', $responseContent);
216 
217  // Ensure provider specific content is added as well
218  self::assertMatchesRegularExpression('/<div.*id="qr-code".*>/s', $responseContent);
219  self::assertMatchesRegularExpression('/<form.*name="setup".*id="mfaSetupController".*>/s', $responseContent);
220  self::assertMatchesRegularExpression('/<input.*id="totp"/s', $responseContent);
221  }
222 
223  #[Test]
225  {
226  $queryParams = [
227  'action' => 'activate',
228  'redirect' => 'web_list',
229  'redirectParams' => 'some=param',
230  ];
231 
232  ‪$request = $this->request->‪withMethod('POST')->withQueryParams($queryParams);
233  ‪$GLOBALS['TYPO3_REQUEST'] = ‪$request;
234  $response = $this->subject->handleRequest(‪$request);
235  $redirectUrl = parse_url($response->getHeaderLine('location'));
236 
237  self::assertEquals(302, $response->getStatusCode());
238  self::assertStringContainsString('/typo3/setup/mfa', $redirectUrl['path']);
239 
240  // Also redirect parameters are still kept
241  self::assertStringContainsString('redirect=web_list&redirectParams=some%3Dparam', $redirectUrl['query']);
242  }
243 
244  #[Test]
246  {
247  $queryParams = [
248  'action' => 'activate',
249  'redirect' => 'web_list',
250  'redirectParams' => 'some=param',
251  ];
252 
253  $parsedBody = [
254  'identifier' => 'recovery-codes',
255  ];
256 
257  ‪$request = $this->request->‪withMethod('POST')->withQueryParams($queryParams)->withParsedBody($parsedBody);
258  ‪$GLOBALS['TYPO3_REQUEST'] = ‪$request;
259  $response = $this->subject->handleRequest(‪$request);
260  $redirectUrl = parse_url($response->getHeaderLine('location'));
261 
262  self::assertEquals(302, $response->getStatusCode());
263  self::assertStringContainsString('/typo3/setup/mfa', $redirectUrl['path']);
264 
265  // Also redirect parameters are still kept
266  self::assertStringContainsString('redirect=web_list&redirectParams=some%3Dparam', $redirectUrl['query']);
267  }
268 
269  #[Test]
271  {
272  $queryParams = [
273  'action' => 'activate',
274  'redirect' => 'web_list',
275  'redirectParams' => 'some=param',
276  ];
277 
278  $timestamp = $this->get(Context::class)->getPropertyFromAspect('date', 'timestamp');
279  $parsedBody = [
280  'identifier' => 'totp',
281  'totp' => (new ‪Totp('KRMVATZTJFZUC53FONXW2ZJB'))->generateTotp((int)floor($timestamp / 30)),
282  'secret' => 'KRMVATZTJFZUC53FONXW2ZJB',
283  'checksum' => $this->hashService->hmac('KRMVATZTJFZUC53FONXW2ZJB', 'totp-setup'),
284  ];
285 
286  ‪$request = $this->request->‪withMethod('POST')->withQueryParams($queryParams)->withParsedBody($parsedBody);
287  ‪$GLOBALS['TYPO3_REQUEST'] = ‪$request;
288  $response = $this->subject->handleRequest(‪$request);
289  $redirectUrl = parse_url($response->getHeaderLine('location'));
290 
291  // Successful activation will initiate a redirect to the login endpoint
292  self::assertEquals(302, $response->getStatusCode());
293  self::assertEquals('/typo3/login', $redirectUrl['path']);
294 
295  // Successful activation will set the "mfa" session key
296  self::assertTrue(‪$GLOBALS['BE_USER']->getSessionData('mfa'));
297 
298  // Successful activation will set "totp" as default provider
299  self::assertEquals('totp', ‪$GLOBALS['BE_USER']->uc['mfa']['defaultProvider']);
300 
301  // Successful activation will add a flash message
302  self::assertEquals(
303  'MFA setup successful',
304  $this->get(FlashMessageService::class)->getMessageQueueByIdentifier()->getAllMessages()[0]->getTitle()
305  );
306 
307  // Flash message properly resolves the provider title
308  self::assertStringContainsString(
309  'You have successfully activated MFA provider Time-based one-time password.',
310  $this->get(FlashMessageService::class)->getMessageQueueByIdentifier()->getAllMessages()[0]->getMessage()
311  );
312 
313  // Also redirect parameters are still kept
314  self::assertStringContainsString('redirect=web_list&redirectParams=some%3Dparam', $redirectUrl['query']);
315  }
316 
317  #[Test]
319  {
320  $queryParams = [
321  'action' => 'activate',
322  'redirect' => 'web_list',
323  'redirectParams' => 'some=param',
324  ];
325 
326  $parsedBody = [
327  'identifier' => 'totp',
328  'totp' => '123456', // invalid !!!
329  'secret' => 'KRMVATZTJFZUC53FONXW2ZJB',
330  'checksum' => $this->hashService->hmac('KRMVATZTJFZUC53FONXW2ZJB', 'totp-setup'),
331  ];
332 
333  ‪$request = $this->request->‪withMethod('POST')->withQueryParams($queryParams)->withParsedBody($parsedBody);
334  ‪$GLOBALS['TYPO3_REQUEST'] = ‪$request;
335  $response = $this->subject->handleRequest(‪$request);
336  $redirectUrl = parse_url($response->getHeaderLine('location'));
337 
338  // Failure will redirect to setup view
339  self::assertEquals(302, $response->getStatusCode());
340  self::assertEquals('/typo3/setup/mfa', $redirectUrl['path']);
341 
342  // Failure will add "identifier" and "hasErrors" parameters
343  self::assertStringContainsString('identifier=totp&hasErrors=1', $redirectUrl['query']);
344 
345  // Also redirect parameters are still kept
346  self::assertStringContainsString('redirect=web_list&redirectParams=some%3Dparam', $redirectUrl['query']);
347  }
348 
349  #[Test]
350  public function ‪handleRequestCancelsSetup(): void
351  {
352  $queryParams = [
353  'action' => 'cancel',
354  'redirect' => 'web_list',
355  'redirectParams' => 'some=param',
356  ];
357 
358  ‪$request = $this->request->‪withQueryParams($queryParams);
359  ‪$GLOBALS['TYPO3_REQUEST'] = ‪$request;
360  $response = $this->subject->handleRequest(‪$request);
361  $redirectUrl = parse_url($response->getHeaderLine('location'));
362 
363  self::assertEquals(302, $response->getStatusCode());
364  self::assertEquals('/typo3/login', $redirectUrl['path']);
365 
366  // Also redirect parameters are still kept
367  self::assertStringContainsString('redirect=web_list&redirectParams=some%3Dparam', $redirectUrl['query']);
368  }
369 }
‪TYPO3\CMS\Core\Localization\LanguageServiceFactory
Definition: LanguageServiceFactory.php:25
‪TYPO3\CMS\Backend\View\AuthenticationStyleInformation
Definition: AuthenticationStyleInformation.php:34
‪TYPO3\CMS\Backend\Tests\Functional\Controller\MfaSetupControllerTest\handleRequestThrowsExceptionWhenInSwitchUserMode
‪handleRequestThrowsExceptionWhenInSwitchUserMode()
Definition: MfaSetupControllerTest.php:96
‪TYPO3\CMS\Backend\Tests\Functional\Controller\MfaSetupControllerTest\setUp
‪setUp()
Definition: MfaSetupControllerTest.php:59
‪TYPO3\CMS\Core\Http\ServerRequest\withQueryParams
‪static withQueryParams(array $query)
Definition: ServerRequest.php:159
‪TYPO3\CMS\Backend\Tests\Functional\Controller\MfaSetupControllerTest\handleRequestActivatesRequestedProvider
‪handleRequestActivatesRequestedProvider()
Definition: MfaSetupControllerTest.php:270
‪TYPO3\CMS\Backend\Tests\Functional\Controller\MfaSetupControllerTest\handleRequestThrowsExceptionWhenMfaWasAlreadyPassed
‪handleRequestThrowsExceptionWhenMfaWasAlreadyPassed()
Definition: MfaSetupControllerTest.php:83
‪TYPO3\CMS\Backend\View\BackendViewFactory
Definition: BackendViewFactory.php:35
‪TYPO3\CMS\Core\Configuration\ExtensionConfiguration
Definition: ExtensionConfiguration.php:47
‪TYPO3\CMS\Core\Core\SystemEnvironmentBuilder
Definition: SystemEnvironmentBuilder.php:41
‪TYPO3\CMS\Backend\Tests\Functional\Controller\MfaSetupControllerTest\handleRequestReturnsSetupView
‪handleRequestReturnsSetupView()
Definition: MfaSetupControllerTest.php:199
‪TYPO3\CMS\Core\Core\SystemEnvironmentBuilder\REQUESTTYPE_BE
‪const REQUESTTYPE_BE
Definition: SystemEnvironmentBuilder.php:45
‪TYPO3\CMS\Backend\Tests\Functional\Controller\MfaSetupControllerTest\handleRequestAddsRedirectParameters
‪handleRequestAddsRedirectParameters()
Definition: MfaSetupControllerTest.php:176
‪TYPO3\CMS\Backend\Controller\MfaSetupController
Definition: MfaSetupController.php:50
‪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\Core\Page\PageRenderer
Definition: PageRenderer.php:45
‪TYPO3\CMS\Backend\Tests\Functional\Controller\MfaSetupControllerTest\handleRequestThrowsExceptionWhenMfaAlreadyActivated
‪handleRequestThrowsExceptionWhenMfaAlreadyActivated()
Definition: MfaSetupControllerTest.php:122
‪TYPO3\CMS\Backend\Tests\Functional\Controller\MfaSetupControllerTest\handleRequestThrowsExceptionWhenMfaNotRequired
‪handleRequestThrowsExceptionWhenMfaNotRequired()
Definition: MfaSetupControllerTest.php:109
‪TYPO3\CMS\Backend\Tests\Functional\Controller\MfaSetupControllerTest\handleRequestRedirectsToSetupOnInvalidProvider
‪handleRequestRedirectsToSetupOnInvalidProvider()
Definition: MfaSetupControllerTest.php:245
‪TYPO3\CMS\Backend\Routing\UriBuilder
Definition: UriBuilder.php:44
‪TYPO3\CMS\Backend\Tests\Functional\Controller\MfaSetupControllerTest\$configurationToUseInTestInstance
‪array $configurationToUseInTestInstance
Definition: MfaSetupControllerTest.php:53
‪TYPO3\CMS\Core\Http\ServerRequest
Definition: ServerRequest.php:39
‪TYPO3\CMS\Backend\Tests\Functional\Controller\MfaSetupControllerTest\handleRequestRedirectsToSetupOnMissingProvider
‪handleRequestRedirectsToSetupOnMissingProvider()
Definition: MfaSetupControllerTest.php:224
‪TYPO3\CMS\Backend\Tests\Functional\Controller\MfaSetupControllerTest\$hashService
‪HashService $hashService
Definition: MfaSetupControllerTest.php:43
‪TYPO3\CMS\Backend\Tests\Functional\Controller\MfaSetupControllerTest\handleRequestCancelsSetup
‪handleRequestCancelsSetup()
Definition: MfaSetupControllerTest.php:350
‪TYPO3\CMS\Backend\Tests\Functional\Controller\MfaSetupControllerTest\handleRequestRedirectsWithErrorOnActivationFailure
‪handleRequestRedirectsWithErrorOnActivationFailure()
Definition: MfaSetupControllerTest.php:318
‪TYPO3\CMS\Backend\Tests\Functional\Controller\MfaSetupControllerTest\$subject
‪MfaSetupController $subject
Definition: MfaSetupControllerTest.php:41
‪TYPO3\CMS\Core\Http\Request\withMethod
‪withMethod(string $method)
Definition: Request.php:269
‪TYPO3\CMS\Backend\Tests\Functional\Controller\MfaSetupControllerTest\handleRequestReturns404OnWrongHttpMethod
‪handleRequestReturns404OnWrongHttpMethod()
Definition: MfaSetupControllerTest.php:145
‪$GLOBALS
‪$GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['adminpanel']['modules']
Definition: ext_localconf.php:25
‪TYPO3\CMS\Backend\Tests\Functional\Controller\MfaSetupControllerTest\handleRequestFallsBackToSelectionView
‪handleRequestFallsBackToSelectionView()
Definition: MfaSetupControllerTest.php:155
‪TYPO3\CMS\Backend\Tests\Functional\Controller\MfaSetupControllerTest
Definition: MfaSetupControllerTest.php:40
‪TYPO3\CMS\Backend\Tests\Functional\Controller\MfaSetupControllerTest\handleRequestReturns404OnInvalidAction
‪handleRequestReturns404OnInvalidAction()
Definition: MfaSetupControllerTest.php:135
‪TYPO3\CMS\Backend\Tests\Functional\Controller\MfaSetupControllerTest\$request
‪ServerRequest $request
Definition: MfaSetupControllerTest.php:42
‪TYPO3\CMS\Core\Crypto\HashService
Definition: HashService.php:27
‪TYPO3\CMS\Core\Messaging\FlashMessageService
Definition: FlashMessageService.php:27
‪TYPO3\CMS\Backend\Tests\Functional\Controller
Definition: BackendControllerTest.php:18
‪TYPO3\CMS\Core\Authentication\Mfa\MfaProviderRegistry
Definition: MfaProviderRegistry.php:30