Two-Factor Auth: TOTP in PHP Setup Guide

12 min read 2,392 words
Two-Factor Authentication with TOTP: Building It in PHP Without a Library featured image

Two-Factor Authentication with TOTP: A Complete PHP Implementation Guide

Time-based one-time passwords (TOTP) are the six-digit codes generated by authenticator applications such as Google Authenticator, Authy, and 1Password. They represent one of the most widely supported second-factor authentication methods available, working entirely offline without requiring a phone number or third-party push notification service. This guide walks through the complete implementation process: generating secrets securely, presenting QR codes for app setup, verifying codes during login, and handling recovery when users lose access to their authenticator device.

Unlike SMS-based two-factor authentication, which can be intercepted through vulnerabilities in telecom signalling protocols or SIM swapping attacks, TOTP codes are generated locally on the user's device and change every 30 seconds. The shared secret never travels over the network after the initial setup phase. This architecture makes TOTP considerably more resistant to interception than any method that relies on a server sending a code to the user over a communications network.

How the TOTP Algorithm Works

TOTP follows the standard defined in RFC 6238. The server and the authenticator app share a Base32-encoded secret, typically 160 bits or 32 characters. Both sides independently calculate the current time step as the number of 30-second intervals since the Unix epoch in UTC. The formula is straightforward: T equals the floor of the current Unix timestamp divided by 30.

The shared secret and the current time step are combined using HMAC-SHA1, producing a 20-byte hash. A dynamic truncation step extracts a 6-digit number from this hash. The same process runs on the phone simultaneously, and because both sides possess the same secret and approximately the same clock, they produce matching codes. Clock skew between the server and the phone is handled by accepting codes at the current time step plus or minus one step on either side, providing a tolerance window of roughly 90 seconds.

Why TOTP Outperforms SMS Authentication

SMS-based authentication relies on mobile network infrastructure to deliver codes, creating several attack vectors that TOTP avoids entirely. Signalling protocol vulnerabilities can allow attackers to intercept text messages by exploiting weaknesses in telecom networks. SIM swapping attacks convince mobile carriers to transfer a phone number to a different SIM card under attacker control. With TOTP, there is no message to intercept because the code is generated mathematically on the device using a shared secret that never leaves the device during authentication.

Building the TOTP Generation Functions in PHP

The core TOTP generation function takes the Base32 secret and an optional time step parameter. It packs the time counter into 8 bytes using big-endian byte order, computes the HMAC-SHA1 hash, and extracts a 6-digit code using the dynamic truncation offset from the last byte of the hash. This implementation adheres strictly to the RFC 6238 specification and produces codes compatible with Google Authenticator, Authy, and other standard authenticator applications.

function totp_generate(string $secret, int $timeStep = null): string {
    $timeStep = $timeStep ?? (int)(time() / 30);
    $secretBin = base32_decode($secret);
    $time = pack('N*', 0) . pack('N*', $timeStep);
    $hash = hash_hmac('sha1', $time, $secretBin, true);
    $offset = ord($hash[19]) & 0xf;
    $code = (
        ((ord($hash[$offset++]) & 0x7f) << 24) |
        ((ord($hash[$offset++]) & 0xff) << 16) |
        ((ord($hash[$offset++]) & 0xff) << 8)  |
        (ord($hash[$offset++]) & 0xff)
    ) % 1000000;
    return str_pad((string)$code, 6, '0', STR_PAD_LEFT);
}

function base32_decode(string $input): string {
    $chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
    $input = strtoupper(preg_replace('/[^A-Z2-7]/', '', $input));
    $output = '';
    $buffer = 0;
    $bitsLeft = 0;
    foreach (str_split($input) as $char) {
        $buffer = ($buffer << 5) | strpos($chars, $char);
        $bitsLeft += 5;
        if ($bitsLeft >= 8) {
            $output .= chr(($buffer >> ($bitsLeft - 8)) & 0xff);
            $bitsLeft -= 8;
        }
    }
    return $output;
}

The Base32 decoding function handles the character mapping used by standard authenticator applications. It strips any invalid characters, converts to uppercase, and processes each character in 5-bit chunks to produce the raw binary secret. This encoding is used both when generating the secret initially and when displaying it for manual entry.

Generating and Storing TOTP Secrets Securely

Generate a new secret for each user when they enable two-factor authentication. Use random_bytes(20) to generate 20 cryptographically secure random bytes, then encode the result as Base32 for display and storage. The secret should be associated with the user record in your database.

