· 7 min read

Authentification 2FA avec Symfony

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

Back to Blog