implémentation formulaire, cookie
This commit is contained in:
8
.env
8
.env
@@ -16,7 +16,7 @@
|
|||||||
|
|
||||||
###> symfony/framework-bundle ###
|
###> symfony/framework-bundle ###
|
||||||
APP_ENV=dev
|
APP_ENV=dev
|
||||||
APP_SECRET=
|
APP_SECRET=1235dsd51sdkjbsd3531dsckjsedlkknds448552se
|
||||||
###< symfony/framework-bundle ###
|
###< symfony/framework-bundle ###
|
||||||
|
|
||||||
###> doctrine/doctrine-bundle ###
|
###> doctrine/doctrine-bundle ###
|
||||||
@@ -26,18 +26,18 @@ APP_SECRET=
|
|||||||
# DATABASE_URL="sqlite:///%kernel.project_dir%/var/data_%kernel.environment%.db"
|
# DATABASE_URL="sqlite:///%kernel.project_dir%/var/data_%kernel.environment%.db"
|
||||||
# DATABASE_URL="mysql://app:!ChangeMe!@127.0.0.1:3306/app?serverVersion=8.0.32&charset=utf8mb4"
|
# DATABASE_URL="mysql://app:!ChangeMe!@127.0.0.1:3306/app?serverVersion=8.0.32&charset=utf8mb4"
|
||||||
# DATABASE_URL="mysql://app:!ChangeMe!@127.0.0.1:3306/app?serverVersion=10.11.2-MariaDB&charset=utf8mb4"
|
# DATABASE_URL="mysql://app:!ChangeMe!@127.0.0.1:3306/app?serverVersion=10.11.2-MariaDB&charset=utf8mb4"
|
||||||
DATABASE_URL="postgresql://app:!ChangeMe!@127.0.0.1:5432/app?serverVersion=16&charset=utf8"
|
DATABASE_URL="postgres://lasql:lasql@postgres:5432/lasql?sslmode=disable"
|
||||||
###< doctrine/doctrine-bundle ###
|
###< doctrine/doctrine-bundle ###
|
||||||
|
|
||||||
###> symfony/messenger ###
|
###> symfony/messenger ###
|
||||||
# Choose one of the transports below
|
# Choose one of the transports below
|
||||||
# MESSENGER_TRANSPORT_DSN=amqp://guest:guest@localhost:5672/%2f/messages
|
# MESSENGER_TRANSPORT_DSN=amqp://guest:guest@localhost:5672/%2f/messages
|
||||||
# MESSENGER_TRANSPORT_DSN=redis://localhost:6379/messages
|
# MESSENGER_TRANSPORT_DSN=redis://localhost:6379/messages
|
||||||
MESSENGER_TRANSPORT_DSN=doctrine://default?auto_setup=0
|
MESSENGER_TRANSPORT_DSN=doctrine://default?auto_setup=1
|
||||||
###< symfony/messenger ###
|
###< symfony/messenger ###
|
||||||
|
|
||||||
###> symfony/mailer ###
|
###> symfony/mailer ###
|
||||||
MAILER_DSN=null://null
|
MAILER_DSN='smtp://mailhog:1025'
|
||||||
###< symfony/mailer ###
|
###< symfony/mailer ###
|
||||||
BASE_URL='http://localhost:8080'
|
BASE_URL='http://localhost:8080'
|
||||||
HYDRA_ADMIN_BASE_URL='http://hydra:4445'
|
HYDRA_ADMIN_BASE_URL='http://hydra:4445'
|
||||||
|
@@ -8,9 +8,12 @@ RUN apt-get update && apt-get install -y \
|
|||||||
COPY --from=composer:latest /usr/bin/composer /usr/local/bin/composer
|
COPY --from=composer:latest /usr/bin/composer /usr/local/bin/composer
|
||||||
# Installe les extensions PHP nécessaires pour Symfony (pdo_mysql par exemple, si tu utilises MySQL)
|
# Installe les extensions PHP nécessaires pour Symfony (pdo_mysql par exemple, si tu utilises MySQL)
|
||||||
RUN install-php-extensions \
|
RUN install-php-extensions \
|
||||||
|
sockets \
|
||||||
zip \
|
zip \
|
||||||
intl \
|
intl \
|
||||||
opcache
|
opcache \
|
||||||
|
openssl \
|
||||||
|
pdo_pgsql
|
||||||
|
|
||||||
# Définit le répertoire de travail
|
# Définit le répertoire de travail
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
@@ -4,6 +4,7 @@ framework:
|
|||||||
|
|
||||||
# Note that the session will be started ONLY if you read or write from it.
|
# Note that the session will be started ONLY if you read or write from it.
|
||||||
session: true
|
session: true
|
||||||
|
trusted_headers: ['x-forwarded-for', 'x-forwarded-proto', 'x-forwarded-prefix']
|
||||||
|
|
||||||
#esi: true
|
#esi: true
|
||||||
#fragments: true
|
#fragments: true
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
twig:
|
twig:
|
||||||
file_name_pattern: '*.twig'
|
file_name_pattern: '*.twig'
|
||||||
|
form_themes:
|
||||||
|
- "bootstrap_5_layout.html.twig"
|
||||||
when@test:
|
when@test:
|
||||||
twig:
|
twig:
|
||||||
strict_variables: true
|
strict_variables: true
|
||||||
|
@@ -4,7 +4,7 @@
|
|||||||
# Put parameters here that don't need to change on each machine where the app is deployed
|
# Put parameters here that don't need to change on each machine where the app is deployed
|
||||||
# https://symfony.com/doc/current/best_practices.html#use-parameters-for-application-configuration
|
# https://symfony.com/doc/current/best_practices.html#use-parameters-for-application-configuration
|
||||||
parameters:
|
parameters:
|
||||||
|
app.2fa_remember_duration: 'P30D'
|
||||||
services:
|
services:
|
||||||
# default configuration for services in *this* file
|
# default configuration for services in *this* file
|
||||||
_defaults:
|
_defaults:
|
||||||
|
@@ -2,16 +2,84 @@
|
|||||||
|
|
||||||
namespace App\Controller;
|
namespace App\Controller;
|
||||||
|
|
||||||
|
use App\Form\CodeType;
|
||||||
|
use App\Hydra\Client;
|
||||||
|
use App\Service\CodeService;
|
||||||
|
use App\Service\CookieService;
|
||||||
|
use App\Service\DeviceService;
|
||||||
|
use App\Service\MailerService;
|
||||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
|
use Symfony\Component\HttpFoundation\Cookie;
|
||||||
|
use Symfony\Component\HttpFoundation\Exception\BadRequestException;
|
||||||
|
use Symfony\Component\HttpFoundation\RedirectResponse;
|
||||||
use Symfony\Component\HttpFoundation\Request;
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
use Symfony\Component\HttpFoundation\Response;
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
use Symfony\Component\Mailer\MailerInterface;
|
||||||
|
use Symfony\Component\Mime\Email;
|
||||||
use Symfony\Component\Routing\Annotation\Route;
|
use Symfony\Component\Routing\Annotation\Route;
|
||||||
|
|
||||||
class MainController extends AbstractController
|
class MainController extends AbstractController
|
||||||
{
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly CodeService $codeService,
|
||||||
|
private readonly Client $client,
|
||||||
|
private readonly CookieService $cookieService,
|
||||||
|
private readonly MailerService $mailer
|
||||||
|
|
||||||
|
){
|
||||||
|
|
||||||
|
}
|
||||||
#[Route('/', name: 'app_home')]
|
#[Route('/', name: 'app_home')]
|
||||||
public function home(Request $request): Response
|
public function home(Request $request): Response
|
||||||
{
|
{
|
||||||
return new Response('<h1>Hello world</h1>');
|
return new Response('<h1>Hello world</h1>');
|
||||||
}
|
}
|
||||||
|
#[Route('/2fa', name: 'app_2fa')]
|
||||||
|
public function doubleFacteur(Request $request): Response
|
||||||
|
{
|
||||||
|
$loginChallenge = $request->query->get('loginchallenge');
|
||||||
|
$identifier = $request->query->get('identifier');
|
||||||
|
$res = $this->client->fetchLoginRequestInfo($loginChallenge);
|
||||||
|
$loginRequestInfo = $res->toArray();
|
||||||
|
if (200 !== $res->getStatusCode()) {
|
||||||
|
throw new BadRequestException();
|
||||||
|
}
|
||||||
|
|
||||||
|
if($this->cookieService->isValid($request, $identifier)){
|
||||||
|
$loginAcceptRes = $this->client->acceptLoginRequest($loginChallenge, [
|
||||||
|
'subject' => $identifier,
|
||||||
|
'remember' => true,
|
||||||
|
])->toArray();
|
||||||
|
|
||||||
|
return new RedirectResponse($loginAcceptRes['redirect_to']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$code = $this->codeService->generateCode();
|
||||||
|
|
||||||
|
$this->mailer->send($code, $identifier);
|
||||||
|
$form = $this->createForm(CodeType::class);
|
||||||
|
$form->handleRequest($request);
|
||||||
|
|
||||||
|
if ($form->isSubmitted() && $form->isValid()){
|
||||||
|
$cookie = null;
|
||||||
|
if($form->get('remember_device')){
|
||||||
|
$cookie = $this->cookieService->createCookie($identifier);
|
||||||
|
}
|
||||||
|
|
||||||
|
$loginAcceptRes = $this->client->acceptLoginRequest($loginChallenge, [
|
||||||
|
'subject' => $identifier,
|
||||||
|
'remember' => true,
|
||||||
|
])->toArray();
|
||||||
|
|
||||||
|
|
||||||
|
dump($loginAcceptRes);
|
||||||
|
$response = new RedirectResponse($loginAcceptRes['redirect_to']);
|
||||||
|
|
||||||
|
null !== $cookie ?: $response->headers->setCookie($cookie);
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
return $this->render('base.html.twig', [
|
||||||
|
'form'=>$form
|
||||||
|
]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
47
src/Form/CodeType.php
Normal file
47
src/Form/CodeType.php
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Form;
|
||||||
|
|
||||||
|
use App\Validator\Code;
|
||||||
|
use Symfony\Component\Form\AbstractType;
|
||||||
|
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
|
||||||
|
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
|
||||||
|
use Symfony\Component\Form\Extension\Core\Type\TextType;
|
||||||
|
use Symfony\Component\Form\FormBuilderInterface;
|
||||||
|
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||||
|
use Symfony\Component\Validator\Constraints\NotBlank;
|
||||||
|
|
||||||
|
class CodeType extends AbstractType
|
||||||
|
{
|
||||||
|
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||||
|
{
|
||||||
|
$builder
|
||||||
|
->add('code', TextType::class, [
|
||||||
|
'label' => 'Entrez le code reçu par mail',
|
||||||
|
'required' => true,
|
||||||
|
'mapped' => false,
|
||||||
|
'constraints' => [
|
||||||
|
new NotBlank(['message' => 'Le code ne peut pas être vide.']),
|
||||||
|
new Code(),
|
||||||
|
],
|
||||||
|
])
|
||||||
|
->add('remember_device', CheckboxType::class, [
|
||||||
|
'label'=>('Se souvenir de cet ordinateur'),
|
||||||
|
'required'=> false,
|
||||||
|
'mapped'=>false,
|
||||||
|
])
|
||||||
|
->add('submit', SubmitType::class, ['label' => 'Valider']);
|
||||||
|
}
|
||||||
|
public function configureOptions(OptionsResolver $resolver): void
|
||||||
|
{
|
||||||
|
$resolver
|
||||||
|
->setDefaults([
|
||||||
|
'data_class' => null,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
public function getBlockPrefix(): string
|
||||||
|
{
|
||||||
|
return 'code';
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
36
src/Service/CodeService.php
Normal file
36
src/Service/CodeService.php
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Service;
|
||||||
|
|
||||||
|
use Symfony\Component\HttpFoundation\RequestStack;
|
||||||
|
|
||||||
|
class CodeService
|
||||||
|
{
|
||||||
|
private const int VALIDITY_TIME = 1800;
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
private readonly RequestStack $requestStack
|
||||||
|
){
|
||||||
|
|
||||||
|
}
|
||||||
|
public function generateCode(): array
|
||||||
|
{
|
||||||
|
$code = $this->requestStack->getSession()->get('code2fa');
|
||||||
|
|
||||||
|
if($code){
|
||||||
|
$createdAt = $code['createdAt'] ?? null;
|
||||||
|
$maxTime = $createdAt + self::VALIDITY_TIME;
|
||||||
|
if ($maxTime < time()) {
|
||||||
|
|
||||||
|
return $code;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$code = [
|
||||||
|
'code' => '0005',
|
||||||
|
'createdAt' => time(),
|
||||||
|
];
|
||||||
|
$this->requestStack->getSession()->set('code2fa', $code);
|
||||||
|
|
||||||
|
return $code;
|
||||||
|
}
|
||||||
|
}
|
69
src/Service/CookieService.php
Normal file
69
src/Service/CookieService.php
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Service;
|
||||||
|
|
||||||
|
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
|
||||||
|
use Symfony\Component\HttpFoundation\Cookie;
|
||||||
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
|
||||||
|
class CookieService
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly ParameterBagInterface $params
|
||||||
|
){}
|
||||||
|
private const COOKIE_2FA = 'user_info-2fa';
|
||||||
|
public function isValid(Request $request, string $login): bool
|
||||||
|
{
|
||||||
|
$cookieValue = $request->cookies->get(self::COOKIE_2FA);
|
||||||
|
if(!$cookieValue){
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
[$encodedData, $signature] = explode('.', $cookieValue);
|
||||||
|
$dataJson = base64_decode($encodedData);
|
||||||
|
$secret = $this->params->get('kernel.secret');
|
||||||
|
if (hash_hmac('sha256', $dataJson, $secret) !== $signature) {
|
||||||
|
return false; // Signature invalide
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = json_decode($dataJson, true);
|
||||||
|
if (!$data || $data['login'] !== $login) {
|
||||||
|
return false; // Login non correspondant
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recalculer la validité avec la durée paramétrable actuelle
|
||||||
|
$duration = new \DateInterval($this->params->get('app.2fa_remember_duration')); // ex. P30D
|
||||||
|
$expirationTime = $data['created_at'] + $duration->format('%s'); // Convertir en secondes
|
||||||
|
if (time() > $expirationTime) {
|
||||||
|
return false; // Expiré selon la durée actuelle
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
public function generateSignedRememberCookie(string $login): string
|
||||||
|
{
|
||||||
|
$data = json_encode([
|
||||||
|
'login' => $login,
|
||||||
|
'created_at' => time(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$secret = $this->params->get('kernel.secret'); // Utilise framework.secret ou une clé dédiée
|
||||||
|
$signature = hash_hmac('sha256', $data, $secret);
|
||||||
|
|
||||||
|
return base64_encode($data) . '.' . $signature; // Base64 pour éviter les caractères spéciaux
|
||||||
|
}
|
||||||
|
|
||||||
|
public function createCookie(string $identifier): Cookie
|
||||||
|
{
|
||||||
|
$cookieValue = $this->generateSignedRememberCookie($identifier);
|
||||||
|
$duration = new \DateInterval($this->params->get('app.2fa_remember_duration'));
|
||||||
|
$expiresAt = (new \DateTime())->add($duration)->getTimestamp();
|
||||||
|
return Cookie::create('remember_2fa')
|
||||||
|
->withValue($cookieValue)
|
||||||
|
->withExpires($expiresAt) // 1 an max (mais validité dynamique)
|
||||||
|
->withSecure($this->params->get('kernel.environment') === 'prod')
|
||||||
|
->withHttpOnly(true)
|
||||||
|
->withSameSite('Lax')
|
||||||
|
->withPath('/hydra-otp')
|
||||||
|
;
|
||||||
|
}
|
||||||
|
}
|
35
src/Service/MailerService.php
Normal file
35
src/Service/MailerService.php
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Service;
|
||||||
|
|
||||||
|
use Symfony\Component\Mailer\MailerInterface;
|
||||||
|
use Symfony\Component\Mime\Email;
|
||||||
|
|
||||||
|
class MailerService
|
||||||
|
{
|
||||||
|
private const int VALIDITY_TIME = 1800;
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
private readonly MailerInterface $mailer
|
||||||
|
){}
|
||||||
|
|
||||||
|
public function send(array $code, $identifier): void
|
||||||
|
{
|
||||||
|
$createdAt = $code['createdAt'] ?? null;
|
||||||
|
$maxTime = $createdAt + self::VALIDITY_TIME;
|
||||||
|
if ($maxTime < time()) {
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$email = (new Email())
|
||||||
|
->from('me@me.fr')
|
||||||
|
->to($identifier)
|
||||||
|
|
||||||
|
->subject('Votre code de confirmation')
|
||||||
|
->text('Voici votre code : '.$code['code'])
|
||||||
|
->html('<p>Voici votre code : '.$code['code'].'</p>');
|
||||||
|
|
||||||
|
$this->mailer->send($email);
|
||||||
|
}
|
||||||
|
}
|
10
src/Validator/Code.php
Normal file
10
src/Validator/Code.php
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Validator;
|
||||||
|
|
||||||
|
use Symfony\Component\Validator\Constraint;
|
||||||
|
|
||||||
|
class Code extends Constraint
|
||||||
|
{
|
||||||
|
public string $message = 'Le code est invalide.';
|
||||||
|
}
|
48
src/Validator/CodeValidator.php
Normal file
48
src/Validator/CodeValidator.php
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Validator;
|
||||||
|
|
||||||
|
use Symfony\Component\HttpFoundation\File\Exception\UnexpectedTypeException;
|
||||||
|
use Symfony\Component\HttpFoundation\RequestStack;
|
||||||
|
use Symfony\Component\Validator\Constraint;
|
||||||
|
use Symfony\Component\Validator\ConstraintValidator;
|
||||||
|
|
||||||
|
class CodeValidator extends ConstraintValidator
|
||||||
|
{
|
||||||
|
private const int VALIDITY_TIME = 1800;
|
||||||
|
public function __construct(
|
||||||
|
readonly RequestStack $requestStack
|
||||||
|
){}
|
||||||
|
public function validate($value, Constraint $constraint): void
|
||||||
|
{
|
||||||
|
if (!$constraint instanceof Code) {
|
||||||
|
throw new UnexpectedTypeException($constraint, Code::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Récupérer la session
|
||||||
|
$session = $this->requestStack->getSession();
|
||||||
|
$codeDatas = $session->get('code2fa');
|
||||||
|
|
||||||
|
// Vérifier si les données de session existent
|
||||||
|
if (!$codeDatas || !isset($codeDatas['code'], $codeDatas['createdAt'])) {
|
||||||
|
$this->context->buildViolation('Aucun code de validation trouvé. Veuillez demander un nouveau code.')
|
||||||
|
->addViolation();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vérifier l'expiration
|
||||||
|
$createdAt = $codeDatas['createdAt'];
|
||||||
|
$maxTime = $createdAt + self::VALIDITY_TIME;
|
||||||
|
if ($maxTime < time()) {
|
||||||
|
$this->context->buildViolation('Le code a expiré.')
|
||||||
|
->addViolation();
|
||||||
|
return; // Arrêter ici pour éviter une double erreur
|
||||||
|
}
|
||||||
|
|
||||||
|
// Comparer les codes (égalité non stricte pour éviter les problèmes de typage)
|
||||||
|
if ($value != $codeDatas['code']) {
|
||||||
|
$this->context->buildViolation('Le code est incorrect.')
|
||||||
|
->addViolation();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -10,8 +10,30 @@
|
|||||||
{% block javascripts %}
|
{% block javascripts %}
|
||||||
{% block importmap %}{{ importmap('app') }}{% endblock %}
|
{% block importmap %}{{ importmap('app') }}{% endblock %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center; /* centre horizontalement */
|
||||||
|
align-items: center; /* centre verticalement */
|
||||||
|
min-height: 100vh; /* prend toute la hauteur de la fenêtre */
|
||||||
|
margin: 0; /* évite les marges par défaut */
|
||||||
|
}
|
||||||
|
|
||||||
|
div {
|
||||||
|
text-align: center; /* centre le contenu dans le bloc */
|
||||||
|
}
|
||||||
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
{% block body %}{% endblock %}
|
{% block body %}
|
||||||
|
{% form_theme form 'bootstrap_5_layout.html.twig' %}
|
||||||
|
|
||||||
|
<div syle="">
|
||||||
|
{{ form_start(form) }}
|
||||||
|
{{ form_errors(form.code) }}
|
||||||
|
{{ form_row(form.code) }}
|
||||||
|
{{form_end(form)}}
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
Reference in New Issue
Block a user