Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

2fa con TOTP (Time-Based One Time Password)

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

Puede que aún no lo parezca, pero el paquete ya está configurado... excepto por una gran pieza que falta: ¿cómo queremos que nuestros usuarios obtengan el token temporal que introducirán en el formulario?

En los documentos, hay 3 opciones... bueno, sólo 2. Las dos primeras consisten en utilizar una aplicación de autentificación, como Google Authenticator o Authy. La otra opción es enviar el código por correo electrónico.

Vamos a utilizar esta autenticación "totp", que es básicamente lo mismo que el autenticador de Google y significa "contraseña de un solo uso basada en el tiempo".

La lógica para esto vive en realidad en una biblioteca separada. Copia la línea requerida por Composer, busca tu terminal y pégala:

composer require "scheb/2fa-totp:^5.13"

Esta vez no hay ninguna receta ni nada extravagante: simplemente se instala la biblioteca. A continuación, si vuelves a la documentación, tenemos que habilitar esto como método de autenticación dentro del archivo de configuración. Eso está en config/packages/scheb_2fa.yaml. Pégalo en la parte inferior:

# See the configuration reference at https://github.com/scheb/2fa/blob/master/doc/configuration.md
scheb_two_factor:
... lines 3 - 8
totp:
enabled: true

Implementación de TwoFactorInterface

El último paso, si miras la documentación, es hacer que nuestro Userimplemente un TwoFactorInterface. Abre nuestra clase de usuario: src/Entity/User.php, añade TwoFactorInterface:

... lines 1 - 9
use Scheb\TwoFactorBundle\Model\Totp\TwoFactorInterface;
... lines 11 - 19
class User implements UserInterface, PasswordAuthenticatedUserInterface, TwoFactorInterface
{
... lines 22 - 253
}

Luego dirígete a la parte inferior. Ahora ve al menú "Código"->"Generar" -oCommand+N en un Mac- y elige implementar métodos para generar los 3 que necesitamos:

... lines 1 - 8
use Scheb\TwoFactorBundle\Model\Totp\TotpConfigurationInterface;
... lines 10 - 19
class User implements UserInterface, PasswordAuthenticatedUserInterface, TwoFactorInterface
{
... lines 22 - 239
public function isTotpAuthenticationEnabled(): bool
{
// TODO: Implement isTotpAuthenticationEnabled() method.
}
public function getTotpAuthenticationUsername(): string
{
// TODO: Implement getTotpAuthenticationUsername() method.
}
public function getTotpAuthenticationConfiguration(): ?TotpConfigurationInterface
{
// TODO: Implement getTotpAuthenticationConfiguration() method.
}
}

Hermoso. Así es como funciona la autenticación TOTP. Cada usuario que decida activar la autenticación de dos factores para su cuenta tendrá un secreto TOTP -una cadena aleatoria- almacenado en una propiedad. Ésta se utilizará para validar el código y servirá para ayudar al usuario a configurar su aplicación de autenticación cuando active por primera vez la autenticación de dos factores.

Los métodos de la interfaz son bastante sencillos.isTotpAuthenticationEnabled() devuelve si el usuario ha activado o no la autenticación de dos factores... y podemos comprobar simplemente si la propiedad está establecida. El métodogetTotpAuthenticationUsername() se utiliza para ayudar a generar algo de información sobre el código QR. El último método - getTotpAuthenticationConfiguration() - es el más interesante: determina cómo se generan los códigos, incluyendo el número de dígitos y la duración de cada uno. Normalmente, las aplicaciones de autenticación generan un nuevo código cada 30 segundos.

Copia la propiedad $totpSecret, desplázate hasta las propiedades de nuestra clase y pégala:

... lines 1 - 19
class User implements UserInterface, PasswordAuthenticatedUserInterface, TwoFactorInterface
{
... lines 22 - 63
/**
* @ORM\Column(type="string", length=255, nullable=true)
*/
private $totpSecret;
... lines 68 - 270
}

Luego vuelve a la parte inferior y utiliza el menú "Código"->"Generar" para generar un getter y un setter para esto. Pero podemos hacerlo más bonito: dale al argumento un tipo de cadena anulable, un tipo de retorno self y devuelve $this... porque el resto de nuestros establecedores son "fluidos" como éste:

