sudo mode
Sensitive endpoints — change password, delete account, mint an API key, rotate a payment method — deserve a fresh authentication proof even from an already-signed-in user. GitHub calls this “sudo mode”; AWS calls it “step-up auth”; Google calls it “reauthentication.” Same pattern.
The session manager ships two primitives:
requireFreshAuth(req, { maxAgeSeconds })— has the user re-authenticated in the last N seconds? Returns a boolean.markFresh(req)— stamp the current session with a freshfreshAttimestamp after a successful re-auth.
Basic flow
// A middleware that protects a whole route group
async function requireSudo(req, res, next) {
if (!(await req.sessions.requireFreshAuth(req, { maxAgeSeconds: 300 }))) {
return res.status(401).json({ error: 'sudo_required', reauth: '/reauth' });
}
next();
}
// Any handler that needs step-up
app.delete('/api/api-keys/:id', requireSudo, async (req, res) => {
await db.apiKeys.destroy(req.params.id);
res.json({ ok: true });
});
// The re-auth handler
app.post('/reauth', async (req, res) => {
const user = await db.users.findById(req.session.userId);
const ok = await password.verify(req.body.password, user.pw_hash);
if (!ok) return res.sendStatus(401);
await req.sessions.markFresh(req);
res.json({ ok: true });
});requireFreshAuth
requireFreshAuth(req, { maxAgeSeconds: number, now?: number }): Promise<boolean>- Loads the current session via
verify(req)(respects the per-request cache). - Returns
falseif no session, or if the session has nofreshAt, or ifDate.now() - freshAt > maxAgeSeconds * 1000. - Returns
trueif the session has been re-authenticated within the window.
maxAgeSeconds is per-endpoint — a very sensitive endpoint (delete
account) might want 60s, a less sensitive one (change username)
might allow 15 minutes.
markFresh
markFresh(req, { now?: number }): Promise<{ freshAt: number }>- Loads the current session.
- Writes
freshAt: nowon the record viastore.update. - Invalidates the per-request cache so a subsequent
verify(req)in the same request sees the updatedfreshAt. - Throws
SessionError { code: INVALID_TOKEN }if the request has no valid session.
What “re-authentication” means is up to you
markFresh doesn’t care what method proved the user’s identity. The
common patterns:
- Password re-prompt — user types their current password again (the recipe above).
- TOTP — request a fresh 2FA code from
@exortek/otp. - WebAuthn touch — biometric on a passkey (when
@exortek/passkeyships). - Magic-link click — send a one-shot email link that lands on a
callback which calls
markFresh.
Any of these works. The manager only records the moment of freshness; it does not judge how you obtained it.
Combined with rotation
For high-security actions you might want fresh auth AND session rotation — a step-up ceremony that also mints a new session cookie:
app.post('/reauth', async (req, res) => {
const user = await db.users.findById(req.session.userId);
const ok = await password.verify(req.body.password, user.pw_hash);
if (!ok) return res.sendStatus(401);
await req.sessions.markFresh(req);
const { cookie } = await req.sessions.rotate(req);
res.setHeader('Set-Cookie', cookie);
res.json({ ok: true });
});Any stale cookie captured before the re-auth ceremony is now useless.
Not the same as MFA gating
Sudo mode enforces freshness on top of an already-authenticated session. It doesn’t replace multi-factor authentication at initial login. Users who signed in with just a password still need to pass the sudo prompt; users who signed in with password + TOTP see the sudo prompt too. The primitive is orthogonal to the initial auth strength.
Compliance
requireFreshAuth covers NIST SP 800-63B §5.2.10
(“Reauthentication requirements shall be applied to bindings between
authenticators and identities”) and OWASP ASVS §V2.10 — see the
compliance mapping.
A dedicated step-up authentication row on the compliance page
will flip from 🟡 to ✅ once @exortek/passkey ships, since
step-up over WebAuthn is the strongest form of this ceremony
available today.