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_sessionstable 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?