feat(altcha): add altcha validation layer to login
Some checks failed
Cadoles/hydra-sql/pipeline/head There was a failure building this commit
Some checks failed
Cadoles/hydra-sql/pipeline/head There was a failure building this commit
This commit is contained in:
parent
1cb5ae6bc3
commit
488ca00a41
8
.env
8
.env
@ -41,3 +41,11 @@ LOCK_DSN=flock
|
|||||||
SENTRY_DSN=
|
SENTRY_DSN=
|
||||||
###< sentry/sentry-symfony ###
|
###< sentry/sentry-symfony ###
|
||||||
REDIS_DSN=redis://redis:6379
|
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
|
||||||
|
@ -75,3 +75,19 @@ services:
|
|||||||
$securityPattern: '%security_pattern%'
|
$securityPattern: '%security_pattern%'
|
||||||
# add more service definitions when explicit configuration is needed
|
# add more service definitions when explicit configuration is needed
|
||||||
# please note that last definitions always *replace* previous ones
|
# 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)%"
|
48
src/Altcha/AltchaTransformer.php
Normal file
48
src/Altcha/AltchaTransformer.php
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Altcha;
|
||||||
|
|
||||||
|
use App\Altcha\Form\AltchaModel;
|
||||||
|
use Symfony\Component\Form\DataTransformerInterface;
|
||||||
|
|
||||||
|
class AltchaTransformer implements DataTransformerInterface
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
public function reverseTransform($value): AltchaModel
|
||||||
|
{
|
||||||
|
if (empty($value)) {
|
||||||
|
return new AltchaModel();
|
||||||
|
}
|
||||||
|
|
||||||
|
$decodedValue = base64_decode($value);
|
||||||
|
$data = json_decode($decodedValue);
|
||||||
|
|
||||||
|
$model = new AltchaModel();
|
||||||
|
|
||||||
|
foreach ($data as $property => $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);
|
||||||
|
}
|
||||||
|
}
|
52
src/Altcha/AltchaValidator.php
Normal file
52
src/Altcha/AltchaValidator.php
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Altcha;
|
||||||
|
|
||||||
|
use Symfony\Component\Form\FormError;
|
||||||
|
use Symfony\Component\Form\FormEvent;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||||
|
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||||
|
|
||||||
|
class AltchaValidator
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly string $altchaHost,
|
||||||
|
private readonly string $altchaBaseUrl,
|
||||||
|
private readonly HttpClientInterface $httpClient,
|
||||||
|
private readonly TranslatorInterface $translator
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public function validate(FormEvent $formEvent): void
|
||||||
|
{
|
||||||
|
$form = $formEvent->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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
39
src/Altcha/Form/AltchaModel.php
Normal file
39
src/Altcha/Form/AltchaModel.php
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Altcha\Form;
|
||||||
|
|
||||||
|
use Symfony\Component\Validator\Constraints as Assert;
|
||||||
|
|
||||||
|
class AltchaModel
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @Assert\NotBlank()
|
||||||
|
* @Assert\Regex("/^(SHA-1|SHA-256|SHA-512)$/")
|
||||||
|
*/
|
||||||
|
public string $algorithm;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Assert\NotBlank()
|
||||||
|
*/
|
||||||
|
public string $challenge;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Assert\NotBlank()
|
||||||
|
*/
|
||||||
|
public string $salt;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Assert\NotBlank()
|
||||||
|
*/
|
||||||
|
public int $number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Assert\NotBlank()
|
||||||
|
*/
|
||||||
|
public string $signature;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Assert\NotBlank()
|
||||||
|
*/
|
||||||
|
public int $took;
|
||||||
|
}
|
81
src/Altcha/Form/AltchaType.php
Normal file
81
src/Altcha/Form/AltchaType.php
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Altcha\Form;
|
||||||
|
|
||||||
|
use App\Altcha\AltchaValidator;
|
||||||
|
use App\Altcha\AltchaTransformer;
|
||||||
|
use Symfony\Component\Form\FormView;
|
||||||
|
use Symfony\Component\Form\FormEvents;
|
||||||
|
use Symfony\Component\Form\AbstractType;
|
||||||
|
use Symfony\Component\Form\FormInterface;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
use Symfony\Component\Form\FormBuilderInterface;
|
||||||
|
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||||
|
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||||
|
use Symfony\Component\Form\Extension\Core\Type\TextType;
|
||||||
|
|
||||||
|
class AltchaType extends AbstractType
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly string $altchaHost,
|
||||||
|
private readonly string $altchaBaseUrl,
|
||||||
|
private readonly string $altchaDebug,
|
||||||
|
private readonly string $altchaWorkers,
|
||||||
|
private readonly string $altchaDelay,
|
||||||
|
private readonly string $altchaMockError,
|
||||||
|
private readonly AltchaValidator $altchaValidator,
|
||||||
|
private readonly HttpClientInterface $httpClient,
|
||||||
|
private readonly TranslatorInterface $translator
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||||
|
{
|
||||||
|
$builder->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';
|
||||||
|
}
|
||||||
|
}
|
@ -2,12 +2,13 @@
|
|||||||
|
|
||||||
namespace App\Form;
|
namespace App\Form;
|
||||||
|
|
||||||
|
use App\Altcha\Form\AltchaType;
|
||||||
use Symfony\Component\Form\AbstractType;
|
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\Form\FormBuilderInterface;
|
||||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
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
|
class LoginType extends AbstractType
|
||||||
{
|
{
|
||||||
@ -27,6 +28,10 @@ class LoginType extends AbstractType
|
|||||||
'translation_domain' => 'form',
|
'translation_domain' => 'form',
|
||||||
'label' => 'form.label.remember_me',
|
'label' => 'form.label.remember_me',
|
||||||
])
|
])
|
||||||
|
->add('altcha', AltchaType::class, [
|
||||||
|
'label' => false,
|
||||||
|
'required' => true,
|
||||||
|
])
|
||||||
;
|
;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -9,19 +9,17 @@ use Symfony\Contracts\HttpClient\ResponseInterface;
|
|||||||
|
|
||||||
class Client
|
class Client
|
||||||
{
|
{
|
||||||
private HttpClientInterface $client;
|
|
||||||
private const MAX_RETRY = 3;
|
private const MAX_RETRY = 3;
|
||||||
private const SLEEP_TIME = [
|
private const SLEEP_TIME = [
|
||||||
5,
|
5,
|
||||||
500,
|
500,
|
||||||
5000,
|
5000,
|
||||||
];
|
];
|
||||||
private string $hydraAdminBaseUrl;
|
|
||||||
|
|
||||||
public function __construct(HttpClientInterface $client, string $hydraAdminBaseUrl)
|
public function __construct(
|
||||||
{
|
private readonly HttpClientInterface $client,
|
||||||
$this->client = $client;
|
private readonly string $hydraAdminBaseUrl
|
||||||
$this->hydraAdminBaseUrl = $hydraAdminBaseUrl;
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
public function fetchLoginRequestInfo(string $loginChallenge): ResponseInterface
|
public function fetchLoginRequestInfo(string $loginChallenge): ResponseInterface
|
||||||
|
18
templates/altcha.html.twig
Normal file
18
templates/altcha.html.twig
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
{% block altcha_widget %}
|
||||||
|
<style>
|
||||||
|
.altcha label {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<altcha-widget
|
||||||
|
challengejson={{challengeJson}}
|
||||||
|
name={{form.vars.full_name}}
|
||||||
|
strings="{{translations}}"
|
||||||
|
hidelogo
|
||||||
|
hidefooter
|
||||||
|
workers= {{ workers }}
|
||||||
|
delay={{ delay }}
|
||||||
|
{{ debug ? 'debug' : ''}}
|
||||||
|
{{ mockError ? 'mockerror' : ''}}
|
||||||
|
></altcha-widget>
|
||||||
|
{% endblock %}
|
Loading…
x
Reference in New Issue
Block a user