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:
| Package | Role |
|---|---|
@railman/auth-login-factor | L0 stamps + requireAccess |
@railman/auth-elevate | Step-up; passkeyCounterGuard: passkeyStack.controller |
twoFactor | Optional 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
- Topic guide: Elevate / sudo
- Passkey ceremony: Passkeys & PRF
- Add a blind store: Vault composition
- Multi-node stores: Multi-instance
Pass credential management from "session" to a step-up assertion before production. See gradual security on Passkey login.