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.
5 Comments
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.
Hey @R!
Sorry for my slow reply - and really sorry about the trouble - that's not the experience that we want! Ok, let's check into what's going on...
For this tutorial, we use version
5.13ofscheb/2fa-totpbecause the project uses version 5 ofscheb/2fa-bundle. Both of those libraries ARE still maintained, but you're totally right that this is an old version: the new version is 6 for both of these libraries.In 2 chapters, when we talk about rendering the QR code - https://symfonycasts.com/screencast/symfony-security/qr-code - there is another spot where you can follow the tutorial exactly, or you can use a different library which is now the recommended way. Is this where you hit a problem?
Anyway, I'm really sorry about the issues! If you can give me some more information about your blockage, I'll do my best to help :).
Cheers!
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()
Hey Kiuega,
Yeah, you're right, no need to make it null in eraseCredentials() because it should be stored in the DB permanently.
Cheers!
"Houston: no signs of life"
Start the conversation!