auth: distress-password lockdown + explicit token-revoke endpoint

Add a duress password (DISTRESS_PASSWORD env): entering it at login trips an
in-memory full lockdown — all new logins (password + passkey) and every existing
session are refused until the server is restarted, and the login itself returns a
normal "invalid credentials" so it gives nothing away. Also add
POST /api/auth/blacklist-token as a clearly-named alias for revoking the current
JWT (same effect as signout).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-23 00:06:42 +00:00
co-authored by Claude Opus 4.8
parent f03bf733c8
commit cf2962fd67
5 changed files with 29 additions and 0 deletions
+2
View File
@@ -32,6 +32,8 @@ authRouter.use('/', async (ctx) => ctx.json({ officerAuthServer: 'ok' }));
authRouter.get('/me', userMiddleware, usersMe);
authRouter.post('/signin', signinRateLimiter, signinHandler);
authRouter.post('/signout', userMiddleware, signoutHandler);
// Explicit "revoke the current token" alias (same effect as signout: blacklists this JWT).
authRouter.post('/blacklist-token', userMiddleware, signoutHandler);
authRouter.post('/signup', signupRateLimiter, signupHandler);
authRouter.post('/bootstrap', signupRateLimiter, bootstrapHandler);
authRouter.post('/verify', verifyHandler);
+10
View File
@@ -0,0 +1,10 @@
// Duress lockdown. The distress password (entered at login) trips this in-memory flag; while it is
// set, ALL new logins and ALL existing sessions are refused. It is intentionally in-memory only, so
// the only way to clear it is a manual server restart.
let locked = false;
export const isLockdown = (): boolean => locked;
export const triggerLockdown = (): void => {
locked = true;
};
+2
View File
@@ -4,6 +4,7 @@ import { createRouter } from '../../create-router';
import { userMiddleware, passkeyRateLimiter } from '../../_middlewares';
import { sign } from '../../jwt';
import * as errors from '../../custom-errors';
import { isLockdown } from './distress';
import {
getUserByEmail,
getPasskeysByUserIdAndOrigin,
@@ -128,6 +129,7 @@ passkeyRouter.get('/signin/:email', passkeyRateLimiter, passkeyRouterGet);
// Verify passkey authentication and issue token
const passkeyRouterPostVerify: Handler = async (ctx) => {
if (isLockdown()) throw errors.UNAUTHORIZED(); // duress lockdown blocks passkey logins too
const { email } = ctx.req.param();
const origin = ctx.get('origin') as string;
const rpId = getRpId(origin);
+11
View File
@@ -6,11 +6,22 @@ import { sign } from '@@/jwt';
import { getClaudeDir } from '@@/data-path';
import argon2 from 'argon2';
import * as errors from '@@/custom-errors';
import { isLockdown, triggerLockdown } from './distress';
const TEST_USERS: number[] = [];
const { DISTRESS_PASSWORD } = process.env;
export const signinHandler: Handler = async function (ctx) {
const { email, password } = ctx.get('body');
// Duress: if lockdown is active, refuse everyone (looks like a normal failed login). If the distress
// password was entered, trip the lockdown now and then fail the same way, giving nothing away.
if (isLockdown()) throw errors.UNAUTHORIZED();
if (DISTRESS_PASSWORD && typeof password === 'string' && password === DISTRESS_PASSWORD) {
triggerLockdown();
throw errors.UNAUTHORIZED();
}
const origin = ctx.get('origin');
const dbUser = await getUserByEmail(email);
if (!dbUser) throw errors.UNAUTHORIZED();