fix(oidc): a successful SSO login writes the user.login audit row

Every other login method writes one; the SSO callback touched last_login and
login_count and wrote nothing, so an instance moving its users to SSO watched
its audit log go quiet while looking complete. The row is written in the
callback, where the provider has vouched for the user and the client IP is,
next to the role-change row, with method: oidc in the details the way the
passkey login names its method.

Closes #2417
This commit is contained in:
Maurice
2026-09-20 22:28:35 +02:00
parent bf0507ddb5
commit cce64c4030
4 changed files with 59 additions and 3 deletions
+21
View File
@@ -190,6 +190,27 @@ describe('GET /api/auth/oidc/callback', () => {
expect(res.headers.location).toContain('/login?oidc_code=');
});
it('OIDC-004b: a successful login writes the user.login row every other method writes (#2417)', async () => {
const { user } = createUser(testDb, { email: 'audited@example.com' });
mockDiscover.mockResolvedValueOnce(MOCK_DISCOVERY_DOC);
mockExchangeCode.mockResolvedValueOnce({ access_token: 'test-access-token', id_token: 'fake.id.token', _ok: true, _status: 200 });
mockVerifyIdToken.mockResolvedValueOnce({ ok: true, claims: { sub: 'sub-audited-1' } });
mockGetUserInfo.mockResolvedValueOnce({ sub: 'sub-audited-1', email: 'audited@example.com', name: 'Audited', email_verified: true });
const { state } = oidcSvc.createState('http://localhost:3001/api/auth/oidc/callback');
const res = await request(app).get(`/api/auth/oidc/callback?code=authcode123&state=${state}`).set('Cookie', `trek_oidc_state=${state}`);
expect(res.status).toBe(302);
// The query from the report, against the same table.
const rows = testDb.prepare('SELECT user_id, action, details FROM audit_log WHERE user_id = ? ORDER BY id DESC').all(user.id) as
{ user_id: number; action: string; details: string | null }[];
expect(rows.map(r => r.action)).toContain('user.login');
const login = rows.find(r => r.action === 'user.login')!;
expect(JSON.parse(login.details || '{}')).toEqual({ method: 'oidc' });
const counted = testDb.prepare('SELECT login_count FROM users WHERE id = ?').get(user.id) as { login_count: number };
expect(counted.login_count).toBe(1);
});
it('OIDC-005: new user gets created when registration is open', async () => {
mockDiscover.mockResolvedValueOnce(MOCK_DISCOVERY_DOC);
mockExchangeCode.mockResolvedValueOnce({ access_token: 'new-token', id_token: 'fake.id.token', _ok: true, _status: 200 });