If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.
With a Subscription, click any sentence in the script to jump to that part of the video!
Login SubscribePara 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.
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.
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 |
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.
Lol, must have leant on the keyboard. In my security.yaml I found the 'form_login' entry point had morphed into 'form_loginNqq'. So git diff was my friend!
Hello,
What must I do to make this work when (the first) authentication is not done with users entity but with ldap (active directory). The ldap authentication works great. We even use ldap for our voters. Roles and groups are organized in ldap.
What security_tokens do i use and do i need a custom persister?
Thanks in advance for your great help !
Security.yaml (symfony 5.4)
security:
providers:
my_ldap:
ldap:
service: Symfony\Component\Ldap\Ldap
firewall:
main:
stateless: true
provider: my_ldap
http_basic_ldap:
service: Symfony\Component\Ldap\Ldap
.
.
Hey truuslee!
I can't remember exactly what token class would be used for http_basic_ldap (it's up to the "authenticator" to choose the token class). But you should be able to see it after logging in: hover over the security icon on the web debug toolbar. You should see: Token class: and then the name of the token class (hover over it to see the full class name). If I had to guess, I'd think that it would use the normal `UsernamePasswordToken</close> token class, but... that's just a guess :).
Cheers!
Hi Ryan,
You were right. It's the UsernamePasswordToken.
By the way, we are not using an user entity at all.
Is it at all possible to use 2fa without an User entity?
Thank you again for your great help.
Hey Annemieke,
I didn't try but it should be possible I think, you will just need to implement some extra methods on your Use object, see the next chapter, this exact code block to be specified: https://symfonycasts.com/sc...
I hope this helps!
Cheers!
// 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
}
}
Oh dear!
When I run the command 'composer require scheb/2fa-totp' at the cache:clear I see
The service "security.exception_listener.main" has a dependency on a non-ex
!! istent service "form_loginNqq".