Documentation / @zerotal/auth / TwoFactorService
Class: TwoFactorService
Defined in: 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: auth/src/TwoFactorService.ts:188
Parameters
options?
TwoFactorOptions = {}
Returns
TwoFactorService
Other
verifyCodeWithCounter()
verifyCodeWithCounter(
secret,token,lastUsedCounter?):object
Defined in: 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: 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: 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: 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: 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: 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: 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.