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
@@ -441,6 +441,55 @@ describe('upsertUser', () => {
|
||||
// select must only have been called once (identity lookup, then early-return)
|
||||
expect(mockDb.select).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
// WR-01: upsertUser's fresh OIDC insert must set claimed=true.
|
||||
// An OIDC-created user is identity-bound at insert time and must NOT be born
|
||||
// with claimed=false, which would make it indistinguishable from a pending
|
||||
// wizard bootstrap user (oidcIss IS NULL AND claimed=false) in the TOCTOU guard.
|
||||
it('WR-01: fresh OIDC insert sets claimed=true (OIDC user is never a pending wizard user)', async () => {
|
||||
const iss = 'https://auth.example.com';
|
||||
const sub = 'sub-wr01-claimed';
|
||||
|
||||
let selectCallCount = 0;
|
||||
mockDb.select.mockImplementation(() => {
|
||||
selectCallCount++;
|
||||
if (selectCallCount === 1) return makeSelectChain([]); // identity lookup — not found
|
||||
if (selectCallCount === 2) return makeSelectChain([]); // setup_complete — not set
|
||||
if (selectCallCount === 3) {
|
||||
// Used-colors query
|
||||
return { from: vi.fn().mockResolvedValue([]) };
|
||||
}
|
||||
if (selectCallCount === 4) {
|
||||
// Admin COUNT
|
||||
return makeSelectChain([{ count: 0 }]);
|
||||
}
|
||||
// Re-fetch after insert
|
||||
return makeSelectChain([
|
||||
{
|
||||
id: 20,
|
||||
oidcIss: iss,
|
||||
oidcSub: sub,
|
||||
displayName: null,
|
||||
color: COLOR_PALETTE[0],
|
||||
isAdmin: true,
|
||||
claimed: true,
|
||||
createdAt: new Date(),
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
mockDb.insert.mockReturnValue(makeInsertChain([{ id: 20 }]));
|
||||
|
||||
await upsertUser(iss, sub);
|
||||
|
||||
// The insert values must include claimed: true
|
||||
const insertValues = mockDb.insert.mock.results[0]?.value?.values.mock.calls[0]?.[0];
|
||||
expect(insertValues).toBeDefined();
|
||||
expect(insertValues.claimed).toBe(true);
|
||||
// And it must carry a real oidcIss (not null — distinguishable from wizard row)
|
||||
expect(insertValues.oidcIss).toBe(iss);
|
||||
expect(insertValues.oidcSub).toBe(sub);
|
||||
});
|
||||
});
|
||||
|
||||
// ── Plan 12-03: D-08 first-login-claims (Wave-2 GREEN — full test implementations) ──
|
||||
|
||||
Reference in New Issue
Block a user