mbkauthe 4.9.0 → 5.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/docs/api.md +29 -178
- package/docs/db.md +1 -1
- package/docs/db.sql +305 -253
- package/index.js +5 -3
- package/lib/config/cookies.js +84 -18
- package/lib/config/index.js +3 -1
- package/lib/config/tokenScopes.js +1 -1
- package/lib/createTable.js +95 -8
- package/lib/db/AuthRepository.js +57 -16
- package/lib/db/BaseRepository.js +9 -1
- package/lib/db/dialects/postgres.js +1 -1
- package/lib/main.js +5 -5
- package/lib/middleware/auth.js +201 -218
- package/lib/middleware/index.js +13 -14
- package/lib/middleware/scopeValidator.js +8 -3
- package/lib/pool.js +5 -6
- package/lib/routes/auth.js +42 -47
- package/lib/routes/dbLogs.js +247 -29
- package/lib/routes/misc.js +6 -4
- package/lib/routes/oauth.js +19 -23
- package/lib/utils/dbQueryLogger.js +485 -80
- package/lib/utils/errors.js +1 -1
- package/lib/utils/logger.js +12 -0
- package/lib/utils/timingSafeToken.js +1 -1
- package/package.json +1 -1
- package/public/main.css +1 -1
- package/test.spec.js +515 -48
- package/views/pages/dbLogs.handlebars +618 -420
package/test.spec.js
CHANGED
|
@@ -1,37 +1,186 @@
|
|
|
1
1
|
import request from 'supertest';
|
|
2
|
+
import express from 'express';
|
|
3
|
+
import { engine } from 'express-handlebars';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import { fileURLToPath } from 'url';
|
|
6
|
+
import { jest } from '@jest/globals';
|
|
2
7
|
|
|
3
|
-
const
|
|
8
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
9
|
+
const __dirname = path.dirname(__filename);
|
|
10
|
+
|
|
11
|
+
process.env.test = 'dev';
|
|
12
|
+
process.env.env = 'dev';
|
|
13
|
+
process.env.dbLogs = 'true';
|
|
14
|
+
process.env.dbLogsCallsite = 'false';
|
|
15
|
+
|
|
16
|
+
const { default: router } = await import('./lib/main.js');
|
|
17
|
+
const { packageJson } = await import('./lib/config/index.js');
|
|
18
|
+
const { dblogin } = await import('./lib/pool.js');
|
|
19
|
+
const {
|
|
20
|
+
attachDevQueryLogger,
|
|
21
|
+
resetQueryCount,
|
|
22
|
+
resetQueryLog,
|
|
23
|
+
runWithRequestContext
|
|
24
|
+
} = await import('./lib/utils/dbQueryLogger.js');
|
|
25
|
+
|
|
26
|
+
const viewsPath = path.join(__dirname, 'views');
|
|
27
|
+
|
|
28
|
+
const handlebarsHelpers = {
|
|
29
|
+
eq: (a, b) => a === b,
|
|
30
|
+
encodeURIComponent: (str) => encodeURIComponent(str),
|
|
31
|
+
formatTimestamp: (timestamp) => new Date(timestamp).toLocaleString(),
|
|
32
|
+
jsonStringify: (context) => JSON.stringify(context),
|
|
33
|
+
json: (obj) => JSON.stringify(obj, null, 2),
|
|
34
|
+
objectEntries: (obj) => {
|
|
35
|
+
if (!obj || typeof obj !== 'object') return [];
|
|
36
|
+
return Object.entries(obj).map(([key, value]) => ({ key, value }));
|
|
37
|
+
},
|
|
38
|
+
cacheBuster: () => `?v=${packageJson.version}`
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const app = express();
|
|
42
|
+
app.set('views', [
|
|
43
|
+
viewsPath,
|
|
44
|
+
path.join(__dirname, 'node_modules/mbkauthe/views')
|
|
45
|
+
]);
|
|
46
|
+
app.engine('handlebars', engine({
|
|
47
|
+
defaultLayout: false,
|
|
48
|
+
cache: true,
|
|
49
|
+
partialsDir: [
|
|
50
|
+
viewsPath,
|
|
51
|
+
path.join(__dirname, 'node_modules/mbkauthe/views'),
|
|
52
|
+
path.join(__dirname, 'node_modules/mbkauthe/views/Error'),
|
|
53
|
+
],
|
|
54
|
+
helpers: handlebarsHelpers
|
|
55
|
+
}));
|
|
56
|
+
app.set('view engine', 'handlebars');
|
|
57
|
+
app.use(router);
|
|
58
|
+
|
|
59
|
+
const shouldSilenceConsole = (args) => {
|
|
60
|
+
const [firstArg = ''] = args;
|
|
61
|
+
const text = typeof firstArg === 'string' ? firstArg : '';
|
|
62
|
+
|
|
63
|
+
return text.includes('[mbkauthe]');
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const originalConsoleLog = console.log;
|
|
67
|
+
const originalConsoleWarn = console.warn;
|
|
68
|
+
const originalConsoleError = console.error;
|
|
69
|
+
|
|
70
|
+
beforeAll(() => {
|
|
71
|
+
jest.spyOn(console, 'log').mockImplementation((...args) => {
|
|
72
|
+
if (!shouldSilenceConsole(args)) {
|
|
73
|
+
originalConsoleLog(...args);
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
jest.spyOn(console, 'warn').mockImplementation((...args) => {
|
|
78
|
+
if (!shouldSilenceConsole(args)) {
|
|
79
|
+
originalConsoleWarn(...args);
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
jest.spyOn(console, 'error').mockImplementation((...args) => {
|
|
84
|
+
if (!shouldSilenceConsole(args)) {
|
|
85
|
+
originalConsoleError(...args);
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
afterAll(async () => {
|
|
91
|
+
jest.restoreAllMocks();
|
|
92
|
+
await dblogin.end().catch(() => {});
|
|
93
|
+
});
|
|
4
94
|
|
|
5
95
|
// Helper to get CSRF token and cookies
|
|
6
96
|
const getCSRFTokenAndCookies = async () => {
|
|
7
|
-
const response = await request(
|
|
97
|
+
const response = await request(app).get('/mbkauthe/login');
|
|
8
98
|
const html = response.text;
|
|
9
|
-
const csrfMatch = html.match(/name="_csrf".*?value="([^"]+)"/i) ||
|
|
10
|
-
|
|
11
|
-
|
|
99
|
+
const csrfMatch = html.match(/name="_csrf".*?value="([^"]+)"/i) ||
|
|
100
|
+
html.match(/content="([^"]+)".*?name="_csrf"/i);
|
|
101
|
+
|
|
12
102
|
return {
|
|
13
103
|
csrfToken: csrfMatch?.[1] || '',
|
|
14
104
|
cookies: response.headers['set-cookie'] || []
|
|
15
105
|
};
|
|
16
106
|
};
|
|
17
107
|
|
|
108
|
+
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
109
|
+
|
|
110
|
+
const createFakePool = ({ name = 'fake-db-pool' } = {}) => {
|
|
111
|
+
const pool = {
|
|
112
|
+
totalCount: 1,
|
|
113
|
+
idleCount: 1,
|
|
114
|
+
waitingCount: 0,
|
|
115
|
+
options: { application_name: name },
|
|
116
|
+
async connect() {
|
|
117
|
+
pool.idleCount = 0;
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
async query(configOrText, maybeValues) {
|
|
121
|
+
const text = typeof configOrText === 'string' ? configOrText : configOrText?.text || '';
|
|
122
|
+
const values = Array.isArray(maybeValues)
|
|
123
|
+
? maybeValues
|
|
124
|
+
: Array.isArray(configOrText?.values)
|
|
125
|
+
? configOrText.values
|
|
126
|
+
: [];
|
|
127
|
+
|
|
128
|
+
if (text.includes('slow_table')) {
|
|
129
|
+
await sleep(6);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (text.includes('broken_table')) {
|
|
133
|
+
const error = new Error('broken query');
|
|
134
|
+
error.code = 'FAKE_ERR';
|
|
135
|
+
throw error;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return {
|
|
139
|
+
command: 'SELECT',
|
|
140
|
+
rowCount: values.length ? 1 : 0,
|
|
141
|
+
rows: values.length ? [{ value: values[0] }] : [],
|
|
142
|
+
};
|
|
143
|
+
},
|
|
144
|
+
release() {
|
|
145
|
+
pool.idleCount = 1;
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
},
|
|
149
|
+
async query(configOrText, maybeValues) {
|
|
150
|
+
const client = await this.connect();
|
|
151
|
+
try {
|
|
152
|
+
return await client.query(configOrText, maybeValues);
|
|
153
|
+
} finally {
|
|
154
|
+
client.release();
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
attachDevQueryLogger(pool);
|
|
160
|
+
return pool;
|
|
161
|
+
};
|
|
162
|
+
|
|
18
163
|
describe('mbkauthe Routes', () => {
|
|
19
|
-
|
|
164
|
+
beforeEach(() => {
|
|
165
|
+
resetQueryCount();
|
|
166
|
+
resetQueryLog();
|
|
167
|
+
});
|
|
168
|
+
|
|
20
169
|
describe('Redirect Routes', () => {
|
|
21
170
|
test('GET /login redirects to /mbkauthe/login', async () => {
|
|
22
|
-
const response = await request(
|
|
171
|
+
const response = await request(app)
|
|
23
172
|
.get('/login')
|
|
24
173
|
.redirects(0);
|
|
25
|
-
|
|
174
|
+
|
|
26
175
|
expect(response.status).toBe(302);
|
|
27
176
|
expect(response.headers.location).toContain('/mbkauthe/login');
|
|
28
177
|
});
|
|
29
178
|
|
|
30
179
|
test('GET /signin redirects to /mbkauthe/login', async () => {
|
|
31
|
-
const response = await request(
|
|
180
|
+
const response = await request(app)
|
|
32
181
|
.get('/signin')
|
|
33
182
|
.redirects(0);
|
|
34
|
-
|
|
183
|
+
|
|
35
184
|
expect(response.status).toBe(302);
|
|
36
185
|
expect(response.headers.location).toContain('/mbkauthe/login');
|
|
37
186
|
});
|
|
@@ -39,17 +188,24 @@ describe('mbkauthe Routes', () => {
|
|
|
39
188
|
|
|
40
189
|
describe('Authentication Pages', () => {
|
|
41
190
|
test('GET /mbkauthe/login contains loginUsername input', async () => {
|
|
42
|
-
const response = await request(
|
|
43
|
-
|
|
191
|
+
const response = await request(app).get('/mbkauthe/login');
|
|
192
|
+
|
|
44
193
|
expect(response.status).toBe(200);
|
|
45
194
|
expect(response.text).toMatch(/id\s*=\s*["']loginUsername["']/i);
|
|
46
195
|
});
|
|
47
196
|
|
|
197
|
+
test('GET /mbkauthe/login renders csrf token field', async () => {
|
|
198
|
+
const response = await request(app).get('/mbkauthe/login');
|
|
199
|
+
|
|
200
|
+
expect(response.status).toBe(200);
|
|
201
|
+
expect(response.text).toMatch(/name\s*=\s*["']_csrf["']/i);
|
|
202
|
+
});
|
|
203
|
+
|
|
48
204
|
test('GET /mbkauthe/2fa contains token input or redirects', async () => {
|
|
49
|
-
const response = await request(
|
|
205
|
+
const response = await request(app)
|
|
50
206
|
.get('/mbkauthe/2fa')
|
|
51
207
|
.redirects(0);
|
|
52
|
-
|
|
208
|
+
|
|
53
209
|
if (response.status === 302) {
|
|
54
210
|
expect(response.headers.location).toContain('/mbkauthe/login');
|
|
55
211
|
} else {
|
|
@@ -63,40 +219,51 @@ describe('mbkauthe Routes', () => {
|
|
|
63
219
|
test.each([
|
|
64
220
|
['/mbkauthe/info', 'info page'],
|
|
65
221
|
['/mbkauthe/i', 'info short page'],
|
|
66
|
-
])('GET %s contains CurrentVersion div', async (
|
|
67
|
-
const response = await request(
|
|
68
|
-
|
|
222
|
+
])('GET %s contains CurrentVersion div', async (routePath) => {
|
|
223
|
+
const response = await request(app).get(routePath);
|
|
224
|
+
|
|
69
225
|
expect(response.status).toBe(200);
|
|
70
226
|
expect(response.text).toMatch(/id\s*=\s*["']CurrentVersion["']/i);
|
|
71
227
|
});
|
|
72
228
|
|
|
73
229
|
test('GET /mbkauthe/ErrorCode contains error-603 div', async () => {
|
|
74
|
-
const response = await request(
|
|
75
|
-
|
|
230
|
+
const response = await request(app).get('/mbkauthe/ErrorCode');
|
|
231
|
+
|
|
76
232
|
expect(response.status).toBe(200);
|
|
77
233
|
expect(response.text).toMatch(/id\s*=\s*["']error-603["']/i);
|
|
78
234
|
});
|
|
235
|
+
|
|
236
|
+
test('GET /mbkauthe/db renders DB monitor filters and summary sections', async () => {
|
|
237
|
+
const response = await request(app).get('/mbkauthe/db');
|
|
238
|
+
|
|
239
|
+
expect(response.status).toBe(200);
|
|
240
|
+
expect(response.text).toContain('DB Query Monitor');
|
|
241
|
+
expect(response.text).toContain('Top Repeated Query Shapes');
|
|
242
|
+
expect(response.text).toContain('Slowest Recent Queries');
|
|
243
|
+
expect(response.text).toMatch(/name="username"/i);
|
|
244
|
+
expect(response.text).toMatch(/name="url"/i);
|
|
245
|
+
expect(response.text).toMatch(/name="success"/i);
|
|
246
|
+
});
|
|
79
247
|
});
|
|
80
248
|
|
|
81
249
|
describe('Static Assets', () => {
|
|
82
250
|
test('GET /mbkauthe/main.js returns JavaScript', async () => {
|
|
83
|
-
const response = await request(
|
|
84
|
-
|
|
251
|
+
const response = await request(app).get('/mbkauthe/main.js');
|
|
252
|
+
|
|
85
253
|
expect(response.status).toBe(200);
|
|
86
254
|
expect(response.headers['content-type']).toContain('javascript');
|
|
87
255
|
});
|
|
88
256
|
|
|
89
257
|
test('GET /icon.svg returns SVG content', async () => {
|
|
90
|
-
const response = await request(
|
|
91
|
-
|
|
258
|
+
const response = await request(app).get('/icon.svg');
|
|
259
|
+
|
|
92
260
|
expect(response.status).toBe(200);
|
|
93
261
|
expect(response.headers['content-type']).toContain('image/png');
|
|
94
|
-
|
|
95
262
|
});
|
|
96
263
|
|
|
97
264
|
test('GET /mbkauthe/bg.webp returns WEBP content', async () => {
|
|
98
|
-
const response = await request(
|
|
99
|
-
|
|
265
|
+
const response = await request(app).get('/mbkauthe/bg.webp');
|
|
266
|
+
|
|
100
267
|
expect(response.status).toBe(200);
|
|
101
268
|
expect(response.headers['content-type']).toContain('image/webp');
|
|
102
269
|
});
|
|
@@ -104,12 +271,12 @@ describe('mbkauthe Routes', () => {
|
|
|
104
271
|
|
|
105
272
|
describe('Protected Routes', () => {
|
|
106
273
|
test('GET /mbkauthe/test responds appropriately', async () => {
|
|
107
|
-
const response = await request(
|
|
274
|
+
const response = await request(app).get('/mbkauthe/test');
|
|
108
275
|
expect([200, 302, 401, 403, 429]).toContain(response.status);
|
|
109
276
|
});
|
|
110
277
|
|
|
111
278
|
test('GET /mbkauthe/test with curl UA returns JSON 401', async () => {
|
|
112
|
-
const response = await request(
|
|
279
|
+
const response = await request(app)
|
|
113
280
|
.get('/mbkauthe/test')
|
|
114
281
|
.set('User-Agent', 'curl/8.0.1')
|
|
115
282
|
.set('Accept', '*/*');
|
|
@@ -119,16 +286,28 @@ describe('mbkauthe Routes', () => {
|
|
|
119
286
|
expect(response.body).toHaveProperty('success', false);
|
|
120
287
|
expect(response.body).toHaveProperty('errorCode');
|
|
121
288
|
});
|
|
289
|
+
|
|
290
|
+
test('GET /mbkauthe/test with browser accept redirects to login when unauthenticated', async () => {
|
|
291
|
+
const response = await request(app)
|
|
292
|
+
.get('/mbkauthe/test')
|
|
293
|
+
.set('User-Agent', 'Mozilla/5.0')
|
|
294
|
+
.set('Accept', 'text/html')
|
|
295
|
+
.redirects(0);
|
|
296
|
+
|
|
297
|
+
expect(response.status).toBe(302);
|
|
298
|
+
expect(response.headers.location).toContain('/mbkauthe/login');
|
|
299
|
+
expect(response.headers.location).toContain('reason=logged_out');
|
|
300
|
+
});
|
|
122
301
|
});
|
|
123
302
|
|
|
124
303
|
describe('OAuth Routes', () => {
|
|
125
304
|
test('GET /mbkauthe/api/github/login handles GitHub App flow', async () => {
|
|
126
|
-
const response = await request(
|
|
305
|
+
const response = await request(app)
|
|
127
306
|
.get('/mbkauthe/api/github/login')
|
|
128
307
|
.redirects(0);
|
|
129
|
-
|
|
308
|
+
|
|
130
309
|
expect([200, 302, 403, 500, 429]).toContain(response.status);
|
|
131
|
-
|
|
310
|
+
|
|
132
311
|
if (response.status === 302) {
|
|
133
312
|
const location = response.headers.location;
|
|
134
313
|
expect(location).toMatch(/github\.com|login/);
|
|
@@ -136,17 +315,17 @@ describe('mbkauthe Routes', () => {
|
|
|
136
315
|
});
|
|
137
316
|
|
|
138
317
|
test('GET /mbkauthe/api/github/login/callback handles GitHub App callback', async () => {
|
|
139
|
-
const response = await request(
|
|
318
|
+
const response = await request(app).get('/mbkauthe/api/github/login/callback');
|
|
140
319
|
expect([200, 302, 400, 401, 403, 429]).toContain(response.status);
|
|
141
320
|
});
|
|
142
321
|
|
|
143
322
|
test('GET /mbkauthe/api/google/login handles Google OAuth', async () => {
|
|
144
|
-
const response = await request(
|
|
323
|
+
const response = await request(app)
|
|
145
324
|
.get('/mbkauthe/api/google/login')
|
|
146
325
|
.redirects(0);
|
|
147
|
-
|
|
326
|
+
|
|
148
327
|
expect([200, 302, 403, 500, 429]).toContain(response.status);
|
|
149
|
-
|
|
328
|
+
|
|
150
329
|
if (response.status === 302) {
|
|
151
330
|
const location = response.headers.location;
|
|
152
331
|
expect(location).toMatch(/accounts\.google\.com|login/);
|
|
@@ -154,43 +333,247 @@ describe('mbkauthe Routes', () => {
|
|
|
154
333
|
});
|
|
155
334
|
|
|
156
335
|
test('GET /mbkauthe/api/google/login/callback handles callback', async () => {
|
|
157
|
-
const response = await request(
|
|
336
|
+
const response = await request(app).get('/mbkauthe/api/google/login/callback');
|
|
158
337
|
expect([200, 302, 400, 401, 403, 429]).toContain(response.status);
|
|
159
338
|
});
|
|
339
|
+
|
|
340
|
+
test('GET /mbkauthe/api/github/login rejects unsafe redirect targets', async () => {
|
|
341
|
+
const response = await request(app)
|
|
342
|
+
.get('/mbkauthe/api/github/login?redirect=https://evil.example')
|
|
343
|
+
.redirects(0);
|
|
344
|
+
|
|
345
|
+
expect([200, 302, 403, 500, 429]).toContain(response.status);
|
|
346
|
+
if (response.status === 302) {
|
|
347
|
+
expect(response.headers.location).not.toContain('evil.example');
|
|
348
|
+
}
|
|
349
|
+
});
|
|
160
350
|
});
|
|
161
351
|
|
|
162
352
|
describe('API Endpoints', () => {
|
|
353
|
+
test('GET /mbkauthe/db.json returns newest-first logs with summary stats and fingerprints', async () => {
|
|
354
|
+
const fakePool = createFakePool({ name: 'reporting-db' });
|
|
355
|
+
|
|
356
|
+
await runWithRequestContext(
|
|
357
|
+
{
|
|
358
|
+
method: 'POST',
|
|
359
|
+
originalUrl: '/mbkauthe/api/login',
|
|
360
|
+
url: '/mbkauthe/api/login',
|
|
361
|
+
ip: '127.0.0.1',
|
|
362
|
+
session: { user: { id: '1', username: 'support' } }
|
|
363
|
+
},
|
|
364
|
+
() => fakePool.query({ text: 'SELECT * FROM users WHERE id = $1', values: [1], name: 'userLookup' })
|
|
365
|
+
);
|
|
366
|
+
|
|
367
|
+
await sleep(4);
|
|
368
|
+
|
|
369
|
+
await runWithRequestContext(
|
|
370
|
+
{
|
|
371
|
+
method: 'POST',
|
|
372
|
+
originalUrl: '/mbkauthe/api/login',
|
|
373
|
+
url: '/mbkauthe/api/login',
|
|
374
|
+
ip: '127.0.0.1',
|
|
375
|
+
session: { user: { id: '1', username: 'support' } }
|
|
376
|
+
},
|
|
377
|
+
() => fakePool.query({ text: 'SELECT * FROM users WHERE id = $1', values: [2], name: 'userLookup' })
|
|
378
|
+
);
|
|
379
|
+
|
|
380
|
+
await sleep(4);
|
|
381
|
+
|
|
382
|
+
await runWithRequestContext(
|
|
383
|
+
{
|
|
384
|
+
method: 'GET',
|
|
385
|
+
originalUrl: '/mbkauthe/api/audit',
|
|
386
|
+
url: '/mbkauthe/api/audit',
|
|
387
|
+
ip: '127.0.0.1',
|
|
388
|
+
session: { user: { id: '2', username: 'auditor' } }
|
|
389
|
+
},
|
|
390
|
+
() => fakePool.query({ text: 'SELECT * FROM slow_table WHERE team_id = $1', values: [55], name: 'slowAudit' })
|
|
391
|
+
);
|
|
392
|
+
|
|
393
|
+
await sleep(4);
|
|
394
|
+
|
|
395
|
+
await runWithRequestContext(
|
|
396
|
+
{
|
|
397
|
+
method: 'GET',
|
|
398
|
+
originalUrl: '/mbkauthe/api/admin',
|
|
399
|
+
url: '/mbkauthe/api/admin',
|
|
400
|
+
ip: '127.0.0.1',
|
|
401
|
+
session: { user: { id: '3', username: 'admin' } }
|
|
402
|
+
},
|
|
403
|
+
async () => {
|
|
404
|
+
await fakePool.query({ text: 'SELECT * FROM broken_table WHERE id = $1', values: [99], name: 'brokenLookup' })
|
|
405
|
+
.catch(() => {});
|
|
406
|
+
}
|
|
407
|
+
);
|
|
408
|
+
|
|
409
|
+
const response = await request(app).get('/mbkauthe/db.json?limit=10');
|
|
410
|
+
|
|
411
|
+
expect(response.status).toBe(200);
|
|
412
|
+
expect(response.body.isDev).toBe(true);
|
|
413
|
+
expect(response.body.queryCount).toBe(4);
|
|
414
|
+
expect(response.body.summary.totalVisible).toBe(4);
|
|
415
|
+
expect(response.body.summary.errorCount).toBe(1);
|
|
416
|
+
expect(response.body.queryLog).toHaveLength(4);
|
|
417
|
+
expect(response.body.queryLog[0].query).toContain('broken_table');
|
|
418
|
+
expect(response.body.queryLog[1].query).toContain('slow_table');
|
|
419
|
+
expect(response.body.queryLog[0].fingerprint).toMatch(/^[0-9a-f]{12}$/);
|
|
420
|
+
expect(response.body.queryLog[0].poolWait).toMatchObject({
|
|
421
|
+
source: 'pool.query',
|
|
422
|
+
captured: true
|
|
423
|
+
});
|
|
424
|
+
expect(response.body.queryLog[0].trigger).toMatchObject({
|
|
425
|
+
type: 'request',
|
|
426
|
+
source: 'route'
|
|
427
|
+
});
|
|
428
|
+
expect(response.body.summary.slowestQueries[0].query).toContain('slow_table');
|
|
429
|
+
expect(response.body.summary.repeatedGroups[0]).toMatchObject({
|
|
430
|
+
count: 2,
|
|
431
|
+
sampleName: 'userLookup'
|
|
432
|
+
});
|
|
433
|
+
expect(response.body.summary.repeatedGroups[0].fingerprint).toMatch(/^[0-9a-f]{12}$/);
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
test('GET /mbkauthe/db.json filters by username, url, and success', async () => {
|
|
437
|
+
const fakePool = createFakePool({ name: 'filter-db' });
|
|
438
|
+
|
|
439
|
+
await runWithRequestContext(
|
|
440
|
+
{
|
|
441
|
+
method: 'POST',
|
|
442
|
+
originalUrl: '/mbkauthe/api/login',
|
|
443
|
+
url: '/mbkauthe/api/login',
|
|
444
|
+
ip: '127.0.0.1',
|
|
445
|
+
session: { user: { id: '1', username: 'support' } }
|
|
446
|
+
},
|
|
447
|
+
() => fakePool.query({ text: 'SELECT * FROM users WHERE id = $1', values: [7], name: 'loginLookup' })
|
|
448
|
+
);
|
|
449
|
+
|
|
450
|
+
await runWithRequestContext(
|
|
451
|
+
{
|
|
452
|
+
method: 'GET',
|
|
453
|
+
originalUrl: '/mbkauthe/api/reports',
|
|
454
|
+
url: '/mbkauthe/api/reports',
|
|
455
|
+
ip: '127.0.0.1',
|
|
456
|
+
session: { user: { id: '2', username: 'auditor' } }
|
|
457
|
+
},
|
|
458
|
+
async () => {
|
|
459
|
+
await fakePool.query({ text: 'SELECT * FROM broken_table WHERE id = $1', values: [8], name: 'badLookup' })
|
|
460
|
+
.catch(() => {});
|
|
461
|
+
}
|
|
462
|
+
);
|
|
463
|
+
|
|
464
|
+
const usernameResponse = await request(app).get('/mbkauthe/db.json?username=support');
|
|
465
|
+
expect(usernameResponse.status).toBe(200);
|
|
466
|
+
expect(usernameResponse.body.queryLog).toHaveLength(1);
|
|
467
|
+
expect(usernameResponse.body.queryLog[0].request.username).toBe('support');
|
|
468
|
+
|
|
469
|
+
const urlResponse = await request(app).get('/mbkauthe/db.json?url=/mbkauthe/api/reports');
|
|
470
|
+
expect(urlResponse.status).toBe(200);
|
|
471
|
+
expect(urlResponse.body.queryLog).toHaveLength(1);
|
|
472
|
+
expect(urlResponse.body.queryLog[0].request.url).toBe('/mbkauthe/api/reports');
|
|
473
|
+
expect(urlResponse.body.queryLog[0].trigger.label).toContain('/mbkauthe/api/reports');
|
|
474
|
+
|
|
475
|
+
const successResponse = await request(app).get('/mbkauthe/db.json?success=false');
|
|
476
|
+
expect(successResponse.status).toBe(200);
|
|
477
|
+
expect(successResponse.body.queryLog).toHaveLength(1);
|
|
478
|
+
expect(successResponse.body.queryLog[0].success).toBe(false);
|
|
479
|
+
expect(successResponse.body.summary.errorCount).toBe(1);
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
test('session-store-shaped queries are labeled as session-store triggers during a request', async () => {
|
|
483
|
+
const fakePool = createFakePool({ name: 'session-db' });
|
|
484
|
+
|
|
485
|
+
await runWithRequestContext(
|
|
486
|
+
{
|
|
487
|
+
method: 'GET',
|
|
488
|
+
originalUrl: '/mbkauthe/db.json?limit=50',
|
|
489
|
+
url: '/mbkauthe/db.json?limit=50',
|
|
490
|
+
ip: '127.0.0.1',
|
|
491
|
+
session: {}
|
|
492
|
+
},
|
|
493
|
+
() => fakePool.query({
|
|
494
|
+
text: 'SELECT sess FROM "session" WHERE sid = $1 AND expire >= to_timestamp($2)',
|
|
495
|
+
values: ['abc', 123],
|
|
496
|
+
name: 'sessionLookup'
|
|
497
|
+
})
|
|
498
|
+
);
|
|
499
|
+
|
|
500
|
+
const response = await request(app).get('/mbkauthe/db.json?limit=10');
|
|
501
|
+
|
|
502
|
+
expect(response.status).toBe(200);
|
|
503
|
+
expect(response.body.queryLog[0].trigger).toMatchObject({
|
|
504
|
+
type: 'request',
|
|
505
|
+
source: 'session-store',
|
|
506
|
+
route: 'GET /mbkauthe/db.json?limit=50'
|
|
507
|
+
});
|
|
508
|
+
expect(response.body.queryLog[0].trigger.label).toContain('Session store during GET /mbkauthe/db.json?limit=50');
|
|
509
|
+
});
|
|
510
|
+
|
|
163
511
|
test('POST /mbkauthe/api/login handles login API', async () => {
|
|
164
|
-
const response = await request(
|
|
512
|
+
const response = await request(app)
|
|
165
513
|
.post('/mbkauthe/api/login')
|
|
166
514
|
.send({ username: 'test', password: 'test' });
|
|
167
|
-
|
|
515
|
+
|
|
168
516
|
expect([200, 400, 401, 403, 429]).toContain(response.status);
|
|
169
517
|
expect(response.headers['content-type']).toContain('application/json');
|
|
170
518
|
});
|
|
171
519
|
|
|
520
|
+
test('POST /mbkauthe/api/login rejects missing credentials', async () => {
|
|
521
|
+
const response = await request(app)
|
|
522
|
+
.post('/mbkauthe/api/login')
|
|
523
|
+
.send({});
|
|
524
|
+
|
|
525
|
+
expect(response.status).toBe(400);
|
|
526
|
+
expect(response.headers['content-type']).toContain('application/json');
|
|
527
|
+
expect(response.body).toHaveProperty('errorCode', 1001);
|
|
528
|
+
});
|
|
529
|
+
|
|
530
|
+
test('POST /mbkauthe/api/login rejects short passwords before DB auth', async () => {
|
|
531
|
+
const response = await request(app)
|
|
532
|
+
.post('/mbkauthe/api/login')
|
|
533
|
+
.send({ username: 'tester', password: 'short' });
|
|
534
|
+
|
|
535
|
+
expect(response.status).toBe(400);
|
|
536
|
+
expect(response.headers['content-type']).toContain('application/json');
|
|
537
|
+
expect(response.body).toHaveProperty('errorCode', 1003);
|
|
538
|
+
});
|
|
539
|
+
|
|
172
540
|
test('POST /mbkauthe/api/verify-2fa handles 2FA API', async () => {
|
|
173
541
|
const { csrfToken, cookies } = await getCSRFTokenAndCookies();
|
|
174
|
-
|
|
175
|
-
const response = await request(
|
|
542
|
+
|
|
543
|
+
const response = await request(app)
|
|
176
544
|
.post('/mbkauthe/api/verify-2fa')
|
|
177
545
|
.set('Cookie', cookies)
|
|
178
546
|
.send({ token: '123456', _csrf: csrfToken });
|
|
179
|
-
|
|
547
|
+
|
|
180
548
|
expect([200, 400, 401, 403, 429]).toContain(response.status);
|
|
181
549
|
expect(response.headers['content-type']).toContain('application/json');
|
|
182
550
|
});
|
|
183
551
|
|
|
552
|
+
test('POST /mbkauthe/api/verify-2fa rejects malformed tokens', async () => {
|
|
553
|
+
const { csrfToken, cookies } = await getCSRFTokenAndCookies();
|
|
554
|
+
|
|
555
|
+
const response = await request(app)
|
|
556
|
+
.post('/mbkauthe/api/verify-2fa')
|
|
557
|
+
.set('Cookie', cookies)
|
|
558
|
+
.send({ token: '12ab', _csrf: csrfToken });
|
|
559
|
+
|
|
560
|
+
expect([400, 401]).toContain(response.status);
|
|
561
|
+
expect(response.headers['content-type']).toContain('application/json');
|
|
562
|
+
if (response.status === 400) {
|
|
563
|
+
expect(response.body).toHaveProperty('errorCode', 1004);
|
|
564
|
+
}
|
|
565
|
+
});
|
|
566
|
+
|
|
184
567
|
test('POST /mbkauthe/api/terminateAllSessions handles session termination', async () => {
|
|
185
568
|
const { csrfToken, cookies } = await getCSRFTokenAndCookies();
|
|
186
|
-
|
|
187
|
-
const response = await request(
|
|
569
|
+
|
|
570
|
+
const response = await request(app)
|
|
188
571
|
.post('/mbkauthe/api/terminateAllSessions')
|
|
189
572
|
.set('Cookie', cookies)
|
|
190
573
|
.send({ _csrf: csrfToken });
|
|
191
|
-
|
|
574
|
+
|
|
192
575
|
expect([200, 400, 401, 403, 429]).toContain(response.status);
|
|
193
|
-
|
|
576
|
+
|
|
194
577
|
if (response.status === 401) {
|
|
195
578
|
expect(response.headers['content-type']).not.toContain('application/json');
|
|
196
579
|
} else {
|
|
@@ -199,18 +582,102 @@ describe('mbkauthe Routes', () => {
|
|
|
199
582
|
});
|
|
200
583
|
|
|
201
584
|
test('GET /mbkauthe/api/checkSession handles session check', async () => {
|
|
202
|
-
const response = await request(
|
|
585
|
+
const response = await request(app).get('/mbkauthe/api/checkSession');
|
|
203
586
|
expect(response.status).toBe(200);
|
|
204
587
|
expect(response.headers['content-type']).toContain('application/json');
|
|
205
588
|
expect(response.body).toHaveProperty('sessionValid');
|
|
206
589
|
});
|
|
207
590
|
|
|
591
|
+
test('POST /mbkauthe/api/checkSession rejects missing session ids', async () => {
|
|
592
|
+
const response = await request(app)
|
|
593
|
+
.post('/mbkauthe/api/checkSession')
|
|
594
|
+
.send({});
|
|
595
|
+
|
|
596
|
+
expect(response.status).toBe(400);
|
|
597
|
+
expect(response.headers['content-type']).toContain('application/json');
|
|
598
|
+
expect(response.body).toHaveProperty('errorCode', 1001);
|
|
599
|
+
});
|
|
600
|
+
|
|
601
|
+
test('POST /mbkauthe/api/checkSession rejects non-uuid session ids', async () => {
|
|
602
|
+
const response = await request(app)
|
|
603
|
+
.post('/mbkauthe/api/checkSession')
|
|
604
|
+
.send({ sessionId: 'bad-session-id' });
|
|
605
|
+
|
|
606
|
+
expect(response.status).toBe(400);
|
|
607
|
+
expect(response.headers['content-type']).toContain('application/json');
|
|
608
|
+
expect(response.body).toHaveProperty('errorCode', 802);
|
|
609
|
+
});
|
|
610
|
+
|
|
611
|
+
test('POST /mbkauthe/api/verifySession rejects missing session ids', async () => {
|
|
612
|
+
const response = await request(app)
|
|
613
|
+
.post('/mbkauthe/api/verifySession')
|
|
614
|
+
.send({});
|
|
615
|
+
|
|
616
|
+
expect(response.status).toBe(400);
|
|
617
|
+
expect(response.headers['content-type']).toContain('application/json');
|
|
618
|
+
expect(response.body).toHaveProperty('errorCode', 1001);
|
|
619
|
+
});
|
|
620
|
+
|
|
621
|
+
test('POST /mbkauthe/api/verifySession rejects invalid encrypted session ids', async () => {
|
|
622
|
+
const response = await request(app)
|
|
623
|
+
.post('/mbkauthe/api/verifySession')
|
|
624
|
+
.send({ sessionId: 'definitely-not-encrypted', isEncrypt: true });
|
|
625
|
+
|
|
626
|
+
expect(response.status).toBe(400);
|
|
627
|
+
expect(response.headers['content-type']).toContain('application/json');
|
|
628
|
+
expect(response.body).toHaveProperty('errorCode', 802);
|
|
629
|
+
});
|
|
630
|
+
|
|
208
631
|
test('POST /mbkauthe/api/logout handles logout', async () => {
|
|
209
|
-
const response = await request(
|
|
632
|
+
const response = await request(app).post('/mbkauthe/api/logout').send();
|
|
210
633
|
expect([200, 400, 401, 403, 429]).toContain(response.status);
|
|
211
634
|
if (response.status === 200) {
|
|
212
635
|
expect(response.headers['content-type']).toContain('application/json');
|
|
213
636
|
}
|
|
214
637
|
});
|
|
638
|
+
|
|
639
|
+
test('GET /mbkauthe/api/account-sessions handles remembered account listing', async () => {
|
|
640
|
+
const response = await request(app).get('/mbkauthe/api/account-sessions');
|
|
641
|
+
expect([200, 401, 403, 429]).toContain(response.status);
|
|
642
|
+
if (response.status === 200) {
|
|
643
|
+
expect(response.headers['content-type']).toContain('application/json');
|
|
644
|
+
expect(response.body).toHaveProperty('accounts');
|
|
645
|
+
}
|
|
646
|
+
});
|
|
647
|
+
|
|
648
|
+
test('GET /mbkauthe/api/account-sessions tolerates malformed remembered-account cookies', async () => {
|
|
649
|
+
const response = await request(app)
|
|
650
|
+
.get('/mbkauthe/api/account-sessions')
|
|
651
|
+
.set('Cookie', ['mbkauthe_accounts=not-json']);
|
|
652
|
+
|
|
653
|
+
expect([200, 401, 403, 429]).toContain(response.status);
|
|
654
|
+
if (response.status === 200) {
|
|
655
|
+
expect(Array.isArray(response.body.accounts)).toBe(true);
|
|
656
|
+
}
|
|
657
|
+
});
|
|
658
|
+
|
|
659
|
+
test('POST /mbkauthe/api/switch-session rejects invalid session ids', async () => {
|
|
660
|
+
const response = await request(app)
|
|
661
|
+
.post('/mbkauthe/api/switch-session')
|
|
662
|
+
.send({ sessionId: 'not-a-uuid' });
|
|
663
|
+
|
|
664
|
+
expect([400, 429]).toContain(response.status);
|
|
665
|
+
expect(response.headers['content-type']).toContain('application/json');
|
|
666
|
+
if (response.status === 400) {
|
|
667
|
+
expect(response.body).toHaveProperty('errorCode');
|
|
668
|
+
}
|
|
669
|
+
});
|
|
670
|
+
|
|
671
|
+
test('POST /mbkauthe/api/logout-all handles no-session callers safely', async () => {
|
|
672
|
+
const response = await request(app)
|
|
673
|
+
.post('/mbkauthe/api/logout-all')
|
|
674
|
+
.send({});
|
|
675
|
+
|
|
676
|
+
expect([200, 500, 429]).toContain(response.status);
|
|
677
|
+
if (response.status === 200) {
|
|
678
|
+
expect(response.headers['content-type']).toContain('application/json');
|
|
679
|
+
expect(response.body).toHaveProperty('success', true);
|
|
680
|
+
}
|
|
681
|
+
});
|
|
215
682
|
});
|
|
216
|
-
});
|
|
683
|
+
});
|