Two-Factor Authentication with TOTP: A Complete PHP Implementation Guide
Time-based one-time passwords (TOTP) are the six-digit codes generated by authenticator apps such as Google Authenticator, Authy, and 1Password. They represent the most widely supported second-factor authentication method available today, work completely offline, and do not require a phone number or third-party push notification service. This guide covers the full 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 2FA, which can be intercepted through SS7 vulnerabilities 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 significantly more resistant to interception than any method that involves a server sending a code to the user over a communications network.
Understanding the TOTP Algorithm
TOTP follows the IETF standard defined in RFC 6238. The server and the authenticator app on the user's phone 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 used 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 gracefully by accepting codes at the current time step plus or minus one step on either side, providing a ±30 second tolerance window.
Why TOTP Is More Secure Than SMS
SMS-based authentication relies on the mobile network infrastructure to deliver codes. This creates several attack vectors that TOTP avoids entirely. SS7 vulnerabilities allow attackers to intercept text messages by exploiting signalling protocols in telecom networks. SIM swapping attacks convince mobile carriers to transfer a phone number to a different SIM card under the attacker's 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.
PHP Implementation: Generating TOTP Codes
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 RFC 6238 specifications.
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 same encoding is used when generating the secret initially and when displaying it for manual entry.
Generating and Storing 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, though it could theoretically be used to impersonate the server during the initial setup phase. Consider using bcrypt or Argon2id for hashing the secret, similar to how you would hash passwords.
Presenting the QR Code for 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:
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 ±1 time step to handle minor clock drift between the server and the user's device.
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');
}
}
Rate Limiting TOTP 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 (six digits) within a reasonable timeframe 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 three 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.
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.
Important 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.
- 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.
When to Consider 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.
Next Steps for Secure Authentication
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.
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.