Symfony: Protéger l’accès de certaines pages avec un OTP
Symfony permet de sécuriser l’accès à nos pages très facilement juste avec de la configuration (firewall, access_control, isGranted).
En revanche , il n’est pas prévu nativement avec symfony de demander confirmation au moment d’accéder à une page spécifique. Cela peut être très utile sur des pages sensibles de demander confirmation via un OTP(email, sms) ou un code TOTP (Google Authenticator).
Nous allons voir ici, comment mettre en place ce système de re-confirmation d’accès. L’ensemble du code est disponible ici: https://github.com/alamirault/protected-area
Pour proteger une page nous voulons uniquement ajouter une annotation à une action ou a un controlleur.
// src/Controller/AccountController.php/**
* @ProtectedArea(name="critical-access")
* @Route("/account", name="account")
*/
public function account(): Response
{
return $this->render('account/index.html.twig');
}// src/Annotation/ProtectedArea.php/**
* @Annotation
*/
class ProtectedArea
{
public string $name;
}
Au moment de se rendre sur les pages qui ont une annotation de protection, nous devons vérifier que les conditions sont remplies (OTP valide). Si ce n’est pas le cas, nous devons les demander tant que ce n’est pas valide. Nous allons donc faire cela grâce à un subscriber.
// src/EventSubscriber/ProtectedAreaSubscriber.php
// On écoute l'évènement 'kernel.controller'
public static function getSubscribedEvents()
{
return [
'kernel.controller' => 'onKernelController',
];
}public function onKernelController(ControllerEvent $event)
{
// On vérifie qu'il y a l'annotation ProtectedArea
// On verifie que la zone protégée n'est pas déja validée
// Si ce n'est pas le cas on redirige vers le formulaire d'OTPif (!is_array($controllers = $event->getController())) {
return;
}
$request = $event->getRequest();
list($controller, $methodName) = $controllers;
$protectedAreaAnnotation = $this->getAnnotation($controller, $methodName);
if(!$protectedAreaAnnotation){
return;
}
$sessionKey = $this->protectedAreaSessionManager->getSessionKey($protectedAreaAnnotation);
// If no protected area process, or process is not finished
if (!$request->getSession()->get($sessionKey) || $request->getSession()->get($sessionKey)["status"] != ProtectedAreaSessionManager::OK) {
$this->protectedAreaSessionManager->setRequestedProtectedAreaInSession($protectedAreaAnnotation, $request);
//Internal redirect to form otp when user has not secured area in session.
$event->setController(function () use ($request, $protectedAreaAnnotation) {
return $this->forward($request, 'App\\Controller\\ProtectedAreaController::form', [
'protected-area' => $protectedAreaAnnotation->name,
]);
});
}}
Il nous faut maintenant créer le formulaire qui demande l’OTP et le valider. S’il est valide alors l’accès à la zone protégée est accepté.
/**
* @Route("/protected-area")
*/
public function form(Request $request, ProtectedAreaSessionManager $protectedAreaSessionManager): Response
{
$protectedAreaName = $request->query->get("protected-area");
$protectedAreaData = $request->getSession()->get($protectedAreaSessionManager->getSessionKeyFromString($protectedAreaName));
//Here send otp how you cant
$otpSent = 'ABC-DEF';
$form = $this->createForm(OtpType::class, null, [
"otpSent" => $otpSent,
]);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$data = $protectedAreaSessionManager->setAuthorizedProtectedAreaInSession($protectedAreaName, $request);
//Return to original requested url
return $this->redirect($data["protected_url"]);
}
return $this->render('protected_area/index.html.twig', [
'form' => $form->createView(),
'cancelUrl' => $protectedAreaData["cancel_url"],
'protectedArea' => $protectedAreaName,
]);
}
La validation de l’OTP se fait dans l’OtpType. Quand le formulaire est valide, il suffit de mettre en session le fait que l’accès est autorisé et nous redirigons sur la page initiallement demandée. Le subscriber va vérifier les conditions, elles sont remplies, la page est affichée !
L’ajout de zone protégée peut rendre les tests fonctionnels plus complexes. Heureusement nous pouvons outre-passer toute cette partie en passant directement en sessions cette vérification.
protected function passProtectedArea(string $name = "critical-access")
{
$session = static::$kernel->getContainer()->get("session");
$session->set("protected-area-" . $name, [
"status" => "OK",
]);
}public function testAccountListing(): void { $this->passProtectedArea();
// Test normal}
L’ensemble du code est disponible ici: https://github.com/alamirault/protected-area
Si vous avez des remarques ou des commentaires n’hésitez pas. Vous pouvez également me contacter sur twitter : a_lamirault