createTOTP
createTOTP
Section titled “createTOTP”Signature
Section titled “Signature”function createTOTP(config?: TOTPConfig): TOTPInstanceCreates a TOTP (Time-based One-Time Password) instance for two-factor authentication, compliant with RFC 6238.
import { createTOTP } from 'ideal-auth';
const totp = createTOTP();TOTPConfig
Section titled “TOTPConfig”interface TOTPConfig { digits?: number; period?: number; window?: number;}| Field | Type | Required | Default |
|---|---|---|---|
digits |
number |
No | 6 |
period |
number (seconds) |
No | 30 |
window |
number |
No | 1 |
digits— Number of digits in the generated TOTP code. Most authenticator apps expect 6.period— Time step duration in seconds. Codes rotate everyperiodseconds.window— Number of time steps to check in each direction when verifying. A window of1accepts the current code plus the codes from one step before and one step after (3 total), accounting for clock skew.
const totp = createTOTP({ digits: 6, period: 30, window: 1,});TOTPInstance
Section titled “TOTPInstance”generateSecret
Section titled “generateSecret”generateSecret(): stringGenerates a cryptographically random secret encoded in Base32. This secret should be stored in the database alongside the user record (encrypted at rest if possible).
const totp = createTOTP();const secret = totp.generateSecret();// "JBSWY3DPEHPK3PXP..."generateQrUri
Section titled “generateQrUri”generateQrUri(options: { secret: string; issuer: string; account: string;}): stringGenerates an otpauth:// URI suitable for encoding into a QR code. Users scan this QR code with an authenticator app (Google Authenticator, Authy, 1Password, etc.).
const totp = createTOTP();const secret = totp.generateSecret();
const uri = totp.generateQrUri({ secret, issuer: 'My App', account: 'user@example.com',});// "otpauth://totp/My%20App:user%40example.com?secret=JBSWY3DP...&issuer=My%20App&digits=6&period=30"| Parameter | Description |
|---|---|
secret |
The Base32-encoded secret from generateSecret() |
issuer |
Your application name, displayed in the authenticator app |
account |
The user’s account identifier (typically email), displayed in the authenticator app |
verify
Section titled “verify”verify( token: string, secret: string, lastUsedCounter: number | null,): { valid: boolean; counter: number | null }Verifies a TOTP code against a secret, with built-in replay protection. lastUsedCounter is the persisted time-step counter of the last successfully verified code — pass null if the user has never verified one. Codes at or before lastUsedCounter are rejected, so a code can never be accepted twice.
On success, counter is the time step the code matched. You must persist it (e.g. a totp_last_used_counter BIGINT column on the users table) and pass it back on the next verification.
| Field | Type | Description |
|---|---|---|
valid |
boolean |
Whether the code is valid within the configured time window and newer than lastUsedCounter |
counter |
number | null |
The matched time step on success, null on failure. Store it and pass it as lastUsedCounter next time |
const totp = createTOTP();
const { valid, counter } = totp.verify( '123456', user.totpSecret, user.totpLastUsedCounter,);
if (!valid) { return { error: 'Invalid verification code.' };}
// Persist the matched counter so this code can never be replayedawait db.user.update({ where: { id: user.id }, data: { totpLastUsedCounter: counter },});Tokens are normalized before verification: whitespace is stripped (so "123 456" works), and anything that is not exactly digits digits is rejected.
The window configuration determines how many adjacent time steps are checked. With the default window of 1, the current time step and one step in each direction are checked (3 codes total). This accommodates minor clock drift between the server and the user’s authenticator app.
generateRecoveryCodes
Section titled “generateRecoveryCodes”Signature
Section titled “Signature”function generateRecoveryCodes( hash: HashInstance, count?: number,): Promise<{ codes: string[]; hashed: string[] }>Generates a set of one-time recovery codes for 2FA backup. Each code is bcrypt-hashed for secure storage.
import { createHash, generateRecoveryCodes } from 'ideal-auth';
const hash = createHash();
const { codes, hashed } = await generateRecoveryCodes(hash);| Parameter | Type | Required | Default |
|---|---|---|---|
hash |
HashInstance |
Yes | — |
count |
number |
No | 8 |
Return value
Section titled “Return value”| Field | Type | Description |
|---|---|---|
codes |
string[] |
Plain-text codes to display to the user once |
hashed |
string[] |
Bcrypt-hashed codes to store in the database |
Code format
Section titled “Code format”Each code follows the pattern xxxxxxxx-xxxxxxxx — 16 hexadecimal characters separated by a hyphen, providing 64 bits of entropy per code.
a1b2c3d4-e5f6a7b8Example: 2FA setup
Section titled “Example: 2FA setup”import { createTOTP, createHash, generateRecoveryCodes } from 'ideal-auth';
const hash = createHash();const totp = createTOTP();
async function enableTwoFactor(userId: string) { const secret = totp.generateSecret();
const { codes, hashed } = await generateRecoveryCodes(hash);
await db.user.update({ where: { id: userId }, data: { totpSecret: secret, recoveryCodes: hashed, // Store hashed codes }, });
// Return the plain-text codes and QR URI to the user return { qrUri: totp.generateQrUri({ secret, issuer: 'My App', account: 'user@example.com', }), recoveryCodes: codes, // Show these once, never again };}verifyRecoveryCode
Section titled “verifyRecoveryCode”Signature
Section titled “Signature”function verifyRecoveryCode( code: string, hashedCodes: string[], hash: HashInstance,): Promise<{ valid: boolean; remaining: string[] }>Verifies a recovery code against the stored hashed codes. If valid, returns the remaining hashed codes with the matched entry removed.
import { verifyRecoveryCode, createHash } from 'ideal-auth';
const hash = createHash();
const { valid, remaining } = await verifyRecoveryCode( 'a1b2c3d4-e5f6a7b8', user.recoveryCodes, hash,);| Parameter | Type | Description |
|---|---|---|
code |
string |
The plain-text recovery code entered by the user |
hashedCodes |
string[] |
The array of bcrypt-hashed codes stored in the database |
hash |
HashInstance |
A hash instance created by createHash() |
Return value
Section titled “Return value”| Field | Type | Description |
|---|---|---|
valid |
boolean |
Whether the code matched any of the hashed codes |
remaining |
string[] |
The hashed codes array with the matched code removed |
Example: recovery code login
Section titled “Example: recovery code login”import { verifyRecoveryCode, createHash } from 'ideal-auth';
const hash = createHash();
async function loginWithRecoveryCode(userId: string, code: string) { const user = await db.user.findUnique({ where: { id: userId } });
if (!user || !user.recoveryCodes) { return { error: 'Invalid recovery code.' }; }
const { valid, remaining } = await verifyRecoveryCode( code, user.recoveryCodes, hash, );
if (!valid) { return { error: 'Invalid recovery code.' }; }
// Remove the used code from the stored set await db.user.update({ where: { id: userId }, data: { recoveryCodes: remaining }, });
// Complete the login await auth().loginById(userId);
return { success: true };}import type { TOTPConfig, TOTPInstance } from 'ideal-auth';