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. elevate.withPasskey() is sudo, not PRF unlock — Client boundaries.
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({
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.