Zero Accessby Railmandocs
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.

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.

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