totp-web
A lightweight, secure TOTP (Time-based One-Time Password) implementation for web browsers
Generate and verify TOTP tokens for two-factor authentication (2FA) in web applications using the Web Crypto API
Features
- 🔒 Secure Implementation: Uses the Web Crypto API for cryptographic operations
- 🌐 Browser Compatible: Works in all modern browsers without polyfills
- 🪶 Zero Dependencies: Lightweight with no external dependencies
- 📦 TypeScript Support: Full type definitions out of the box
- ⚡ Module Support: Compatible with both ESM and CommonJS
- 🔄 Real-time Updates: Generate tokens that update every 30 seconds
- 📱 QR Code Support: Generate QR codes for easy setup with authenticator apps
- 🧪 Verification: Verify tokens with configurable time windows for clock skew
- 🔐 Multiple Algorithms: Support for SHA-1, SHA-256, and SHA-512
- 🎨 Custom Character Sets: Generate tokens with custom character sets
- 🛡️ Rate Limiting: Built-in rate limiting utility to prevent brute force attacks
Motivation
Two-factor authentication (2FA) has become a standard security practice for protecting user accounts. While many services offer 2FA, implementing it in web applications has traditionally required server-side components or complex client-side libraries with dependencies.
totp-web was created to address these challenges:
- Client-Side Only: Enable 2FA functionality without requiring server-side token generation
- Simplicity: Provide a straightforward API for generating and verifying TOTP tokens
- Security: Leverage the Web Crypto API for secure cryptographic operations
- Performance: Keep the library lightweight with zero dependencies
- Accessibility: Make 2FA implementation accessible to developers of all skill levels
This package is particularly useful for:
- Progressive Web Apps (PWAs) that need offline 2FA capabilities
- Single Page Applications (SPAs) that want to reduce server load
- Applications that need to implement 2FA without backend changes
- Developers who want to understand how TOTP works through a clean implementation
Installation
npm install totp-web
yarn add totp-web
pnpm add totp-web
Usage
Basic Token Generation
import { generateTOTP } from 'totp-web';
const secret = 'JBSWY3DPEHPK3PXP';
const result = await generateTOTP({ secret });
console.log('Current TOTP:', result.token);
console.log('Seconds until next token:', result.remainingSeconds);
Advanced Token Generation
import { generateTOTP } from 'totp-web';
const result = await generateTOTP({
secret: 'JBSWY3DPEHPK3PXP',
algorithm: 'SHA-256',
digits: 8,
charSet: '0123456789ABCDEF',
period: 60,
window: 1
});
console.log('Current TOTP:', result.token);
console.log('Seconds until next token:', result.remainingSeconds);
Token Verification
import { verifyTOTP } from 'totp-web';
const secret = 'JBSWY3DPEHPK3PXP';
const token = '123456';
const isValid = await verifyTOTP(token, { secret });
console.log('Token is valid:', isValid);
const isValidWithWindow = await verifyTOTP(token, { secret, window: 2 });
const isValidWithOptions = await verifyTOTP(token, {
secret,
algorithm: 'SHA-256',
digits: 8,
charSet: '0123456789ABCDEF',
period: 60
});
Generating QR Codes for Authenticator Apps
import { getTOTPAuthUri } from 'totp-web';
const secret = 'JBSWY3DPEHPK3PXP';
const uri = getTOTPAuthUri({
secret,
accountName: 'user@example.com',
issuer: 'MyApp',
algorithm: 'SHA-256',
digits: 8,
period: 60
});
import { QRCodeSVG } from 'qrcode.react';
function QRCodeComponent() {
return <QRCodeSVG value={uri} size={200} />;
}
Rate Limiting
import { RateLimiter } from 'totp-web';
const limiter = new RateLimiter(5, 60000);
function verifyUserInput(userId: string, token: string) {
if (limiter.isRateLimited(userId)) {
const remainingTime = limiter.getTimeUntilReset(userId);
return {
success: false,
message: `Too many attempts. Please try again in ${Math.ceil(remainingTime / 1000)} seconds.`
};
}
limiter.reset(userId);
return { success: true };
}
function getRemainingAttempts(userId: string) {
return limiter.getRemainingAttempts(userId);
}
Complete Example with React
import React, { useState, useEffect } from 'react';
import { QRCodeSVG } from 'qrcode.react';
import { generateTOTP, verifyTOTP, getTOTPAuthUri, RateLimiter } from 'totp-web';
function TOTPComponent() {
const [secret, setSecret] = useState('');
const [token, setToken] = useState('');
const [verificationToken, setVerificationToken] = useState('');
const [isValid, setIsValid] = useState(null);
const [qrUri, setQrUri] = useState('');
const [remainingSeconds, setRemainingSeconds] = useState(30);
const [error, setError] = useState('');
const limiter = new RateLimiter(5, 60000);
useEffect(() => {
const generateNewSecret = async () => {
const result = await generateTOTP({
algorithm: 'SHA-256',
digits: 8
});
setSecret(result.secret);
setRemainingSeconds(result.remainingSeconds);
setQrUri(getTOTPAuthUri({
secret: result.secret,
accountName: 'user@example.com',
issuer: 'MyApp',
algorithm: 'SHA-256',
digits: 8
}));
};
generateNewSecret();
}, []);
useEffect(() => {
const updateToken = async () => {
if (!secret) return;
const result = await generateTOTP({
secret,
algorithm: 'SHA-256',
digits: 8
});
setToken(result.token);
setRemainingSeconds(result.remainingSeconds);
};
updateToken();
const interval = setInterval(updateToken, 1000);
return () => clearInterval(interval);
}, [secret]);
const handleVerify = async () => {
if (!secret || !verificationToken) return;
if (limiter.isRateLimited('user')) {
const remainingTime = limiter.getTimeUntilReset('user');
setError(`Too many attempts. Please try again in ${Math.ceil(remainingTime / 1000)} seconds.`);
return;
}
try {
const result = await verifyTOTP(verificationToken, {
secret,
algorithm: 'SHA-256',
digits: 8
});
setIsValid(result);
if (result) {
limiter.reset('user');
}
} catch (err) {
setError(err instanceof Error ? err.message : 'Verification failed');
}
};
return (
<div>
<h2>Your Secret</h2>
<code>{secret}</code>
<h2>QR Code</h2>
{qrUri && <QRCodeSVG value={qrUri} size={200} />}
<h2>Current Token</h2>
<div>
<code>{token}</code>
<div>Expires in {remainingSeconds} seconds</div>
</div>
<h2>Verify Token</h2>
<input
type="text"
value={verificationToken}
onChange={(e) => setVerificationToken(e.target.value)}
placeholder="Enter token to verify"
/>
<button onClick={handleVerify}>Verify</button>
{error && <div style={{ color: 'red' }}>{error}</div>}
{isValid !== null && !error && (
<div style={{ color: isValid ? 'green' : 'red' }}>
{isValid ? 'Token is valid!' : 'Token is invalid!'}
</div>
)}
<div>
Remaining attempts: {limiter.getRemainingAttempts('user')}
</div>
</div>
);
}
API Reference
generateTOTP(options: TOTPOptions): Promise
Generates a TOTP token and related information.
TOTPOptions
secret?: string: Optional Base32 encoded secret key. If not provided, a new secret will be generated.
algorithm?: TOTPAlgorithm: Hash algorithm to use (default: 'SHA-1'). Options: 'SHA-1', 'SHA-256', 'SHA-512'.
digits?: number: Number of digits in the token (default: 6).
charSet?: string: Custom character set for token generation (default: '0123456789').
period?: number: Time period in seconds (default: 30).
window?: number: Time window for validation (default: 0).
TOTPResult
token: The TOTP token
secret: The secret used to generate the token
remainingSeconds: Seconds until the token expires
verifyTOTP(token: string, options: TOTPVerifyOptions): Promise
Verifies a TOTP token against a secret.
TOTPVerifyOptions
secret: Base32 encoded secret key
algorithm?: TOTPAlgorithm: Hash algorithm to use (default: 'SHA-1')
digits?: number: Number of digits in the token (default: 6)
charSet?: string: Custom character set for token generation (default: '0123456789')
period?: number: Time period in seconds (default: 30)
window?: number: Time window for validation (default: 1, meaning ±30 seconds)
getTOTPAuthUri(options: TOTPAuthUriOptions): string
Generates a URI for QR code generation compatible with authenticator apps.
TOTPAuthUriOptions
secret: Base32 encoded secret key
accountName: The account name to display in the authenticator app
issuer?: string: Optional issuer name to display in the authenticator app
algorithm?: TOTPAlgorithm: Hash algorithm to use (default: 'SHA-1')
digits?: number: Number of digits in the token (default: 6)
period?: number: Time period in seconds (default: 30)
RateLimiter
A utility class for implementing rate limiting to prevent brute force attacks.
Constructor
new RateLimiter(maxAttempts: number = 5, windowMs: number = 60000)
maxAttempts: Maximum number of attempts allowed within the time window (default: 5)
windowMs: Time window in milliseconds (default: 60000, which is 1 minute)
Methods
isRateLimited(key: string): boolean: Check if a key is rate limited
reset(key: string): void: Reset the rate limit for a key
getRemainingAttempts(key: string): number: Get the number of remaining attempts for a key
getTimeUntilReset(key: string): number: Get the time in milliseconds until the rate limit resets for a key
Development Utilities
The package includes several development utilities to help with common TOTP operations:
import { TOTPUtils } from 'totp-web';
const secret = TOTPUtils.generateSecret();
const backupCodes = TOTPUtils.generateBackupCodes(8, 8);
const isValid = await TOTPUtils.testConfiguration({
algorithm: 'SHA-256',
digits: 6,
period: 30
});
const timeRemaining = TOTPUtils.formatTimeRemaining(29);
const validation = TOTPUtils.validateConfiguration({
algorithm: 'SHA-256',
digits: 6,
period: 30
});
CLI Tool
The package includes a command-line interface for testing and debugging TOTP tokens:
npm install -g totp-web
totp-web generate
totp-web generate --algorithm SHA-256 --digits 8
totp-web verify ABC123 --secret JBSWY3DPEHPK3PXP
totp-web uri --secret JBSWY3DPEHPK3PXP --accountName user@example.com --issuer Example
CLI Options
Generate Command
--secret <secret>: Secret key (optional, will generate if not provided)
--algorithm <algo>: Algorithm (SHA-1, SHA-256, SHA-512)
--digits <number>: Number of digits (default: 6)
--period <seconds>: Period in seconds (default: 30)
--charSet <chars>: Custom character set
Verify Command
--secret <secret>: Secret key (required)
--algorithm <algo>: Algorithm (SHA-1, SHA-256, SHA-512)
--digits <number>: Number of digits (default: 6)
--period <seconds>: Period in seconds (default: 30)
--charSet <chars>: Custom character set
--window <number>: Time window for verification (default: 1)
URI Command
--secret <secret>: Secret key (required)
--accountName <name>: Account name (required)
--issuer <name>: Issuer name (optional)
--algorithm <algo>: Algorithm (SHA-1, SHA-256, SHA-512)
--digits <number>: Number of digits (default: 6)
--period <seconds>: Period in seconds (default: 30)
Examples
Setting Up 2FA for a User
import { generateTOTP, getTOTPAuthUri } from 'totp-web';
const { secret } = await generateTOTP({
algorithm: 'SHA-256',
digits: 8
});
const uri = getTOTPAuthUri({
secret,
accountName: 'user@example.com',
issuer: 'MyApp',
algorithm: 'SHA-256',
digits: 8
});
Verifying User Input with Rate Limiting
import { verifyTOTP, RateLimiter } from 'totp-web';
const limiter = new RateLimiter(5, 60000);
async function verifyUserInput(userId: string, token: string, secret: string) {
if (limiter.isRateLimited(userId)) {
const remainingTime = limiter.getTimeUntilReset(userId);
return {
success: false,
message: `Too many attempts. Please try again in ${Math.ceil(remainingTime / 1000)} seconds.`
};
}
try {
const isValid = await verifyTOTP(token, {
secret,
algorithm: 'SHA-256',
digits: 8
});
if (isValid) {
limiter.reset(userId);
}
return {
success: true,
isValid
};
} catch (error) {
return {
success: false,
message: error instanceof Error ? error.message : 'Verification failed'
};
}
}
Security
This library uses the Web Crypto API for cryptographic operations, which provides a secure implementation of cryptographic primitives. The TOTP implementation follows the RFC 6238 specification.
Best Practices
- Secret Storage: Always store secrets securely. In a web application, this typically means storing them on the server, not in the client.
- Rate Limiting: Use the built-in
RateLimiter class to prevent brute force attacks.
- Algorithm Selection: For maximum security, use SHA-256 or SHA-512 instead of the default SHA-1.
- Token Length: Consider using 8-digit tokens instead of 6-digit tokens for increased security.
- Custom Character Sets: For even more security, use custom character sets with more entropy.
License
MIT License - see the LICENSE file for details.