hectoday
DocsCoursesChangelog GitHub
DocsCoursesChangelog GitHub

Access Required

Enter your access code to view courses.

Invalid code

← All courses Production Auth Patterns with @hectoday/http

Before They Start

  • Why Production Auth Is Different
  • Project Setup

Email Verification

  • Why Verify Emails
  • Building Email Verification
  • Restricting Unverified Accounts

Session Management

  • Tracking Sessions Across Devices
  • Listing and Revoking Sessions
  • Session Security

Step-Up Authentication

  • What Is Step-Up Auth
  • Building Step-Up Auth
  • Applying Step-Up to Sensitive Routes

Account Deletion

  • The Right to Be Forgotten
  • Building Account Deletion
  • Data Cleanup

SAML and Enterprise SSO

  • What Is SAML
  • Building a SAML Service Provider
  • Just-in-Time Provisioning

Putting It All Together

  • Production Auth Checklist
  • Capstone: Production-Ready Auth

Project Setup

Starting point

This course builds on the auth course capstone with the 2FA additions from the Two-Factor and Passwordless Auth course. You need signup, login, sessions, cookies, and optionally TOTP/passkey support working.

Additional dependencies

npm install saml2-js ua-parser-js
npm install -D @types/ua-parser-js

saml2-js: SAML 2.0 service provider library. Parses SAML responses, validates signatures, extracts user attributes.

ua-parser-js: Parses User-Agent strings into readable device info (browser, OS, device type). Used for session tracking.

New database tables

Add these to src/db.ts:

db.exec(`
  CREATE TABLE IF NOT EXISTS email_verifications (
    id TEXT PRIMARY KEY,
    user_id TEXT NOT NULL,
    token_hash TEXT NOT NULL,
    expires_at INTEGER NOT NULL,
    FOREIGN KEY (user_id) REFERENCES users(id)
  );

  CREATE TABLE IF NOT EXISTS device_sessions (
    id TEXT PRIMARY KEY,
    user_id TEXT NOT NULL,
    ip TEXT,
    user_agent TEXT,
    device_name TEXT,
    last_active_at TEXT NOT NULL DEFAULT (datetime('now')),
    created_at TEXT NOT NULL DEFAULT (datetime('now')),
    FOREIGN KEY (user_id) REFERENCES users(id)
  );

  CREATE TABLE IF NOT EXISTS deletion_requests (
    id TEXT PRIMARY KEY,
    user_id TEXT NOT NULL UNIQUE,
    requested_at TEXT NOT NULL DEFAULT (datetime('now')),
    execute_at TEXT NOT NULL,
    cancelled INTEGER NOT NULL DEFAULT 0,
    FOREIGN KEY (user_id) REFERENCES users(id)
  );

  CREATE TABLE IF NOT EXISTS saml_providers (
    id TEXT PRIMARY KEY,
    domain TEXT NOT NULL UNIQUE,
    entity_id TEXT NOT NULL,
    sso_login_url TEXT NOT NULL,
    certificate TEXT NOT NULL,
    created_at TEXT NOT NULL DEFAULT (datetime('now'))
  );

  CREATE TABLE IF NOT EXISTS saml_identities (
    id TEXT PRIMARY KEY,
    user_id TEXT NOT NULL,
    provider_id TEXT NOT NULL,
    name_id TEXT NOT NULL,
    FOREIGN KEY (user_id) REFERENCES users(id),
    FOREIGN KEY (provider_id) REFERENCES saml_providers(id),
    UNIQUE(provider_id, name_id)
  );
`);

And update the users table to include verification status:

// Add email_verified column if it does not exist
try {
  db.exec("ALTER TABLE users ADD COLUMN email_verified INTEGER NOT NULL DEFAULT 0");
} catch {
  // Column already exists
}

What each table does:

email_verifications: Verification tokens, same pattern as password reset and magic links. Hashed, time-limited, single-use.

device_sessions: Tracks where each session is active. IP, user agent, device name, and last activity. Linked to the session ID.

deletion_requests: Soft delete requests with a grace period. execute_at is when the hard delete runs. cancelled lets the user change their mind.

saml_providers: SAML identity provider configurations. One per customer domain. Stores the IdP’s metadata (entity ID, login URL, certificate).

saml_identities: Links a SAML identity (name_id from the IdP) to a user in your system. Similar to the oauth_accounts table from the OAuth course.

Updated session store

Replace the in-memory session Map with the device_sessions table for production session tracking:

// src/sessions.ts — add device session tracking
import UAParser from "ua-parser-js";
import db from "./db.js";

export function createTrackedSession(userId: string, request: Request): string {
  const id = crypto.randomUUID();
  const ip = request.headers.get("x-forwarded-for")?.split(",")[0].trim() ?? "unknown";
  const userAgent = request.headers.get("user-agent") ?? "unknown";
  const parser = new UAParser(userAgent);
  const browser = parser.getBrowser();
  const os = parser.getOS();
  const deviceName = `${browser.name ?? "Unknown"} on ${os.name ?? "Unknown"}`;

  db.prepare(
    "INSERT INTO device_sessions (id, user_id, ip, user_agent, device_name) VALUES (?, ?, ?, ?, ?)",
  ).run(id, userId, ip, userAgent, deviceName);

  // Also store in the in-memory session map for fast lookups
  createSession(userId); // existing function — reuse the same ID

  return id;
}

[!NOTE] We keep the in-memory session store for fast authentication checks (every request) and use the device_sessions table for the management UI (listing, revoking). In production, both would be in Redis or a database.

File structure

src/
  app.ts
  server.ts
  db.ts                    # updated with new tables
  auth.ts                  # updated with email verification check
  sessions.ts              # updated with device tracking
  cookies.ts
  routes/
    auth.ts                # signup (sends verification), login
    verification.ts        # email verification endpoints
    sessions-mgmt.ts       # list/revoke sessions
    step-up.ts             # re-authentication endpoints
    deletion.ts            # account deletion endpoints
    saml.ts                # SAML SSO endpoints

Exercises

Exercise 1: Run the app and verify the new tables exist with sqlite3 app.db ".tables".

Exercise 2: Look at the device_sessions table. What information does it capture? Why is user_agent stored separately from device_name? (Answer: device_name is the parsed, human-readable version. user_agent is the raw string, useful for debugging.)

Why do we add an email_verified column to the users table instead of a separate table?

← Why Production Auth Is Different Why Verify Emails →

© 2026 hectoday. All rights reserved.