It is super easy to implement authentication via an app where your users get a new code every 30 seconds.
A lot of the services that I use implement this feature, GitHub, Microsoft, Digital Ocean, just to name a few. The name of it is Time-based One Time Password, or TOTP. There is another version of OTP algorithm, but that is beyond the scope of this post.
It also does not matter which authenticator app your users want to use, any that support OTP can use this.
- Google Authenticator
- LastPass Authenticator
- Microsoft Authenticator
- Authy
- etc
I mentioned that this is super easy to implement because there is a package that does the heavy lifting for us, spomky-labs/otphp
. And to generate the QR code, I’m using bacon/bacon-qr-code
. Here is what my composer.json file looks like:
1 2 3 4 5 6 |
{ "require": { "spomky-labs/otphp": "~10.0.1", "bacon/bacon-qr-code": "~2.0.0" } } |
Here is the simple implementation to generate the QR code.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
<?php use BaconQrCode\Renderer\Image\SvgImageBackEnd; use BaconQrCode\Renderer\ImageRenderer; use BaconQrCode\Renderer\RendererStyle\RendererStyle; use BaconQrCode\Writer; use OTPHP\TOTP; require_once dirname(__DIR__) . '/vendor/autoloader.php'; $totp = TOTP::create('LUPECODE3DEVELOP3MOONLIGHTCODING'); $totp->setLabel('Auth Test'); $totp->setIssuer('2fa.lupe.dev'); $writer = new Writer(new ImageRenderer(new RendererStyle(400, 1), new SvgImageBackEnd())); $svg = $writer->writeString($totp->getProvisioningUri()); file_put_contents(__DIR__ . '/oauth.svg', $svg); |
That’s not too long. Let’s talk a little about what is going on here.
In the create
, we need to pass in a base-32 string. In real world implementations, this will be unique to each user that needs to authenticate.
The label
is most of the time the username or some other unique identifier that the user will recognize as belonging to them.
The issuer
is you, the service that requires the authentication.
This will generate a static SVG file of the QR code. The exact method you show this to your users is up to you, but they will need to scan it with their authentication app.
Right, now we have the user’s app set up to provide codes, now how do we check them? Once you have your form set up, we can check with a single if
statement (in addition to setting up the TOTP
object).
1 2 3 4 5 6 7 8 9 10 |
if (array_key_exists('code', $_POST) && $_POST['code']) { $totp = TOTP::create('LUPECODE3DEVELOP3MOONLIGHTCODING'); $totp->setLabel('Auth Test'); $totp->setIssuer('2fa.lupe.dev'); if ($totp->verify($_POST['code'], null, 1)) { // Code was correct } else { // Code was incorrect } } |
We set up $totp
the exact same way as when we generated the QR code. I should also note that I recommend having the third parameter on the verify
function as 1, like I have it. This is the $window
parameter, and tells the verification algorithm to allow the previous or next code from the current one. Given that codes are only valid for 30 seconds, this window allows for some difference in the server time vs your user’s mobile’s time.
You can check out a test implementation set up here.