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:
parent
4677b1685c
commit
084002bea9
11 changed files with 211 additions and 8 deletions
|
|
@ -17,8 +17,11 @@ import { hashPassword, verifyPassword } from "../src/server/auth/password.js";
|
|||
import { SessionStore } from "../src/server/auth/sessions.js";
|
||||
import { LoginThrottle } from "../src/server/auth/login-throttle.js";
|
||||
import { readCookie } from "../src/server/auth/cookie.js";
|
||||
import { requireSameOriginForWrites } from "../src/server/auth/auth.middleware.js";
|
||||
import type { AuthConfig } from "../src/server/auth/auth-config.js";
|
||||
import {
|
||||
createAuthGuard,
|
||||
requireSameOriginForWrites,
|
||||
} from "../src/server/auth/auth.middleware.js";
|
||||
import { readAuthConfig, type AuthConfig } from "../src/server/auth/auth-config.js";
|
||||
import { createUserSchema } from "../src/shared/validation/user.schemas.js";
|
||||
import { loginSchema } from "../src/shared/validation/auth.schemas.js";
|
||||
import {
|
||||
|
|
@ -37,6 +40,7 @@ function createUserService() {
|
|||
sessionMaxAgeMs: 7 * 24 * HOUR,
|
||||
cookieSecure: false,
|
||||
bootstrap: { username: "admin", password: "bootstrap-password-123" },
|
||||
devAutoLogin: null,
|
||||
};
|
||||
return {
|
||||
repository,
|
||||
|
|
@ -202,6 +206,122 @@ describe("same-origin guard for writes", () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe("development auto-login", () => {
|
||||
const baseConfig = {
|
||||
sessionTtlMs: 12 * HOUR,
|
||||
sessionMaxAgeMs: 7 * 24 * HOUR,
|
||||
cookieSecure: false,
|
||||
bootstrap: null,
|
||||
};
|
||||
|
||||
// Runs the /api auth guard for one request without a session cookie.
|
||||
async function guard(
|
||||
users: UserRepository,
|
||||
devAutoLogin: string | null,
|
||||
sessions = new SessionStore(HOUR, 24 * HOUR),
|
||||
cookie?: string
|
||||
) {
|
||||
const req = {
|
||||
path: "/projects",
|
||||
headers: cookie ? { cookie } : {},
|
||||
} as unknown as Request;
|
||||
let status: number | undefined;
|
||||
const res = {
|
||||
status(code: number) {
|
||||
status = code;
|
||||
return this;
|
||||
},
|
||||
json() {
|
||||
return this;
|
||||
},
|
||||
cookie() {
|
||||
return this;
|
||||
},
|
||||
} as unknown as Response;
|
||||
const passed = await new Promise<boolean>((resolve, reject) => {
|
||||
const middleware = createAuthGuard(
|
||||
{ ...baseConfig, devAutoLogin },
|
||||
sessions,
|
||||
users
|
||||
);
|
||||
middleware(req, res, (error?: unknown) =>
|
||||
error ? reject(error) : resolve(true)
|
||||
);
|
||||
setTimeout(() => resolve(false), 200);
|
||||
});
|
||||
return { passed, status, authUser: req.authUser };
|
||||
}
|
||||
|
||||
it("is off unless AUTH_DEV_AUTOLOGIN names a user", () => {
|
||||
assert.equal(readAuthConfig({}).devAutoLogin, null);
|
||||
assert.equal(readAuthConfig({ AUTH_DEV_AUTOLOGIN: " " }).devAutoLogin, null);
|
||||
assert.equal(readAuthConfig({ AUTH_DEV_AUTOLOGIN: " Julian " }).devAutoLogin, "julian");
|
||||
});
|
||||
|
||||
it("signs every request without a session in as the configured active user", async () => {
|
||||
const { repository, service } = createUserService();
|
||||
const user = await service.createUser({
|
||||
username: "julian",
|
||||
name: "Julian",
|
||||
role: "admin",
|
||||
initialPassword: "start-passwort",
|
||||
});
|
||||
assert.ok(user);
|
||||
|
||||
const result = await guard(repository, "julian");
|
||||
assert.equal(result.passed, true);
|
||||
assert.equal(result.authUser?.id, user.id);
|
||||
assert.equal(result.authUser?.role, "admin");
|
||||
// A development instance must not stop at the forced password change.
|
||||
assert.equal(result.authUser?.mustChangePassword, false);
|
||||
});
|
||||
|
||||
it("still requires a login without the switch, for unknown and for deactivated users", async () => {
|
||||
const { repository, service } = createUserService();
|
||||
const user = await service.createUser({
|
||||
username: "julian",
|
||||
name: "Julian",
|
||||
role: "admin",
|
||||
initialPassword: "start-passwort",
|
||||
});
|
||||
assert.ok(user);
|
||||
assert.deepEqual(
|
||||
{ passed: (await guard(repository, null)).passed, status: (await guard(repository, null)).status },
|
||||
{ passed: false, status: 401 }
|
||||
);
|
||||
assert.equal((await guard(repository, "niemand")).status, 401);
|
||||
await service.createUser({
|
||||
username: "zweiter",
|
||||
name: "Zweiter",
|
||||
role: "admin",
|
||||
initialPassword: "start-passwort",
|
||||
});
|
||||
await service.updateRoleAndActive(user.id, { active: false });
|
||||
assert.equal((await guard(repository, "julian")).status, 401);
|
||||
});
|
||||
|
||||
it("keeps a real session in front of the auto-login user", async () => {
|
||||
const { repository, service } = createUserService();
|
||||
await service.createUser({
|
||||
username: "julian",
|
||||
name: "Julian",
|
||||
role: "admin",
|
||||
initialPassword: "start-passwort",
|
||||
});
|
||||
const other = await service.createUser({
|
||||
username: "max",
|
||||
name: "Max",
|
||||
role: "user",
|
||||
initialPassword: "start-passwort",
|
||||
});
|
||||
assert.ok(other);
|
||||
const sessions = new SessionStore(HOUR, 24 * HOUR);
|
||||
const sessionId = sessions.create(other.id).id;
|
||||
const result = await guard(repository, "julian", sessions, `lb_session=${sessionId}`);
|
||||
assert.equal(result.authUser?.username, "max");
|
||||
});
|
||||
});
|
||||
|
||||
describe("input validation", () => {
|
||||
it("rejects look-alike and whitespace usernames and normalizes case", () => {
|
||||
const base = { name: "Test", role: "user", initialPassword: "start-passwort" };
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue