Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Renderización del código QR

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

Bien, acabamos de añadir una URL a la que el usuario puede ir para activar la autenticación de dos factores en su cuenta. Lo que esto significa realmente es bastante sencillo: generamos un totpSecret y lo guardamos en su registro de usuario en la base de datos. Gracias a esto, cuando el usuario intente iniciar sesión, el paquete de 2 factores lo notará y lo enviará al formulario de "rellenar el código".

Pero, para saber qué código debe introducir, el usuario debe configurar una aplicación de autenticación. Y para ello, necesitamos mostrar un código QR que puedan escanear.

Volcar el contenido del QR

¿Cómo? El $totpAuthenticator tiene un método que puede ayudar. Prueba a volcar$totpAuthenticator->getQRContent() y pásale $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));
}
}

Cuando refresquemos veremos... ¡una URL de aspecto súper raro! Esta es la información que necesitamos enviar a nuestra aplicación de autenticación. Contiene nuestra dirección de correo electrónico -que no es más que una etiqueta que ayudará a la app- y lo más importante, el secreto totp, que la app utilizará para generar los códigos.

En teoría, podríamos introducir esta URL manualmente en una app autentificadora. Pero, ¡eso es una locura! En el mundo real, traducimos esta cadena en una imagen de código QR.

Generar el código QR

Afortunadamente, de esto también se encarga la biblioteca Scheb. Si te desplazas un poco hacia abajo, hay un punto sobre los códigos QR. Si quieres generar uno, necesitas una última biblioteca. En realidad, justo después de que grabara esto, el encargado de mantener esta biblioteca 2fa-qr-code la ha retirado ¡Maldita sea! Así que aún puedes instalarla, pero también te mostraré cómo generar el código QR sin ella. La librería fue eliminada porque, bueno, es bastante fácil crear el código QR incluso sin ella.

De todos modos, lo copiaré, buscaré mi terminal y lo pegaré.

composer require scheb/2fa-qr-code

Para utilizar la nueva forma de generar los códigos QR -que recomiendo- sáltate este paso y en su lugar ejecuta

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

Mientras eso funciona. Vuelve a los documentos... y copia este controlador de la documentación. En SecurityController, en la parte inferior, pega. Modificaré la URL para que sea /authentication/2fa/qr-code y llamaré a la rutaapp_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']);
}
}

También tengo que volver a escribir la "R" en QrCodeGenerator para obtener su declaración de uso:

... 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
}
}

Si utilizas la nueva forma de generar los códigos QR, entonces tu controlador debería ser así. Puedes copiarlo del bloque de código de esta página:

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']);
    }
}

Esta ruta especial devuelve literalmente la imagen del código QR, como un png. Ah, y lo olvidé aquí, pero deberías añadir un @IsGranted("ROLE_USER") encima de esto: sólo los usuarios autentificados deberían poder cargar esta imagen.

De todos modos, el usuario no irá a esta URL directamente: la utilizaremos dentro de una etiqueta img. Pero para ver si funciona, copia la URL, pégala en tu navegador y... ¡qué bien! ¡Hola código QR!

Por último, después de que el usuario habilite la autenticación de dos factores, vamos a renderizar una plantilla con una imagen a esta URL. Vuelve a $this->render('security/enable2fa.html.twig').

Copia el nombre de la plantilla, entra en templates/security, y créala:enable2fa.html.twig. Pondré una estructura básica... es sólo un h1 que te dice que escanees el código QR... pero todavía no hay imagen:

{% 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 %}

Vamos a añadirla: un img con src ajustado a {{ path() }} y luego el nombre de la ruta al controlador que acabamos de construir. Así que app_qr_code. Para el alt, diré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 %}

¡Genial! Es hora de probar todo el flujo. Comienza en la página de inicio, activa la autenticación de dos factores y... ¡sí! ¡Vemos el código QR! Estamos listos para escanearlo e intentar iniciar la sesión.

Hacer que el usuario confirme que ha escaneado el código QR

Oh, pero antes de hacerlo, en una aplicación real, probablemente añadiría una propiedad extra en mi usuario, llamada isTotpEnabled y la utilizaría en el método isTotpAuthenticationEnabled()de mi clase User. ¿Por qué? Porque nos permitiría tener el siguiente flujo. En primer lugar, el usuario hace clic en "Activar la autenticación de dos factores", generamos el totpSecret, lo guardamos, y renderizamos el código QR. Es decir, exactamente lo que estamos haciendo ahora. Pero, esa nueva banderaisTotpEnabled seguiría siendo falsa. Así, si algo saliera mal y el usuario nunca escaneara el código QR, seguiría pudiendo iniciar la sesión sin que le pidiéramos el código. Luego, en la parte inferior de esta página, podríamos añadir un botón de "Confirmación". Cuando el usuario haga clic en él, finalmente estableceríamos la propiedadisTotpEnabled como verdadera. Incluso podrías exigir al usuario que introdujera un código desde su aplicación de autenticación para demostrar que ha configurado las cosas: el servicioTotpAuthenticatorInterface tiene un método checkCode() por si alguna vez quieres comprobar manualmente un código.

A continuación: vamos a escanear este código QR con una aplicación de autenticación y, finalmente, a probar el flujo completo de autenticación de dos factores. A continuación, aprenderemos a personalizar la "plantilla de introducción del código" para que se ajuste a nuestro diseño.

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 hace 1 año

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 hace 1 año | 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 hace 1 año | 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 hace 1 año

Good Course

Reply
Cat in space

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

¡Este tutorial también funciona muy bien para 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