Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Rendering the QR Code

Keep on Learning!

If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.

Start your All-Access Pass
Buy just this tutorial for $12.00

With a Subscription, click any sentence in the script to jump to that part of the video!

Login Subscribe

Ok, we've just added a URL the user can go to in order to enable two-factor authentication on their account. What this really means is pretty simple: we generate a totpSecret and save it to their user record in the database. Thanks to this, when the user tries to log in, the 2-factor bundle will notice this and send them to the "fill in the code" form.

But, in order to know what code to enter, the user needs to set up an authenticator app. And to do that, we need to render a QR code they can scan.

Dumping the QR Content

How? The $totpAuthenticator has a method that can help. Try dumping $totpAuthenticator->getQRContent() and pass it $user:

... lines 1 - 12
class SecurityController extends BaseController
{
... lines 15 - 37
public function enable2fa(TotpAuthenticatorInterface $totpAuthenticator, EntityManagerInterface $entityManager)
{
... lines 40 - 46
dd($totpAuthenticator->getQRContent($user));
}
}

When we refresh we see... a super weird-looking URL! This is the info that we need to send to our authenticator app. It contains our email address - that's just a label that will help the app - and most importantly the totp secret, which the app will use to generate the codes.

In theory, we could enter this URL manually into an authenticator app. But, pfff. That's crazy! In the real world, we translate this string into a QR code image.

Generating the QR Code

Fortunately, this is also handled by the Scheb library. If you scroll down a bit, there's a spot about QR codes. If you want to generate one, you need one last library. Actually, right after I recorded this, the maintainer deprecated this 2fa-qr-code library! Dang! So, you can still install it, but I'll also show you how to generate the QR code without it. The library was deprecated because, well, it's pretty darn easy to create the QR code even without it.

Anyways, I'll copy that, find my terminal, and paste.

composer require "scheb/2fa-qr-code:^5.12.1"

To use the new way of generating QR codes - which I recommend - skip this step and instead run:

composer require "endroid/qr-code:^3.0"

While that's working. Head back to the docs... and copy this controller from the documentation. Over in SecurityController, at the bottom, paste. I'll tweak the URL to be /authentication/2fa/qr-code and call the route app_qr_code:

... lines 1 - 13
class SecurityController extends BaseController
{
... lines 16 - 50
/**
* @Route("/authentication/2fa/qr-code", name="app_qr_code")
*/
public function displayGoogleAuthenticatorQrCode(QrCodeGenerator $qrCodeGenerator)
{
// $qrCode is provided by the endroid/qr-code library. See the docs how to customize the look of the QR code:
// https://github.com/endroid/qr-code
$qrCode = $qrCodeGenerator->getTotpQrCode($this->getUser());
return new Response($qrCode->writeString(), 200, ['Content-Type' => 'image/png']);
}
}

I also need to re-type the "R" on QrCodeGenerator to get its use statement:

... lines 1 - 6
use Scheb\TwoFactorBundle\Security\TwoFactor\QrCode\QrCodeGenerator;
... lines 8 - 13
class SecurityController extends BaseController
{
... lines 16 - 53
public function displayGoogleAuthenticatorQrCode(QrCodeGenerator $qrCodeGenerator)
{
... lines 56 - 60
}
}

If you're using the new way of generating the QR codes, then your controller should like this instead. You can copy this from the code block on this page:

namespace App\Controller;

use Endroid\QrCode\Builder\Builder;
use Scheb\TwoFactorBundle\Security\TwoFactor\Provider\Totp\TotpAuthenticatorInterface;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\IsGranted;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;

class SecurityController extends BaseController
{
    // ...

    /**
     * @Route("/authentication/2fa/qr-code", name="app_qr_code")
     * @IsGranted("ROLE_USER")
     */
    public function displayGoogleAuthenticatorQrCode(TotpAuthenticatorInterface $totpAuthenticator)
    {
        $qrCodeContent = $totpAuthenticator->getQRContent($this->getUser());
        $result = Builder::create()
            ->data($qrCodeContent)
            ->build();

        return new Response($result->getString(), 200, ['Content-Type' => 'image/png']);
    }
}

This special endpoint literally returns the QR code image, as a png. Oh, and I forgot it here, but you should add an @IsGranted("ROLE_USER") above this: only authenticated users should be able to load this image.

Anyways, the user won't go to this URL directly: we'll use it inside an img tag. But to see if it's working, copy the URL, paste that into your browser and... sweet! Hello QR code!

Finally, after the user enables two-factor authentication, let's render a template with an image to this URL. Return $this->render('security/enable2fa.html.twig').

Copy the template name, head into templates/security, and create that: enable2fa.html.twig. I'll paste in a basic structure... it's just an h1 that tells you to scan the QR code... but no image yet:

{% extends 'base.html.twig' %}
{% block title %}2fa Activation{% endblock %}
{% block body %}
<div class="container">
<div class="row">
<div class="login-form bg-light mt-4 p-4">
<h1 class="h3 mb-3 font-weight-normal">Use Authy or Google Authenticator to Scan the QR Code</h1>
... lines 10 - 11
</div>
</div>
</div>
{% endblock %}

