stable

totp

RFC 6238 TOTP authenticator-app codes — zero-dependency secret generation, otpauth:// provisioning URIs, constant-time verification.

@noy-db/on-totp · pnpm add @noy-db/on-totp Authenticator-app second factor (RFC 6238).

What it does

generateTotpSecret() returns a random 20-byte Base32 secret (the RFC 4226 recommended minimum). totpProvisioningUri(secret, { account, issuer, digits, period, algorithm }) builds a standard otpauth://totp/ URI for QR-code scanning into any authenticator app. generateTotpCode(secret, options) computes the current code; verifyTotp(secret, code, options) checks a user-entered code with constant-time comparison and a configurable clock-drift window (default ±1 step) to tolerate authenticator/server skew. Defaults: 6 digits, 30s period, SHA1 (for compatibility with Google Authenticator and similar apps). Zero dependencies — HMAC via Web Crypto, validated against the official RFC test vectors.

Usage

import { generateTotpSecret, totpProvisioningUri, verifyTotp } from '@noy-db/on-totp'

const secret = generateTotpSecret()
const uri = totpProvisioningUri(secret, { account: 'alice@acme.com', issuer: 'Acme Notes' })
// show `uri` as a QR code

const ok = await verifyTotp(secret, userEnteredCode) // constant-time, ±1 step window

⛔ Replay is the caller's to prevent. Without it, one code verifies for the whole drift window (~90 s) — anyone who sees it can reuse it. Since 0.10.0-pre.2, verifyTotpStep returns the step a code matched (or null), and the lastUsedStep option on both verifiers refuses that step and every earlier one (RFC 6238 §5.2). The package stays stateless, so you persist the step — per secret, somewhere the party presenting codes cannot rewind it:

import { verifyTotpStep } from '@noy-db/on-totp'

const step = await verifyTotpStep(secret, userEnteredCode, { lastUsedStep })
if (step === null) throw new Error('bad or reused code')
lastUsedStep = step // store it before treating the factor as passed

See also