2fa-lib
v0.1.2
Published
Tiny zero-dependency TOTP and HOTP library for Node.js, browsers, Deno, Bun, Cloudflare Workers and edge runtimes. RFC 6238 / RFC 4226 compliant. Google Authenticator compatible two-factor authentication (2FA / MFA) for JavaScript and TypeScript.
Maintainers
Keywords
Readme
2fa-lib
Tiny, zero-dependency TOTP & HOTP library for JavaScript and TypeScript
Two-Factor Authentication (2FA / MFA) made simple. Works with Google Authenticator, Microsoft Authenticator, Authy, 1Password, Bitwarden, Duo, and any RFC 6238 compatible app.
Install • Quick Start • API • Examples • Compatibility • Why 2fa-lib?
✨ Features
- 🪶 Tiny — under 5 KB minified, single file
- 🚫 Zero dependencies — uses native Web Crypto API
- 🌍 Universal — Node.js 18+, browsers, Deno, Bun, Cloudflare Workers, Vercel Edge, Netlify Edge
- 🔒 Secure — constant-time comparison, cryptographically secure secrets
- 📘 TypeScript — full type definitions included, no
@typespackage needed - ✅ RFC compliant — RFC 6238 (TOTP) and RFC 4226 (HOTP), tested against official vectors
- 📱 Universal app support — Google Authenticator, Microsoft Authenticator, Authy, 1Password, Bitwarden, Duo, FreeOTP
- 🛡️ Built-in security helpers — rate limiter, replay guard, hashed backup codes
- 🔄 QR code URI — generates
otpauth://URIs for any QR library - 🎮 Steam Guard — Steam's 5-character format supported
- 📥 Import from QR — parse
otpauth://URIs from other apps - 💎 ESM + CJS — works with
importandrequire - 🆕 Backup codes — generate + securely hash recovery codes
📦 Installation
npm install 2fa-libyarn add 2fa-libpnpm add 2fa-libbun add 2fa-lib🚀 Quick Start
import { generateSecret, totp, verify, buildURI } from '2fa-lib';
// 1. Create a secret for the user (store it encrypted in your DB)
const secret = generateSecret();
// 2. Build a provisioning URI → render as QR code in your UI
const uri = buildURI({
secret,
label: '[email protected]',
issuer: 'MyApp',
});
// 3. Generate the current 6-digit code
const code = await totp(secret);
console.log(code); // → "123456"
// 4. Verify a user-submitted code (allows ±1 step clock drift)
const result = await verify('123456', secret, { window: 1 });
if (result) {
console.log('✅ Valid! Drift:', result.delta);
} else {
console.log('❌ Invalid code');
}📘 TypeScript
Full TypeScript types are bundled — no extra @types/2fa-lib package required.
import { totp, verify, type VerifyResult } from '2fa-lib';
const code: string = await totp('JBSWY3DPEHPK3PXP');
const result: VerifyResult | null = await verify(code, 'JBSWY3DPEHPK3PXP');📱 Authenticator App Compatibility
| App | Status | Notes |
|---|---|---|
| Google Authenticator | ✅ Full | All algorithms supported |
| Microsoft Authenticator | ✅ Full | Use microsoftAuthenticatorURI() (forces SHA1/6/30) |
| Authy | ✅ Full | All algorithms supported |
| 1Password | ✅ Full | All algorithms supported |
| Bitwarden | ✅ Full | All algorithms supported |
| Duo Mobile | ✅ Full | Standard TOTP |
| FreeOTP / FreeOTP+ | ✅ Full | All algorithms supported |
| Steam Guard | ✅ Full | Use steamTOTP() for 5-char codes |
⚠️ Microsoft Authenticator silently ignores
algorithm,digits, andperiodURI parameters — it always uses SHA1, 6 digits, 30 seconds. Use the dedicatedmicrosoftAuthenticatorURI()helper to guarantee compatibility.
📚 API
Core
totp(secret, options?)
Generate a time-based one-time password (TOTP).
await totp('JBSWY3DPEHPK3PXP');
await totp(secret, { digits: 8, period: 60, algorithm: 'SHA256' });hotp(secret, counter, options?)
Generate an HMAC-based one-time password (HOTP).
await hotp('JBSWY3DPEHPK3PXP', 0); // → "755224"verify(token, secret, options?)
Verify a token. Returns { delta } or null. The window option allows ±N steps of clock drift (default 1 = ±30s).
const result = await verify('123456', secret, { window: 1 });
if (result) console.log('Valid, drift:', result.delta);generateSecret(length?)
Generate a cryptographically secure base32-encoded secret. Default length: 20 bytes.
const secret = generateSecret(); // → "JBSWY3DPEHPK3PXP..."timeRemaining(options?)
Seconds remaining in the current TOTP step. Useful for countdown UIs.
console.log(`${timeRemaining()}s until next code`);totpPair(secret, options?)
Returns the current and next TOTP codes — useful for "next code" UI hints.
const { current, next } = await totpPair(secret);QR Code Provisioning
buildURI(options)
Build an otpauth:// URI for QR code provisioning.
const uri = buildURI({
secret,
label: '[email protected]',
issuer: 'MyApp',
algorithm: 'SHA1', // 'SHA1' | 'SHA256' | 'SHA512'
digits: 6,
period: 30,
});microsoftAuthenticatorURI({ secret, label, issuer })
Build a URI guaranteed to work with Microsoft Authenticator.
const uri = microsoftAuthenticatorURI({
secret,
label: '[email protected]',
issuer: 'MyApp',
});parseURI(uri)
Parse an otpauth:// URI into its components — perfect for importing secrets from QR codes.
const parsed = parseURI('otpauth://totp/MyApp:alice?secret=...&issuer=MyApp');
// { type, label, issuer, secret, algorithm, digits, period, counter? }Security Helpers
generateBackupCodes(count?, length?)
Generate human-friendly single-use recovery codes (no confusing characters).
const codes = generateBackupCodes(10, 8); // 10 codes, 8 chars eachhashBackupCode(code) & verifyBackupCode(code, hashes)
Store backup codes as SHA-256 hashes in your database, never plaintext.
const codes = generateBackupCodes();
const hashes = await Promise.all(codes.map(hashBackupCode));
// store `hashes` in DB
// Later...
const idx = await verifyBackupCode(userInput, hashes);
if (idx !== -1) {
// Valid! Remove hashes[idx] so it can't be reused
}createRateLimiter({ max, windowMs })
Built-in brute-force protection.
const limiter = createRateLimiter({ max: 5, windowMs: 60_000 });
if (!limiter.allow(userId)) {
throw new Error('Too many attempts. Try again later.');
}createReplayGuard()
Prevent the same TOTP code from being used twice within its valid window.
const guard = createReplayGuard();
const result = await verify(token, secret);
if (!result) throw new Error('Invalid');
const step = Math.floor(Date.now() / 1000 / 30) + result.delta;
if (!guard.accept(userId, step)) {
throw new Error('Token already used');
}isValidTokenFormat(token, options?)
Pre-validate token shape before calling verify().
if (!isValidTokenFormat(input)) return res.status(400).send('Bad format');Steam Guard
steamTOTP(secret, options?)
Generate a 5-character Steam Guard code.
const code = await steamTOTP(secret); // → "K8R3F"💡 Examples
Express.js — Enable 2FA for a user
import express from 'express';
import QRCode from 'qrcode';
import { generateSecret, buildURI, verify } from '2fa-lib';
const app = express();
app.post('/2fa/setup', async (req, res) => {
const secret = generateSecret();
await db.users.update(req.user.id, { totpSecret: secret, totpEnabled: false });
const uri = buildURI({
secret,
label: req.user.email,
issuer: 'MyApp',
});
const qrDataUrl = await QRCode.toDataURL(uri);
res.json({ qrDataUrl, secret });
});
app.post('/2fa/verify', async (req, res) => {
const user = await db.users.findById(req.user.id);
const result = await verify(req.body.code, user.totpSecret, { window: 1 });
if (!result) return res.status(401).json({ error: 'Invalid code' });
await db.users.update(user.id, { totpEnabled: true });
res.json({ success: true });
});Next.js App Router — Login with 2FA
// app/api/login/route.ts
import { verify, createRateLimiter } from '2fa-lib';
const limiter = createRateLimiter({ max: 5, windowMs: 60_000 });
export async function POST(req: Request) {
const { email, password, code } = await req.json();
if (!limiter.allow(email)) {
return Response.json({ error: 'Too many attempts' }, { status: 429 });
}
const user = await db.users.findByEmail(email);
// ... validate password ...
if (user.totpEnabled) {
const result = await verify(code, user.totpSecret);
if (!result) return Response.json({ error: 'Invalid 2FA code' }, { status: 401 });
}
return Response.json({ token: createSessionToken(user) });
}Cloudflare Workers — Edge runtime
import { totp, verify } from '2fa-lib';
export default {
async fetch(request, env) {
const code = await totp(env.USER_SECRET);
return new Response(code);
},
};🤔 Why 2fa-lib?
| Feature | 2fa-lib | otplib | speakeasy | otpauth |
|---|:---:|:---:|:---:|:---:|
| Zero dependencies | ✅ | ❌ | ❌ | ✅ |
| TypeScript types built-in | ✅ | ✅ | ❌ | ✅ |
| Edge runtime support | ✅ | ⚠️ | ❌ | ✅ |
| Microsoft Authenticator helper | ✅ | ❌ | ❌ | ❌ |
| otpauth:// URI parser | ✅ | ❌ | ❌ | ✅ |
| Backup code hashing | ✅ | ❌ | ❌ | ❌ |
| Built-in rate limiter | ✅ | ❌ | ❌ | ❌ |
| Replay protection | ✅ | ❌ | ❌ | ❌ |
| Steam Guard | ✅ | ❌ | ❌ | ❌ |
| Bundle size (min+gzip) | <5 KB | ~15 KB | ~30 KB | ~8 KB |
🔐 Security Best Practices
- Encrypt secrets at rest. Never store TOTP secrets in plaintext in your database.
- Use the rate limiter. Brute-forcing 6 digits takes ~1M attempts on average — enforce limits.
- Use the replay guard. Don't allow the same code to be used twice.
- Hash backup codes. Never store them in plaintext.
- Use HTTPS. Always.
- Allow drift. Use
window: 1(±30s) to handle clock skew between server and phone.
🧪 Testing
npm testThe library is tested against the official RFC 4226 and RFC 6238 test vectors.
📖 Standards
- RFC 6238 — Time-based One-Time Password (TOTP)
- RFC 4226 — HMAC-based One-Time Password (HOTP)
- RFC 4648 — Base32 encoding
- Key URI Format — Google Authenticator
🤝 Contributing
PRs welcome! Please open an issue first for major changes.
📄 License
Keywords
totp · hotp · 2fa · mfa · two-factor authentication · multi-factor authentication
google authenticator · microsoft authenticator · authy · 1password · bitwarden
one-time password · rfc 6238 · rfc 4226 · otpauth · qr code · authentication
security · nodejs · typescript · deno · bun · cloudflare workers · edge
zero dependencies · lightweight · tiny · esm · web crypto · hmac
⭐ If you find this useful, please star the repo on GitHub! ⭐
