Activación de 2FA
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.
With a Subscription, click any sentence in the script to jump to that part of the video!
Login SubscribeBien: este es el flujo. Cuando enviemos un correo electrónico y una contraseña válidos, el paquete de dos factores lo interceptará y nos redirigirá a un formulario de "introducir el código". Para validar el código, leerá el totpSecret
que está almacenado para ese User
:
// ... lines 1 - 20 | |
class User implements UserInterface, PasswordAuthenticatedUserInterface, TwoFactorInterface | |
{ | |
// ... lines 23 - 64 | |
/** | |
* @ORM\Column(type="string", length=255, nullable=true) | |
*/ | |
private $totpSecret; | |
// ... lines 69 - 266 | |
} |
Pero para saber qué código debe escribir, el usuario tiene que activar primero la autenticación de dos factores en su cuenta y escanear un código QR que le proporcionamos con su aplicación de autenticación.
Construyamos ahora ese lado de las cosas: la activación y el código QR.
Ah, pero antes de que se me olvide otra vez, en el último capítulo añadimos una nueva propiedad a nuestro User
... y se me olvidó hacer una migración para ella. En tu terminal, ejecuta:
symfony console make:migration
Vamos a comprobar ese archivo:
// ... lines 1 - 4 | |
namespace DoctrineMigrations; | |
use Doctrine\DBAL\Schema\Schema; | |
use Doctrine\Migrations\AbstractMigration; | |
/** | |
* Auto-generated Migration: Please modify to your needs! | |
*/ | |
final class Version20211012201423 extends AbstractMigration | |
{ | |
public function getDescription(): string | |
{ | |
return ''; | |
} | |
public function up(Schema $schema): void | |
{ | |
// this up() migration is auto-generated, please modify it to your needs | |
$this->addSql('ALTER TABLE user ADD totp_secret VARCHAR(255) DEFAULT NULL'); | |
} | |
public function down(Schema $schema): void | |
{ | |
// this down() migration is auto-generated, please modify it to your needs | |
$this->addSql('ALTER TABLE user DROP totp_secret'); | |
} | |
} |
Y... bien. Sin sorpresas, añade una columna a nuestra tabla. Ejecuta eso:
symfony console doctrine:migrations:migrate
Añadir una forma de Activar 2fa
Este es el plan. Un usuario no tendrá activada la autenticación de dos factores por defecto, sino que la activará haciendo clic en un enlace. Cuando lo hagan, generaremos untotpSecret
, se lo pondremos al usuario, lo guardaremos en la base de datos y le mostraremos un código QR para que lo escanee.
Dirígete a src/Controller/SecurityController.php
. Vamos a crear la ruta que activa la autenticación de dos factores: public function
enable2fa(). Dale una ruta: ¿qué tal /authenticate/2fa/enable
- y name="app_2fa_enable"
:
// ... lines 1 - 12 | |
class SecurityController extends BaseController | |
{ | |
// ... lines 15 - 33 | |
/** | |
* @Route("/authentication/2fa/enable", name="app_2fa_enable") | |
// ... line 36 | |
*/ | |
public function enable2fa(TotpAuthenticatorInterface $totpAuthenticator, EntityManagerInterface $entityManager) | |
{ | |
// ... lines 40 - 47 | |
} | |
} |
Sólo ten cuidado de no empezar la URL con /2fa
... eso está reservado para el proceso de autenticación de dos factores:
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 |
Dentro del método, necesitamos dos servicios. El primero es un servicio autoconvocable del paquete - TotpAuthenticatorInterface $totpAuthenticator
. Que nos ayudará a generar el secreto. El segundo es EntityManagerInterface $entityManager
:
// ... lines 1 - 4 | |
use Doctrine\ORM\EntityManagerInterface; | |
use Scheb\TwoFactorBundle\Security\TwoFactor\Provider\Totp\TotpAuthenticatorInterface; | |
// ... lines 7 - 12 | |
class SecurityController extends BaseController | |
{ | |
// ... lines 15 - 37 | |
public function enable2fa(TotpAuthenticatorInterface $totpAuthenticator, EntityManagerInterface $entityManager) | |
{ | |
// ... lines 40 - 47 | |
} | |
} |
Y, por supuesto, sólo puedes utilizar esta ruta si estás autentificado. Añade@IsGranted("ROLE_USER")
. Permíteme volver a escribir eso y pulsar el tabulador para que aparezca la declaración use
en la parte superior:
// ... lines 1 - 6 | |
use Sensio\Bundle\FrameworkExtraBundle\Configuration\IsGranted; | |
// ... lines 8 - 12 | |
class SecurityController extends BaseController | |
{ | |
// ... lines 15 - 33 | |
/** | |
// ... line 35 | |
* @IsGranted("ROLE_USER") | |
*/ | |
public function enable2fa(TotpAuthenticatorInterface $totpAuthenticator, EntityManagerInterface $entityManager) | |
{ | |
// ... lines 40 - 47 | |
} | |
} |
Tip
Este párrafo siguiente es... ¡equivocado! Utilizar ROLE_USER
no obligará a un usuario a volver a introducir su contraseña si sólo está autenticado a través de una cookie "recuérdame". Para ello, debes utilizar IS_AUTHENTICATED_FULLY
. Y eso es lo que debería haber utilizado aquí.
En su mayor parte, he utilizado IS_AUTHENTICATED_REMEMBERED
por seguridad... para que sólo tengas que iniciar sesión... aunque sea a través de una cookie "recuérdame". Pero aquí estoy utilizando ROLE_USER
, que es efectivamente idéntico aIS_AUTHENTICATED_FULLY
. Eso es a propósito. El resultado es que si el usuario se autentificó... pero sólo gracias a una cookie "recuérdame", Symfony le obligará a volver a escribir su contraseña antes de llegar aquí. Un poco de seguridad extra antes de habilitar la autenticación de dos factores.
De todos modos, digamos $user = this->getUser()
... y luego si no$user->isTotpAuthenticationEnabled()
:
// ... lines 1 - 12 | |
class SecurityController extends BaseController | |
{ | |
// ... lines 15 - 37 | |
public function enable2fa(TotpAuthenticatorInterface $totpAuthenticator, EntityManagerInterface $entityManager) | |
{ | |
$user = $this->getUser(); | |
if (!$user->isTotpAuthenticationEnabled()) { | |
// ... lines 42 - 44 | |
} | |
// ... lines 46 - 47 | |
} | |
} |
Hmm, quiero ver si la autenticación totp no está ya habilitada... pero no me aparece el autocompletado para esto.
Ya sabemos por qué: el método getUser()
sólo sabe que devuelve un UserInterface
. Lo hemos arreglado antes haciendo nuestro propio controlador base. Vamos a ampliarlo:
// ... lines 1 - 12 | |
class SecurityController extends BaseController | |
{ | |
// ... lines 15 - 48 | |
} |
Aquí abajo, si no es $user->isTotpAuthenticationEnabled()
-por lo que si el usuario no tiene ya un totpSecret
- vamos a establecer uno:$user->setTotpSecret()
pasando por $totpAuthentiator->generateSecret()
. Luego, guarda con $entityManager->flush()
.
En la parte inferior, por ahora, sólo dd($user)
para que podamos asegurarnos de que esto funciona:
// ... lines 1 - 12 | |
class SecurityController extends BaseController | |
{ | |
// ... lines 15 - 37 | |
public function enable2fa(TotpAuthenticatorInterface $totpAuthenticator, EntityManagerInterface $entityManager) | |
{ | |
$user = $this->getUser(); | |
if (!$user->isTotpAuthenticationEnabled()) { | |
$user->setTotpSecret($totpAuthenticator->generateSecret()); | |
$entityManager->flush(); | |
} | |
dd($user); | |
} | |
} |
Enlazando con la Ruta
¡Genial! ¡Vamos a enlazar con esto! Copia el nombre de la ruta... y abretemplates/base.html.twig
. Busca "Cerrar sesión". Ya está. Pegaré ese nombre de ruta, duplicaré todo li
, limpiaré las cosas, pegaré el nuevo nombre de ruta, eliminaré mi código temporal y diré "Activar 2FA":
// ... line 1 | |
<html> | |
// ... lines 3 - 14 | |
<body | |
// ... lines 16 - 21 | |
<nav | |
class="navbar navbar-expand-lg navbar-light bg-light px-1" | |
{{ is_granted('ROLE_PREVIOUS_ADMIN') ? 'style="background-color: red !important"' }} | |
> | |
<div class="container-fluid"> | |
// ... lines 27 - 35 | |
<div class="collapse navbar-collapse" id="navbar-collapsable"> | |
// ... lines 37 - 47 | |
{% if is_granted('IS_AUTHENTICATED_REMEMBERED') %} | |
<div class="dropdown"> | |
// ... lines 50 - 60 | |
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="user-dropdown"> | |
// ... lines 62 - 68 | |
<li> | |
<a class="dropdown-item" href="{{ path('app_2fa_enable') }}">Enble 2fa</a> | |
</li> | |
// ... lines 72 - 74 | |
</ul> | |
</div> | |
{% else %} | |
// ... lines 78 - 79 | |
{% endif %} | |
</div> | |
</div> | |
</nav> | |
// ... lines 84 - 95 | |
</body> | |
</html> |
¡Hora de probar! Ah, pero antes, en tu terminal, recarga tus instalaciones:
symfony console doctrine:fixtures:load
Eso hará que todos los usuarios tengan correos electrónicos verificados para que podamos iniciar la sesión. Cuando esto termine, inicia la sesión con abraca_admin@example.com
, contraseña tada
. Precioso. A continuación, pulsa "Habilitar 2FA" y... ¡ya está! Se accede a nuestro volcado de usuarios! Y lo más importante, ¡tenemos un conjunto de totpSecret
!
¡Eso es genial! Pero el último paso es mostrar al usuario un código QR que pueda escanear para configurar su aplicación de autenticación. Hagamos eso a continuación.
Hello, any idea on how to generate secret but in a different situation? Imagine there is a system where SUPER ADMINS have to use 2fa. So it is not a matter of enabling it for someone after he logged in, but it is required. So I should somehow hook in so that when user is authenticated with username and password, generate Secret for him, store in in User entity, so that when scheb/2fa takes its hands on the user it redirects him to show the QR Code. If I do not do this, I will get TwoFactorProviderLogicException that the secret is missing.