Let's add it: an img with src set to {{ path() }} and then the route name to the controller we just built. So app_qr_code. For the alt, I'll say 2FA QR code:

{% extends 'base.html.twig' %}
{% block title %}2fa Activation{% endblock %}
{% block body %}
<div class="container">
<div class="row">
<div class="login-form bg-light mt-4 p-4">
<h1 class="h3 mb-3 font-weight-normal">Use Authy or Google Authenticator to Scan the QR Code</h1>
<img src="{{ path('app_qr_code') }}" alt="2fa QR Code">
</div>
</div>
</div>
{% endblock %}

Sweet! Time to try the whole flow. Start on the homepage, enable two-factor authentication and... yes! We see the QR code! We are ready to scan this and try logging in.

Making the User Confirm The Scanned the QR Code

Oh, but before we do, in a real app, I would probably add an extra property on my user, called isTotpEnabled and use that in the isTotpAuthenticationEnabled() method on my User class. Why? Because it would allow us to have the following flow. First, the user clicks "Enable two-factor authentication", we generate the totpSecret, save it, and render the QR code. So, exactly what we're doing now. But, that new isTotpEnabled flag would still be false. So if something went wrong and the user never scanned the QR code, they would still be able to log in without us requiring the code. Then, at the bottom of this page, we could add a "Confirm" button. When the user clicks that, we would finally set that isTotpEnabled property to true. Heck, you could even require the user to enter a code from their authenticator app to prove they set things up: the TotpAuthenticatorInterface service has a checkCode() method in case you ever want to manually check a code.

Next: let's scan this QR code with an authenticator app and finally try the full two-factor authentication flow. We'll then learn how to customize the "enter the code template" to match our design.

Leave a comment!

This tutorial also works great for Symfony 6!

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": "^7.4.1 || ^8.0.0",
        "ext-ctype": "*",
        "ext-iconv": "*",
        "babdev/pagerfanta-bundle": "^3.3", // v3.3.0
        "composer/package-versions-deprecated": "^1.11", // 1.11.99.4
        "doctrine/annotations": "^1.0", // 1.13.2
        "doctrine/doctrine-bundle": "^2.1", // 2.6.3
        "doctrine/doctrine-migrations-bundle": "^3.0", // 3.1.1
        "doctrine/orm": "^2.7", // 2.10.1
        "knplabs/knp-markdown-bundle": "^1.8", // 1.9.0
        "knplabs/knp-time-bundle": "^1.11", // v1.16.1
        "pagerfanta/doctrine-orm-adapter": "^3.3", // v3.3.0
        "pagerfanta/twig": "^3.3", // v3.3.0
        "phpdocumentor/reflection-docblock": "^5.2", // 5.2.2
        "scheb/2fa-bundle": "^5.12", // v5.12.1
        "scheb/2fa-qr-code": "^5.12", // v5.12.1
        "scheb/2fa-totp": "^5.12", // v5.12.1
        "sensio/framework-extra-bundle": "^6.0", // v6.2.0
        "stof/doctrine-extensions-bundle": "^1.4", // v1.6.0
        "symfony/asset": "5.3.*", // v5.3.4
        "symfony/console": "5.3.*", // v5.3.7
        "symfony/dotenv": "5.3.*", // v5.3.8
        "symfony/flex": "^1.3.1", // v1.17.5
        "symfony/form": "5.3.*", // v5.3.8
        "symfony/framework-bundle": "5.3.*", // v5.3.8
        "symfony/monolog-bundle": "^3.0", // v3.7.0
        "symfony/property-access": "5.3.*", // v5.3.8
        "symfony/property-info": "5.3.*", // v5.3.8
        "symfony/rate-limiter": "5.3.*", // v5.3.4
        "symfony/runtime": "5.3.*", // v5.3.4
        "symfony/security-bundle": "5.3.*", // v5.3.8
        "symfony/serializer": "5.3.*", // v5.3.8
        "symfony/stopwatch": "5.3.*", // v5.3.4
        "symfony/twig-bundle": "5.3.*", // v5.3.4
        "symfony/ux-chartjs": "^1.3", // v1.3.0
        "symfony/validator": "5.3.*", // v5.3.8
        "symfony/webpack-encore-bundle": "^1.7", // v1.12.0
        "symfony/yaml": "5.3.*", // v5.3.6
        "symfonycasts/verify-email-bundle": "^1.5", // v1.5.0
        "twig/extra-bundle": "^2.12|^3.0", // v3.3.3
        "twig/string-extra": "^3.3", // v3.3.3
        "twig/twig": "^2.12|^3.0" // v3.3.3
    },
    "require-dev": {
        "doctrine/doctrine-fixtures-bundle": "^3.3", // 3.4.0
        "symfony/debug-bundle": "5.3.*", // v5.3.4
        "symfony/maker-bundle": "^1.15", // v1.34.0
        "symfony/var-dumper": "5.3.*", // v5.3.8
        "symfony/web-profiler-bundle": "5.3.*", // v5.3.8
        "zenstruck/foundry": "^1.1" // v1.13.3
    }
}