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