Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

autenticación de 2 factores y tokens de autenticación

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

Para nuestro último truco en este tutorial, vamos a hacer algo divertido: añadir la autenticación de dos factores. Esto puede adoptar varias formas, pero el flujo básico es el siguiente, que probablemente te resulte familiar. Primero, el usuario envía un correo electrónico y una contraseña válidos al formulario de inicio de sesión. Pero entonces, en lugar de iniciar la sesión, se les redirige a un formulario en el que tienen que introducir un código temporal.

Este código puede ser algo que le enviemos por correo electrónico o por mensaje de texto a su teléfono... o puede ser un código de una aplicación de autentificación como Google authenticator o Authy. Una vez que el usuario rellene el código y lo envíe, estará finalmente conectado.

Instalación del paquete scheb/2fa

En el mundo de Symfony, tenemos la gran suerte de contar con una fantástica biblioteca que nos ayuda con la autenticación de dos factores. Busca Symfony 2fa para encontrar la biblioteca scheb/2fa. Desplázate hacia abajo... y haz clic en la documentación, que se encuentra en Symfony.com. Luego dirígete a la página de instalación.

¡Genial! ¡Vamos a instalar esta cosa! En tu terminal, ejecuta

composer require "2fa:^5.13"

Donde 2fa es un alias de Flex para el nombre real del paquete.

Una vez que esto termine... Ejecutaré:

git status

para ver qué ha hecho la receta del bundle. Genial: ha añadido un nuevo archivo de configuración... y también un nuevo archivo de rutas.

Ese archivo de rutas, que vive en config/routes/scheb_2fa.yaml, añade dos rutas a nuestra aplicación:

2fa_login:
path: /2fa
defaults:
_controller: "scheb_two_factor.form_controller:form"
2fa_login_check:
path: /2fa_check

La primera mostrará el formulario de "introducir el código" que vemos después de enviar nuestro correo electrónico y contraseña. La segunda ruta es la URL a la que se enviará este formulario.

Configuración del paquete / Setup

De vuelta a la documentación, vamos a repasar esto. El paso 2 - habilitar el paquete - lo ha hecho Flex automáticamente... y el paso 3 - definir las rutas - se ha gestionado gracias a la receta. ¡Muy bien!

El paso 4 es configurar el cortafuegos. Esta parte sí tenemos que hacerla.

Empieza por copiar el material de two_factor. Luego abreconfig/packages/security.yaml. Esta nueva configuración puede vivir en cualquier lugar bajo nuestro cortafuegosmain. La pegaré después de form_login... y podemos eliminar este comentario: destacaba que 2fa_login debía coincidir con el nombre de la ruta en nuestro archivo de rutas, lo cual hace:

security:
... lines 2 - 20
firewalls:
... lines 22 - 24
main:
... lines 26 - 49
two_factor:
auth_form_path: 2fa_login
check_path: 2fa_login_check
... lines 53 - 71

Ah, y ¿recuerdas que la función de la mayoría de las claves de nuestro cortafuegos es activar otro autentificador? Pues la clave two_factor no es una excepción: activa un nuevo autentificador que gestiona el envío del formulario "introduce tu código" que veremos en unos minutos.

El README también recomienda un par de controles de acceso, que son una buena idea. Cópialos... y pégalos en la parte superior de nuestro access_control:

security:
... lines 2 - 61
access_control:
# This makes the logout route accessible during two-factor authentication. Allows the user to
# cancel two-factor authentication, if they need to.
- { path: ^/logout, role: PUBLIC_ACCESS }
# This ensures that the form can only be accessed when two-factor authentication is in progress.
- { path: ^/2fa, role: IS_AUTHENTICATED_2FA_IN_PROGRESS }
- { path: ^/admin/login, roles: PUBLIC_ACCESS }
- { path: ^/admin, roles: ROLE_ADMIN }
# - { path: ^/profile, roles: ROLE_USER }

Este segundo se asegura de que no puedas ir a /2fa -que es la URL que muestra el formulario "introduce tu código"- a menos que ya hayas enviado tu correo electrónico y contraseña válidos. Cuando estás en esa especie de estado de "entrecruzamiento", el paquete 2fa se asegura de que tengas este atributo IS_AUTHENTICATED_2FA_IN_PROGRESS:

security:
... lines 2 - 61
access_control:
... lines 63 - 65
# This ensures that the form can only be accessed when two-factor authentication is in progress.
- { path: ^/2fa, role: IS_AUTHENTICATED_2FA_IN_PROGRESS }
... lines 68 - 71

La primera entrada -para /logout - se asegura de que si estás en ese estado "intermedio", todavía puedes cancelar el inicio de sesión yendo a /logout. Pero cambia esto por PUBLIC_ACCESS:

