Authenticator Lab
A small, deliberately-layered TOTP enrolment and verification flow — QR provisioning, six-digit verification, and a repository seam where real persistence plugs in.
- 4
- API routes
- 6
- Digit TOTP codes
- 1
- Repository interface
- 0
- Secrets stored unverified
- Role
- Author
- Year
- 2026
- Status
- Open source
- Type
- TOTP two-factor reference implementation
01Overview
A TypeScript and Fastify project for testing a time-based one-time password flow from a browser: generate a QR code, scan it with an authenticator app, enter the current six-digit code to verify enrolment, then use it to log in.
It is small on purpose. Four routes, one service, one repository interface. The point is not the feature — it is the shape of the code around a security primitive most people paste in from a tutorial.
It also comes with a warning in its own README, which is the part I would defend hardest.
02The problem
Two-factor authentication is where a lot of applications quietly get it wrong. The library call is four lines; everything around it is where the mistakes live — storing the secret in plaintext, treating an unverified secret as enrolled, leaving no seam to swap the store.
Most example code makes this worse by collapsing the whole flow into one route handler, which is exactly the shape you cannot later harden.
03Architecture
- 01
A layer per responsibility
`server.ts` starts the HTTP server. `app.ts` is the composition root. `routes/` registers routes. The controller validates input and maps results to HTTP. The service owns QR generation and TOTP verification rules. The repository owns persistence. Each file has one reason to change.
- 02
Four routes
`GET /api/authenticator/register` creates a pending secret and a QR code. `POST .../register/verify` accepts a token and saves the secret only if it verifies. `POST .../login` checks a token against the saved secret. `GET .../status` reports whether the demo user is enrolled.
- 03
Pending versus verified secrets
A generated secret is pending until a valid code proves the user actually scanned it. Only then is it saved. Enrolling someone who never completed the scan locks them out of their own account.
- 04
The repository seam
`UserRepository` is an interface with an in-memory implementation. The secret disappears on restart, which is correct for a lab. Swapping in Postgres, SQLite or Prisma means implementing the interface and changing nothing else.
04Key decisions
In-memory storage, and say so loudly
The README states plainly that a TOTP secret must never be stored in plaintext in production without an encryption and key-management strategy. A demo that silently models bad practice teaches bad practice.
Verify before you persist
The two-step enrolment is the entire difference between a working 2FA implementation and a lockout generator.
Layers, at this size, deliberately
Four routes do not need five files. But the whole purpose is to show where the seams go — and a reference implementation that cuts corners is not a reference.
05Stack
- TypeScript
- Fastify
- speakeasy
- qrcode
- Vanilla JS