Skip to content
All work
19TOTP two-factor reference implementation2026

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

  1. 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.

  2. 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.

  3. 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.

  4. 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

Decision 01

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.

Decision 02

Verify before you persist

The two-step enrolment is the entire difference between a working 2FA implementation and a lockout generator.

Decision 03

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