Programster's Blog

Tutorials focusing on Linux, programming, and open-source

Setting Up TOTP in PHP

Introduction

Setting up MFA/2FA on your website or CLI utility will can significantly increase it's security because it requires an attacker to have the user's second device, even if they know the user's password (or manage to brute-force it). Watch this video on MFA for more info on the topic if you are interested.

I wanted a way to be able to easily implement 2FA through time-based one-time-passwords (TOTP) in PHP without requiring an internet connection. I specifically mention the lack of an internet connection because there are some libraries out there that actually just send your data to a third-party webservice to generate the QR code, rather than generating it locally using the code. Neither of the methods below will do this.

This tutorial will focus on just adding support for 2FA in PHP. This could be for either a website, or a CLI utility. It makes no difference. I have tested that both of these methods work with both the Google Authenticator mobile application, and Passbolt's TOTP feature. For a TOTP code to provide you with protection, one needs to combine it with login rate-limiting, as a 6 digit integer-only code is not going to take long to brute-force, and we are reliant on the fact that a new one is created every 30 seconds or so. Thus, we need to restrict the number of login attempts that could happen in that 30 seconds. Requiring 1 second between login attempts is short enough that a user would not notice it, but would restrict a bot to just 30 attempts at the 1,000,000 different possible answers.

Method 1 - Google2FA Package

There are actually quite a few packages for implementing two-factor-authentication in PHP. For this method, I am going to use pragmarx/google2fa because it has 6.26 million installs at the time of writing this tutorial. That doesn't necessarily mean it's great, but it's a good rule of thumb and it worked for me.

First we need to install the package, which is as easy as:

composer require pragmarx/google2fa

Then we can make use of it to generate a secret for our user(s).

<?php

# Include packages
require_once(__DIR__ . '/vendor/autoload.php');

# Create the 2FA class
$google2fa = new PragmaRX\Google2FA\Google2FA();

# Print a user secret for user to enter into their phone. 
# The application needs to persist this somewhere safely where other users can't get it.
$userSecret = $google2fa->generateSecretKey();

print "Please enter the following secret into your phone:" . PHP_EOL .  $userSecret . PHP_EOL;

Now you have a secret for our user(s), they can feed it into their Google authenticator application.

Now when that user wishes to authenticate, the application needs to do the following:

<?php

# Include packages
require_once(__DIR__ . '/vendor/autoload.php');

# Create the 2FA class
$google2fa = new PragmaRX\Google2FA\Google2FA();

# Get the 2FA code from the user. If this is a website, you would fetch from a posted field instead.
print "Please enter your 2FA code:" . PHP_EOL;
$code = readline();

# Fetch/load the user secret in whatever way you do.
$userSecret = fetchUserSecretFromPersistenceStore();

# Verify the code is correct against our persisted user secret.
# This returns true if correct, false if not.
$valid = $google2fa->verifyKey($userSecret, $code); 

print ($valid) ? "Authenication PASSED!": "Authentication FAILED!";
print PHP_EOL;

Taking It Further With QR Codes

The biggest problem with the solution above is that $google2fa->generateSecretKey() generates a long string that is tedious to enter into a phone manually. To get around this, we can expose a QR code to the user to make it easier to add to the authenticator application.

Install Package

First we need to install our QR code generator package.

composer require bacon/bacon-qr-code

Generate QR Code

<?php

# Include packages
require_once(__DIR__ . '/vendor/autoload.php');

# Create the 2FA class
$google2fa = new PragmaRX\Google2FA\Google2FA();

# Create data that will go into the QR code
# The title will show up first in the authenticator app, 
# followed by the username wrapped in `()`
$title = "blog.programster.org";
$usernameOrEmail = "admin@programster.org";
$userSecret = fetchUserSecretFromPersistenceStore();

$qrCodeData = $google2fa->getQRCodeUrl(
    $title,
    $usernameOrEmail,
    $userSecret
);

print "QR code url is: $qrCodeUrl" . PHP_EOL;

# Now create the QR code image from the URL.
$renderer = new \BaconQrCode\Renderer\ImageRenderer(
    new \BaconQrCode\Renderer\RendererStyle\RendererStyle(400),
    new \BaconQrCode\Renderer\Image\ImagickImageBackEnd()
);

$writer = new BaconQrCode\Writer($renderer);
$writer->writeFile($qrCodeData, 'qrcode.png');

print "Please open qrcode.png and scan it with your google authenticator app." . PHP_EOL;

Debugging

If you get the message:

PHP Fatal error:  Uncaught BaconQrCode\Exception\RuntimeException: You need to install the imagick extension to use this back end

Then you just need to make sure to install the php8.0-imagick package.

Method 2

I'm including this method as I bothered to go to the effort of learning it before realizing the google2fa package was TOTP under the hood anyway, but having two libraries that achieve the same effect is useful, in case there is some niche requirement that one doesn't cover.

Install the required package with:

composer require mincdev/php-otpauth

Below is a really short codebase whereby we create the secret, and generate the QR code in base64 string form and then display it in a webpage:

<?php

use MincDev\OtpAuth\OtpAuthenticator;
require_once(__DIR__ . '/vendor/autoload.php');

$otpAuth = new OtpAuthenticator();
$userName = "usernameHere";
$appName = "My Website Name";
$userSecret = $otpAuth->newSecret();
$qrBase64 = $otpAuth->getQR($userName, $appName, $userSecret);
?>

<!DOCTYPE html>
<html>
    <head>
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    </head>
    <body>
        <h2>QR Code</h2>
        <img src="data:image/png;base64, <?= $qrBase64; ?>">
        <h2>Secret</h2>
        <pre><?= $userSecret; ?></pre>
    </body>
</html>

If you want to create a PNG image file locally instead like the previous method, then you just need to base64 decode the image string and write that content to a file. E.g.

$content = base64_decode($qrBase64);
$pngFile = fopen('output.png', "wb");
fwrite($pngFile, $content); 
fclose($pngFile);

Conclusion

You should now have enough to get started with setting up 2FA in your application. This tutorial was aimed at getting you started quickly, but if you are using the Google 2FA library, you may wish to read up on how you can take it further by:

References

Last updated: 31st May 2024
First published: 23rd March 2021