security:
... lines 2 - 61
access_control:
# This makes the logout route accessible during two-factor authentication. Allows the user to
# cancel two-factor authentication, if they need to.
- { path: ^/logout, role: PUBLIC_ACCESS }
... lines 66 - 71

Configurar los security_tokens

El último paso del README es configurar este security_tokens config.

Me explico. Cuando enviamos un correo electrónico y una contraseña válidos en el formulario de inicio de sesión, el sistema de autenticación de dos factores -a través de un oyente- va a decidir si debe interrumpir la autenticación e iniciar el proceso de autenticación de dos factores... en el que redirige al usuario al formulario de "introducir el código".

Si lo pensamos bien, definitivamente queremos que esto ocurra cuando un usuario se registre a través del formulario de acceso. Pero... probablemente no querríamos que esto ocurriera si, por ejemplo, un usuario se autentificara a través de un token de la API. El paquete necesita una forma de averiguar si queremos o no 2fa en función de cómo se acaba de autenticar el usuario.

No hemos hablado mucho de ello, pero siempre que te conectas, te autentificas con un determinado tipo de objeto token. Este objeto token es... una especie de envoltura del objeto User... y casi nunca te preocupas por él.

Pero, diferentes sistemas de autenticación -como form_login o remember_me - utilizan diferentes clases de tokens... lo que significa que puedes averiguar cómo se conectó originalmente el usuario, mirando el token actualmente autenticado.

Por ejemplo, esta clase de token superior es en realidad el token que obtienes si te conectas a través del autentificador form_login. Te lo demostraré. Pulsa Shift+Shift y busca FormLoginAuthenticator. Dentro... tiene un método createAuthenticatedToken(), un método que tiene todo autentificador. Devuelve un nuevo UsernamePasswordToken.

Este es el punto. Si iniciamos la sesión a través de este autentificador... y la clase de token correspondiente aparece en nuestra configuración de scheb_two_factor, el proceso de autentificación de dos factores se hará cargo y redirigirá al usuario al formulario de "introducir el código".

Vamos a ver qué aspecto tiene nuestro archivo: config/packages/scheb_2fa.yaml:

# See the configuration reference at https://github.com/scheb/2fa/blob/master/doc/configuration.md
scheb_two_factor:
security_tokens:
- Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken
# If you're using guard-based authentication, you have to use this one:
# - Symfony\Component\Security\Guard\Token\PostAuthenticationGuardToken
# If you're using authenticator-based security (introduced in Symfony 5.1), you have to use this one:
# - Symfony\Component\Security\Http\Authenticator\Token\PostAuthenticationToken

Por defecto, la única clase no comentada es UsernamePasswordToken, lo cual es perfecto para nosotros.

Pero fíjate en el último comentario. Si te estás autentificando mediante un autentificador personalizado -como hemos hecho antes-, debes utilizar esta clase.

Veamos exactamente por qué es así. Abre nuestro LoginFormAuthenticator personalizado. Ya no lo usamos, pero haz como si lo hiciéramos. Esto extiendeAbstractLoginFormAuthenticator:

... lines 1 - 15
use Symfony\Component\Security\Http\Authenticator\AbstractLoginFormAuthenticator;
... lines 17 - 26
class LoginFormAuthenticator extends AbstractLoginFormAuthenticator
{
... lines 29 - 81
}

Mantén pulsado Cmd o Ctrl para abrirlo... luego abre su clase base AbstractAuthenticator. Desplázate un poco hacia abajo y... ¡hola createAuthenticatedToken()! Esto devuelve un nuevoPostAuthenticatedToken. Y así, por defecto, esta es la clase token que obtienes con un autentificador personalizado.

Estas clases de token no son superimportantes... básicamente todas extienden el mismo AbstractToken... y en su mayoría sólo ayudan a identificar cómo se ha conectado el usuario.

Aprovechando este conocimiento, junto con la configuración scheb, puedes decirle al paquete de dos factores qué autenticadores requieren la autenticación de dos factores y cuáles no.

Ah, y si utilizas dos autenticadores personalizados... y sólo uno de ellos necesita la autenticación de dos factores, tendrás que crear una clase de token personalizada y anular el método createAuthenticatedToken() de tu autenticador para que lo devuelva. Entonces podrás apuntar sólo a la clase personalizada aquí.

¡Uf! Puede parecer que no hemos hecho mucho todavía... aparte de escucharme hablar de tokens... pero el paquete ya está... básicamente configurado. Pero ahora tenemos que elegir cómo recibirán los tokens nuestros usuarios. ¿Los enviaremos por correo electrónico? ¿O utilizarán una aplicación de autentificación con un código QR? Vamos a hacer lo segundo.

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