2fa with 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.
With a Subscription, click any sentence in the script to jump to that part of the video!
Login SubscribeIt may not feel like it yet, but the bundle is now set up... except for one big missing piece: how do we want our users to get the temporary token they'll enter into the form?
In the docs, there are 3 choices... well kind of only 2. These first two are where you use an authenticator app - like Google authenticator or Authy. The other option is to send the code via email.
We're going to use this "totp" authentication, which is basically the same as Google authenticator and stands for "time-based one-time password".
The logic for this actually lives in a separate library. Copy the Composer require line, find your terminal, and paste:
composer require "scheb/2fa-totp:^5.13"
This time there's no recipe or anything fancy: it just installs the library. Next, if you head back to the documentation, we need to enable this as an authentication method inside the config file. That's back in config/packages/scheb_2fa.yaml
. Paste that at the bottom:
# See the configuration reference at https://github.com/scheb/2fa/blob/master/doc/configuration.md | |
scheb_two_factor: | |
// ... lines 3 - 8 | |
totp: | |
enabled: true |
Implementing TwoFactorInterface
The last step, if you look over at the docs, is to make our User
implements a TwoFactorInterface
. Open up our user class: src/Entity/User.php
, add TwoFactorInterface
:
// ... lines 1 - 9 | |
use Scheb\TwoFactorBundle\Model\Totp\TwoFactorInterface; | |
// ... lines 11 - 19 | |
class User implements UserInterface, PasswordAuthenticatedUserInterface, TwoFactorInterface | |
{ | |
// ... lines 22 - 253 | |
} |
Then head down to the bottom. Now go to the "Code"->"Generate" menu - or Command
+N
on a Mac - and choose implement methods to generate the 3 we need:
// ... 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. | |
} | |
} |
Beautiful. Here's how TOTP authentication works. Each user that decides to activate two-factor authentication for their account will have a TOTP secret - a random string - stored on a property. This will be used to validate the code and will be used to help the user set up their authenticator app when they first activate two-factor authentication.
The methods from the interface are fairly straightforward. isTotpAuthenticationEnabled()
returns whether or not the user has activated two-factor auth... and we can just check to see if the property is set. The getTotpAuthenticationUsername()
method is used to help generate some info on the QR code. The last method - getTotpAuthenticationConfiguration()
- is the most interesting: it determines how the codes are generated, including the number of digits and how long each will last. Usually, authenticator apps generate a new code every 30 seconds.
Copy the $totpSecret
property, scroll up to the properties in our class and paste:
// ... 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 | |
} |
Then head back to the bottom and use the "Code"->"Generate" menu to generate a getter and setter for this. But we can make this nicer: give the argument a nullable string type, a self
return type, and return $this
... because the rest of our setters are "fluent" like this:
// ... 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; | |
} | |
} |
For the getter... let's delete this entirely. We just won't need it... and it's kind of a sensitive value.
Let's fill in the three methods. I'll steal the code for the first... and paste:
// ... lines 1 - 20 | |
class User implements UserInterface, PasswordAuthenticatedUserInterface, TwoFactorInterface | |
{ | |
// ... lines 23 - 245 | |
public function isTotpAuthenticationEnabled(): bool | |
{ | |
return $this->totpSecret ? true : false; | |
} | |
// ... lines 250 - 266 | |
} |
For the username, in our case, return $this->getUserIdentifier()
, which is really just our email:
// ... lines 1 - 20 | |
class User implements UserInterface, PasswordAuthenticatedUserInterface, TwoFactorInterface | |
{ | |
// ... lines 23 - 250 | |
public function getTotpAuthenticationUsername(): string | |
{ | |
return $this->getUserIdentifier(); | |
} | |
// ... lines 255 - 266 | |
} |
For the last method, copy the config from the docs... and paste:
// ... 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 | |
} |
I'll re-type the end of TotpConfiguration
and hit tab so that PhpStorm adds the use
statement on top:
// ... 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 | |
} |
But, be careful. Change the 20 to 30, and the 8 to 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 | |
} |
This says that each code should last for 30 seconds and contain 6 digits. The reason I'm using these exact values - including the algorithm - is to support the Google Authenticator app. Other apps, apparently, allow you to tweak these, but Google Authenticator doesn’t. So if you want to support Google Authenticator, stick with this config.
Okay, our user system is ready! In theory, if we set a totpSecret
value for one of our users in the database, and then tried to log in as that user, we would be redirected to the "enter your code" form. But, we're missing a step.
Next: let's add a way for a user to activate two-factor authentication on their account. When they do that, we'll generate a totpSecret
and - most importantly - use it to show a QR code the user can scan to set up their authenticator app.
sincerely that you put in the tutorial that this library is no longer maintained and you have to use this other one.
otherwise you change the source code of the example.
I have been trying to implement this functionality for 4 days.
and I am not going to be able to implement it.
I have a total blockage
to me personally it does not help me at all.