Critical security practice: store a hash of the secret rather than the plaintext secret itself. If your database is compromised, the hash cannot be used to generate valid TOTP codes. Consider using bcrypt or Argon2id for hashing the secret, similar to how you would handle password storage. The approach to BCrypt cost factor selection for PHP password hashing applies here as well, with higher cost factors providing better protection at the expense of slower verification.

Presenting the QR Code for Initial Setup

When a user enables TOTP, generate the secret and present it along with a QR code. The QR code encodes an otpauth:// URI that authenticator applications can scan to import the account automatically. The URI format follows the Google Authenticator specification and works across all major authenticator apps.

use Endroid\QrCode\QrCode;

$secret = base32_encode(random_bytes(20));
$issuer = urlencode('YourServiceName');
$account = urlencode($user->email);
$uri = "otpauth://totp/{$issuer}:{$account}?secret={$secret}&issuer={$issuer}&algorithm=SHA1&digits=6&period=30";

$qr = new QrCode($uri);
header('Content-Type: ' . $qr->getContentType());
echo $qr->writeString();

For QR code generation in PHP, popular libraries include endroid/qr-code and bacon/bacon-qr-code. Both support the standard URI format and produce compatible QR codes that work with Google Authenticator, Authy, and other RFC 6238-compliant applications. Always serve the QR code page over HTTPS to prevent man-in-the-middle interception during setup.

Verifying TOTP Codes During Login

When the user submits a login form containing a TOTP code, verify it by generating the expected code from the stored secret and comparing the two values. Allow a verification window of plus or minus one time step to handle minor clock drift between the server and the user's device. This tolerance window is standard practice and prevents legitimate users from being locked out due to minor timing differences.

function totp_verify(string $secret, string $code, int $window = 1): bool {
    $currentTimeStep = (int)(time() / 30);
    for ($offset = -$window; $offset <= $window; $offset++) {
        if (totp_generate($secret, $currentTimeStep + $offset) === $code) {
            return true;
        }
    }
    return false;
}

// In your login handler:
$user = find_user_by_email($_POST['email']);
if ($user && $user->totp_secret_hash) {
    if (!totp_verify(decrypt_secret($user->totp_secret_hash), $_POST['totp_code'])) {
        flash_error('Invalid authentication code.');
        redirect('/login');
    }
}

The verification function iterates through the acceptable time window and returns true as soon as it finds a matching code. This approach is efficient and handles the standard tolerance window without requiring multiple separate checks.

Rate Limiting TOTP Verification Attempts

Rate limiting is essential for TOTP verification. An attacker with physical access to the phone could theoretically attempt all 1,000,000 possible codes if there is no throttling in place. Track failed attempts per account and temporarily lock the account after a small number of consecutive failures. Use a fixed time window for the failure counter rather than a sliding window to prevent attackers from distributing guesses across many tiny time periods.

Implement exponential backoff for repeated failures, and consider requiring a CAPTCHA after several failed attempts to deter automated attacks. Log all verification failures with timestamps and IP addresses for security auditing purposes, while being mindful of data protection requirements. Combining CSRF token implementation in PHP with your login form adds another layer of protection against automated attack vectors.

Recovery Codes: Planning for Lost Devices

Users will inevitably lose their phone or delete their authenticator application. Without a recovery mechanism, they would be permanently locked out of their accounts. Provide a set of one-time recovery codes when TOTP is first enabled. Generate 10 random 8-character codes, hash and store them securely, and present the plaintext codes to the user exactly once for them to store safely offline.

$recoveryCodes = [];
for ($i = 0; $i < 10; $i++) {
    $code = strtoupper(bin2hex(random_bytes(4))) . '-' . strtoupper(bin2hex(random_bytes(4)));
    $recoveryCodes[$code] = password_hash($code, PASSWORD_DEFAULT);
}
// Store $recoveryCodes array hashed in user record
// Show $recoveryCodes plaintext to user ONCE to write down

Each recovery code can only be used once. After all recovery codes are exhausted, require the user to set up two-factor authentication again before they can access their account. Consider notifying users via email when they use a recovery code, so they know their authenticator may have been compromised or lost.

Essential Security Considerations

