From 488ca00a414ebeab6b397e84ed2afcd5b1822a64 Mon Sep 17 00:00:00 2001 From: Gauthier DUPONT Date: Mon, 24 Mar 2025 17:20:17 +0100 Subject: [PATCH] feat(altcha): add altcha validation layer to login --- .env | 8 ++++ config/services.yaml | 16 +++++++ src/Altcha/AltchaTransformer.php | 48 +++++++++++++++++++ src/Altcha/AltchaValidator.php | 52 ++++++++++++++++++++ src/Altcha/Form/AltchaModel.php | 39 +++++++++++++++ src/Altcha/Form/AltchaType.php | 81 ++++++++++++++++++++++++++++++++ src/Form/LoginType.php | 11 +++-- src/Hydra/Client.php | 10 ++-- templates/altcha.html.twig | 18 +++++++ 9 files changed, 274 insertions(+), 9 deletions(-) create mode 100644 src/Altcha/AltchaTransformer.php create mode 100644 src/Altcha/AltchaValidator.php create mode 100644 src/Altcha/Form/AltchaModel.php create mode 100644 src/Altcha/Form/AltchaType.php create mode 100644 templates/altcha.html.twig diff --git a/.env b/.env index e13dd09..ce22709 100644 --- a/.env +++ b/.env @@ -41,3 +41,11 @@ LOCK_DSN=flock SENTRY_DSN= ###< sentry/sentry-symfony ### REDIS_DSN=redis://redis:6379 + +### Altcha +ALTCHA_HOST='http://localhost:3333' +ALTCHA_BASE_URL='' +ALTCHA_DEBUG=false +ALTCHA_WORKERS=8 +ALTCHA_DELAY=100 +ALTCHA_MOCK_ERROR=false diff --git a/config/services.yaml b/config/services.yaml index 30bc817..404edfe 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -75,3 +75,19 @@ services: $securityPattern: '%security_pattern%' # add more service definitions when explicit configuration is needed # please note that last definitions always *replace* previous ones + + App\Altcha\Form\AltchaType: + arguments: + $altchaHost: "%env(string:ALTCHA_HOST)%" + $altchaBaseUrl: "%env(string:ALTCHA_BASE_URL)%" + $altchaDebug: "%env(bool:ALTCHA_DEBUG)%" + $altchaWorkers: "%env(string:ALTCHA_WORKERS)%" + $altchaDelay: "%env(string:ALTCHA_DELAY)%" + $altchaMockError: "%env(bool:ALTCHA_MOCK_ERROR)%" + tags: + - { name: form.type, alias: altcha} + + App\Altcha\AltchaValidator: + arguments: + $altchaHost: "%env(string:ALTCHA_HOST)%" + $altchaBaseUrl: "%env(string:ALTCHA_BASE_URL)%" \ No newline at end of file diff --git a/src/Altcha/AltchaTransformer.php b/src/Altcha/AltchaTransformer.php new file mode 100644 index 0000000..8378a7c --- /dev/null +++ b/src/Altcha/AltchaTransformer.php @@ -0,0 +1,48 @@ + $value) { + $model->{$property} = $value; + } + + return $model; + } + + /** + * {@inheritDoc} + */ + public function transform($value): string + { + if (empty($value)) { + return ''; + } + + $json = json_encode($value); + + if (false === $json) { + return ''; + } + + return base64_encode($json); + } +} diff --git a/src/Altcha/AltchaValidator.php b/src/Altcha/AltchaValidator.php new file mode 100644 index 0000000..793d969 --- /dev/null +++ b/src/Altcha/AltchaValidator.php @@ -0,0 +1,52 @@ +getForm(); + $data = $form->getData(); + + $response = $this->httpClient->request( + 'POST', + $this->altchaHost.$this->altchaBaseUrl.'/verify', + [ + 'body' => json_encode($data), + 'headers' => [ + 'Content-Type' => 'application/json', + ], + ], + ); + + if (Response::HTTP_OK !== $response->getStatusCode()) { + $form->addError(new FormError($this->translator->trans('altcha.validator.server_validation_error', [], 'form'))); + + return; + } + + $content = $response->getContent(); + $parsedResponse = json_decode($content); + + if (true !== $parsedResponse->success) { + $form->addError(new FormError($this->translator->trans('altcha.validator.server_validation_error', [], 'form'))); + + return; + } + } +} diff --git a/src/Altcha/Form/AltchaModel.php b/src/Altcha/Form/AltchaModel.php new file mode 100644 index 0000000..abb2ba5 --- /dev/null +++ b/src/Altcha/Form/AltchaModel.php @@ -0,0 +1,39 @@ +addModelTransformer(new AltchaTransformer()); + + $builder->addEventListener(FormEvents::POST_SUBMIT, [$this->altchaValidator, 'validate']); + } + + public function buildView(FormView $view, FormInterface $form, array $options) + { + $translations = [ + 'label' => $this->translator->trans('altcha.widget.label', [], 'form'), + 'verified' => $this->translator->trans('altcha.widget.verified', [], 'form'), + 'verifying' => $this->translator->trans('altcha.widget.verifying', [], 'form'), + 'waitAlert' => $this->translator->trans('altcha.widget.waitalert', [], 'form'), + 'error' => $this->translator->trans('altcha.widget.error', [], 'form'), + 'expired' => $this->translator->trans('altcha.widget.expired', [], 'form'), + ]; + $view->vars['translations'] = json_encode($translations); + $view->vars['challengeJson'] = $this->requestChallenge(); + $view->vars['debug'] = $this->altchaDebug; + $view->vars['workers'] = $this->altchaWorkers; + $view->vars['delay'] = $this->altchaDelay; + $view->vars['mockError'] = $this->altchaMockError; + } + + private function requestChallenge(): string + { + $resp = $this->httpClient->request('GET', $this->altchaHost.$this->altchaBaseUrl.'/request'); + if (Response::HTTP_OK === $resp->getStatusCode()) { + return $resp->getContent(); + } + + return ''; + } + + public function getParent(): string + { + return TextType::class; + } + + public function getName(): string + { + return $this->getBlockPrefix(); + } + + public function getBlockPrefix(): string + { + return 'altcha'; + } +} diff --git a/src/Form/LoginType.php b/src/Form/LoginType.php index c857984..a3e4193 100644 --- a/src/Form/LoginType.php +++ b/src/Form/LoginType.php @@ -2,12 +2,13 @@ namespace App\Form; +use App\Altcha\Form\AltchaType; use Symfony\Component\Form\AbstractType; -use Symfony\Component\Form\Extension\Core\Type\CheckboxType; -use Symfony\Component\Form\Extension\Core\Type\PasswordType; -use Symfony\Component\Form\Extension\Core\Type\TextType; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\OptionsResolver; +use Symfony\Component\Form\Extension\Core\Type\TextType; +use Symfony\Component\Form\Extension\Core\Type\CheckboxType; +use Symfony\Component\Form\Extension\Core\Type\PasswordType; class LoginType extends AbstractType { @@ -27,6 +28,10 @@ class LoginType extends AbstractType 'translation_domain' => 'form', 'label' => 'form.label.remember_me', ]) + ->add('altcha', AltchaType::class, [ + 'label' => false, + 'required' => true, + ]) ; } diff --git a/src/Hydra/Client.php b/src/Hydra/Client.php index 00592f9..f0e7569 100644 --- a/src/Hydra/Client.php +++ b/src/Hydra/Client.php @@ -9,19 +9,17 @@ use Symfony\Contracts\HttpClient\ResponseInterface; class Client { - private HttpClientInterface $client; private const MAX_RETRY = 3; private const SLEEP_TIME = [ 5, 500, 5000, ]; - private string $hydraAdminBaseUrl; - public function __construct(HttpClientInterface $client, string $hydraAdminBaseUrl) - { - $this->client = $client; - $this->hydraAdminBaseUrl = $hydraAdminBaseUrl; + public function __construct( + private readonly HttpClientInterface $client, + private readonly string $hydraAdminBaseUrl + ) { } public function fetchLoginRequestInfo(string $loginChallenge): ResponseInterface diff --git a/templates/altcha.html.twig b/templates/altcha.html.twig new file mode 100644 index 0000000..e70036f --- /dev/null +++ b/templates/altcha.html.twig @@ -0,0 +1,18 @@ +{% block altcha_widget %} + + +{% endblock %} \ No newline at end of file