A zero-dependency TOTP (Time-based One-Time Password) library for Node.js. It implements the RFC 4226/6238 code calculation and generates otpauth:// setup URIs. Full verifier compliance requires application-managed replay protection, attempt throttling, and secure per-account secret storage.
- RFC 4226/6238 code calculation, with an API for replay-safe verification
- Zero external dependencies
- Timing attack resistant token comparison
- TypeScript definitions included
- Tested against RFC vectors
- Strict input validation and safe defaults
npm install @sourceregistry/node-totpimport { generateURI, verifyToken } from '@sourceregistry/node-totp';
// Generate setup URI for authenticator apps
const { uri, secret } = generateURI({
issuer: 'MyApp',
account: 'user@example.com',
digits: 6,
period: 30
});
console.log('Scan this URI in your authenticator app:', uri);
// otpauth://totp/MyApp:user%40example.com?issuer=MyApp&secret=...
// Later, check user input (stateless; see verifyTokenOnce for login flows)
const userInput = '123456';
const isValid = verifyToken(userInput, secret, {
digits: 6,
period: 30,
window: 1 // Accept tokens from +/-30 seconds
});
console.log('Token valid:', isValid);import totp from '@sourceregistry/node-totp';
// Generate with custom secret length (algorithm-appropriate defaults)
const { secret } = totp.generateURI({
issuer: 'SecureApp',
account: 'admin@secureapp.com',
algorithm: 'SHA512', // Uses 64-byte secret by default
byteLength: 48 // Override default secret length
});
// Verify with custom time (useful for testing)
const testTime = Math.floor(Date.now() / 1000);
const testToken = totp.generateToken(
totp.base32.decode(secret),
Math.floor(testTime / 30),
6,
'SHA512'
);
const isValid = totp.verifyToken(testToken, secret, {
algorithm: 'SHA512',
digits: 6,
period: 30,
window: 2,
now: testTime // Use specific timestamp instead of Date.now()
});Generates an otpauth:// URI and a secret for TOTP setup.
issuer(string, required): Service name such asMyApp; cannot contain a colon or control charactersaccount(string, required): User identifier such as an email address orAlice Smith; cannot contain a colon or control characterssecret(string, optional): Canonical unpadded Base32 secret containing at least 16 decoded bytesalgorithm(string, optional):SHA1,SHA256, orSHA512(defaultSHA1); casing is normalized at runtimedigits(number, optional):6or8forotpauth://compatibility (default6)period(number, optional): Time step in seconds (default30)byteLength(number, optional): Generated secret length in bytes (minimum16; defaults to20,32, or64for SHA1, SHA256, or SHA512)
Returns { uri: string, secret: string }.
Checks a TOTP token against a secret without tracking prior successful uses. Use verifyTokenOnce for authentication.
token(string): User-provided token with 6 to 8 digitssecret(string): Canonical unpadded Base32 secretoptions.window(number): Time steps to check on either side of the current step (0to2; default1, meaning +/-30 seconds with the default period)options.period(number): Time step in seconds (default30)options.algorithm(string): Hash algorithm (defaultSHA1)options.digits(number): Expected token length (default6)options.now(number): Unix timestamp in seconds, useful for testing
Returns boolean.
Checks a token and passes its matched time-step counter to claimCounter. Returns Promise<boolean> and accepts the token only when the callback returns true. It rejects secrets shorter than 16 bytes.
The callback must atomically advance a durable, per-account lastAcceptedCounter only if the matched counter is greater than the stored value. Return true only when the update succeeds. A database conditional update or transaction can provide this guarantee across concurrent requests and server instances. An in-memory set cannot protect a multi-instance deployment or survive a restart.
import { verifyTokenOnce } from '@sourceregistry/node-totp';
const accepted = await verifyTokenOnce(token, user.totpSecret, counter =>
db.claimTotpCounter(user.id, counter) // App-defined atomic conditional update
);options has the same shape and defaults as verifyToken. Rate-limit attempts per account before calling either verifier.
Generates a TOTP token.
secret(Buffer): Decoded secret buffercounter(number): Time step counterdigits(number): Token length (6,7, or8)algorithm(string): Hash algorithm (SHA1,SHA256, orSHA512)
Returns a zero-padded string token.
RFC 4648 Base32 helpers:
base32.encode(buffer: Buffer): stringbase32.decode(base32Str: string): Buffer
The library accepts canonical unpadded Base32 secrets. Padded or non-canonical forms are normalized or rejected before use.
- Token comparison uses
crypto.timingSafeEqual() - Supported algorithms are limited to
SHA1,SHA256, andSHA512 - The HOTP/TOTP calculation supports
6,7, or8digits; setup URIs support6or8as specified by the published key URI format - Verification windows are limited to
0,1, or2steps - Period must be a positive integer
- New secrets must contain at least 16 bytes; generated secrets use algorithm-appropriate byte lengths by default
- Supplied secrets must also be randomly generated; length alone does not make a predictable secret secure
- The stateless
verifyTokenstill accepts shorter existing secrets for migration;verifyTokenOncerequires at least 16 bytes - Give every enrolled account a distinct random secret and protect secrets in storage
- Enforce attempt throttling across login sessions; the library has no account or attempt store
- Use
verifyTokenOncewith a durable atomic counter claim to prevent code reuse - Authenticator support for non-default algorithms and periods varies; test the chosen settings with the client apps you support
npm test
npm run test:coverageimport qrcode from 'qrcode';
import { generateURI } from '@sourceregistry/node-totp';
const { uri } = generateURI({
issuer: 'MyApp',
account: 'user@example.com'
});
const qrDataUrl = await qrcode.toDataURL(uri);
// Display qrDataUrl in your HTML templateFor a SQL-backed application, db.claimTotpCounter(userId, counter) can run a conditional update and return true only when one row changed:
UPDATE user_totp
SET last_accepted_counter = :counter
WHERE user_id = :user_id
AND (last_accepted_counter IS NULL OR last_accepted_counter < :counter);Keep the secret and last_accepted_counter per account. Enforce attempt throttling across sessions before calling verifyTokenOnce, and activate a new enrollment only after the user proves possession of the generated secret.
Reset the stored counter atomically when rotating that account's secret.
Apache-2.0 © A.P.A. Slaa
Note: This library is designed for server-side Node.js applications. For browser usage, consider using a Web Crypto API compatible alternative.