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

Applying Step-Up to Sensitive Routes

Which routes get step-up

Apply requireRecentAuth to every route where a compromised or stale session could cause serious damage:

// Email change
route.put("/me/email", {
  resolve: async (c) => {
    const user = authenticate(c.request);
    if (user instanceof Response) return user;
    const reauth = requireRecentAuth(c.request);
    if (reauth instanceof Response) return reauth;

    const body = await c.request.json();
    const newEmail = (body as any).email;

    if (!newEmail) return Response.json({ error: "Email required" }, { status: 400 });

    // Check for duplicate
    const existing = db.prepare("SELECT id FROM users WHERE email = ? AND id != ?")
      .get(newEmail, user.id);
    if (existing) return Response.json({ error: "Email already in use" }, { status: 409 });

    // Update email and reset verification
    db.prepare("UPDATE users SET email = ?, email_verified = 0 WHERE id = ?")
      .run(newEmail, user.id);

    // Send verification to new email
    const token = await createVerificationToken(user.id);
    console.log(`\n📧 Verify new email ${newEmail}:\nhttp://localhost:3000/verify-email?token=${token}\n`);

    return Response.json({ message: "Email updated. Verify your new email address." });
  },
}),

// Password change
route.put("/me/password", {
  resolve: async (c) => {
    const user = authenticate(c.request);
    if (user instanceof Response) return user;
    const reauth = requireRecentAuth(c.request);
    if (reauth instanceof Response) return reauth;

    const body = await c.request.json();
    const newPassword = (body as any).newPassword;

    if (!newPassword || newPassword.length < 8) {
      return Response.json({ error: "Password must be at least 8 characters" }, { status: 400 });
    }

    const hash = await bcrypt.hash(newPassword, 10);
    db.prepare("UPDATE users SET password_hash = ? WHERE id = ?").run(hash, user.id);

    // Rotate the session (invalidate old sessions)
    const newSessionId = rotateSession(user.sessionId, user.id, c.request);

    return Response.json(
      { message: "Password changed. All other sessions have been revoked." },
      { headers: { "set-cookie": sessionCookie(newSessionId) } },
    );
  },
}),

Notice: email change resets email_verified to 0 and sends a new verification email. Password change rotates the session (from the session security lesson).

Routes that already have step-up from other courses

The 2FA course’s disable endpoint already requires a TOTP or recovery code. That is a form of step-up auth — the user must prove they have the second factor. You can add requireRecentAuth on top for an additional layer, or keep the existing TOTP requirement as sufficient.

The authorization course’s API key creation requires org:settings permission. Adding requireRecentAuth here means an attacker with a hijacked session cannot create long-lived API keys without re-authenticating.

The pattern

Every sensitive route follows the same structure:

const user = authenticate(c.request); // Step 1: Who are you?
if (user instanceof Response) return user;

const reauth = requireRecentAuth(c.request); // Step 2: Prove it recently
if (reauth instanceof Response) return reauth;

// Step 3: Perform the action

What the frontend does

When the frontend receives 403 with "Re-authentication required", it shows a modal:

┌──────────────────────────────────────┐
│  Confirm your identity               │
│                                      │
│  [Password]                          │
│  — or —                              │
│  [Enter TOTP code]                   │
│  — or —                              │
│  [Use passkey 🔑]                    │
│                                      │
│  [Confirm]                           │
└──────────────────────────────────────┘

After confirmation, the frontend retries the original request. The re-authentication is valid for 5 minutes, so subsequent sensitive actions do not trigger the modal again.

Exercises

Exercise 1: Add step-up to the email change route. Try changing email without re-authenticating (403). Re-authenticate, then change email (200).

Exercise 2: Add step-up to API key creation. Try creating a key without re-authenticating. It should require confirmation.

Exercise 3: Re-authenticate, then perform three sensitive actions within 5 minutes. Only the first should trigger the confirmation modal.

Why does changing the email reset email_verified to 0?

← Building Step-Up Auth The Right to Be Forgotten →

© 2026 hectoday. All rights reserved.