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