QR Data & Scanning with an Authenticator App

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

Okay, status check. Any user can now enable two-factor authentication on their account by clicking this link. Behind the scenes, when they do that, we populate the totpSecret on the User object, save that to the database, and then render a QR code the user can scan. This QR code is a fancy image that contains two pieces of information. The first is the email of our user. Or, more precisely, if I scroll down to the "totp methods" in User, it contains whatever we return from getTotpAuthenticationUsername():

... lines 1 - 20
class User implements UserInterface, PasswordAuthenticatedUserInterface, TwoFactorInterface
... lines 23 - 250
public function getTotpAuthenticationUsername(): string
return $this->getUserIdentifier();
... lines 255 - 266

The second thing the QR code image contains is the totpSecret. In a minute, I'm going to scan this code with an authenticator app, which will allow me to generate the correct two-factor authentication code that I'll need to log in. It does that by leveraging that secret.

Adding Extra Info to the QR Code

But first, there is some extra info that we can add to the QR code. Head over to config/packages/scheb_2fa.yaml. Under totp:, one of the most important things that you can add is called an issuer. I'm going to set this to Cauldron Overflow:

# See the configuration reference at https://github.com/scheb/2fa/blob/master/doc/configuration.md
... lines 3 - 8
... line 10
issuer: 'Cauldron Overflow'

That literally just added new information to the QR code image. Watch the image when we refresh. See that? It changed!

Thanks to this, in addition to the email and totpSecret, the code now contains an "issuer" key. If you want to learn about all the extra information that you can put here, check out the documentation or read about totp authentication in general. Because, for example, "issuer" is just a "totp concept"... that helps authenticator apps generate a label for our site when we scan this code.

Scanning with our Authenticator App

At this point, I want to pretend that we're a real user and test the entire flow. If we were a real user we would pull out our phone, open an authenticator app - like Google authenticator or Authy - and scan this code.

I like using Authy, here's what it looks like for me. I add a new account, scan and... got it! It reads my email and the "issuer" from the QR code and suggests a name and logo. If your company is well-known, it might actually guess the correct logo, but you can also add an image to your QR code in the same way that we added the “issuer”. When I accept this, it gives me codes!

Logging in

So we are ready! Let's try it! Log out... and then log back in with abraca_admin@example.com, password tada. Submit and... sweet! Instead of actually being logged in, we're redirected to the two-factor authentication page! This happened for two reasons. First, the user has two-factor authentication enabled on their account. Specifically, this isTotpAuthenticationEnabled() method returned true. Second, the security "token" - that internal thing that wraps your User object when you log in - well, it matches one of the tokens in our configuration. Specifically, we get the UsernamePasswordToken when we log in via the form_login mechanism.

If we try going anywhere else on the site, it kicks us right back here. The only place we can go to is /logout if we wanted to cancel the process. This is because the two-factor bundle will now deny access to any page on our site unless you've explicitly allowed it via the access_control rules, like we did for /logout and for the URL showing this form. This form is ugly, but we'll fix that soon.

Ok, back to pretending I'm a real user. I'll open up my authenticator app, type in a valid code: 5, 3, 9, 9, 2, 2 and... got it! We're logged in! So cool!

Next, let's customize that two-factor authentication form... because it was ugly.

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", //
        "doctrine/annotations": "^1.0", // 1.13.2
        "doctrine/doctrine-bundle": "^2.1", // 2.4.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