Add AUTH_DEV_AUTOLOGIN to switch off the login on development instances

With AUTH_DEV_AUTOLOGIN=<username>, every /api request without a valid
session runs as that existing, active user and skips the forced password
change. Unset keeps the normal login; unknown or deactivated users still
get 401, real sessions take precedence, the CSRF application header stays
mandatory and the API logs a warning at startup. The switch is not
limited to localhost because the development instance is reached from
other machines and through a reverse proxy (THREATMODEL D2/R15). It must
never be set on an instance with real project data.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Julian Appel 2026-10-30 09:03:29 +01:00
parent 4677b1685c
commit 084002bea9
11 changed files with 211 additions and 8 deletions

View file

@ -50,6 +50,31 @@ export async function identityFromRequest(
};
}
/**
* Session identity, or on a development instance with AUTH_DEV_AUTOLOGIN the
* configured active user. The auto-login user skips the forced password
* change because nobody signs in interactively.
*/
export async function resolveRequestIdentity(
req: { headers: { cookie?: string | undefined } },
authConfig: Pick<AuthConfig, "devAutoLogin">,
sessions: SessionStore,
users: Pick<UserStore, "findById" | "findByUsername">,
slideExpiry = true
): Promise<AuthUser | undefined> {
const identity = await identityFromRequest(req, sessions, users, slideExpiry);
if (identity || !authConfig.devAutoLogin) return identity;
const user = await users.findByUsername(authConfig.devAutoLogin);
if (!user || !user.active) return undefined;
return {
id: user.id,
username: user.username,
name: user.name,
role: user.role as UserRole,
mustChangePassword: false,
};
}
export function cookieOptions(authConfig: AuthConfig, maxAgeMs: number) {
return {
httpOnly: true,
@ -107,7 +132,7 @@ export function requireSameOriginForWrites(): RequestHandler {
export function createAuthGuard(
authConfig: AuthConfig,
sessions: SessionStore,
users: Pick<UserStore, "findById">,
users: Pick<UserStore, "findById" | "findByUsername">,
openPaths: readonly string[] = ["/auth/login", "/auth/status", "/auth/logout"]
): RequestHandler {
return (req: Request, res: Response, next: NextFunction) => {
@ -115,7 +140,7 @@ export function createAuthGuard(
next();
return;
}
identityFromRequest(req, sessions, users)
resolveRequestIdentity(req, authConfig, sessions, users)
.then((identity) => {
if (!identity) {
res.status(401).json({ error: "Nicht angemeldet." });