
Two-factor Authentication with Symfony
two-factor authentication protects against phishing, social engineering, and password brute-force attacks and secures your logins from attackers exploiting weak or stolen credentials. This dramatically improves the security of login attempts, so how can we do this in Symfony? We will learn this thing in this blog so let’s get started.
What is two-factor authentication and why do we need it?
what is two-factor authentication?
Two factor authentication verifies your identity by using two factors.
- something you know (like a password)
- and something you have (like a key or One-time password)
so from the above list, we can understand one thing here if we are trying to log in to some application we know our username and password but we also need something which will be different every time. which is a one-time password.
why do we need two-factor authentication?
in simple words, if someone knew our password and username then he/she can log in to the system easily.
if the user enabled two factor authentication then he/she needs a token/OTP to login into the system which will be automatically generated every time, and that’s why a username and password are useless if he/she doesn’t have OTP/token.
two-factor authentication is used to improve our security and it’s good if we use that in our project.
How To Use two-factor authentication in Symfony?
so for two-factor authentication, we are going to use the two factor bundle. you can visit the link provided.
we are not going through the documentation, we will start working on our two factor authentication in Symfony.
we can do two factor authentication without any bundle we can send users an email each time they logged into the system and validate that email and after that, we can grant them access but this is a bit awkward. so we are going to use a bundle,
Install packages.
first, create a new Symfony project and do a database connection and create USER and authentication.
These all are basic requirements so must do the above steps before you start working on TOTP Authentication.
for this tutorial we are going to create custom authentication for this we need the below packages, so simply run this command in your terminal and install them.
we are going to use custom authentication, we will create a QR code that has our authentication code and for that, we need the below packages.
composer require scheb/2fa-bundle
composer require scheb/2fa-totp
for generating the QR code we need the below bundle.
composer require endroid/qr-code-bundle
Use the packages which we installed in our system.
first of all, make sure your project is running, and start the server using.
symfony server:start

so once you see the above screen that means your project running and you’re ready to go.
The next step is to install the above packages and use them with your USER Entity.
make sure you do these changes inside the new package configuration as I did here.
make changes in this file: config/packages/scheb_2fa.yamlscheb_two_factor: totp: enabled: true server_name: Vivan Web Solution issuer: Vivan Web Solution window: 1 template: security/two_factor_auth.html.twig security_tokens: - Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken - Symfony\Component\Security\Http\Authenticator\Token\PostAuthenticationToken
do not change anything inside routes/scheb_2fa.yaml.
you can do the below changes in routes.yaml file.
app_admin_custom_routes: resource: ../src/Controller/APP/ type: attribute prefix: prefix index: path: / controller: App\Controller\MainController::index
now create make changes in user entity like these.
<?php
namespace App\Entity;
use App\Repository\UserRepository;
use Doctrine\ORM\Mapping as ORM;
use Scheb\TwoFactorBundle\Model\Totp\TotpConfiguration;
use Scheb\TwoFactorBundle\Model\Totp\TotpConfigurationInterface;
use Scheb\TwoFactorBundle\Model\Totp\TwoFactorInterface;
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
use Symfony\Component\Security\Core\User\UserInterface;
#[ORM\Entity(repositoryClass: UserRepository::class)]
class User implements UserInterface, PasswordAuthenticatedUserInterface, TwoFactorInterface
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\Column(length: 180, unique: true)]
private ?string $username = null;
#[ORM\Column]
private array $roles = [];
/**
* @var string The hashed password
*/
#[ORM\Column]
private ?string $password = null;
#[ORM\Column(type:'boolean', nullable: false, options: ['default' => true])]
private bool $enforcedTotp = true;
#[ORM\Column(type:'boolean', nullable: false)]
private bool $isVerified = false;
#[ORM\Column(type: 'string', length: 255, nullable: true)]
private ?string $totpSecret;
public function getId(): ?int
{
return $this->id;
}
public function getUsername(): ?string
{
return $this->username;
}
public function setUsername(string $username): self
{
$this->username = $username;
return $this;
}
/**
* A visual identifier that represents this user.
*
* @see UserInterface
*/
public function getUserIdentifier(): string
{
return (string) $this->username;
}
/**
* @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;
}
/**
* @see UserInterface
*/
public function eraseCredentials()
{
// If you store any temporary, sensitive data on the user, clear it here
// $this->plainPassword = null;
}
public function isTotpAuthenticationEnabled(): bool
{
return $this->enforcedTotp;
}
public function getTotpAuthenticationUsername(): string
{
return $this->getUserIdentifier();
}
public function getTotpAuthenticationConfiguration(): ?TotpConfigurationInterface
{
if ($this->totpSecret) {
return new TotpConfiguration($this->totpSecret, TotpConfiguration::ALGORITHM_SHA1, 30, 6);
}
return null;
}
public function setEnforcedTotp(bool $enforcedTotp): self
{
$this->enforcedTotp = $enforcedTotp;
return $this;
}
public function getEnforcedTotp(): bool
{
return $this->enforcedTotp;
}
public function setIsVerified(bool $isVerified): self
{
$this->isVerified = $isVerified;
return $this;
}
public function getIsVerified(): bool
{
return $this->isVerified;
}
public function getTotpSecret(): ?string
{
return $this->totpSecret;
}
public function setTotpSecret(?string $totpSecret): self
{
$this->totpSecret = $totpSecret;
return $this;
}
}
make sure you have same directory structure as I have

