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

Just-in-Time Provisioning

Users who have never visited your app

When a company sets up SAML SSO, their employees do not sign up on your app individually. Instead, the first time an employee logs in via SSO, your app creates their account automatically. This is just-in-time (JIT) provisioning.

The SAML assertion contains the user’s email, name, and often their department, role, or group memberships. Your app uses this information to create the user account and assign appropriate permissions.

The provisioning flow

// src/jit-provisioning.ts
import db from "./db.js";

interface SamlUser {
  email: string;
  name: string;
  nameId: string;
  providerId: string;
  groups?: string[];
}

export function provisionSamlUser(samlUser: SamlUser): { id: string; email: string; name: string } {
  // Check if user already exists
  let user = db.prepare("SELECT id, email, name FROM users WHERE email = ?").get(samlUser.email) as
    | { id: string; email: string; name: string }
    | undefined;

  if (user) {
    // Update name if changed in the IdP
    db.prepare("UPDATE users SET name = ? WHERE id = ?").run(samlUser.name, user.id);

    // Ensure SAML identity link exists
    const link = db
      .prepare("SELECT id FROM saml_identities WHERE user_id = ? AND provider_id = ?")
      .get(user.id, samlUser.providerId);

    if (!link) {
      db.prepare(
        "INSERT INTO saml_identities (id, user_id, provider_id, name_id) VALUES (?, ?, ?, ?)",
      ).run(crypto.randomUUID(), user.id, samlUser.providerId, samlUser.nameId);
    }

    return user;
  }

  // Create new user
  const id = crypto.randomUUID();
  db.prepare(
    "INSERT INTO users (id, email, name, password_hash, email_verified) VALUES (?, ?, ?, ?, 1)",
  ).run(id, samlUser.email, samlUser.name, "saml-no-password", 1);

  // Link SAML identity
  db.prepare(
    "INSERT INTO saml_identities (id, user_id, provider_id, name_id) VALUES (?, ?, ?, ?)",
  ).run(crypto.randomUUID(), id, samlUser.providerId, samlUser.nameId);

  // Map groups to roles (if the IdP provides group info)
  if (samlUser.groups) {
    mapGroupsToRoles(id, samlUser.providerId, samlUser.groups);
  }

  return { id, email: samlUser.email, name: samlUser.name };
}

Key details:

No password. SSO users authenticate through their IdP, not through your app’s password system. The password_hash is set to a placeholder ("saml-no-password") that can never match a bcrypt comparison. The user cannot log in with a password — only through SSO.

Email verified automatically. The IdP verified the user’s email when the company set up their account. Your app trusts the IdP’s assertion.

Name sync. If the user’s name changes in the IdP (marriage, preferred name update), the next SSO login updates it in your app.

Mapping groups to roles

Enterprise IdPs often provide group memberships in the SAML assertion. Your app can map these to your role system:

function mapGroupsToRoles(userId: string, providerId: string, groups: string[]): void {
  // Get the group-to-role mapping for this provider
  const provider = db.prepare("SELECT domain FROM saml_providers WHERE id = ?").get(providerId) as {
    domain: string;
  };

  // Find the organization for this domain
  // (In a real app, the admin configures which org maps to which SSO provider)
  const org = db
    .prepare("SELECT id FROM organizations WHERE id = ?")
    .get(`org-${provider.domain.split(".")[0]}`) as { id: string } | undefined;

  if (!org) return;

  // Map groups to roles
  let role = "viewer"; // default

  if (groups.includes("admins") || groups.includes("owners")) {
    role = "owner";
  } else if (groups.includes("editors") || groups.includes("developers")) {
    role = "editor";
  }

  // Create or update membership
  const existing = db
    .prepare("SELECT id FROM memberships WHERE user_id = ? AND org_id = ?")
    .get(userId, org.id);

  if (existing) {
    db.prepare("UPDATE memberships SET role = ? WHERE user_id = ? AND org_id = ?").run(
      role,
      userId,
      org.id,
    );
  } else {
    db.prepare("INSERT INTO memberships (id, user_id, org_id, role) VALUES (?, ?, ?, ?)").run(
      crypto.randomUUID(),
      userId,
      org.id,
      role,
    );
  }
}

The group-to-role mapping is configurable. A simple approach: “admins” group → owner role, “developers” group → editor role, everyone else → viewer. The specific mapping depends on the customer’s organization.

Domain-based SSO enforcement

Once a company configures SSO, you can enforce it: all users with that company’s email domain must log in via SSO. They cannot use password login.

// In the login route, before checking the password:
const domain = email.split("@")[1];
const samlProvider = getSamlProviderByDomain(domain);
if (samlProvider) {
  return Response.json(
    {
      error: "This domain uses SSO. Please log in via your company's identity provider.",
      ssoRequired: true,
      redirectUrl: "/auth/saml/login",
    },
    { status: 403 },
  );
}

This prevents employees from bypassing SSO by creating a password-based account. All authentication for that domain goes through the company’s IdP, which enforces the company’s security policies (MFA, password rules, session length).

Exercises

Exercise 1: Configure a test SAML provider for acmecorp.com. Simulate a SAML login. Verify the user is created automatically with email_verified = 1.

Exercise 2: Add group-to-role mapping. Simulate a login with groups: ["developers"]. Verify the user gets the “editor” role.

Exercise 3: Enable domain enforcement. Try logging in with a password for an acmecorp.com email. It should be blocked with an SSO redirect.

Why do SSO users have 'saml-no-password' instead of a real password hash?

← Building a SAML Service Provider Production Auth Checklist →

© 2026 hectoday. All rights reserved.