TOTP Code Generator (RFC 6238)
Generate and verify time-based one-time passwords from base32 secrets, with configurable digits, period and HMAC algorithm.
Code
import { createHmac, timingSafeEqual } from "node:crypto";
export interface TotpOptions {
digits?: number;
period?: number;
algorithm?: "sha1" | "sha256" | "sha512";
}
export function base32Decode(secret: string): Buffer {
const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
const clean = secret.toUpperCase().replace(/[\s=-]/g, "");
let bits = 0;
let value = 0;
const bytes: number[] = [];
for (const character of clean) {
const index = alphabet.indexOf(character);
if (index === -1) throw new TypeError("invalid base32 character: " + character);
value = (value << 5) | index;
bits += 5;
if (bits >= 8) {
bytes.push((value >>> (bits - 8)) & 0xff);
bits -= 8;
}
}
return Buffer.from(bytes);
}
export function generateTotp(secret: string, timestamp = Date.now(), options: TotpOptions = {}): string {
const { digits = 6, period = 30, algorithm = "sha1" } = options;
if (!Number.isInteger(digits) || digits < 6 || digits > 10) {
throw new RangeError("digits must be an integer between 6 and 10");
}
if (!Number.isInteger(period) || period <= 0) throw new RangeError("period must be a positive integer");
const counter = Math.floor(timestamp / 1000 / period);
const buffer = Buffer.alloc(8);
buffer.writeBigUInt64BE(BigInt(counter));
const hmac = createHmac(algorithm, base32Decode(secret)).update(buffer).digest();
const offset = hmac[hmac.length - 1]! & 0x0f;
const binary = ((hmac[offset]! & 0x7f) << 24)
| (hmac[offset + 1]! << 16)
| (hmac[offset + 2]! << 8)
| hmac[offset + 3]!;
return String(binary % 10 ** digits).padStart(digits, "0");
}
export function verifyTotp(
secret: string,
token: string,
timestamp = Date.now(),
window = 1,
options: TotpOptions = {},
): boolean {
if (!/^\d+$/.test(token)) return false;
if (!Number.isInteger(window) || window < 0) {
throw new RangeError("window must be a non-negative integer");
}
const periodSeconds = options.period ?? 30;
for (let offset = -window; offset <= window; offset += 1) {
const candidate = generateTotp(secret, timestamp + offset * periodSeconds * 1000, options);
if (candidate.length === token.length && timingSafeEqual(Buffer.from(candidate), Buffer.from(token))) {
return true;
}
}
return false;
}Report a problem with this exampleOpens a pre-filled issue on GitHub.