... lines 1 - 19
class User implements UserInterface, PasswordAuthenticatedUserInterface, TwoFactorInterface
{
... lines 22 - 259
public function getTotpSecret(): ?string
{
return $this->totpSecret;
}
public function setTotpSecret(?string $totpSecret): self
{
$this->totpSecret = $totpSecret;
return $this;
}
}

Para el getter... vamos a eliminarlo por completo. No lo vamos a necesitar... y es un valor algo sensible.

Vamos a rellenar los tres métodos. Robaré el código del primero... y lo pegaré:

... lines 1 - 20
class User implements UserInterface, PasswordAuthenticatedUserInterface, TwoFactorInterface
{
... lines 23 - 245
public function isTotpAuthenticationEnabled(): bool
{
return $this->totpSecret ? true : false;
}
... lines 250 - 266
}

Para el nombre de usuario, en nuestro caso, devuelve $this->getUserIdentifier(), que en realidad es nuestro correo electrónico:

... lines 1 - 20
class User implements UserInterface, PasswordAuthenticatedUserInterface, TwoFactorInterface
{
... lines 23 - 250
public function getTotpAuthenticationUsername(): string
{
return $this->getUserIdentifier();
}
... lines 255 - 266
}

Para el último método, copia la configuración de los documentos... y pega:

... lines 1 - 20
class User implements UserInterface, PasswordAuthenticatedUserInterface, TwoFactorInterface
{
... lines 23 - 255
public function getTotpAuthenticationConfiguration(): ?TotpConfigurationInterface
{
return new TotpConfiguration($this->totpSecret, TotpConfiguration::ALGORITHM_SHA1, 30, 6);
}
... lines 260 - 266
}

Vuelvo a escribir el final de TotpConfiguration y pulso el tabulador para que PhpStorm añada la declaración use encima:

... lines 1 - 8
use Scheb\TwoFactorBundle\Model\Totp\TotpConfiguration;
... lines 10 - 20
class User implements UserInterface, PasswordAuthenticatedUserInterface, TwoFactorInterface
{
... lines 23 - 255
public function getTotpAuthenticationConfiguration(): ?TotpConfigurationInterface
{
return new TotpConfiguration($this->totpSecret, TotpConfiguration::ALGORITHM_SHA1, 30, 6);
}
... lines 260 - 266
}

Pero, ten cuidado. Cambia el 20 por el 30, y el 8 por el 6:

... lines 1 - 20
class User implements UserInterface, PasswordAuthenticatedUserInterface, TwoFactorInterface
{
... lines 23 - 255
public function getTotpAuthenticationConfiguration(): ?TotpConfigurationInterface
{
return new TotpConfiguration($this->totpSecret, TotpConfiguration::ALGORITHM_SHA1, 30, 6);
}
... lines 260 - 266
}

Esto dice que cada código debe durar 30 segundos y contener 6 dígitos. La razón por la que utilizo estos valores exactos -incluyendo el algoritmo- es para dar soporte a la aplicación Google Authenticator. Otras aplicaciones, aparentemente, te permiten ajustar estos valores, pero Google Authenticator no lo hace. Así que, si quieres apoyar a Google Authenticator, quédate con esta configuración.

Bien, ¡nuestro sistema de usuarios está listo! En teoría, si estableciéramos un valor totpSecret para uno de nuestros usuarios en la base de datos, y luego intentáramos iniciar sesión como ese usuario, seríamos redirigidos al formulario "introduce tu código". Pero, nos falta un paso.

Siguiente: vamos a añadir una forma de que un usuario active la autenticación de dos factores en su cuenta. Cuando lo haga, generaremos un totpSecret y, lo que es más importante, lo utilizaremos para mostrar un código QR que el usuario puede escanear para configurar su aplicación de autenticación.

Leave a comment!

3
Login or Register to join the conversation
Fabrice Avatar
Fabrice Avatar Fabrice | posted hace 1 año

Hello ! Is it relevant to add the $totpSecret property in the eraseCredentials() method of the User entity?

EDIT : Okay my bad, the property is registered in the database, I thought it was used in one-shot every time, so no need to use it in the eraseCredentials()

Reply

Hey Kiuega,

Yeah, you're right, no need to make it null in eraseCredentials() because it should be stored in the DB permanently.

Cheers!

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