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!

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