Total Complexity | 51 |
Total Lines | 379 |
Duplicated Lines | 0 % |
Changes | 1 | ||
Bugs | 0 | Features | 0 |
Complex classes like RecoveryCodesProvider often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.
Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.
While breaking up the class, it is a good idea to analyze how other classes use RecoveryCodesProvider, and based on these observations, apply Extract Interface, too.
1 | <?php |
||
42 | class RecoveryCodesProvider implements MfaProviderInterface |
||
43 | { |
||
44 | protected MfaProviderRegistry $mfaProviderRegistry; |
||
45 | protected Context $context; |
||
46 | protected UriBuilder $uriBuilder; |
||
47 | protected FlashMessageService $flashMessageService; |
||
48 | |||
49 | public function __construct( |
||
50 | MfaProviderRegistry $mfaProviderRegistry, |
||
51 | Context $context, |
||
52 | UriBuilder $uriBuilder, |
||
53 | FlashMessageService $flashMessageService |
||
54 | ) { |
||
55 | $this->mfaProviderRegistry = $mfaProviderRegistry; |
||
56 | $this->context = $context; |
||
57 | $this->uriBuilder = $uriBuilder; |
||
58 | $this->flashMessageService = $flashMessageService; |
||
59 | } |
||
60 | |||
61 | private const MAX_ATTEMPTS = 3; |
||
62 | |||
63 | /** |
||
64 | * Check if a recovery code is given in the current request |
||
65 | * |
||
66 | * @param ServerRequestInterface $request |
||
67 | * @return bool |
||
68 | */ |
||
69 | public function canProcess(ServerRequestInterface $request): bool |
||
70 | { |
||
71 | return $this->getRecoveryCode($request) !== ''; |
||
72 | } |
||
73 | |||
74 | /** |
||
75 | * Evaluate if the provider is activated by checking the |
||
76 | * active state from the provider properties. This provider |
||
77 | * furthermore has a mannerism that it only works if at least |
||
78 | * one other MFA provider is activated for the user. |
||
79 | * |
||
80 | * @param MfaProviderPropertyManager $propertyManager |
||
81 | * @return bool |
||
82 | */ |
||
83 | public function isActive(MfaProviderPropertyManager $propertyManager): bool |
||
87 | } |
||
88 | |||
89 | /** |
||
90 | * Evaluate if the provider is temporarily locked by checking |
||
91 | * the current attempts state from the provider properties and |
||
92 | * if there are still recovery codes left. |
||
93 | * |
||
94 | * @param MfaProviderPropertyManager $propertyManager |
||
95 | * @return bool |
||
96 | */ |
||
97 | public function isLocked(MfaProviderPropertyManager $propertyManager): bool |
||
105 | } |
||
106 | |||
107 | /** |
||
108 | * Verify the given recovery code and remove it from the |
||
109 | * provider properties if valid. |
||
110 | * |
||
111 | * @param ServerRequestInterface $request |
||
112 | * @param MfaProviderPropertyManager $propertyManager |
||
113 | * |
||
114 | * @return bool |
||
115 | */ |
||
116 | public function verify(ServerRequestInterface $request, MfaProviderPropertyManager $propertyManager): bool |
||
117 | { |
||
118 | if (!$this->isActive($propertyManager) || $this->isLocked($propertyManager)) { |
||
119 | // Can not verify an inactive or locked provider |
||
120 | return false; |
||
121 | } |
||
122 | |||
123 | $recoveryCode = $this->getRecoveryCode($request); |
||
124 | $codes = $propertyManager->getProperty('codes', []); |
||
125 | $recoveryCodes = GeneralUtility::makeInstance(RecoveryCodes::class, $this->getMode($propertyManager)); |
||
126 | if (!$recoveryCodes->verifyRecoveryCode($recoveryCode, $codes)) { |
||
127 | $attempts = $propertyManager->getProperty('attempts', 0); |
||
128 | $propertyManager->updateProperties(['attempts' => ++$attempts]); |
||
129 | return false; |
||
130 | } |
||
131 | |||
132 | // Since the codes were passed by reference to the verify method, the matching code was |
||
133 | // unset so we simply need to write the array back. However, if the update fails, we must |
||
134 | // return FALSE even if the authentication was successful to prevent data inconsistency. |
||
135 | return $propertyManager->updateProperties([ |
||
136 | 'codes' => $codes, |
||
137 | 'attempts' => 0, |
||
138 | 'lastUsed' => $this->context->getPropertyFromAspect('date', 'timestamp') |
||
139 | ]); |
||
140 | } |
||
141 | |||
142 | /** |
||
143 | * Render the provider specific response for the given content type |
||
144 | * |
||
145 | * @param ServerRequestInterface $request |
||
146 | * @param MfaProviderPropertyManager $propertyManager |
||
147 | * @param string $type |
||
148 | * @return ResponseInterface |
||
149 | * @throws PropagateResponseException |
||
150 | */ |
||
151 | public function handleRequest( |
||
152 | ServerRequestInterface $request, |
||
153 | MfaProviderPropertyManager $propertyManager, |
||
154 | string $type |
||
155 | ): ResponseInterface { |
||
156 | $view = GeneralUtility::makeInstance(StandaloneView::class); |
||
157 | $view->setTemplateRootPaths(['EXT:core/Resources/Private/Templates/Authentication/MfaProvider/RecoveryCodes']); |
||
158 | switch ($type) { |
||
159 | case MfaViewType::SETUP: |
||
160 | if (!$this->activeProvidersExist($propertyManager)) { |
||
161 | // If no active providers are present for the current user, add a flash message and redirect |
||
162 | $lang = $this->getLanguageService(); |
||
163 | $this->addFlashMessage( |
||
164 | $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_mfa_provider.xlf:setup.recoveryCodes.noActiveProviders.message'), |
||
165 | $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_mfa_provider.xlf:setup.recoveryCodes.noActiveProviders.title'), |
||
166 | FlashMessage::WARNING |
||
167 | ); |
||
168 | if (($normalizedParams = $request->getAttribute('normalizedParams'))) { |
||
169 | $returnUrl = $normalizedParams->getHttpReferer(); |
||
170 | } else { |
||
171 | // @todo this will not work for FE - make this more generic! |
||
172 | $returnUrl = $this->uriBuilder->buildUriFromRoute('mfa'); |
||
173 | } |
||
174 | throw new PropagateResponseException(new RedirectResponse($returnUrl, 303), 1612883326); |
||
175 | } |
||
176 | $codes = GeneralUtility::makeInstance(RecoveryCodes::class, $this->getMode($propertyManager))->generatePlainRecoveryCodes(); |
||
177 | $view->setTemplate('Setup'); |
||
178 | $view->assignMultiple([ |
||
179 | 'recoveryCodes' => implode(PHP_EOL, $codes), |
||
180 | // Generate hmac of the recovery codes to prevent them from being changed in the setup from |
||
181 | 'checksum' => GeneralUtility::hmac(json_encode($codes), 'recovery-codes-setup') |
||
182 | ]); |
||
183 | break; |
||
184 | case MfaViewType::EDIT: |
||
185 | $view->setTemplate('Edit'); |
||
186 | $view->assignMultiple([ |
||
187 | 'name' => $propertyManager->getProperty('name'), |
||
188 | 'amountOfCodesLeft' => count($propertyManager->getProperty('codes', [])), |
||
|
|||
189 | 'lastUsed' => $this->getDateTime($propertyManager->getProperty('lastUsed', 0)), |
||
190 | 'updated' => $this->getDateTime($propertyManager->getProperty('updated', 0)) |
||
191 | ]); |
||
192 | break; |
||
193 | case MfaViewType::AUTH: |
||
194 | $view->setTemplate('Auth'); |
||
195 | $view->assign('isLocked', $this->isLocked($propertyManager)); |
||
196 | break; |
||
197 | } |
||
198 | return new HtmlResponse($view->assign('providerIdentifier', $propertyManager->getIdentifier())->render()); |
||
199 | } |
||
200 | |||
201 | /** |
||
202 | * Activate the provider by hashing and storing the given recovery codes |
||
203 | * |
||
204 | * @param ServerRequestInterface $request |
||
205 | * @param MfaProviderPropertyManager $propertyManager |
||
206 | * @return bool |
||
207 | */ |
||
208 | public function activate(ServerRequestInterface $request, MfaProviderPropertyManager $propertyManager): bool |
||
209 | { |
||
210 | if ($this->isActive($propertyManager)) { |
||
211 | // Can not activate an active provider |
||
212 | return false; |
||
213 | } |
||
214 | |||
215 | if (!$this->activeProvidersExist($propertyManager)) { |
||
216 | // Can not activate since no other provider is activated yet |
||
217 | return false; |
||
218 | } |
||
219 | |||
220 | $recoveryCodes = GeneralUtility::trimExplode(PHP_EOL, (string)($request->getParsedBody()['recoveryCodes'] ?? '')); |
||
221 | $checksum = (string)($request->getParsedBody()['checksum'] ?? ''); |
||
222 | if ($recoveryCodes === [] |
||
223 | || !hash_equals(GeneralUtility::hmac(json_encode($recoveryCodes), 'recovery-codes-setup'), $checksum) |
||
224 | ) { |
||
225 | // Return since the request does not contain the initially created recovery codes |
||
226 | return false; |
||
227 | } |
||
228 | |||
229 | // Hash given plain recovery codes and prepare the properties array with active state and custom name |
||
230 | $hashedCodes = GeneralUtility::makeInstance(RecoveryCodes::class, $this->getMode($propertyManager))->generatedHashedRecoveryCodes($recoveryCodes); |
||
231 | $properties = ['codes' => $hashedCodes, 'active' => true]; |
||
232 | if (($name = (string)($request->getParsedBody()['name'] ?? '')) !== '') { |
||
233 | $properties['name'] = $name; |
||
234 | } |
||
235 | |||
236 | // Usually there should be no entry if the provider is not activated, but to prevent the |
||
237 | // provider from being unable to activate again, we update the existing entry in such case. |
||
238 | return $propertyManager->hasProviderEntry() |
||
239 | ? $propertyManager->updateProperties($properties) |
||
240 | : $propertyManager->createProviderEntry($properties); |
||
241 | } |
||
242 | |||
243 | /** |
||
244 | * Handle the deactivate action by removing the provider entry |
||
245 | * |
||
246 | * @param ServerRequestInterface $request |
||
247 | * @param MfaProviderPropertyManager $propertyManager |
||
248 | * @return bool |
||
249 | */ |
||
250 | public function deactivate(ServerRequestInterface $request, MfaProviderPropertyManager $propertyManager): bool |
||
251 | { |
||
252 | // Only check for the active property here to enable bulk deactivation, |
||
253 | // e.g. in FormEngine. Otherwise it would not be possible to deactivate |
||
254 | // this provider if the last "fully" provider was deactivated before. |
||
255 | if (!(bool)$propertyManager->getProperty('active')) { |
||
256 | // Can not deactivate an inactive provider |
||
257 | return false; |
||
258 | } |
||
259 | |||
260 | // Delete the provider entry |
||
261 | return $propertyManager->deleteProviderEntry(); |
||
262 | } |
||
263 | |||
264 | /** |
||
265 | * Handle the unlock action by resetting the attempts |
||
266 | * provider property and issuing new codes. |
||
267 | * |
||
268 | * @param ServerRequestInterface $request |
||
269 | * @param MfaProviderPropertyManager $propertyManager |
||
270 | * @return bool |
||
271 | */ |
||
272 | public function unlock(ServerRequestInterface $request, MfaProviderPropertyManager $propertyManager): bool |
||
273 | { |
||
274 | if (!$this->isActive($propertyManager) || !$this->isLocked($propertyManager)) { |
||
275 | // Can not unlock an inactive or not locked provider |
||
276 | return false; |
||
277 | } |
||
278 | |||
279 | // Reset attempts |
||
280 | if ((int)$propertyManager->getProperty('attempts', 0) !== 0 |
||
281 | && !$propertyManager->updateProperties(['attempts' => 0]) |
||
282 | ) { |
||
283 | // Could not reset the attempts, so we can not unlock the provider |
||
284 | return false; |
||
285 | } |
||
286 | |||
287 | // Regenerate codes |
||
288 | if ($propertyManager->getProperty('codes', []) === []) { |
||
289 | // Generate new codes and store the hashed ones |
||
290 | $recoveryCodes = GeneralUtility::makeInstance(RecoveryCodes::class, $this->getMode($propertyManager))->generateRecoveryCodes(); |
||
291 | if (!$propertyManager->updateProperties(['codes' => array_values($recoveryCodes)])) { |
||
292 | // Codes could not be stored, so we can not unlock the provider |
||
293 | return false; |
||
294 | } |
||
295 | // Add the newly generated codes to a flash message so the user can copy them |
||
296 | $lang = $this->getLanguageService(); |
||
297 | $this->addFlashMessage( |
||
298 | sprintf( |
||
299 | $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_mfa_provider.xlf:unlock.recoveryCodes.message'), |
||
300 | implode(' ', array_keys($recoveryCodes)) |
||
301 | ), |
||
302 | $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_mfa_provider.xlf:unlock.recoveryCodes.title'), |
||
303 | FlashMessage::WARNING |
||
304 | ); |
||
305 | } |
||
306 | |||
307 | return true; |
||
308 | } |
||
309 | |||
310 | public function update(ServerRequestInterface $request, MfaProviderPropertyManager $propertyManager): bool |
||
311 | { |
||
312 | if (!$this->isActive($propertyManager) || $this->isLocked($propertyManager)) { |
||
313 | // Can not update an inactive or locked provider |
||
314 | return false; |
||
315 | } |
||
316 | |||
317 | $name = (string)($request->getParsedBody()['name'] ?? ''); |
||
318 | if ($name !== '' && !$propertyManager->updateProperties(['name' => $name])) { |
||
319 | return false; |
||
320 | } |
||
321 | |||
322 | if ((bool)($request->getParsedBody()['regenerateCodes'] ?? false)) { |
||
323 | // Generate new codes and store the hashed ones |
||
324 | $recoveryCodes = GeneralUtility::makeInstance(RecoveryCodes::class, $this->getMode($propertyManager))->generateRecoveryCodes(); |
||
325 | if (!$propertyManager->updateProperties(['codes' => array_values($recoveryCodes)])) { |
||
326 | // Codes could not be stored, so we can not update the provider |
||
327 | return false; |
||
328 | } |
||
329 | // Add the newly generated codes to a flash message so the user can copy them |
||
330 | $lang = $this->getLanguageService(); |
||
331 | $this->addFlashMessage( |
||
332 | sprintf( |
||
333 | $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_mfa_provider.xlf:update.recoveryCodes.message'), |
||
334 | implode(' ', array_keys($recoveryCodes)) |
||
335 | ), |
||
336 | $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_mfa_provider.xlf:update.recoveryCodes.title'), |
||
337 | FlashMessage::OK |
||
338 | ); |
||
339 | } |
||
340 | |||
341 | // Provider properties successfully updated |
||
342 | return true; |
||
343 | } |
||
344 | |||
345 | /** |
||
346 | * Check if the current user has other active providers |
||
347 | * |
||
348 | * @param MfaProviderPropertyManager $currentPropertyManager |
||
349 | * @return bool |
||
350 | */ |
||
351 | protected function activeProvidersExist(MfaProviderPropertyManager $currentPropertyManager): bool |
||
361 | } |
||
362 | |||
363 | /** |
||
364 | * Internal helper method for fetching the recovery code from the request |
||
365 | * |
||
366 | * @param ServerRequestInterface $request |
||
367 | * @return string |
||
368 | */ |
||
369 | protected function getRecoveryCode(ServerRequestInterface $request): string |
||
370 | { |
||
371 | return trim((string)($request->getQueryParams()['rc'] ?? $request->getParsedBody()['rc'] ?? '')); |
||
372 | } |
||
373 | |||
374 | /** |
||
375 | * Determine the mode (used for the hash instance) based on the current users table |
||
376 | * |
||
377 | * @param MfaProviderPropertyManager $propertyManager |
||
378 | * @return string |
||
379 | */ |
||
380 | protected function getMode(MfaProviderPropertyManager $propertyManager): string |
||
381 | { |
||
382 | return $propertyManager->getUser()->loginType; |
||
383 | } |
||
384 | |||
385 | /** |
||
386 | * Add a custom flash message for this provider |
||
387 | * Note: The flash messages added by the main controller are still shown to the user. |
||
388 | * |
||
389 | * @param string $message |
||
390 | * @param string $title |
||
391 | * @param int $severity |
||
392 | */ |
||
393 | protected function addFlashMessage(string $message, string $title = '', int $severity = FlashMessage::INFO): void |
||
394 | { |
||
395 | $this->flashMessageService->getMessageQueueByIdentifier()->enqueue( |
||
396 | GeneralUtility::makeInstance(FlashMessage::class, $message, $title, $severity, true) |
||
397 | ); |
||
398 | } |
||
399 | |||
400 | /** |
||
401 | * Return the timestamp as local time (date string) by applying the globally configured format |
||
402 | * |
||
403 | * @param int $timestamp |
||
404 | * @return string |
||
405 | */ |
||
406 | protected function getDateTime(int $timestamp): string |
||
407 | { |
||
408 | if ($timestamp === 0) { |
||
409 | return ''; |
||
410 | } |
||
411 | |||
412 | return date( |
||
413 | $GLOBALS['TYPO3_CONF_VARS']['SYS']['ddmmyy'] . ' ' . $GLOBALS['TYPO3_CONF_VARS']['SYS']['hhmm'], |
||
414 | $timestamp |
||
415 | ) ?: ''; |
||
416 | } |
||
417 | |||
418 | protected function getLanguageService(): LanguageService |
||
421 | } |
||
422 | } |
||
423 |