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:
@@ -2,6 +2,7 @@ import type { MiddlewareHandler } from 'hono';
|
||||
import { verify } from '@@/jwt';
|
||||
import * as errors from '@@/custom-errors';
|
||||
import { isOriginAllowed } from './origin-validation';
|
||||
import { isLockdown } from '../api/auth/distress';
|
||||
import { getUserById, isTokenBlacklisted } from 'officerdb';
|
||||
|
||||
// Role permissions: which HTTP methods each role can use
|
||||
@@ -21,6 +22,9 @@ function isMethodAllowed(role: string | null, method: string): boolean {
|
||||
}
|
||||
|
||||
export const userMiddleware: MiddlewareHandler = async function (ctx, next) {
|
||||
// Duress lockdown: reject every authenticated request, cutting off all existing sessions.
|
||||
if (isLockdown()) throw errors.UNAUTHORIZED();
|
||||
|
||||
const { authorization } = ctx.req.header();
|
||||
|
||||
// Support token in query param for media elements (<audio>, <video>, <img>)
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user