Skip to content

createTOTP

function createTOTP(config?: TOTPConfig): TOTPInstance

Creates a TOTP (Time-based One-Time Password) instance for two-factor authentication, compliant with RFC 6238.

import { createTOTP } from 'ideal-auth';
const totp = createTOTP();

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 every period seconds.
  • window — Number of time steps to check in each direction when verifying. A window of 1 accepts 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,
});

generateSecret(): string

Generates 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(options: {
secret: string;
issuer: string;
account: string;
}): string

Generates 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(
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 replayed
await 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.


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
Field Type Description
codes string[] Plain-text codes to display to the user once
hashed string[] Bcrypt-hashed codes to store in the database

Each code follows the pattern xxxxxxxx-xxxxxxxx — 16 hexadecimal characters separated by a hyphen, providing 64 bits of entropy per code.

a1b2c3d4-e5f6a7b8
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
};
}

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()
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
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';