Implementing TOTP correctly requires attention to several security details beyond the core algorithm. The secret generation, storage, and transmission all need careful handling to maintain the security benefits that TOTP provides.

  • Secret storage: Never store TOTP secrets in plaintext. Use encryption at rest with keys managed separately from the database. If you handle sensitive data, a webhook receiver setup in PHP demonstrates similar patterns for secure data handling.
  • Setup transmission: Serve QR codes and setup pages exclusively over HTTPS to prevent interception during the initial enrollment process.
  • Backup verification: After enabling TOTP, test the verification process with a different device or browser to confirm everything works before relying on it.
  • Account recovery process: Establish a secure account recovery path that does not bypass TOTP entirely, such as identity verification through supporting documents.
  • Session management: Consider how long TOTP enrollment lasts before requiring re-authentication, and implement appropriate session timeout policies.

Integrating TOTP with Existing Authentication Systems

TOTP is designed to work as a second factor alongside your existing authentication system. The typical flow checks the password first, then prompts for the TOTP code if password verification succeeds. Never skip password verification when TOTP is enabled, as this would reduce the security to a single factor. The password and TOTP code together form the two factors required for two-factor authentication.

For WordPress sites, the WordPress two-factor authentication setup guide covers how to add TOTP protection using established plugins without custom development. For custom PHP applications, the implementation described in this guide provides full control over the authentication flow and secret management.

When to Seek Professional Help

Two-factor authentication is a critical security component, and implementation mistakes can leave users locked out or create security vulnerabilities. If you are working with legacy PHP applications without proper password hashing infrastructure, or if your authentication system has custom modifications that complicate standard implementations, it may be worth consulting an IT specialist who has experience with secure authentication patterns.

For businesses implementing two-factor authentication across multiple systems, a structured approach to security implementation helps ensure consistency. An IT specialist can review your existing authentication flow, identify gaps, and implement TOTP in a way that works reliably with your current setup.

Building a Complete Two-Factor Authentication Solution

Implementing TOTP correctly adds a strong layer of security to your authentication system, but it is one component of a broader security posture. Regularly review your authentication logs for unusual patterns, keep your PHP version and dependencies updated, and consider additional hardening measures such as IP-based login notifications or device fingerprinting.

The implementation covered in this guide provides the foundation for adding time-based one-time password protection to any PHP application. Each element from secret generation through recovery code handling plays a role in creating a complete solution that users can rely on for securing their accounts.

If you need help reviewing your current authentication implementation or want to add two-factor authentication to your PHP application, prepare details about your current login system, user database structure, and any existing security measures before getting in touch. A practical review of your setup can identify areas for improvement and ensure the implementation is robust.

Frequently Asked Questions

Should I use TOTP or push notification services like Authy or Duo?
TOTP using authenticator applications is generally the better default for most web applications. It does not require internet connectivity on the phone to generate codes, does not depend on a third-party service being available at authentication time, and is more resistant to phishing because the code is generated locally rather than received from a server.
How do I handle TOTP for users who do not have a smartphone?
Hardware TOTP tokens such as YubiKey with OATH-HOTP/TOTP support or TOTP-compatible RSA SecurID tokens work with the same RFC 6238 standard used by authenticator apps. Generate the same otpauth:// URI and present it as a manual entry key rather than a QR code. Users enter the Base32 secret manually into their hardware token. For users without any compatible device, consider providing multiple recovery codes and a secure identity verification process for account recovery.
Can TOTP secrets be stolen from the server?
Yes, if your database is compromised and the secrets are stored without proper protection, an attacker can generate valid codes for any account. Always store a hash of the TOTP secret rather than the secret itself. The hash should use a one-way function like bcrypt or Argon2id. Additionally, protect against man-in-the-middle attacks during the TOTP setup phase by serving the QR code page over HTTPS only and implementing HSTS headers.
What happens if there is clock drift between the server and the phone?
TOTP tolerates minor clock differences by accepting codes within a verification window. The standard approach accepts the current time step plus or minus one step, which covers roughly 90 seconds. For most deployments, this handles the typical clock differences between servers and mobile devices. If clock drift is severe, consider using an NTP time source on your server to maintain accurate time, and investigate whether the user's device clock is set correctly.
How many recovery codes should I generate?
Ten recovery codes is a common standard that balances convenience with security. Fewer codes mean users may run out during an emergency and need to go through account recovery. More codes create a larger attack surface if the codes are compromised. Each code should be long enough to make brute-forcing impractical. Eight characters using hexadecimal characters provides approximately 4.3 billion possible combinations per code.
Does TOTP replacement affect existing user sessions?
When a user enables TOTP, it typically applies to subsequent login attempts only. Existing active sessions remain valid until they expire or are terminated through logout. When disabling TOTP or regenerating secrets, you should invalidate all existing sessions to ensure the old credentials cannot be used.