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
| Approach | Key distinction |
|---|---|
| Implementing Two-Factor Authentication with TOTP | Generate time-based one-time passwords from a shared secret and short time windows for a second authentication factor. |
| Related technique | TOTP is vulnerable to phishing; passkeys with phishing-resistant authentication are stronger. |
Common Beginner Mistakes
| Mistake | Correct approach |
|---|---|
| Confusing this operation with a neighboring cryptographic or security mechanism | TOTP is vulnerable to phishing; passkeys with phishing-resistant authentication are stronger. |
| Ignoring a security prerequisite or failure case | Encrypt 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.