Skip to main content
zerotal

Documentation


Documentation / zerotal / auth / TwoFactorService

Class: TwoFactorService

Defined in: packages/auth/src/TwoFactorService.ts:183

Native, dependency-free TOTP two-factor authentication service.

Remarks

Stateless: the service holds only configuration (issuer, window, recoveryCodeCount). All per-user state (the secret, the last-used counter, and the recovery-code hashes) is passed in and out of its methods, so you persist it however you like.

Constructors

Constructor

new TwoFactorService(options?): TwoFactorService

Defined in: packages/auth/src/TwoFactorService.ts:188

Parameters

options?

TwoFactorOptions = {}

Returns

TwoFactorService

Other

verifyCodeWithCounter()

verifyCodeWithCounter(secret, token, lastUsedCounter?): object

Defined in: packages/auth/src/TwoFactorService.ts:303

Parameters

secret

string

token

string

lastUsedCounter?

number | null

Returns

object

valid

valid: boolean

counter

counter: number | null

Recovery codes

generateRecoveryCodes()

generateRecoveryCodes(): object

Defined in: packages/auth/src/TwoFactorService.ts:355

Generate single-use recovery codes. Returns the plain codes to show the user once and the hashed codes (SHA-256 hex) to persist. Store only the hashes — a leaked database row then can't reveal the codes.

Each code carries RECOVERY_CODE_BITS bits of CSPRNG entropy. That number is the whole security argument for storing them under a single fast hash: a recovery code is a complete second-factor bypass, so a leaked column must not be searchable. At 160 bits it is not, with or without a salt.

Returns

object

{ plain, hashed } — show plain once, persist hashed. The count is the constructor's recoveryCodeCount (default 8).

plain

plain: string[]

hashed

hashed: string[]

Example

const { plain, hashed } = tf.generateRecoveryCodes();
await user.update({ twoFactorRecoveryCodes: JSON.stringify(hashed) });

verifyRecoveryCode()

verifyRecoveryCode(hashedCodes, code): object

Defined in: packages/auth/src/TwoFactorService.ts:383

Verify a recovery code against the stored hashes. On a match returns the remaining hashes with the used one removed (single-use — persist them); on a miss returns the hashes unchanged. Comparison is constant-time and the input is normalised (trimmed, lowercased, whitespace stripped).

Parameters

hashedCodes

string[]

The user's stored recovery-code hashes.

code

string

The recovery code entered by the user.

Returns

object

{ valid, remaining } — on a match, remaining omits the used hash and must be persisted; on a miss, remaining is the input unchanged.

valid

valid: boolean

remaining

remaining: string[]

Example

const result = tf.verifyRecoveryCode(JSON.parse(user.twoFactorRecoveryCodes), input);
if (result.valid) await user.update({ twoFactorRecoveryCodes: JSON.stringify(result.remaining) });

Secret

generateSecret()

generateSecret(): string

Defined in: packages/auth/src/TwoFactorService.ts:203

Generate a random base-32 secret (160 bits) for a new user enrollment. Store this (encrypted at rest recommended) in user.twoFactorSecret.

Returns

string

A base-32 string to seed the authenticator app.


getQrCodeUrl()

getQrCodeUrl(label, secret, issuer?): string

Defined in: packages/auth/src/TwoFactorService.ts:221

Return an otpauth://totp/… URI (SHA1, 6 digits, 30s period) that authenticator apps can scan as a QR code.

Parameters

label

string

Account label shown in the app, e.g. the user's email.

secret

string

The base-32 secret from generateSecret.

issuer?

string

Overrides the constructor issuer for this URI.

Returns

string

The otpauth:// provisioning URI.

Verifying

verifyCode()

verifyCode(secret, token, lastUsedCounter?): boolean

Defined in: packages/auth/src/TwoFactorService.ts:243

Verify a 6-digit TOTP code against the stored secret synchronously.

Pass the user's persisted last-used counter (see verifyCodeWithCounter) to reject replays of a code within its validity window. Without it, an intercepted code stays valid for up to window periods — RFC 6238 §5.2 requires one-time use.

Parameters

secret

string

The user's stored base-32 secret.

token

string

The 6-digit code entered by the user (whitespace tolerated).

lastUsedCounter?

number | null

The user's persisted last-used counter, for replay protection.

Returns

boolean

true when the code is valid (and, if lastUsedCounter given, not a replay).


generateCode()

generateCode(secret, offset?): string

Defined in: packages/auth/src/TwoFactorService.ts:297

Produce the TOTP code a correctly-configured authenticator would show right now — the counterpart to verifyCode.

This exists for tests. Without it the challenge flow is untestable without reimplementing RFC 6238 in your own suite, which is both tedious and a second implementation to get wrong. Drive your challenge endpoint with this instead.

Parameters

secret

string

The user's stored base-32 secret.

offset?

number = 0

Time-step slots from now. -1 yields the previous code, which is useful for asserting that your replay guard rejects it.

Returns

string

The zero-padded six-digit code.

Example

const code = tf.generateCode(user.twoFactorSecret);
const res = await app.actingAs(user).post("/two-factor/challenge", { code });

Remarks

Never send this to a user. A code the server generated is a code the server knows, which is precisely what a second factor must not be.