php 57 lines · 7 steps

How TOTP codes are generated in PHP

Turn a shared base32 secret and the current time into the same six-digit code an authenticator app shows.

Explained by highlit
1<?php
2 
3namespace App\Support;
4 
5final class Totp
6{
7 private const ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
8 
9 public function __construct(
10 private readonly int $digits = 6,
11 private readonly int $period = 30,
12 private readonly string $algorithm = 'sha1',
13 ) {}
14 
15 public function code(string $base32Secret, ?int $timestamp = null): string
16 {
17 $counter = intdiv($timestamp ?? time(), $this->period);
18 $binary = pack('N*', 0) . pack('N*', $counter);
19 
20 $hash = hash_hmac($this->algorithm, $binary, $this->decodeBase32($base32Secret), true);
21 $offset = ord($hash[strlen($hash) - 1]) & 0x0F;
22 
23 $truncated = (
24 ((ord($hash[$offset]) & 0x7F) << 24) |
25 ((ord($hash[$offset + 1]) & 0xFF) << 16) |
26 ((ord($hash[$offset + 2]) & 0xFF) << 8) |
27 (ord($hash[$offset + 3]) & 0xFF)
28 );
29 
30 $code = $truncated % (10 ** $this->digits);
31 
32 return str_pad((string) $code, $this->digits, '0', STR_PAD_LEFT);
33 }
34 
35 private function decodeBase32(string $secret): string
36 {
37 $secret = strtoupper(rtrim($secret, '='));
38 $bits = '';
39 
40 foreach (str_split($secret) as $char) {
41 $index = strpos(self::ALPHABET, $char);
42 if ($index === false) {
43 throw new \InvalidArgumentException("Invalid base32 character: {$char}");
44 }
45 $bits .= str_pad(decbin($index), 5, '0', STR_PAD_LEFT);
46 }
47 
48 $bytes = '';
49 foreach (str_split($bits, 8) as $chunk) {
50 if (strlen($chunk) === 8) {
51 $bytes .= chr(bindec($chunk));
52 }
53 }
54 
55 return $bytes;
56 }
57}
01 / 01
STEP 01

Walkthrough

Space play step click any line
Three takeaways
  1. 1TOTP is just HMAC over a time counter, so client and server agree by sharing a secret and a clock.
  2. 2Dynamic truncation extracts a stable 31-bit integer from the hash using its own last nibble as an offset.
  3. 3Base32 decoding is a manual bit-buffer: accumulate 5 bits per character, then slice into 8-bit bytes.

Related explainers

Share this explainer

Here's the card — post it anywhere.

How TOTP codes are generated in PHP — share card
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code