From 42499e623045463def5dc75137e7c0c7d95f0a40 Mon Sep 17 00:00:00 2001 From: bidi Date: Thu, 2 Apr 2026 13:59:45 +0300 Subject: [PATCH 1/7] added totp integration files Signed-off-by: bidi --- .../totp/_misc/totp-append-ConfigProvider.php | 11 ++ .../totp/_misc/totp-append-Pipeline.php | 2 + ...otp-append-enable-disable-button.html.twig | 20 ++++ .../totp/_misc/totp-append-routes.php | 8 ++ .../config/autoload/config totp.global.php | 28 +++++ .../totp/src/Admin/src/Form/RecoveryForm.php | 77 ++++++++++++ .../totp/src/Admin/src/Form/TotpForm.php | 77 ++++++++++++ .../Account/GetDisableTotpFormHandler.php | 47 ++++++++ .../Account/GetEnableTotpFormHandler.php | 93 +++++++++++++++ .../Account/GetRecoveryFormHandler.php | 43 +++++++ .../src/Handler/Account/GetTotpHandler.php | 44 +++++++ .../Account/PostDisableTotpHandler.php | 78 +++++++++++++ .../Handler/Account/PostEnableTotpHandler.php | 105 +++++++++++++++++ .../Account/PostValidateRecoveryHandler.php | 82 +++++++++++++ .../Account/PostValidateTotpHandler.php | 78 +++++++++++++ .../templates/admin/recovery-form.html.twig | 29 +++++ .../admin/validate-totp-form.html.twig | 44 +++++++ .../templates/admin/view-account.html.twig | 0 .../src/Middleware/CancelUrlMiddleware.php | 53 +++++++++ .../src/App/src/Middleware/TotpMiddleware.php | 110 ++++++++++++++++++ .../Core/src/Admin/src/Entity/TotpTrait.php | 64 ++++++++++ 21 files changed, 1093 insertions(+) create mode 100644 code_examples/totp/_misc/totp-append-ConfigProvider.php create mode 100644 code_examples/totp/_misc/totp-append-Pipeline.php create mode 100644 code_examples/totp/_misc/totp-append-enable-disable-button.html.twig create mode 100644 code_examples/totp/_misc/totp-append-routes.php create mode 100644 code_examples/totp/config/autoload/config totp.global.php create mode 100644 code_examples/totp/src/Admin/src/Form/RecoveryForm.php create mode 100644 code_examples/totp/src/Admin/src/Form/TotpForm.php create mode 100644 code_examples/totp/src/Admin/src/Handler/Account/GetDisableTotpFormHandler.php create mode 100644 code_examples/totp/src/Admin/src/Handler/Account/GetEnableTotpFormHandler.php create mode 100644 code_examples/totp/src/Admin/src/Handler/Account/GetRecoveryFormHandler.php create mode 100644 code_examples/totp/src/Admin/src/Handler/Account/GetTotpHandler.php create mode 100644 code_examples/totp/src/Admin/src/Handler/Account/PostDisableTotpHandler.php create mode 100644 code_examples/totp/src/Admin/src/Handler/Account/PostEnableTotpHandler.php create mode 100644 code_examples/totp/src/Admin/src/Handler/Account/PostValidateRecoveryHandler.php create mode 100644 code_examples/totp/src/Admin/src/Handler/Account/PostValidateTotpHandler.php create mode 100644 code_examples/totp/src/Admin/templates/admin/recovery-form.html.twig create mode 100644 code_examples/totp/src/Admin/templates/admin/validate-totp-form.html.twig create mode 100644 code_examples/totp/src/Admin/templates/admin/view-account.html.twig create mode 100644 code_examples/totp/src/App/src/Middleware/CancelUrlMiddleware.php create mode 100644 code_examples/totp/src/App/src/Middleware/TotpMiddleware.php create mode 100644 code_examples/totp/src/Core/src/Admin/src/Entity/TotpTrait.php diff --git a/code_examples/totp/_misc/totp-append-ConfigProvider.php b/code_examples/totp/_misc/totp-append-ConfigProvider.php new file mode 100644 index 0000000..629b8f5 --- /dev/null +++ b/code_examples/totp/_misc/totp-append-ConfigProvider.php @@ -0,0 +1,11 @@ +GetEnableTotpFormHandler::class => AttributedServiceFactory::class, +PostEnableTotpHandler::class => AttributedServiceFactory::class, +GetDisableTotpFormHandler::class => AttributedServiceFactory::class, +PostDisableTotpHandler::class => AttributedServiceFactory::class, +PostValidateTotpHandler::class => AttributedServiceFactory::class, +GetTotpHandler::class => AttributedServiceFactory::class, +GetRecoveryFormHandler::class => AttributedServiceFactory::class, +PostValidateRecoveryHandler::class => AttributedServiceFactory::class, + +TotpForm::class => ElementFactory::class, +RecoveryForm::class => ElementFactory::class, \ No newline at end of file diff --git a/code_examples/totp/_misc/totp-append-Pipeline.php b/code_examples/totp/_misc/totp-append-Pipeline.php new file mode 100644 index 0000000..454279b --- /dev/null +++ b/code_examples/totp/_misc/totp-append-Pipeline.php @@ -0,0 +1,2 @@ +$app->pipe(TotpMiddleware::class); +$app->pipe(CancelUrlMiddleware::class); \ No newline at end of file diff --git a/code_examples/totp/_misc/totp-append-enable-disable-button.html.twig b/code_examples/totp/_misc/totp-append-enable-disable-button.html.twig new file mode 100644 index 0000000..b993b76 --- /dev/null +++ b/code_examples/totp/_misc/totp-append-enable-disable-button.html.twig @@ -0,0 +1,20 @@ +
+
TOTP Setup
+ {% if isTotpEnabled %} +
TOTP is enabled
+ + {% else %} +
TOTP is disabled
+ + {% endif %} +
\ No newline at end of file diff --git a/code_examples/totp/_misc/totp-append-routes.php b/code_examples/totp/_misc/totp-append-routes.php new file mode 100644 index 0000000..050e28d --- /dev/null +++ b/code_examples/totp/_misc/totp-append-routes.php @@ -0,0 +1,8 @@ +->get('/enable-totp-form', GetEnableTotpFormHandler::class, 'admin::enable-totp-form') +->post('/enable-totp', PostEnableTotpHandler::class, 'admin::enable-totp') +->get('/disable-totp-form', GetDisableTotpFormHandler::class, 'admin::disable-totp-form') +->post('/disable-totp', PostDisableTotpHandler::class, 'admin::disable-totp') +->get('/validate-totp-form', GetTotpHandler::class, 'admin::validate-totp-form') +->post('/validate-totp', PostValidateTotpHandler::class, 'admin::validate-totp') +->get('/recovery-form', GetRecoveryFormHandler::class, 'admin::recovery-form') +->post('/validate-recovery', PostValidateRecoveryHandler::class, 'admin::validate-recovery') \ No newline at end of file diff --git a/code_examples/totp/config/autoload/config totp.global.php b/code_examples/totp/config/autoload/config totp.global.php new file mode 100644 index 0000000..bbbc7b6 --- /dev/null +++ b/code_examples/totp/config/autoload/config totp.global.php @@ -0,0 +1,28 @@ + [ + 'identity_class_map' => [ + AdminIdentity::class => Admin::class, + ], + 'totp_required_routes' => [ + 'page::components' => true, + ], + 'options' => [ + // Time step in seconds + 'period' => 30, + // Number of digits in the TOTP code + 'digits' => 6, + // Hashing algorithm used to generate the code + 'algorithm' => 'sha1', + ], + 'provision_uri_config' => [ + 'issuer' => 'ALX-Admin', + ], + ], +]; \ No newline at end of file diff --git a/code_examples/totp/src/Admin/src/Form/RecoveryForm.php b/code_examples/totp/src/Admin/src/Form/RecoveryForm.php new file mode 100644 index 0000000..f53ce11 --- /dev/null +++ b/code_examples/totp/src/Admin/src/Form/RecoveryForm.php @@ -0,0 +1,77 @@ + + */ +class RecoveryForm extends AbstractForm +{ + /** + * @param array $options + * @throws ExceptionInterface + */ + #[Inject( + RouterInterface::class, + )] + public function __construct(?string $name = null, array $options = []) + { + parent::__construct($name, $options); + + $this->init(); + + $this->setAttribute('id', 'recovery-form'); + $this->setAttribute('method', 'post'); + + $this->inputFilter = new RecoveryInputFilter(); + $this->inputFilter->init(); + } + + /** + * @throws ExceptionInterface + */ + public function init(): void + { + $this->add( + (new Text('recoveryCode')) + ->setLabel('Recovery Code') + ->setAttribute('class', 'form-control') + ->setAttribute('minlength', 11) + ->setAttribute('maxlength', 11) + ->setAttribute('pattern', '[A-Za-z0-9]{5}-[A-Za-z0-9]{5}') + ->setAttribute('required', true) + ->setAttribute('autofocus', true) + ); + + $this->add( + (new Csrf('recoveryCsrf')) + ->setOptions([ + 'csrf_options' => [ + 'timeout' => 3600, + 'session' => new Container(), + ], + ]) + ->setAttribute('required', true) + ); + + $this->add( + (new Submit('submit')) + ->setAttribute('type', 'submit') + ->setAttribute('value', 'Verify Code') + ->setAttribute('class', 'btn btn-primary mt-2') + ); + } +} \ No newline at end of file diff --git a/code_examples/totp/src/Admin/src/Form/TotpForm.php b/code_examples/totp/src/Admin/src/Form/TotpForm.php new file mode 100644 index 0000000..bcc213e --- /dev/null +++ b/code_examples/totp/src/Admin/src/Form/TotpForm.php @@ -0,0 +1,77 @@ + + */ +class TotpForm extends AbstractForm +{ + /** + * @param array $options + * @throws ExceptionInterface + */ + #[Inject( + RouterInterface::class, + )] + public function __construct(?string $name = null, array $options = []) + { + parent::__construct($name, $options); + + $this->init(); + + $this->setAttribute('id', 'enable-totp-form'); + $this->setAttribute('method', 'post'); + $this->setAttribute('title', 'TOTP Authentication Setup'); + + $this->inputFilter = new TotpInputFilter(); + $this->inputFilter->init(); + } + + /** + * @throws ExceptionInterface + */ + public function init(): void + { + $this->add( + (new Text('code')) + ->setLabel('Authentication Code') + ->setAttribute('class', 'form-control') + ->setAttribute('maxlength', 6) + ->setAttribute('pattern', '\d{6}') + ->setAttribute('required', true) + ->setAttribute('autofocus', true) + ); + + $this->add( + (new Csrf('totpCsrf')) + ->setOptions([ + 'csrf_options' => [ + 'timeout' => 3600, + 'session' => new Container(), + ], + ]) + ->setAttribute('required', true) + ); + + $this->add( + (new Submit('submit')) + ->setAttribute('type', 'submit') + ->setAttribute('value', 'Verify Code') + ->setAttribute('class', 'btn btn-primary mt-2') + ); + } +} \ No newline at end of file diff --git a/code_examples/totp/src/Admin/src/Handler/Account/GetDisableTotpFormHandler.php b/code_examples/totp/src/Admin/src/Handler/Account/GetDisableTotpFormHandler.php new file mode 100644 index 0000000..7f77f71 --- /dev/null +++ b/code_examples/totp/src/Admin/src/Handler/Account/GetDisableTotpFormHandler.php @@ -0,0 +1,47 @@ +totpForm + ->setAttribute('action', $this->router->generateUri('admin::disable-totp')); + + return new HtmlResponse( + $this->template->render('admin::validate-totp-form', [ + 'totpForm' => $this->totpForm->prepare(), + 'cancelUrl' => $this->router->generateUri('admin::edit-account'), + 'error' => null, + ]) + ); + } +} \ No newline at end of file diff --git a/code_examples/totp/src/Admin/src/Handler/Account/GetEnableTotpFormHandler.php b/code_examples/totp/src/Admin/src/Handler/Account/GetEnableTotpFormHandler.php new file mode 100644 index 0000000..6db9a05 --- /dev/null +++ b/code_examples/totp/src/Admin/src/Handler/Account/GetEnableTotpFormHandler.php @@ -0,0 +1,93 @@ +authenticationService->getStorage()->read(); + + if ( + empty($storage->pendingSecret) || + empty($storage->secretTimestamp) || + (time() - $storage->secretTimestamp) > self::SECRET_MAX_AGE + ) { + $storage->pendingSecret = $this->totpService->generateSecretBase32(); + $storage->secretTimestamp = time(); + $this->authenticationService->getStorage()->write($storage); + } + + $uri = $this->totpService->getProvisioningUri( + $storage->getIdentity(), + $this->provisioningUri['issuer'], + $storage->pendingSecret + ); + + $qrSvg = $this->totpService->generateInlineSvgQr($uri); + $storage->totp_verified = false; + + if (isset($storage->recovery_auth) && $storage->recovery_auth) { + $this->totpForm->setAttribute('title', 'Reconfigure Two-Factor Authentication'); + } + + $this->totpForm->setAttribute('action', $this->router->generateUri('admin::enable-totp')); + + return new HtmlResponse( + $this->template->render('admin::validate-totp-form', [ + 'qrSvg' => $qrSvg, + 'cancelUrl' => $request->getAttribute('cancelUrl'), + 'totpForm' => $this->totpForm->prepare(), + 'error' => null, + ]) + ); + } +} \ No newline at end of file diff --git a/code_examples/totp/src/Admin/src/Handler/Account/GetRecoveryFormHandler.php b/code_examples/totp/src/Admin/src/Handler/Account/GetRecoveryFormHandler.php new file mode 100644 index 0000000..2140d64 --- /dev/null +++ b/code_examples/totp/src/Admin/src/Handler/Account/GetRecoveryFormHandler.php @@ -0,0 +1,43 @@ +recoveryForm + ->setAttribute('action', $this->router->generateUri('admin::validate-recovery')); + + return new HtmlResponse( + $this->template->render('admin::recovery-form', [ + 'recoveryForm' => $this->recoveryForm, + 'cancelUrl' => $request->getAttribute('cancelUrl'), + ]) + ); + } +} \ No newline at end of file diff --git a/code_examples/totp/src/Admin/src/Handler/Account/GetTotpHandler.php b/code_examples/totp/src/Admin/src/Handler/Account/GetTotpHandler.php new file mode 100644 index 0000000..b155ed8 --- /dev/null +++ b/code_examples/totp/src/Admin/src/Handler/Account/GetTotpHandler.php @@ -0,0 +1,44 @@ +totpForm + ->setAttribute('action', $this->router->generateUri('admin::validate-totp')); + + return new HtmlResponse( + $this->template->render('admin::validate-totp-form', [ + 'totpForm' => $this->totpForm->prepare(), + 'cancelUrl' => $request->getAttribute('cancelUrl'), + 'error' => null, + ]) + ); + } +} \ No newline at end of file diff --git a/code_examples/totp/src/Admin/src/Handler/Account/PostDisableTotpHandler.php b/code_examples/totp/src/Admin/src/Handler/Account/PostDisableTotpHandler.php new file mode 100644 index 0000000..6115fb2 --- /dev/null +++ b/code_examples/totp/src/Admin/src/Handler/Account/PostDisableTotpHandler.php @@ -0,0 +1,78 @@ +adminService->findAdmin($this->authenticationService->getIdentity()->getId()); + } catch (NotFoundException $exception) { + $this->messenger->addError($exception->getMessage()); + + return new EmptyResponse(StatusCodeInterface::STATUS_NOT_FOUND); + } + + $parsedBody = $request->getParsedBody(); + $storage = $this->authenticationService->getStorage()->read(); + + if (! is_array($parsedBody) || ! isset($parsedBody['code'])) { + $this->messenger->addError(Message::VALIDATOR_INVALID_CODE); + return new RedirectResponse( + $this->router->generateUri('admin::validate-totp-form') + ); + } + + $code = (string) $parsedBody['code']; + + if ($this->totpService->verifyCode((string) $admin->getTotpSecret(), $code)) { + $admin->disableTotp(); + $this->adminService->getAdminRepository()->saveResource($admin); + $storage->totp_verified = false; + return new RedirectResponse($this->router->generateUri('admin::edit-account')); + } else { + $this->messenger->addError(Message::VALIDATOR_INVALID_CODE); + } + + return new RedirectResponse($this->router->generateUri('admin::disable-totp-form')); + } +} \ No newline at end of file diff --git a/code_examples/totp/src/Admin/src/Handler/Account/PostEnableTotpHandler.php b/code_examples/totp/src/Admin/src/Handler/Account/PostEnableTotpHandler.php new file mode 100644 index 0000000..18f7499 --- /dev/null +++ b/code_examples/totp/src/Admin/src/Handler/Account/PostEnableTotpHandler.php @@ -0,0 +1,105 @@ +adminService->findAdmin($this->authenticationService->getIdentity()->getId()); + } catch (NotFoundException $exception) { + $this->messenger->addError($exception->getMessage()); + + return new EmptyResponse(StatusCodeInterface::STATUS_NOT_FOUND); + } + + $parsedBody = $request->getParsedBody(); + $storage = $this->authenticationService->getStorage()->read(); + + if (! is_array($parsedBody) || ! isset($parsedBody['code'])) { + $this->messenger->addError(Message::VALIDATOR_INVALID_CODE); + return new RedirectResponse( + $this->router->generateUri('admin::validate-totp-form') + ); + } + + $code = (string) $parsedBody['code']; + + if ($admin->isTotpEnabled()) { + $pendingSecret = $admin->getTotpSecret(); + } else { + $pendingSecret = $storage->pendingSecret; + } + + if ($this->totpService->verifyCode($pendingSecret, $code)) { + $recoveryCodes = $this->totpService->generateRecoveryCodes(); + $admin->enableTotp($pendingSecret); + + $hashedRecoveryCodes = $this->totpService->hashRecoveryCodes($recoveryCodes); + $admin->setRecoveryCodes($hashedRecoveryCodes); + + $this->adminService->getAdminRepository()->saveResource($admin); + $storage->totp_verified = true; + if (isset($storage->recovery_auth)) { + unset($storage->recovery_auth); + } + + return new HtmlResponse($this->template->render('admin::list-recovery-codes', [ + 'totpForm' => $this->form, + 'plainCodes' => $recoveryCodes, + ])); + } else { + $this->messenger->addError(Message::VALIDATOR_INVALID_CODE); + } + + return new RedirectResponse($this->router->generateUri('admin::enable-totp-form')); + } +} \ No newline at end of file diff --git a/code_examples/totp/src/Admin/src/Handler/Account/PostValidateRecoveryHandler.php b/code_examples/totp/src/Admin/src/Handler/Account/PostValidateRecoveryHandler.php new file mode 100644 index 0000000..46f203f --- /dev/null +++ b/code_examples/totp/src/Admin/src/Handler/Account/PostValidateRecoveryHandler.php @@ -0,0 +1,82 @@ +adminService->findAdmin($this->authenticationService->getIdentity()->getId()); + } catch (NotFoundException $exception) { + $this->messenger->addError($exception->getMessage()); + + return new EmptyResponse(StatusCodeInterface::STATUS_NOT_FOUND); + } + + $parsedBody = $request->getParsedBody(); + $storage = $this->authenticationService->getStorage()->read(); + + if (! is_array($parsedBody) || ! isset($parsedBody['recoveryCode'])) { + $this->messenger->addError(Message::VALIDATOR_INVALID_CODE); + return new RedirectResponse( + $this->router->generateUri('admin::recovery-form') + ); + } + + $recoveryCode = (string) $parsedBody['recoveryCode']; + $hashedCodes = $admin->getRecoveryCodes() ?? []; + + if ($this->totpService->validateRecoveryCode($recoveryCode, $hashedCodes)) { + $admin->setRecoveryCodes(array_values($hashedCodes)); + $admin->disableTotp(); + $storage->recovery_auth = true; + + $this->adminService->getAdminRepository()->saveResource($admin); + return new RedirectResponse($this->router->generateUri('admin::enable-totp-form')); + } else { + $this->messenger->addError(Message::VALIDATOR_INVALID_CODE); + } + + return new RedirectResponse($this->router->generateUri('admin::validate-totp-form')); + } +} \ No newline at end of file diff --git a/code_examples/totp/src/Admin/src/Handler/Account/PostValidateTotpHandler.php b/code_examples/totp/src/Admin/src/Handler/Account/PostValidateTotpHandler.php new file mode 100644 index 0000000..c5aa0ae --- /dev/null +++ b/code_examples/totp/src/Admin/src/Handler/Account/PostValidateTotpHandler.php @@ -0,0 +1,78 @@ +adminService->findAdmin($this->authenticationService->getIdentity()->getId()); + } catch (NotFoundException $exception) { + $this->messenger->addError($exception->getMessage()); + + return new EmptyResponse(StatusCodeInterface::STATUS_NOT_FOUND); + } + + $parsedBody = $request->getParsedBody(); + $storage = $this->authenticationService->getStorage()->read(); + + if (! is_array($parsedBody) || ! isset($parsedBody['code'])) { + $this->messenger->addError(Message::VALIDATOR_INVALID_CODE); + return new RedirectResponse( + $this->router->generateUri('admin::validate-totp-form') + ); + } + + $code = (string) $parsedBody['code']; + + if ($this->totpService->verifyCode((string) $admin->getTotpSecret(), $code)) { + $this->adminService->getAdminRepository()->saveResource($admin); + $storage->totp_verified = true; + + return new RedirectResponse($this->router->generateUri($storage->route ?? 'dashboard::view-dashboard')); + } else { + $this->messenger->addError(Message::VALIDATOR_INVALID_CODE); + } + + return new RedirectResponse($this->router->generateUri('admin::validate-totp-form')); + } +} \ No newline at end of file diff --git a/code_examples/totp/src/Admin/templates/admin/recovery-form.html.twig b/code_examples/totp/src/Admin/templates/admin/recovery-form.html.twig new file mode 100644 index 0000000..4fa8fab --- /dev/null +++ b/code_examples/totp/src/Admin/templates/admin/recovery-form.html.twig @@ -0,0 +1,29 @@ + + + + + + +{{ messagesPartial('partial::notifications', {dismissible: true}) }} +
+
+
+

Recovery code

+ + {{ form().openTag(recoveryForm)|raw }} + + {{ formElement(recoveryForm.get('recoveryCode')) }} + {{ formElement(recoveryForm.get('recoveryCsrf')) }} + +
+ {{ formElement(recoveryForm.get('submit')) }} + Cancel +
+ + {{ form().closeTag()|raw }} +
+
+
+ + + \ No newline at end of file diff --git a/code_examples/totp/src/Admin/templates/admin/validate-totp-form.html.twig b/code_examples/totp/src/Admin/templates/admin/validate-totp-form.html.twig new file mode 100644 index 0000000..bd828b0 --- /dev/null +++ b/code_examples/totp/src/Admin/templates/admin/validate-totp-form.html.twig @@ -0,0 +1,44 @@ + + + + + + +{{ messagesPartial('partial::notifications', {dismissible: true}) }} +
+
+
+

{{ totpForm.getAttribute('title') }}

+ + {% if qrSvg is defined %} +
+ {{ qrSvg|raw }} +
+ +

+ Scan the QR code with your authenticator app and enter the 6-digit code below. +

+ {% endif %} + + {{ form().openTag(totpForm)|raw }} + + {{ formElement(totpForm.get('code')) }} + {{ formElement(totpForm.get('totpCsrf')) }} + +
+ {{ formElement(totpForm.get('submit')) }} + + {% if qrSvg is not defined %} + Recovery + {% endif %} + + Cancel +
+ + {{ form().closeTag()|raw }} +
+
+
+ + + \ No newline at end of file diff --git a/code_examples/totp/src/Admin/templates/admin/view-account.html.twig b/code_examples/totp/src/Admin/templates/admin/view-account.html.twig new file mode 100644 index 0000000..e69de29 diff --git a/code_examples/totp/src/App/src/Middleware/CancelUrlMiddleware.php b/code_examples/totp/src/App/src/Middleware/CancelUrlMiddleware.php new file mode 100644 index 0000000..1929046 --- /dev/null +++ b/code_examples/totp/src/App/src/Middleware/CancelUrlMiddleware.php @@ -0,0 +1,53 @@ +router->generateUri('admin::login-admin-form'); + + $storage = $this->authService->getStorage()->read(); + + if (isset($storage->recovery_auth) || isset($storage->totp_verified)) { + $referer = $request->getHeaderLine('Referer'); + + if (str_contains($referer, $this->router->generateUri('admin::recovery-form'))) { + $cancelUrl = $this->router->generateUri('admin::edit-account-form'); + } else { + $cancelUrl = $referer; + } + } + + return $handler->handle( + $request->withAttribute('cancelUrl', $cancelUrl) + ); + } +} \ No newline at end of file diff --git a/code_examples/totp/src/App/src/Middleware/TotpMiddleware.php b/code_examples/totp/src/App/src/Middleware/TotpMiddleware.php new file mode 100644 index 0000000..fd197cc --- /dev/null +++ b/code_examples/totp/src/App/src/Middleware/TotpMiddleware.php @@ -0,0 +1,110 @@ + */ + private array $routes; + + /** + * @param array $config + */ + public function __construct( + protected AuthenticationService $authService, + protected EntityManagerInterface $entityManager, + protected RouterInterface $router, + protected array $config + ) { + $this->routes = $config['dot_totp']['totp_required_routes'] ?? []; + } + + /** + * @throws OptimisticLockException + * @throws ORMException + * @throws ExceptionInterface + */ + public function process( + ServerRequestInterface $request, + RequestHandlerInterface $handler + ): responseInterface|RedirectResponse { + $storage = $this->authService->getStorage()->read(); + /** @var AdminIdentity $identity */ + $identity = $this->authService->getIdentity(); + $routeResult = $request->getAttribute(RouteResult::class); + $routeName = $routeResult->getMatchedRouteName(); + + $excludedRoutes = [ + 'admin::validate-totp-form', + 'admin::validate-totp', + 'admin::enable-totp-form', + 'admin::logout-admin', + 'admin::recovery-form', + 'admin::validate-recovery', + 'admin::login-admin-form', + ]; + + if ( + in_array($routeName, $excludedRoutes, true) || + (isset($storage->totp_verified) && $storage->totp_verified) + ) { + return $handler->handle($request); + } + + if ($identity instanceof UserInterface) { + $type = $identity::class; + $map = $this->config['dot_totp']['identity_class_map']; + + if (! isset($map[$type])) { + throw new RuntimeException("No identity_class configured for type $type"); + } + + /** @var class-string $entityClass */ + $entityClass = $map[$type]; + /** @var Admin|null $entity */ + $entity = $this->entityManager->find($entityClass, $identity->id); + + if ($entity === null) { + throw new RuntimeException("Entity of type $entityClass with ID {$identity->id} not found"); + } + + if (! isset($storage->totp_verified) && $entity->isTotpEnabled()) { + return new RedirectResponse($this->router->generateUri('admin::validate-totp-form')); + } + + if (! isset($this->routes[$routeName]) || ! $this->routes[$routeName]) { + return $handler->handle($request); + } + + $storage->route = $routeName; + if ($entity->isTotpEnabled()) { + return new RedirectResponse($this->router->generateUri('admin::validate-totp-form')); + } else { + return new RedirectResponse($this->router->generateUri('admin::enable-totp-form')); + } + } + + return $handler->handle($request); + } +} \ No newline at end of file diff --git a/code_examples/totp/src/Core/src/Admin/src/Entity/TotpTrait.php b/code_examples/totp/src/Core/src/Admin/src/Entity/TotpTrait.php new file mode 100644 index 0000000..4086331 --- /dev/null +++ b/code_examples/totp/src/Core/src/Admin/src/Entity/TotpTrait.php @@ -0,0 +1,64 @@ + false])] + protected bool $totpEnabled = false; + + /** @var string[]|null */ + #[ORM\Column(name: 'recovery_codes', type: Types::JSON, nullable: true)] + protected ?array $recoveryCodes = null; + + public function enableTotp(string $secret): self + { + $this->totpSecret = $secret; + $this->totpEnabled = true; + + return $this; + } + + public function disableTotp(): self + { + $this->totpSecret = null; + $this->totpEnabled = false; + $this->recoveryCodes = null; + + return $this; + } + + public function isTotpEnabled(): bool + { + return $this->totpEnabled; + } + + public function getTotpSecret(): ?string + { + return $this->totpSecret; + } + + /** + * @param string[]|null $recoveryCodes + */ + public function setRecoveryCodes(?array $recoveryCodes = null): void + { + $this->recoveryCodes = $recoveryCodes; + } + + /** + * @return string[]|null + */ + public function getRecoveryCodes(): ?array + { + return $this->recoveryCodes; + } +} \ No newline at end of file From 40f5b50e6da61cf1a776885d2159461536acadd5 Mon Sep 17 00:00:00 2001 From: bidi Date: Thu, 2 Apr 2026 14:25:45 +0300 Subject: [PATCH 2/7] phpcs fixes Signed-off-by: bidi --- code_examples/totp/_misc/totp-append-ConfigProvider.php | 2 +- code_examples/totp/_misc/totp-append-Pipeline.php | 2 +- code_examples/totp/_misc/totp-append-routes.php | 2 +- ...able-button.html.twig => totp-append-view-account.html.twig} | 2 +- code_examples/totp/config/autoload/config totp.global.php | 2 +- code_examples/totp/src/Admin/src/Form/RecoveryForm.php | 2 +- code_examples/totp/src/Admin/src/Form/TotpForm.php | 2 +- .../src/Admin/src/Handler/Account/GetDisableTotpFormHandler.php | 2 +- .../src/Admin/src/Handler/Account/GetEnableTotpFormHandler.php | 2 +- .../src/Admin/src/Handler/Account/GetRecoveryFormHandler.php | 2 +- .../totp/src/Admin/src/Handler/Account/GetTotpHandler.php | 2 +- .../src/Admin/src/Handler/Account/PostDisableTotpHandler.php | 2 +- .../src/Admin/src/Handler/Account/PostEnableTotpHandler.php | 2 +- .../Admin/src/Handler/Account/PostValidateRecoveryHandler.php | 2 +- .../src/Admin/src/Handler/Account/PostValidateTotpHandler.php | 2 +- .../totp/src/Admin/templates/admin/recovery-form.html.twig | 2 +- .../totp/src/Admin/templates/admin/validate-totp-form.html.twig | 2 +- .../totp/src/Admin/templates/admin/view-account.html.twig | 0 .../totp/src/App/src/Middleware/CancelUrlMiddleware.php | 2 +- code_examples/totp/src/App/src/Middleware/TotpMiddleware.php | 2 +- code_examples/totp/src/Core/src/Admin/src/Entity/TotpTrait.php | 2 +- 21 files changed, 20 insertions(+), 20 deletions(-) rename code_examples/totp/_misc/{totp-append-enable-disable-button.html.twig => totp-append-view-account.html.twig} (98%) delete mode 100644 code_examples/totp/src/Admin/templates/admin/view-account.html.twig diff --git a/code_examples/totp/_misc/totp-append-ConfigProvider.php b/code_examples/totp/_misc/totp-append-ConfigProvider.php index 629b8f5..44c4354 100644 --- a/code_examples/totp/_misc/totp-append-ConfigProvider.php +++ b/code_examples/totp/_misc/totp-append-ConfigProvider.php @@ -8,4 +8,4 @@ PostValidateRecoveryHandler::class => AttributedServiceFactory::class, TotpForm::class => ElementFactory::class, -RecoveryForm::class => ElementFactory::class, \ No newline at end of file +RecoveryForm::class => ElementFactory::class, diff --git a/code_examples/totp/_misc/totp-append-Pipeline.php b/code_examples/totp/_misc/totp-append-Pipeline.php index 454279b..ce07b19 100644 --- a/code_examples/totp/_misc/totp-append-Pipeline.php +++ b/code_examples/totp/_misc/totp-append-Pipeline.php @@ -1,2 +1,2 @@ $app->pipe(TotpMiddleware::class); -$app->pipe(CancelUrlMiddleware::class); \ No newline at end of file +$app->pipe(CancelUrlMiddleware::class); diff --git a/code_examples/totp/_misc/totp-append-routes.php b/code_examples/totp/_misc/totp-append-routes.php index 050e28d..f3f0f4b 100644 --- a/code_examples/totp/_misc/totp-append-routes.php +++ b/code_examples/totp/_misc/totp-append-routes.php @@ -5,4 +5,4 @@ ->get('/validate-totp-form', GetTotpHandler::class, 'admin::validate-totp-form') ->post('/validate-totp', PostValidateTotpHandler::class, 'admin::validate-totp') ->get('/recovery-form', GetRecoveryFormHandler::class, 'admin::recovery-form') -->post('/validate-recovery', PostValidateRecoveryHandler::class, 'admin::validate-recovery') \ No newline at end of file +->post('/validate-recovery', PostValidateRecoveryHandler::class, 'admin::validate-recovery') diff --git a/code_examples/totp/_misc/totp-append-enable-disable-button.html.twig b/code_examples/totp/_misc/totp-append-view-account.html.twig similarity index 98% rename from code_examples/totp/_misc/totp-append-enable-disable-button.html.twig rename to code_examples/totp/_misc/totp-append-view-account.html.twig index b993b76..c25efd6 100644 --- a/code_examples/totp/_misc/totp-append-enable-disable-button.html.twig +++ b/code_examples/totp/_misc/totp-append-view-account.html.twig @@ -17,4 +17,4 @@ {% endif %} - \ No newline at end of file + diff --git a/code_examples/totp/config/autoload/config totp.global.php b/code_examples/totp/config/autoload/config totp.global.php index bbbc7b6..71ac477 100644 --- a/code_examples/totp/config/autoload/config totp.global.php +++ b/code_examples/totp/config/autoload/config totp.global.php @@ -25,4 +25,4 @@ 'issuer' => 'ALX-Admin', ], ], -]; \ No newline at end of file +]; diff --git a/code_examples/totp/src/Admin/src/Form/RecoveryForm.php b/code_examples/totp/src/Admin/src/Form/RecoveryForm.php index f53ce11..9bdac91 100644 --- a/code_examples/totp/src/Admin/src/Form/RecoveryForm.php +++ b/code_examples/totp/src/Admin/src/Form/RecoveryForm.php @@ -74,4 +74,4 @@ public function init(): void ->setAttribute('class', 'btn btn-primary mt-2') ); } -} \ No newline at end of file +} diff --git a/code_examples/totp/src/Admin/src/Form/TotpForm.php b/code_examples/totp/src/Admin/src/Form/TotpForm.php index bcc213e..e381768 100644 --- a/code_examples/totp/src/Admin/src/Form/TotpForm.php +++ b/code_examples/totp/src/Admin/src/Form/TotpForm.php @@ -74,4 +74,4 @@ public function init(): void ->setAttribute('class', 'btn btn-primary mt-2') ); } -} \ No newline at end of file +} diff --git a/code_examples/totp/src/Admin/src/Handler/Account/GetDisableTotpFormHandler.php b/code_examples/totp/src/Admin/src/Handler/Account/GetDisableTotpFormHandler.php index 7f77f71..dd0d846 100644 --- a/code_examples/totp/src/Admin/src/Handler/Account/GetDisableTotpFormHandler.php +++ b/code_examples/totp/src/Admin/src/Handler/Account/GetDisableTotpFormHandler.php @@ -44,4 +44,4 @@ public function handle(ServerRequestInterface $request): ResponseInterface|Empty ]) ); } -} \ No newline at end of file +} diff --git a/code_examples/totp/src/Admin/src/Handler/Account/GetEnableTotpFormHandler.php b/code_examples/totp/src/Admin/src/Handler/Account/GetEnableTotpFormHandler.php index 6db9a05..61c1259 100644 --- a/code_examples/totp/src/Admin/src/Handler/Account/GetEnableTotpFormHandler.php +++ b/code_examples/totp/src/Admin/src/Handler/Account/GetEnableTotpFormHandler.php @@ -90,4 +90,4 @@ public function handle(ServerRequestInterface $request): ResponseInterface|Empty ]) ); } -} \ No newline at end of file +} diff --git a/code_examples/totp/src/Admin/src/Handler/Account/GetRecoveryFormHandler.php b/code_examples/totp/src/Admin/src/Handler/Account/GetRecoveryFormHandler.php index 2140d64..c95cf68 100644 --- a/code_examples/totp/src/Admin/src/Handler/Account/GetRecoveryFormHandler.php +++ b/code_examples/totp/src/Admin/src/Handler/Account/GetRecoveryFormHandler.php @@ -40,4 +40,4 @@ public function handle(ServerRequestInterface $request): ResponseInterface|Empty ]) ); } -} \ No newline at end of file +} diff --git a/code_examples/totp/src/Admin/src/Handler/Account/GetTotpHandler.php b/code_examples/totp/src/Admin/src/Handler/Account/GetTotpHandler.php index b155ed8..6a89c3c 100644 --- a/code_examples/totp/src/Admin/src/Handler/Account/GetTotpHandler.php +++ b/code_examples/totp/src/Admin/src/Handler/Account/GetTotpHandler.php @@ -41,4 +41,4 @@ public function handle(ServerRequestInterface $request): ResponseInterface|Empty ]) ); } -} \ No newline at end of file +} diff --git a/code_examples/totp/src/Admin/src/Handler/Account/PostDisableTotpHandler.php b/code_examples/totp/src/Admin/src/Handler/Account/PostDisableTotpHandler.php index 6115fb2..ddfa1b0 100644 --- a/code_examples/totp/src/Admin/src/Handler/Account/PostDisableTotpHandler.php +++ b/code_examples/totp/src/Admin/src/Handler/Account/PostDisableTotpHandler.php @@ -75,4 +75,4 @@ public function handle(ServerRequestInterface $request): EmptyResponse|RedirectR return new RedirectResponse($this->router->generateUri('admin::disable-totp-form')); } -} \ No newline at end of file +} diff --git a/code_examples/totp/src/Admin/src/Handler/Account/PostEnableTotpHandler.php b/code_examples/totp/src/Admin/src/Handler/Account/PostEnableTotpHandler.php index 18f7499..dd9652b 100644 --- a/code_examples/totp/src/Admin/src/Handler/Account/PostEnableTotpHandler.php +++ b/code_examples/totp/src/Admin/src/Handler/Account/PostEnableTotpHandler.php @@ -102,4 +102,4 @@ public function handle(ServerRequestInterface $request): EmptyResponse|RedirectR return new RedirectResponse($this->router->generateUri('admin::enable-totp-form')); } -} \ No newline at end of file +} diff --git a/code_examples/totp/src/Admin/src/Handler/Account/PostValidateRecoveryHandler.php b/code_examples/totp/src/Admin/src/Handler/Account/PostValidateRecoveryHandler.php index 46f203f..f50075c 100644 --- a/code_examples/totp/src/Admin/src/Handler/Account/PostValidateRecoveryHandler.php +++ b/code_examples/totp/src/Admin/src/Handler/Account/PostValidateRecoveryHandler.php @@ -79,4 +79,4 @@ public function handle(ServerRequestInterface $request): EmptyResponse|RedirectR return new RedirectResponse($this->router->generateUri('admin::validate-totp-form')); } -} \ No newline at end of file +} diff --git a/code_examples/totp/src/Admin/src/Handler/Account/PostValidateTotpHandler.php b/code_examples/totp/src/Admin/src/Handler/Account/PostValidateTotpHandler.php index c5aa0ae..6b03745 100644 --- a/code_examples/totp/src/Admin/src/Handler/Account/PostValidateTotpHandler.php +++ b/code_examples/totp/src/Admin/src/Handler/Account/PostValidateTotpHandler.php @@ -75,4 +75,4 @@ public function handle(ServerRequestInterface $request): EmptyResponse|RedirectR return new RedirectResponse($this->router->generateUri('admin::validate-totp-form')); } -} \ No newline at end of file +} diff --git a/code_examples/totp/src/Admin/templates/admin/recovery-form.html.twig b/code_examples/totp/src/Admin/templates/admin/recovery-form.html.twig index 4fa8fab..7a7a293 100644 --- a/code_examples/totp/src/Admin/templates/admin/recovery-form.html.twig +++ b/code_examples/totp/src/Admin/templates/admin/recovery-form.html.twig @@ -26,4 +26,4 @@ - \ No newline at end of file + diff --git a/code_examples/totp/src/Admin/templates/admin/validate-totp-form.html.twig b/code_examples/totp/src/Admin/templates/admin/validate-totp-form.html.twig index bd828b0..366e4dd 100644 --- a/code_examples/totp/src/Admin/templates/admin/validate-totp-form.html.twig +++ b/code_examples/totp/src/Admin/templates/admin/validate-totp-form.html.twig @@ -41,4 +41,4 @@ - \ No newline at end of file + diff --git a/code_examples/totp/src/Admin/templates/admin/view-account.html.twig b/code_examples/totp/src/Admin/templates/admin/view-account.html.twig deleted file mode 100644 index e69de29..0000000 diff --git a/code_examples/totp/src/App/src/Middleware/CancelUrlMiddleware.php b/code_examples/totp/src/App/src/Middleware/CancelUrlMiddleware.php index 1929046..8c0649c 100644 --- a/code_examples/totp/src/App/src/Middleware/CancelUrlMiddleware.php +++ b/code_examples/totp/src/App/src/Middleware/CancelUrlMiddleware.php @@ -50,4 +50,4 @@ public function process(ServerRequestInterface $request, RequestHandlerInterface $request->withAttribute('cancelUrl', $cancelUrl) ); } -} \ No newline at end of file +} diff --git a/code_examples/totp/src/App/src/Middleware/TotpMiddleware.php b/code_examples/totp/src/App/src/Middleware/TotpMiddleware.php index fd197cc..3ce1d34 100644 --- a/code_examples/totp/src/App/src/Middleware/TotpMiddleware.php +++ b/code_examples/totp/src/App/src/Middleware/TotpMiddleware.php @@ -107,4 +107,4 @@ public function process( return $handler->handle($request); } -} \ No newline at end of file +} diff --git a/code_examples/totp/src/Core/src/Admin/src/Entity/TotpTrait.php b/code_examples/totp/src/Core/src/Admin/src/Entity/TotpTrait.php index 4086331..989be2a 100644 --- a/code_examples/totp/src/Core/src/Admin/src/Entity/TotpTrait.php +++ b/code_examples/totp/src/Core/src/Admin/src/Entity/TotpTrait.php @@ -61,4 +61,4 @@ public function getRecoveryCodes(): ?array { return $this->recoveryCodes; } -} \ No newline at end of file +} From 042aa9255c9cc9bf5f0231d9818a16484c476d8f Mon Sep 17 00:00:00 2001 From: bidi Date: Thu, 2 Apr 2026 14:38:59 +0300 Subject: [PATCH 3/7] moved trait Signed-off-by: bidi --- .../src/Admin/src => App/src/Middleware}/Entity/TotpTrait.php | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename code_examples/totp/src/{Core/src/Admin/src => App/src/Middleware}/Entity/TotpTrait.php (100%) diff --git a/code_examples/totp/src/Core/src/Admin/src/Entity/TotpTrait.php b/code_examples/totp/src/App/src/Middleware/Entity/TotpTrait.php similarity index 100% rename from code_examples/totp/src/Core/src/Admin/src/Entity/TotpTrait.php rename to code_examples/totp/src/App/src/Middleware/Entity/TotpTrait.php From cdffcc7ab50301be97d17566b2fec37575cd7ed6 Mon Sep 17 00:00:00 2001 From: bidi Date: Thu, 2 Apr 2026 14:39:36 +0300 Subject: [PATCH 4/7] moved trait Signed-off-by: bidi --- .../totp/src/App/src/{Middleware => }/Entity/TotpTrait.php | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename code_examples/totp/src/App/src/{Middleware => }/Entity/TotpTrait.php (100%) diff --git a/code_examples/totp/src/App/src/Middleware/Entity/TotpTrait.php b/code_examples/totp/src/App/src/Entity/TotpTrait.php similarity index 100% rename from code_examples/totp/src/App/src/Middleware/Entity/TotpTrait.php rename to code_examples/totp/src/App/src/Entity/TotpTrait.php From ae6d3a7987677cdb694e8322153b05fbecfab997 Mon Sep 17 00:00:00 2001 From: bidi Date: Thu, 2 Apr 2026 14:45:31 +0300 Subject: [PATCH 5/7] moved trait Signed-off-by: bidi --- .../totp/src/{ => Core/src}/App/src/Entity/TotpTrait.php | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename code_examples/totp/src/{ => Core/src}/App/src/Entity/TotpTrait.php (100%) diff --git a/code_examples/totp/src/App/src/Entity/TotpTrait.php b/code_examples/totp/src/Core/src/App/src/Entity/TotpTrait.php similarity index 100% rename from code_examples/totp/src/App/src/Entity/TotpTrait.php rename to code_examples/totp/src/Core/src/App/src/Entity/TotpTrait.php From acdb7ac2636827ff087e6cb3d80a3aa28eb706c3 Mon Sep 17 00:00:00 2001 From: bidi Date: Thu, 2 Apr 2026 15:02:15 +0300 Subject: [PATCH 6/7] renamed config file Signed-off-by: bidi --- .../config/autoload/{config totp.global.php => totp.global.php} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename code_examples/totp/config/autoload/{config totp.global.php => totp.global.php} (100%) diff --git a/code_examples/totp/config/autoload/config totp.global.php b/code_examples/totp/config/autoload/totp.global.php similarity index 100% rename from code_examples/totp/config/autoload/config totp.global.php rename to code_examples/totp/config/autoload/totp.global.php From e35951b8a1d513c7e5481d483ce5ccfad73c4cfa Mon Sep 17 00:00:00 2001 From: bidi Date: Thu, 2 Apr 2026 15:08:59 +0300 Subject: [PATCH 7/7] updated config Signed-off-by: bidi --- code_examples/totp/config/autoload/totp.global.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/code_examples/totp/config/autoload/totp.global.php b/code_examples/totp/config/autoload/totp.global.php index 71ac477..3383c8a 100644 --- a/code_examples/totp/config/autoload/totp.global.php +++ b/code_examples/totp/config/autoload/totp.global.php @@ -22,7 +22,7 @@ 'algorithm' => 'sha1', ], 'provision_uri_config' => [ - 'issuer' => 'ALX-Admin', + 'issuer' => 'DK-Admin', ], ], ];