Zero Accessby Railmandocs
Compose

Passkey + step-up

Hardened WebAuthn login plus elevate that shares the passkey counter watermark.

Passkey + step-up

Use this when users sign in with passkeys and you also need sudo / re-auth (passkey, TOTP, or password). Login and elevate share one counter controller so watermarks stay consistent.

Still no vault and no chat on this path.

Live: Elevate lab · Chooser: Use cases

What you install

Everything from Passkey login, plus:

PackageRole
@railman/auth-login-factorL0 stamps + requireAccess
@railman/auth-elevateStep-up; passkeyCounterGuard: passkeyStack.controller
twoFactorOptional TOTP enroll/storage

Server composition

import { betterAuth } from "better-auth";
import { passkey } from "@better-auth/passkey";
import { twoFactor } from "better-auth/plugins";
import { enhancePasskey } from "@railman/auth-zero-access-passkey";
import { elevate } from "@railman/auth-elevate";
import { loginFactors } from "@railman/auth-login-factor";

const passkeyStack = enhancePasskey(passkey, {
  rpID: "example.com",
  origin: ["https://example.com"],
  // Lab rung — production: custom assert after elevate / sudo
  assertCredentialAccess: "session",
});

export const auth = betterAuth({
  secret: process.env.BETTER_AUTH_SECRET!,
  session: { cookieCache: { enabled: false } },
  plugins: [
    ...passkeyStack.plugins,
    loginFactors(),
    twoFactor(),
    elevate({
      deploymentMode: "single-instance",
      passkeyCounterGuard: passkeyStack.controller,
      passwordOnlyIfNoStronger: true,
      elevatedTtlSec: 300,
      maxElevatedSec: 3600,
    }),
  ],
});

Drop the elevate({…}) block (and usually loginFactors / twoFactor) if you only need Passkey login.

Typechecked source: docs-site/verified-examples/dx-bc-passkey-elevate.ts.

Authorize privileged actions

Bare elevate presence:

import { requireElevate } from "@railman/auth-elevate";

await requireElevate(session.session.elevateClaim, {
  secret: process.env.BETTER_AUTH_SECRET!,
  userId: session.user.id,
  sessionToken: session.session.token,
  ttlSec: 300,
});

AMR / distinct-from-login maps (AUTH_REQUIRE_PRESETS.privilegedAction, elevateSudo, …) need createElevateOpener bound to the same user and session token:

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

const secret = process.env.BETTER_AUTH_SECRET!;

await requireAccess({
  secret,
  session: {
    userId: session.user.id,
    sessionToken: session.session.token,
    loginFactor: session.session.loginFactor,
    elevateClaim: session.session.elevateClaim,
  },
  map: AUTH_REQUIRE_PRESETS.privilegedAction,
  elevateOpener: createElevateOpener({
    secret,
    userId: session.user.id,
    sessionToken: session.session.token,
    ttlSec: 300,
    maxElevatedSec: 3600,
  }),
  // enrolled: { passkey, totp, password } — server lookup for password AMR
});

Details: Login factor · API — privilege helpers.

Client flow

Same elevate HTTP as Step-up only, plus passkey assertion when that method is available. See API — Elevate.

Next

Pass credential management from "session" to a step-up assertion before production. See gradual security on Passkey login.

On this page