so you have to create an authentication file by a command which is provided by Symfony.
php bin/console make:auth
Files Required for this tutorial.
create authentication for login from the above command.
For TOTP authentication, I did a couple of changes in the below file and created a new TotpAuthentication Controller which will do TOTP Authentication.
<?php
namespace App\Security;
use App\Entity\User;
use App\Repository\UserRepository;
use Scheb\TwoFactorBundle\Security\TwoFactor\Provider\Totp\TotpAuthenticatorInterface;
use Symfony\Component\HttpFoundation\Cookie;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\Exception\CustomUserMessageAuthenticationException;
use Symfony\Component\Security\Core\Security;
use Symfony\Component\Security\Http\Authenticator\AbstractLoginFormAuthenticator;
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\CsrfTokenBadge;
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
use Symfony\Component\Security\Http\Authenticator\Passport\Credentials\PasswordCredentials;
use Symfony\Component\Security\Http\Authenticator\Passport\Passport;
use Symfony\Component\Security\Http\Util\TargetPathTrait;
class UserAuthenticator extends AbstractLoginFormAuthenticator
{
use TargetPathTrait;
public const LOGIN_ROUTE = 'app_login';
public function __construct(private readonly UrlGeneratorInterface $urlGenerator,
private readonly UserRepository $repository,
private readonly TotpAuthenticatorInterface $totpAuthenticator)
{
}
public function authenticate(Request $request): Passport
{
$username = $request->request->get('username', '');
$user = $this->repository->findOneBy(['username' => $username]);
$isVerified = false;
$authCode = (string) $request->request->get('auth_code', '');
if (!$user) {
throw new AuthenticationException('Invalid credentials.');
}
if ($user->getTotpSecret()) {
$isVerified = $this->totpAuthenticator->checkCode($user, $authCode);
}
if ($user->isTotpAuthenticationEnabled()) {
if (!empty($user->getTotpSecret()) && !$isVerified && $user->getIsVerified()) {
throw new CustomUserMessageAuthenticationException('authentication code is invalid');
}
}
$response = new Response();
$response->headers->setCookie(Cookie::create('2faVerified', 'true', strtotime('+1 year'), '/'));
$response->sendHeaders();
$request->getSession()->set(Security::LAST_USERNAME, $username);
return new Passport(
new UserBadge($username),
new PasswordCredentials($request->request->get('password', '')),
[
new CsrfTokenBadge('authenticate', $request->request->get('_csrf_token')),
]
);
}
public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response
{
$user = $token->getUser();
$targetPath = $this->getTargetPath($request->getSession(), $firewallName);
if ($user->isTotpAuthenticationEnabled() && !$user->getIsVerified()) {
return new RedirectResponse($this->urlGenerator->generate('app_2fa'));
}
if ($targetPath) {
return new RedirectResponse($targetPath);
}
return new RedirectResponse($this->urlGenerator->generate('app_dashboard'));
}
protected function getLoginUrl(Request $request): string
{
return $this->urlGenerator->generate(self::LOGIN_ROUTE);
}
}
<?php
namespace App\Controller\APP;
use App\Entity\User;
use App\Form\TotpAuthType;
use Doctrine\ORM\EntityManagerInterface;
use Endroid\QrCode\Builder\BuilderInterface;
use Scheb\TwoFactorBundle\Security\TwoFactor\Provider\Totp\TotpAuthenticatorInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Cookie;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Session\Session;
use Symfony\Component\Routing\Annotation\Route;
class TotpAuthenticationController extends AbstractController
{
public function __construct(
private readonly EntityManagerInterface $entityManager,
private readonly TotpAuthenticatorInterface $totpAuthenticator,
private readonly BuilderInterface $qrCodeBuilder,
) {
}
#[Route('/2fa', name: 'app_2fa')]
public function setUpTwoFa(Request $request): Response
{
$user = $this->getUser();
if ($user instanceof User) {
if ($user->isTotpAuthenticationEnabled() && !(bool) $user->getTotpSecret()) {
$user->setTotpSecret($this->totpAuthenticator->generateSecret());
$this->entityManager->persist($user);
$this->entityManager->flush();
}
$form = $this->createForm(TotpAuthType::class);
$form->handleRequest($request);
if ($form->isSubmitted()) {
$code = $form->get('code')->getData();
$checkCode = $this->totpAuthenticator->checkCode($user, $code);
if ($checkCode) {
$user->setIsVerified(true);
$this->entityManager->persist($user);
$this->entityManager->flush();
$response = new Response();
$response->headers->setCookie(Cookie::create('2faVerified', 'true', strtotime('+1 year'), '/'));
$response->sendHeaders();
$this->addFlash('success', 'authenticate successfully');
return $this->redirectToRoute('app_logout');
} else {
$this->addFlash('danger', 'authentication failed');
}
}
return $this->render('security/2fa_auth.html.twig', [
'user' => $user,
'form' => $form->createView(),
]);
}
return $this->redirectToRoute('app_login');
}
#[Route('/generate-qr-code', name: 'generate_qr_code')]
public function generateQrCode(): Response
{
$user = $this->getUser();
if ($user instanceof User) {
$session = new Session();
$verified = $session->get('2faVerified');
if ($verified) {
return $this->redirectToRoute('app_dashboard');
}
$qrCodeContent = $this->totpAuthenticator->getQRContent($user);
$qrCode = $this->qrCodeBuilder
->data($qrCodeContent)
->size(500)
->build();
return new Response($qrCode->getString(), 200, ['Content-type' => 'image/png']);
}
return $this->redirectToRoute('app_login');
}
}
and also you can design twig files as you like for TOTP authentication here is my twig file.
{% extends 'base.html.twig' %}
{% set active = '2fa-activate' %}
{% set for2Fa = true %}
{% block title %}Two Factor Authentication{% endblock %}
{% block body %}
{% set verified = app.request.cookies.get('2faVerified') %}
{% if verified and verified == 'true' and app.user.isVerified %}
{% set url = path('app_dashboard') %}
<script>
window.location.href = '{{ url }}';
</script>
{% endif %}
<div class="container-lg">
<div class="card">
<div class="card-header">
<h5>Time Based One Time Password</h5>
</div>
<div class="card-body">
<ul class="list-group">
<li class="list-group-item">
<span class="badge bg-dark rounded-circle">1</span>
<span class="pl-2">install <a href="https://play.google.com/store/search?q=authenticator&c=apps">TOTP Authenticator</a> or Any Authenticator Application of your choice.</span>
</li>
<li class="list-group-item">
<span class="badge bg-dark rounded-circle">2</span>
<span class="pl-2">Open Application, Then Set up your account.</span>
</li>
<li class="list-group-item">
<span class="badge bg-dark rounded-circle">3</span>
<span class="pl-2">Click on <strong> + </strong> icon, and choose scan QR Code option.</span>
</li>
<li class="list-group-item">
<img src="{{ path('generate_qr_code') }}" alt="2fa QR Code" height="125" width="125">
</li>
{{ form_start(form) }}
<li class="list-group-item">
<span class="badge bg-dark rounded-circle">4</span>
<span class="pl-2">Enter 6 digit authentication code below and verify your account.</span>
</li>
<li class="list-group-item">{{ form_widget(form.code)}}</li>
<li class="list-group-item">
<input type="submit" class="btn btn-primary" value="save">
<a href="{{ path('app_logout') }}" class="btn btn-secondary btn-sm ml-2">Cancel</a>
</li>
<li class="list-group-item">
<span class="badge bg-dark rounded-circle">5</span>
<span class="pl-2">Once you verify your account then you will be redirect to login page. and login with you credentials and authentication code.</span>
</li>
</ul>
</div>
</div>
</div>
{% endblock %}
and here is a result.
so first when we start the server we are directly on a login page, which will ask us for a username and password and also for the two-factor authentication PIN, if we don’t have that pin we don’t need to use that pin for login, so if we don’t have then what happened? let’s see.

so when we use our username and password but we don’t have an authentication code it will directly redirect us to set up a two-factor authentication code,

you can use google authenticator or any other application to scan your QR code which will have your authentication code, and it will be changed after every 60 seconds, so after every 60 seconds you have a new OTP for login.
so after setup your authentication code you can directly login into the dashboard using TOTP.
now you are ready to use this authentication code for login and you can now add an extra barrier to your application security.
you can check this tutorial here.
also, check out how to use React with Symfony here, and for more blogs, you can directly visit here.