fix(12): WR-01 narrow TOCTOU guard and set claimed=true for OIDC inserts
- apps/api/src/auth/user.ts: upsertUser step-5 insert now sets claimed=true for all OIDC-created users. An identity-bound OIDC user is never a pending wizard bootstrap user; explicit claimed=true prevents ambiguity with the (oidcIss IS NULL AND claimed=false) sentinel used by the TOCTOU guard and isSetupLocked. First-login-claims path is unaffected (it updates a pre-existing oidcIss=null row; this change only touches the fresh insert). - apps/api/src/routes/setup.ts: TOCTOU guard in POST /credential now queries WHERE oidc_iss IS NULL AND claimed = false FOR UPDATE, matching the exact definition of a pending wizard bootstrap user. This provides defense-in-depth against any future path that could produce claimed=false OIDC rows. - apps/api/tests/auth/user.test.ts: new WR-01 test asserts that the fresh OIDC insert sets claimed=true in the values passed to db.insert(). - apps/api/tests/routes/setup.test.ts: new WR-01 integration test seeds an OIDC user with claimed=false (oidcIss NOT NULL) and verifies POST /credential still succeeds (guard ignores the OIDC row, only counts local wizard rows). All 402 API tests, 253 PWA tests, and typecheck pass. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
22d1581484
commit
687f9dc9fa
@@ -478,6 +478,39 @@ describe('POST /api/setup/credential', () => {
|
||||
const body = JSON.parse(bodyText) as { error: string };
|
||||
expect(body.error).toBe('Invalid request');
|
||||
});
|
||||
|
||||
// WR-01: TOCTOU guard must ignore OIDC users that happen to have claimed=false
|
||||
// (oidcIss IS NOT NULL). Only wizard bootstrap users (oidcIss IS NULL AND claimed=false)
|
||||
// must trigger the DUPLICATE_UNCLAIMED guard. This prevents a partially-bootstrapped
|
||||
// instance (where an OIDC user somehow exists pre-setup) from permanently blocking
|
||||
// the wizard credential step with 409.
|
||||
it('WR-01: TOCTOU guard ignores claimed=false OIDC users (oidcIss NOT NULL) — credential step still succeeds', async () => {
|
||||
// Seed an OIDC user with claimed=false to simulate the latent bug scenario.
|
||||
// After the WR-01 fix upsertUser always inserts claimed=true for OIDC users, but
|
||||
// this tests that the guard's WHERE clause is narrowed correctly for defense-in-depth.
|
||||
await db.insert(users).values({
|
||||
oidcIss: 'https://auth.test.setup',
|
||||
oidcSub: `sub-wr01-oidc-claimed-false-${Date.now()}`,
|
||||
displayName: 'OIDC User With claimed=false',
|
||||
color: '#4A90D9',
|
||||
isAdmin: false,
|
||||
claimed: false, // legacy/hypothetical — oidcIss is NOT NULL
|
||||
});
|
||||
|
||||
mockValidateCredentialShouldThrow = false;
|
||||
|
||||
const app = await getApp();
|
||||
const res = await app.fetch(
|
||||
jsonRequest('POST', '/api/setup/credential', {
|
||||
fastmailEmail: 'operator@fastmail.com',
|
||||
appPassword: 'valid-app-password-wr01',
|
||||
}),
|
||||
);
|
||||
// Must succeed — the OIDC-with-oidcIss row must NOT block the wizard
|
||||
expect(res.status).toBe(200);
|
||||
const body = (await res.json()) as { ok: boolean };
|
||||
expect(body.ok).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
// ===========================================================================
|
||||
|
||||
Reference in New Issue
Block a user