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!

14
Login or Register to join the conversation

Small (totally stupid) question: Why is the method that displays the QR code named displayGoogleAuthenticatorQrCode()?...
I mean you recommend Authy - hahaha ;)

I am back to this course to learn about 2FA - It might be worth updating it to SF6 and all the new attributes stuff :)

Reply

Hey elkuku!

Sorry for the slow reply - but happy new year :).

Why is the method that displays the QR code named displayGoogleAuthenticatorQrCode()

Lol, that's a good question! I'm pretty sure the answer to this is...l Ryan copying and pasting from the docs at some point 🤣

I am back to this course to learn about 2FA - It might be worth updating it to SF6 and all the new attributes stuff :)

Definitely - we need to finish out the Symfony 6 course (Doctrine relations, forms and security) as early as we can this year. It's hard to look at annotations once you get used to attributes!

Cheers!

Reply
gazzatav Avatar
gazzatav Avatar gazzatav | posted 1 year ago

I'm getting this error. I'd like to tell it I AM trying to use Endroid\QrCode\Builder.

Attempted to load class "Builder" from namespace "Endroid\QrCode\Builder".
Did you forget a "use" statement for "PhpParser\Builder"?

I have seen that a GD library is a dependency so I've installed that. My php version is a bit low (7.2) but I can't really see it being that. Any ideas?

Reply
gazzatav Avatar

I take that back:
Endroid requires php: ^7.4||^8.0
So no builder class installed. Off to the ppa.

Oh dear, I upgraded to php 8.1 and now I'm in all sorts of other pain :(

Reply

Hey Gary,

I'm happy to see you were able to figure out the problem related to uninstalled package!

Hm, I see this course should work on PHP 8.1, are you having any issues running our course code on PHP 8.1? Or is it something related to your laptop?

Cheers!

Reply
gazzatav Avatar
gazzatav Avatar gazzatav | Victor | posted 1 year ago | edited

Hi @victor , cannot get the qr-code at all. When I try to go to /authentication/2fa/qr-code I end up at /2fa which appears to be the route for the authentication form in the scheb/2fa-bundle. Debug:router shows a route '2fa_login' that I did not make. Grepping for that route, I find:
vendor/scheb/2fa-bundle/Resources/views/Authentication/form.html.twig: {{ provider }}
Any ideas how to configure this so it doesn't hi-jack my path when I type in /authentication/2fa/qr-code manually?

1 Reply
gazzatav Avatar
gazzatav Avatar gazzatav | Victor | posted 1 year ago | edited

@vvictor , Update: I have seen a qr code but it had no secret in the content - kind of defeats the purpose. The login route is redirected to the path 2fa. 2fa is an entry in the firewall which seems to direct to itself so the login and qr code entry can never be completed. The 2fa path problem comes from the scheb/2fa package and is not of my making.

Reply

Hey Gary,

Let me clarify some things, did you download the course code and started from the start/ directory? Are you still on PHP 8.1? And if so, how did you make to install the package on PHP 8.1?

Cheers!

Reply
gazzatav Avatar

Hi Victor,

I downloaded the course files which I diffed/merged with my application which I've kept all the way from 'Charming Development'.

Now php8.1 is installed and working fine. The problem with that (in case anybody else gets stuck) was that after upgrading, php7.2 modules still hang around and need to be purged, though even that is not enough, as if you have a server running it can be holding on to its modules and you need to disable them so that you can purge them. Then there were new php modules to install like php8.1-gd for drawing the qr code. Then there is the simple matter of restarting the symfony server so that it has access to the new modules. (This seemed to be necessary, perhaps you could clarify, does the symfony server have all needed modules loaded in memory?)

In case it helps anyone else these are the php 8.1 modules I have installed (on Ubuntu - but the names should give a clue):

php8.1-apcu [installed by me]
php8.1-bz2 [installed by me]
php8.1-cli [installed by me]
php8.1-common [installed by me]
php8.1-curl [installed by me]
php8.1-gd [installed by me]
php8.1-mbstring [installed by me]
php8.1-opcache [installed by default]
php8.1-pgsql [installed by me]
php8.1-readline [installed by default]
php8.1-xml [installed by me]
php8.1-zip [installed by me]

For this project I have managed to uninstall packages that were deprecated and install more up-to-date packages ('composer update' will not do exactly the right thing!). I was stuck for a while getting pagerfanta to work but that's fine now. The docs for pagerfanta were a bit confusing because the link for the symfony framework on the babdev site took me to a github page instead of the babdev page for symfony. There is a link on the github page which does take you to the symfony framework page but then you get all sorts of confusion:
babdev/pagerfanta is deprecated, pagerfanta/pagerfanta has everything and pagerfanta has native support for twig. I eventually figured out I needed not just pagerfanta/pagerfanta but babdev/pagerfanta-bundle for symfony support and pagerfanta/twig for twig support. Actually you don't need pagerfanta/pagerfanta at all you can install what you need such as pagerfanta/core, pagerfanta/twig and pagerfanta/doctrine-orm-adapter.

I have watched ahead and I now see that the 2fa path and template are used re-purposed towards the end of the course. I can generate a qr-code image and to stop 2fa from taking over I just need to remove the secret from the database.

Cheers

Reply

Hey Gary,

Yeah, it sounds correct, you have to restart the server every time you installed a new PHP module (or remove it). So you did it correct.

About what modules are required? Good question! Symfony has a special tool for checking them, you can use Symfony CLI to check it with:

$ symfony check:requirements

It will show you if you're missing required modules, or recommended modules. You have to install all the required modules, but you can ignore recommended ones to run the Symfony project. Though, it's better to install recommended as well as it may improve your Symfony app.

What about the php8.1-gd - it's a PHP image library... So yes, it might be required for generating QR codes. What about others modules - well, it depends in your specific project. But you don't have to install all of them, Instead, install them by request, i.e. when you get an error that you need some new module - just install it and restart the web server. So, first of all, stick to recommendations of that "check:requirements" command

I hope this helps!

Cheers!

Reply
gazzatav Avatar

Yes, symfony check:requirements is a good tip. Errors about missing modules aren't always easy to read! At least I can go back and try the apcu lesson from way back. I couldn't do that with php7.2, or I didn't because it was experimental or something. Would it be useful to share my lock files so that you can see the versions installed to run on php8.1?

Reply

Hey Gary,

You can try to share, but it might be too long message for Disqus, lock files have really a lot of text. If you really want to share with others your lock file - I'd recommend you to create a Gist here https://gist.github.com/ and share the link to it - that would be the best.

Cheers!

Reply
Default user avatar
Default user avatar Miramar ALINGO | posted 1 year ago

Good Course

Reply
Cat in space

"Houston: no signs of life"
Start the conversation!

This tutorial also works great for Symfony 6!

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": ">=8.1",
        "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
    }
}
userVoice