‪TYPO3CMS  11.5
RecoveryCodesProviderTest.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 
34 use TYPO3\TestingFramework\Core\Functional\FunctionalTestCase;
35 
36 class ‪RecoveryCodesProviderTest extends FunctionalTestCase
37 {
40 
42  'BE' => [
43  'passwordHashing' => [
44  'className' => Argon2iPasswordHash::class,
45  'options' => [
46  // Reduce default costs for quicker unit tests
47  'memory_cost' => 65536,
48  'time_cost' => 4,
49  'threads' => 2,
50  ],
51  ],
52  ],
53  ];
54 
55  protected function ‪setUp(): void
56  {
57  parent::setUp();
58  $this->importCSVDataSet(__DIR__ . '/../../Fixtures/be_users.csv');
59  $this->user = $this->setUpBackendUser(1);
60  ‪$GLOBALS['LANG'] = $this->get(LanguageServiceFactory::class)->createFromUserPreferences($this->user);
61  $this->subject = $this->get(MfaProviderRegistry::class)->getProvider('recovery-codes');
62  }
63 
64  protected function ‪tearDown(): void
65  {
67  parent::tearDown();
68  }
69 
73  public function ‪canProcessTest(): void
74  {
75  self::assertFalse($this->subject->canProcess(new ‪ServerRequest('https://example.com', 'POST')));
76 
77  // Add necessary query parameter
78  self::assertTrue($this->subject->canProcess(
79  (new ‪ServerRequest('https://example.com', 'POST'))
80  ->withQueryParams(['rc' => '12345678'])
81  ));
82  }
83 
87  public function ‪isActiveTest(): void
88  {
89  self::assertFalse($this->subject->isActive(‪MfaProviderPropertyManager::create($this->subject, $this->user)));
90 
91  // Activate provider
92  $this->‪setupUser(['recovery-codes' => ['active' => true]]);
93  self::assertTrue($this->subject->isActive(‪MfaProviderPropertyManager::create($this->subject, $this->user)));
94  }
95 
99  public function ‪isLockedTest(): void
100  {
101  self::assertFalse($this->subject->isLocked(‪MfaProviderPropertyManager::create($this->subject, $this->user)));
102 
103  // Lock provider by setting attempts=3
104  $this->user->user['mfa'] = json_encode(['recovery-codes' => ['active' => true, 'attempts' => 3]]);
105  self::assertTrue($this->subject->isLocked(‪MfaProviderPropertyManager::create($this->subject, $this->user)));
106 
107  // Lock provider by removing the codes
108  $this->user->user['mfa'] = json_encode(['recovery-codes' => ['active' => true, 'codes' => []]]);
109  self::assertTrue($this->subject->isLocked(‪MfaProviderPropertyManager::create($this->subject, $this->user)));
110  }
111 
115  public function ‪verifyTest(): void
116  {
117  $code = '12345678';
118  $hash = GeneralUtility::makeInstance(PasswordHashFactory::class)
119  ->getDefaultHashInstance('BE')
120  ->getHashedPassword($code);
121 
122  $this->‪setupUser(['recovery-codes' => ['active' => true, 'codes' => [$hash], 'attempts' => 0]]);
123 
124  $request = (new ‪ServerRequest('https://example.com', 'POST'));
125  $propertyManager = ‪MfaProviderPropertyManager::create($this->subject, $this->user);
126 
127  self::assertFalse(
128  $this->subject->verify(
129  $request->withQueryParams(['rc' => '87654321']),
130  $propertyManager
131  )
132  );
133 
134  self::assertTrue(
135  $this->subject->verify(
136  $request->withQueryParams(['rc' => $code]),
137  $propertyManager
138  )
139  );
140  }
141 
145  public function ‪activateTest(): void
146  {
147  $request = (new ‪ServerRequest('https://example.com', 'POST'));
148  $propertyManager = ‪MfaProviderPropertyManager::create($this->subject, $this->user);
149 
150  self::assertFalse($this->subject->activate($request->withParsedBody(['totp' => '123456']), $propertyManager));
151 
152  // Setup form data to activate provider
153  $this->‪setupUser(['recovery-codes' => ['active' => false]]);
154  $codes = GeneralUtility::makeInstance(RecoveryCodes::class, 'BE')->generatePlainRecoveryCodes();
155  $parsedBody = [
156  'recoveryCodes' => implode(PHP_EOL, $codes),
157  'checksum' => GeneralUtility::hmac(json_encode($codes) ?: '', 'recovery-codes-setup'),
158  ];
159  self::assertTrue($this->subject->activate($request->withParsedBody($parsedBody), $propertyManager));
160  }
161 
165  public function ‪deactivateTest(): void
166  {
167  $request = (new ‪ServerRequest('https://example.com', 'POST'));
168  self::assertFalse($this->subject->deactivate($request, ‪MfaProviderPropertyManager::create($this->subject, $this->user)));
169 
170  $this->‪setupUser(['recovery-codes' => ['active' => false]]);
171  self::assertFalse($this->subject->deactivate($request, ‪MfaProviderPropertyManager::create($this->subject, $this->user)));
172 
173  // Only an active provider can be deactivated
174  $this->‪setupUser(['recovery-codes' => ['active' => true, 'codes' => ['some-code'], 'attempts' => 0]]);
175  self::assertTrue($this->subject->deactivate($request, ‪MfaProviderPropertyManager::create($this->subject, $this->user)));
176  }
177 
181  public function ‪unlockTest(): void
182  {
183  $request = (new ‪ServerRequest('https://example.com', 'POST'));
184  self::assertFalse($this->subject->unlock($request, ‪MfaProviderPropertyManager::create($this->subject, $this->user)));
185 
186  $this->‪setupUser(['recovery-codes' => ['active' => true, 'codes' => ['some-code'], 'attempts' => 0]]);
187  self::assertFalse($this->subject->unlock($request, ‪MfaProviderPropertyManager::create($this->subject, $this->user)));
188 
189  // Only an active and locked provider can be unlocked
190  $this->‪setupUser(['recovery-codes' => ['active' => true, 'codes' => [], 'attempts' => 3]]);
191  self::assertTrue($this->subject->unlock($request, ‪MfaProviderPropertyManager::create($this->subject, $this->user)));
192  $message = GeneralUtility::makeInstance(FlashMessageService::class)->getMessageQueueByIdentifier()->getAllMessages()[0];
193  self::assertEquals('Your recovery codes were automatically updated!', $message->getTitle());
194  }
195 
199  public function ‪updateTest(): void
200  {
201  $request = (new ‪ServerRequest('https://example.com', 'POST'));
202  self::assertFalse($this->subject->update($request, ‪MfaProviderPropertyManager::create($this->subject, $this->user)));
203 
204  $this->‪setupUser(['recovery-codes' => ['active' => true, 'attempts' => 3]]);
205  self::assertFalse($this->subject->update($request, ‪MfaProviderPropertyManager::create($this->subject, $this->user)));
206 
207  // Only an active and unlocked provider can be updated
208  $this->‪setupUser(['recovery-codes' => ['active' => true, 'codes' => ['some-code'], 'attempts' => 0]]);
209  $request = $request->withParsedBody(['name' => 'some name', 'regenerateCodes' => true]);
210  self::assertTrue($this->subject->update($request, ‪MfaProviderPropertyManager::create($this->subject, $this->user)));
211  $message = GeneralUtility::makeInstance(FlashMessageService::class)->getMessageQueueByIdentifier()->getAllMessages()[0];
212  self::assertEquals('Recovery codes successfully regenerated', $message->getTitle());
213  }
214 
219  {
220  $this->expectException(PropagateResponseException::class);
221  $this->subject->handleRequest(
222  new ‪ServerRequest('https://example.com', 'GET'),
223  ‪MfaProviderPropertyManager::create($this->subject, $this->user),
225  );
226  }
227 
231  public function ‪setupReturnsHtmlWithRecoveryCodes(): void
232  {
233  $this->‪setupUser();
234  $response = $this->subject->handleRequest(
235  new ‪ServerRequest('https://example.com', 'GET'),
236  ‪MfaProviderPropertyManager::create($this->subject, $this->user),
238  );
239  self::assertStringContainsString('<textarea type="text" id="recoveryCodes"', $response->getBody()->getContents());
240  }
241 
245  public function ‪editViewTest(): void
246  {
247  $request = (new ‪ServerRequest('https://example.com', 'POST'));
248  $this->‪setupUser([
249  'recovery-codes' => [
250  'codes' => ['some-code', 'another-code'],
251  'name' => 'some name',
252  'updated' => 1616099471,
253  'lastUsed' => 1616099472,
254  ],
255  ]);
256  $propertyManager = ‪MfaProviderPropertyManager::create($this->subject, $this->user);
257  $response = $this->subject->handleRequest($request, $propertyManager, ‪MfaViewType::EDIT)->getBody()->getContents();
258 
259  self::assertMatchesRegularExpression('/<td>.*Name.*<td>.*some name/s', $response);
260  self::assertMatchesRegularExpression('/<td>.*Recovery codes left.*<td>.*2/s', $response);
261  self::assertMatchesRegularExpression('/<td>.*Last updated.*<td>.*18-03-21/s', $response);
262  self::assertMatchesRegularExpression('/<td>.*Last used.*<td>.*18-03-21/s', $response);
263  self::assertMatchesRegularExpression('/<input.*id="regenerateCodes"/s', $response);
264  }
265 
269  public function ‪authViewTest(): void
270  {
271  $request = (new ‪ServerRequest('https://example.com', 'POST'));
272  $this->‪setupUser(['recovery-codes' => ['active' => true, 'codes' => ['some-code']]]);
273  $propertyManager = ‪MfaProviderPropertyManager::create($this->subject, $this->user);
274  $response = $this->subject->handleRequest($request, $propertyManager, ‪MfaViewType::AUTH)->getBody()->getContents();
275 
276  self::assertMatchesRegularExpression('/<input.*id="recoveryCode"/s', $response);
277 
278  // Lock the provider by setting attempts=3
279  $this->‪setupUser(['recovery-codes' => ['active' => true, 'codes' => ['some-code'], 'attempts' => 3]]);
280  $propertyManager = ‪MfaProviderPropertyManager::create($this->subject, $this->user);
281  $response = $this->subject->handleRequest($request, $propertyManager, ‪MfaViewType::AUTH)->getBody()->getContents();
282 
283  self::assertStringContainsString('The maximum attempts for this provider are exceeded.', $response);
284  }
285 
286  protected function ‪setupUser(array $additional = []): void
287  {
288  $this->user->user['mfa'] = json_encode(
289  array_replace_recursive(['totp' => ['active' => true, 'secret' => 'KRMVATZTJFZUC53FONXW2ZJB']], $additional)
290  );
291  }
292 }
‪TYPO3\CMS\Core\Localization\LanguageServiceFactory
Definition: LanguageServiceFactory.php:25
‪TYPO3\CMS\Core\Tests\Functional\Authentication\Mfa\Provider\RecoveryCodesProviderTest\$configurationToUseInTestInstance
‪$configurationToUseInTestInstance
Definition: RecoveryCodesProviderTest.php:41
‪TYPO3\CMS\Core\Crypto\PasswordHashing\PasswordHashFactory
Definition: PasswordHashFactory.php:27
‪TYPO3\CMS\Core\Authentication\Mfa\MfaViewType\EDIT
‪const EDIT
Definition: MfaViewType.php:28
‪TYPO3\CMS\Core\Authentication\Mfa\Provider\RecoveryCodes
Definition: RecoveryCodes.php:30
‪TYPO3\CMS\Core\Authentication\Mfa\MfaViewType\AUTH
‪const AUTH
Definition: MfaViewType.php:29
‪TYPO3\CMS\Core\Tests\Functional\Authentication\Mfa\Provider\RecoveryCodesProviderTest\unlockTest
‪unlockTest()
Definition: RecoveryCodesProviderTest.php:181
‪TYPO3\CMS\Core\Tests\Functional\Authentication\Mfa\Provider\RecoveryCodesProviderTest\isActiveTest
‪isActiveTest()
Definition: RecoveryCodesProviderTest.php:87
‪TYPO3\CMS\Core\Tests\Functional\Authentication\Mfa\Provider
Definition: RecoveryCodesProviderTest.php:18
‪TYPO3\CMS\Core\Authentication\Mfa\MfaViewType\SETUP
‪const SETUP
Definition: MfaViewType.php:27
‪TYPO3\CMS\Core\Authentication\Mfa\MfaProviderManifestInterface
Definition: MfaProviderManifestInterface.php:26
‪TYPO3\CMS\Core\Tests\Functional\Authentication\Mfa\Provider\RecoveryCodesProviderTest\$subject
‪MfaProviderManifestInterface $subject
Definition: RecoveryCodesProviderTest.php:39
‪TYPO3\CMS\Core\FormProtection\FormProtectionFactory\purgeInstances
‪static purgeInstances()
Definition: FormProtectionFactory.php:231
‪TYPO3\CMS\Core\Tests\Functional\Authentication\Mfa\Provider\RecoveryCodesProviderTest\setUp
‪setUp()
Definition: RecoveryCodesProviderTest.php:55
‪TYPO3\CMS\Core\Tests\Functional\Authentication\Mfa\Provider\RecoveryCodesProviderTest\authViewTest
‪authViewTest()
Definition: RecoveryCodesProviderTest.php:269
‪TYPO3\CMS\Core\Tests\Functional\Authentication\Mfa\Provider\RecoveryCodesProviderTest\deactivateTest
‪deactivateTest()
Definition: RecoveryCodesProviderTest.php:165
‪TYPO3\CMS\Core\Tests\Functional\Authentication\Mfa\Provider\RecoveryCodesProviderTest\canProcessTest
‪canProcessTest()
Definition: RecoveryCodesProviderTest.php:73
‪TYPO3\CMS\Core\Http\ServerRequest
Definition: ServerRequest.php:37
‪TYPO3\CMS\Core\Authentication\BackendUserAuthentication
Definition: BackendUserAuthentication.php:62
‪TYPO3\CMS\Core\Tests\Functional\Authentication\Mfa\Provider\RecoveryCodesProviderTest\verifyTest
‪verifyTest()
Definition: RecoveryCodesProviderTest.php:115
‪TYPO3\CMS\Core\Tests\Functional\Authentication\Mfa\Provider\RecoveryCodesProviderTest\setupUser
‪setupUser(array $additional=[])
Definition: RecoveryCodesProviderTest.php:286
‪TYPO3\CMS\Core\Authentication\Mfa\MfaProviderPropertyManager
Definition: MfaProviderPropertyManager.php:33
‪TYPO3\CMS\Core\Http\PropagateResponseException
Definition: PropagateResponseException.php:47
‪TYPO3\CMS\Core\FormProtection\FormProtectionFactory
Definition: FormProtectionFactory.php:48
‪TYPO3\CMS\Core\Tests\Functional\Authentication\Mfa\Provider\RecoveryCodesProviderTest\updateTest
‪updateTest()
Definition: RecoveryCodesProviderTest.php:199
‪$GLOBALS
‪$GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['adminpanel']['modules']
Definition: ext_localconf.php:25
‪TYPO3\CMS\Core\Tests\Functional\Authentication\Mfa\Provider\RecoveryCodesProviderTest\$user
‪BackendUserAuthentication $user
Definition: RecoveryCodesProviderTest.php:38
‪TYPO3\CMS\Core\Tests\Functional\Authentication\Mfa\Provider\RecoveryCodesProviderTest\setupFailsIfNoOtherMfaProviderIsActive
‪setupFailsIfNoOtherMfaProviderIsActive()
Definition: RecoveryCodesProviderTest.php:218
‪TYPO3\CMS\Core\Authentication\Mfa\MfaViewType
Definition: MfaViewType.php:26
‪TYPO3\CMS\Core\Authentication\Mfa\MfaProviderPropertyManager\create
‪static MfaProviderPropertyManager create(MfaProviderManifestInterface $provider, AbstractUserAuthentication $user)
Definition: MfaProviderPropertyManager.php:224
‪TYPO3\CMS\Core\Tests\Functional\Authentication\Mfa\Provider\RecoveryCodesProviderTest\editViewTest
‪editViewTest()
Definition: RecoveryCodesProviderTest.php:245
‪TYPO3\CMS\Core\Tests\Functional\Authentication\Mfa\Provider\RecoveryCodesProviderTest\isLockedTest
‪isLockedTest()
Definition: RecoveryCodesProviderTest.php:99
‪TYPO3\CMS\Core\Utility\GeneralUtility
Definition: GeneralUtility.php:50
‪TYPO3\CMS\Core\Tests\Functional\Authentication\Mfa\Provider\RecoveryCodesProviderTest\tearDown
‪tearDown()
Definition: RecoveryCodesProviderTest.php:64
‪TYPO3\CMS\Core\Tests\Functional\Authentication\Mfa\Provider\RecoveryCodesProviderTest
Definition: RecoveryCodesProviderTest.php:37
‪TYPO3\CMS\Core\Crypto\PasswordHashing\Argon2iPasswordHash
Definition: Argon2iPasswordHash.php:31
‪TYPO3\CMS\Core\Messaging\FlashMessageService
Definition: FlashMessageService.php:27
‪TYPO3\CMS\Core\Tests\Functional\Authentication\Mfa\Provider\RecoveryCodesProviderTest\activateTest
‪activateTest()
Definition: RecoveryCodesProviderTest.php:145
‪TYPO3\CMS\Core\Authentication\Mfa\MfaProviderRegistry
Definition: MfaProviderRegistry.php:28
‪TYPO3\CMS\Core\Tests\Functional\Authentication\Mfa\Provider\RecoveryCodesProviderTest\setupReturnsHtmlWithRecoveryCodes
‪setupReturnsHtmlWithRecoveryCodes()
Definition: RecoveryCodesProviderTest.php:231