Zero Accessby Railman
Packages

Login factor

Getting started with @railman/auth-login-factor — L0 stamps and requireAccess maps.

Getting started — Login factor

@railman/auth-login-factor stamps how the session was established and exposes requireAccess maps for L0 / L1 gates. Cookie lastLoginMethod is never an authorization input.

HTTP: none (plugin hooks + server helpers) · API helpers: API — privilege

Install

pnpm add @railman/auth-login-factor

Setup

import { betterAuth } from "better-auth";
import {
  AUTH_REQUIRE_PRESETS,
  createElevateOpener,
  loginFactors,
  requireAccess,
} from "@railman/auth-login-factor";

const secret = process.env.BETTER_AUTH_SECRET!;

export const auth = betterAuth({
  secret,
  session: {
    // Required: cookieCache would expose returned session fields to the client
    cookieCache: { enabled: false },
  },
  plugins: [loginFactors()],
});

export async function assertPrivileged(session: {
  userId: string;
  sessionToken: string;
  loginFactor?: string | null;
  elevateClaim?: string | null;
}) {
  await requireAccess({
    secret,
    session,
    map: AUTH_REQUIRE_PRESETS.privilegedAction,
    elevateOpener: createElevateOpener({
      secret,
      userId: session.userId,
      sessionToken: session.sessionToken,
      ttlSec: 300,
      maxElevatedSec: 3600,
    }),
    // enrolled: { passkey, totp, password }, // server lookup for password AMR
  });
}

Ensure the Better Auth schema includes the optional loginFactor string field before enabling the plugin.

There is no client HTTP surface. loginFactorsClient() is a session-typing marker ($InferServerPlugin); stamp evaluation stays on the server. Client boundaries.

Presets

PresetMeaning
sessionOnlySigned L0 session
elevateSudoSession + elevate (distinct when required)
privilegedActionElevate: passkey/TOTP; password only if no stronger enrolled
recoveryControlPlaneSession + passkey/TOTP, distinct from session factor

Maps must be server constants — never from request body/query.

The L0 stamp lives on the Better Auth session row (database). Disable session.cookieCache. See Shared storage.

With elevate

Install loginFactors() whenever you use requireDistinctInitialFactor or requireAccess with elevate AMR. Strict elevate setup rejects a missing login-factor plugin. Pass elevateOpener: createElevateOpener({ secret, userId, sessionToken }) into requireAccess whenever the map inspects elevate claims.

Next

On this page