Adding 2FA to an app sounds like a weekend of reading RFCs, so most side projects never get it. I published 2fa-kit to make it two steps.
Step 1, enrol: generate a secret, show it as a QR code, store it encrypted.
import { createVault, generateSecret, buildUri } from "2fa-kit";
const vault = await createVault(process.env.MASTER_KEY!);
const secret = await generateSecret();
const uri = buildUri({ label: user.email, secret, issuer: "Acme" });
// render `uri` as a QR code, or show `secret` for manual entry
const { encrypted, salt } = await vault.encrypt(secret);
// save both on the user record
Step 2, verify at login:
import { verifyTotpWithDelta } from "2fa-kit";
const secret = await vault.decrypt(user.totpSecret, user.totpSalt);
const { valid, step } = await verifyTotpWithDelta(secret, codeFromForm);
if (!valid || step! <= user.totpLastStep) deny();
user.totpLastStep = step!; // persist, then allow
That is the whole integration. It works with Google Authenticator, Authy, 1Password, or any TOTP app, and it handles a few things that are easy to get wrong by hand:
- Secrets are encrypted at rest, not sitting plaintext in your database
- Replayed codes are rejected (that
stepcheck, required by RFC 6238 and skipped by most tutorials) - Backup codes for when the user loses their phone, stored as keyed hashes
- Importing existing Google Authenticator accounts via their migration QR
Zero dependencies. Runs on Node 20+, Bun, Deno, and browsers.
Docs and source: github.com/amansoomro062/2fa-kit.
What has stopped you from adding 2FA to your own projects?
Top comments (0)