Content · Web Development · PHP

Implementing Two-Factor Authentication with TOTP

Generate time-based one-time passwords from a shared secret and short time windows for a second authentication.

1 views Published Oct 17, 2026 Updated Oct 17, 2026 admin

Generate time-based one-time passwords from a shared secret and short time windows for a second authentication factor.

The examples introduce the underlying control before showing a verification and an application-level safeguard.

What You Will Build


Generate time-based one-time passwords from a shared secret and short time windows for a second authentication factor.

Prerequisites


Use PHP 8.4 or newer, HTTPS for all authentication operations, and an application session or database where required. Example configuration values are placeholders, never production secrets.

Example 1: Basic Operation


Generate a six-digit TOTP from a securely generated 20-byte secret.

<?php
function totp(string $secret, int $step): string {
    $counter = intdiv($step, 30);
    $binary = pack('N2', ($counter >> 32) & 0xffffffff, $counter & 0xffffffff);
    $hmac = hash_hmac('sha1', $binary, $secret, true);
    $offset = ord($hmac[19]) & 15;
    $value = unpack('N', substr($hmac, $offset, 4))[1] & 0x7fffffff;
    return str_pad((string) ($value % 1000000), 6, '0', STR_PAD_LEFT);
}
$secret = random_bytes(20);
echo totp($secret, time());

Example 2: A Different Use Case


Prevent replay: track the most recently accepted time step for each account.

<?php
function codeForCounter(string $secret, int $counter): string {
    $mac = hash_hmac('sha1', pack('N2', ($counter >> 32) & 0xffffffff, $counter & 0xffffffff), $secret, true);
    $offset = ord($mac[19]) & 15;
    $number = unpack('N', substr($mac, $offset, 4))[1] & 0x7fffffff;
    return str_pad((string) ($number % 1000000), 6, '0', STR_PAD_LEFT);
}
$secret = random_bytes(20);
$current = intdiv(time(), 30);
$submitted = codeForCounter($secret, $current); // Replace with validated user input.
$lastAccepted = -1; // Read the last accepted counter from persistent storage.
$matched = null;
foreach ([$current - 1, $current, $current + 1] as $counter) {
    if ($counter > $lastAccepted && hash_equals(codeForCounter($secret, $counter), $submitted)) {
        $matched = $counter;
        break;
    }
}
var_dump($matched !== null);
// Atomically persist $matched as the latest accepted counter to prevent replay.

Practical Example: Checking the Result


Add brute-force protection and secure enrollment to the verification flow.

<?php
$maxAttempts = 5;
$failures = 0;
if ($failures >= $maxAttempts) { http_response_code(429); exit; }
// Real installations need per-account rate limits, secure secret storage,
// carefully controlled recovery codes, and a tested TOTP library.

Important Behavior and Edge Cases


Encrypt stored TOTP secrets, prevent accepted-code replay within its window and rate-limit failed attempts.

Enforce controls on the server, test denial paths and do not assume that a snippet alone implements an entire production workflow.

Comparison with Related Functions


ApproachKey distinction
Implementing Two-Factor Authentication with TOTPGenerate time-based one-time passwords from a shared secret and short time windows for a second authentication factor.
Related techniqueTOTP is vulnerable to phishing; passkeys with phishing-resistant authentication are stronger.

Common Beginner Mistakes


MistakeCorrect approach
Confusing this operation with a neighboring cryptographic or security mechanismTOTP is vulnerable to phishing; passkeys with phishing-resistant authentication are stronger.
Ignoring a security prerequisite or failure caseEncrypt stored TOTP secrets, prevent accepted-code replay within its window and rate-limit failed attempts.

Key Takeaways


  • Generate time-based one-time passwords from a shared secret and short time windows for a second authentication factor.
  • Encrypt stored TOTP secrets, prevent accepted-code replay within its window and rate-limit failed attempts.
  • TOTP is vulnerable to phishing; passkeys with phishing-resistant authentication are stronger.

Apply this behavior to a real application only after confirming the required extension, configuration and failure handling on the deployment runtime.

Share this resourceSend this page to someone who may find it useful.