· 7 min read
Authentification 2FA avec Symfony
Authentification 2FA avec Symfony
DĂ©finition
Source: wikipedia La double authentification, authentification à deux facteurs (A2F)1, authentification à double facteur ou vérification en deux étapes (two-factor authentication en anglais, ou 2FA) est une méthode d’authentification forte par laquelle un utilisateur peut accéder à une ressource informatique (un ordinateur, un téléphone intelligent ou encore un site web) après avoir présenté deux preuves d’identité distinctes à un mécanisme d’authentification.
La vérification en deux étapes permet d’assurer l’authenticité de la personne derrière un compte en autorisant seulement l’authentification à ce dernier après avoir présenté deux preuves d’identité distinctes.
En général, un code à usage unique doit être renseigné en plus du mot de passe habituel de l’utilisateur. S’il n’est pas correct, l’authentification échoue même si le mot de passe renseigné correspond à celui relié au compte.
Projet Symfony From Scratch
On créer un nouveau projet pour l’occasion
symfony new 2FA --full
On passe en sqlite
# DATABASE_URL="sqlite:///%kernel.project_dir%/var/data.db"
# DATABASE_URL="mysql://db_user:db_password@127.0.0.1:3306/db_name?serverVersion=5.7"
# DATABASE_URL="postgresql://db_user:db_password@127.0.0.1:5432/db_name?serverVersion=13&charset=utf8"
###< doctrine/doctrine-bundle ###
Et on créer la db
symfony console d:d:c
On créer un conrolleur
symfony console make:controller Home
On modifie le controller
<?php
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
class HomeController extends AbstractController
{
/**
* @Route("/home", name="home")
*/
public function home(): Response
{
return $this->render('home/index.html.twig', [
'controller_name' => 'HomeController',
]);
}
/**
* @Route("/", name="index")
*/
public function index(): Response
{
return $this->redirectToRoute('home');
}
}
Et on lance le serveur Symfony
symfony serve -d
Puis nous protegeons la route !
access_control:
# - { path: ^/admin, roles: ROLE_ADMIN }
- { path: ^/home, roles: ROLE_USER }
Créons des users
symfony console make:user
symfony console make:migration
symfony console d:m:m
Une page d’authentification “classique”
symfony console make:auth
Et une page de création de comptes
symfony console make:registration-form
Creating a registration form for App\Entity\User
Do you want to add a @UniqueEntity validation annotation on your User class to make sure duplicate accounts aren't created? (yes/no) [yes]:
>
Do you want to send an email to verify the user's email address after registration? (yes/no) [yes]:
> no
Do you want to automatically authenticate the user after registration? (yes/no) [yes]:
> no
What route should the user be redirected to after registration?:
[11] home
> 11
updated: src/Entity/User.php
created: src/Form/RegistrationFormType.php
created: src/Controller/RegistrationController.php
created: templates/registration/register.html.twig
Success!
OK, notre petite applications est désormais protégée par un login/mot de passe.
Nous avons une page d’enregistrement, et une page d’authentification !
Il est temps de monter notre niveau de sécurité d’un niveau : Mettons en place une connection à Double Facteur !
Mise en place de la 2FA
La documentation du Bundle est dispo ici : Documentation
Installation du Bundle
On install le Bundle
composer require 2fa
2FA par Mail
On install l’extention 2FA par mail:
composer require scheb/2fa-email
On paramètre notre firewall, et la sécurité de nos routes
security:
# https://symfony.com/doc/current/security/experimental_authenticators.html
enable_authenticator_manager: true
# https://symfony.com/doc/current/security.html#c-hashing-passwords
password_hashers:
Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: 'auto'
App\Entity\User:
algorithm: auto
# https://symfony.com/doc/current/security.html#where-do-users-come-from-user-providers
providers:
# used to reload user from session & other features (e.g. switch_user)
app_user_provider:
entity:
class: App\Entity\User
property: email
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
main:
lazy: true
provider: app_user_provider
custom_authenticator: App\Security\AppAuthenticator
logout:
path: app_logout
# where to redirect after logout
# target: app_any_route
two_factor:
auth_form_path: 2fa_login # The route name you have used in the routes.yaml
check_path: 2fa_login_check # The route name you have used in the routes.yaml
# activate different ways to authenticate
# https://symfony.com/doc/current/security.html#firewalls-authentication
# https://symfony.com/doc/current/security/impersonating_user.html
# switch_user: true
# Easy way to control access for large sections of your site
# Note: Only the *first* access control that matches will be used
access_control:
- { path: ^/logout, roles: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/home, roles: ROLE_USER }
- { path: ^/2fa, roles: IS_AUTHENTICATED_2FA_IN_PROGRESS }
On modifie le fichier de configuration du Bundle
# config/packages/scheb_2fa.yaml
# See the configuration reference at https://github.com/scheb/2fa/blob/master/doc/configuration.md
scheb_two_factor:
security_tokens:
# - Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken
# If you're using guard-based authentication, you have to use this one:
# - Symfony\Component\Security\Guard\Token\PostAuthenticationGuardToken
# If you're using authenticator-based security (introduced in Symfony 5.1), you have to use this one:
- Symfony\Component\Security\Http\Authenticator\Token\PostAuthenticationToken
email:
digits: 6
enabled: true
sender_email: no-reply@test.com
sender_name: John Doe
Nous devons ensuite modifier notre entité User
<?php
namespace App\Entity;
use App\Repository\UserRepository;
use Doctrine\ORM\Mapping as ORM;
use Scheb\TwoFactorBundle\Model\Email\TwoFactorInterface;
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
use Symfony\Component\Security\Core\User\UserInterface;
/**
* @ORM\Entity(repositoryClass=UserRepository::class)
* @UniqueEntity(fields={"email"}, message="There is already an account with this email")
*/
class User implements UserInterface, PasswordAuthenticatedUserInterface, TwoFactorInterface
{
/**
* @ORM\Id
* @ORM\GeneratedValue
* @ORM\Column(type="integer")
*/
private $id;
/**
* @ORM\Column(type="string", length=180, unique=true)
*/
private $email;
/**
* @ORM\Column(type="string", nullable=true)
*/
private $authCode;
/**
* @ORM\Column(type="json")
*/
private $roles = [];
/**
* @var string The hashed password
* @ORM\Column(type="string")
*/
private $password;
public function getId(): ?int
{
return $this->id;
}
public function getEmail(): ?string
{
return $this->email;
}
public function setEmail(string $email): self
{
$this->email = $email;
return $this;
}
/**
* A visual identifier that represents this user.
*
* @see UserInterface
*/
public function getUserIdentifier(): string
{
return (string) $this->email;
}
/**
* @deprecated since Symfony 5.3, use getUserIdentifier instead
*/
public function getUsername(): string
{
return (string) $this->email;
}
/**
* @see UserInterface
*/
public function getRoles(): array
{
$roles = $this->roles;
// guarantee every user at least has ROLE_USER
$roles[] = 'ROLE_USER';
return array_unique($roles);
}
public function setRoles(array $roles): self
{
$this->roles = $roles;
return $this;
}
/**
* @see PasswordAuthenticatedUserInterface
*/
public function getPassword(): string
{
return $this->password;
}
public function setPassword(string $password): self
{
$this->password = $password;
return $this;
}
/**
* Returning a salt is only needed, if you are not using a modern
* hashing algorithm (e.g. bcrypt or sodium) in your security.yaml.
*
* @see UserInterface
*/
public function getSalt(): ?string
{
return null;
}
/**
* @see UserInterface
*/
public function eraseCredentials()
{
// If you store any temporary, sensitive data on the user, clear it here
// $this->plainPassword = null;
}
public function isEmailAuthEnabled(): bool
{
return true; // This can be a persisted field to switch email code authentication on/off
}
public function getEmailAuthRecipient(): string
{
return $this->email;
}
public function getEmailAuthCode(): string
{
if (null === $this->authCode) {
throw new \LogicException('The email authentication code was not set');
}
return $this->authCode;
}
public function setEmailAuthCode(string $authCode): void
{
$this->authCode = $authCode;
}
}
Et nous n’oublions pas le classique :
symfony console make:migration
symfony console d:m:m
:::warning A cette étape, si nous essayons d’ouvrir une session, il nous manque un MAILER_DSN, c’est bon signe ! :::
Ajoutons donc un serveur de mail de dev avec Docker-compose.
version: '3.7'
services:
mailer:
image: schickling/mailcatcher
ports: [1025, 1080]
Démarrons l’environnement Docker
docker-compose up -d
Et testons ! Cool, ça marche !
Mise en place des appareils de confiances
Notre application dispose désormais d’une authentification à deux facteurs. C’est top pour la sécurité, mais c’est reloud pour les utilisateurs !
On va leur simplifier la vie, en leur permettant de d’avoir des appareils de confiances !
Premièrement, nous installons l’extention.
composer require scheb/2fa-trusted-device
Ajoutons les options dans le fichier de configuration du Bundle config/packages/scheb_2fa.yaml
.
trusted_device:
enabled: true # If the trusted device feature should be enabled
lifetime: 5184000 # Lifetime of the trusted device token
extend_lifetime: false # Automatically extend lifetime of the trusted cookie on re-login
cookie_name: trusted_device # Name of the trusted device cookie
cookie_secure: false # Set the 'Secure' (HTTPS Only) flag on the trusted device cookie
cookie_same_site: "lax" # The same-site option of the cookie, can be "lax" or "strict"
cookie_path: "/" # Path to use when setting the cookie
Et enfin, modifions notre firewall dans notre fichier
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
main:
lazy: true
provider: app_user_provider
custom_authenticator: App\Security\AppAuthenticator
logout:
path: app_logout
# where to redirect after logout
# target: app_any_route
two_factor:
auth_form_path: 2fa_login # The route name you have used in the routes.yaml
check_path: 2fa_login_check # The route name you have used in the routes.yaml
trusted_parameter_name: _trusted # Name of the parameter for the trusted device option
Et c’est tout ! Le Bundle permet d’implémenter plein d’autres options pour améliorer la sécurité dans nos applications