fix(auth): setting first dashboard login password no longer fails with HTTP 400 PASSWORD_REQUIRED (#8950)

This commit is contained in:
Diego Rodrigues de Sa e Souza
2026-08-04 21:35:29 -03:00
committed by GitHub
parent 28a1f4d1b6
commit 0b70a14a3b
3 changed files with 93 additions and 1 deletions

View File

@@ -0,0 +1 @@
- fix(auth): setting first dashboard login password no longer fails with HTTP 400 PASSWORD_REQUIRED (#8950)

View File

@@ -319,7 +319,12 @@ export async function PATCH(request: Request) {
// honoured before T-011 — when no password is configured yet AND login
// is currently disabled, allow the first write to set policy (incl.
// the password itself). Once a hash exists the gate always fires.
const isColdBoot = !storedPasswordHash && passwordState.settings.requireLogin === false;
// #8950: also treat the request as cold boot when newPassword is present
// without a stored hash, so the Security tab's two-step flow (enable
// requireLogin first, then set password) does not deadlock.
const isColdBoot =
!storedPasswordHash &&
(passwordState.settings.requireLogin === false || Boolean(body.newPassword));
if (!isColdBoot) {
if (!body.currentPassword) {
emitSettingsFailureAudit(request, actor, "PASSWORD_REQUIRED", attemptedKeys);

View File

@@ -0,0 +1,86 @@
/**
* REPRO #8950 — Setting the first dashboard login password fails with HTTP 400
* PASSWORD_REQUIRED, deadlocking every fresh install.
*
* Root cause: isColdBoot only fires while requireLogin===false, but the
* Security tab forces requireLogin ON before the password form is reachable,
* so the first newPassword write always demands a currentPassword that cannot
* exist yet.
*
* Fix: add `|| Boolean(body.newPassword)` to the cold-boot condition so that
* setting the first password is always treated as cold boot, regardless of
* the current requireLogin state.
*
* Regression guard: once a password hash exists, the gate fires as before
* (currentPassword required for security-impacting changes).
*/
import test from "node:test";
import assert from "node:assert/strict";
import { setupSettingsFixture, mockSettings } from "../_mocks/settings.ts";
import { makeManagementSessionRequest } from "../../helpers/managementSession.ts";
const fixture = setupSettingsFixture("probe-8950");
process.env.OMNIROUTE_DISABLE_REDIS_AUTH_CACHE = "1";
const core = await import("../../../src/lib/db/core.ts");
const settingsDb = await import("../../../src/lib/db/settings.ts");
const runtime = await import("../../../src/lib/config/runtimeSettings.ts");
const settingsRoute = await import("../../../src/app/api/settings/route.ts");
const managementPassword = await import("../../../src/lib/auth/managementPassword.ts");
test.beforeEach(async () => {
await fixture.resetStorage();
runtime.resetRuntimeSettingsStateForTests();
});
test.after(() => {
core.resetDbInstance();
fixture.cleanup();
});
test("REPRO #8950: setting first password after requireLogin enabled should succeed", async () => {
// Simulate fresh install: no password hash, requireLogin is false.
await mockSettings({ setupComplete: true, requireLogin: false });
// Step 1: Enable requireLogin (what the Security tab does when you open it).
const step1 = await settingsRoute.PATCH(
await makeManagementSessionRequest("http://localhost/api/settings", {
method: "PATCH",
body: { requireLogin: true },
})
);
assert.equal(
step1.status,
200,
`Step 1: enabling requireLogin should succeed, got ${step1.status}`
);
// Step 2: Set the first password (no currentPassword because none exists yet).
const step2 = await settingsRoute.PATCH(
await makeManagementSessionRequest("http://localhost/api/settings", {
method: "PATCH",
body: { newPassword: "my-first-password" },
})
);
// REPRO: this fails with 400 PASSWORD_REQUIRED because isColdBoot only
// checks requireLogin===false, but the DB now has requireLogin=true.
assert.equal(
step2.status,
200,
`Step 2: first password write should succeed without currentPassword, got ${step2.status}`
);
const step2Body = (await step2.json()) as Record<string, unknown>;
assert.equal(
step2Body.error,
undefined,
`Step 2 response should not have an error: ${JSON.stringify(step2Body)}`
);
// Verify the password was actually stored.
const configured = managementPassword.hasManagementPasswordConfigured(
(await settingsDb.getSettings()) as Record<string, unknown>
);
assert.equal(configured, true, "management password should be configured after first write");
});