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

Tracking Sessions Across Devices

Beyond the session ID

The auth course’s session store is a Map<string, { userId, createdAt }>. It tracks which user owns the session and when it was created. But it does not track where the session is being used.

Users want to know: “Where am I logged in? Is that session on my work laptop or my phone? Is there a session I do not recognize?”

To answer these questions, we store device information with each session.

What to capture

When a session is created, record:

  • IP address: Where the login came from. Helps identify location and detect logins from unusual IPs.
  • User agent: The browser and OS. Parsed into a human-readable name like “Chrome on macOS.”
  • Last active time: When the session was last used. Shows whether a session is actively being used or is stale.
  • Created time: When the user logged in. Shows the session’s age.
// src/device-sessions.ts
import db from "./db.js";
import UAParser from "ua-parser-js";

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

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

export function updateLastActive(sessionId: string): void {
  db.prepare("UPDATE device_sessions SET last_active_at = datetime('now') WHERE id = ?").run(
    sessionId,
  );
}

export function getDeviceSessions(userId: string) {
  return db
    .prepare(
      "SELECT id, ip, device_name, last_active_at, created_at FROM device_sessions WHERE user_id = ? ORDER BY last_active_at DESC",
    )
    .all(userId);
}

export function deleteDeviceSession(sessionId: string, userId: string): boolean {
  const result = db
    .prepare("DELETE FROM device_sessions WHERE id = ? AND user_id = ?")
    .run(sessionId, userId);
  return result.changes > 0;
}

export function deleteAllDeviceSessions(userId: string, exceptSessionId?: string): void {
  if (exceptSessionId) {
    db.prepare("DELETE FROM device_sessions WHERE user_id = ? AND id != ?").run(
      userId,
      exceptSessionId,
    );
  } else {
    db.prepare("DELETE FROM device_sessions WHERE user_id = ?").run(userId);
  }
}

Wire it into login

Update the login handler to create a device session alongside the in-memory session:

// In the login handler, after creating the session:
const sessionId = createSession(user.id);
createDeviceSession(sessionId, user.id, c.request);

Update last active on each request

In the onRequest hook, update the session’s last active time:

onRequest: ({ request }) => {
  const sessionId = getSessionId(request);
  if (sessionId) {
    updateLastActive(sessionId);
  }
  return { startTime: Date.now() };
},

This runs on every request. If performance is a concern, update every 5 minutes instead of every request (check the current value before updating).

Exercises

Exercise 1: Log in from two different user agents (use curl -H "User-Agent: ..." with different values). List device sessions and verify both appear with different device names.

Exercise 2: Make a few requests with one session. Check that last_active_at updates.

Exercise 3: What information does ua-parser-js extract? Try parsing several user agent strings. Common browsers, mobile devices, and bots all produce different results.

Why do we store the raw user_agent string in addition to the parsed device_name?

← Restricting Unverified Accounts Listing and Revoking Sessions →

© 2026 hectoday. All rights reserved.