@flink-app/jwt-auth-plugin 0.12.1-alpha.7 → 0.13.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/CHANGELOG.md +7 -0
- package/dist/BcryptUtils.js +2 -3
- package/dist/FlinkJwtAuthPlugin.d.ts +68 -3
- package/dist/FlinkJwtAuthPlugin.js +55 -23
- package/dist/PermissionValidator.js +1 -2
- package/package.json +31 -34
- package/readme.md +1301 -18
- package/spec/FlinkJwtAuthPlugin.spec.ts +692 -5
- package/src/FlinkJwtAuthPlugin.ts +213 -130
- package/tsconfig.json +1 -1
|
@@ -6,7 +6,7 @@ describe("FlinkJwtAuthPlugin", () => {
|
|
|
6
6
|
it("should create and configure plugin", () => {
|
|
7
7
|
const plugin = jwtAuthPlugin({
|
|
8
8
|
secret: "secret",
|
|
9
|
-
getUser: async (id: string) => {
|
|
9
|
+
getUser: async (id: string, req) => {
|
|
10
10
|
return {
|
|
11
11
|
id,
|
|
12
12
|
username: "username",
|
|
@@ -21,7 +21,7 @@ describe("FlinkJwtAuthPlugin", () => {
|
|
|
21
21
|
it("should fail auth if no token was provided", async () => {
|
|
22
22
|
const plugin = jwtAuthPlugin({
|
|
23
23
|
secret: "secret",
|
|
24
|
-
getUser: async (id: string) => {
|
|
24
|
+
getUser: async (id: string, req) => {
|
|
25
25
|
return {
|
|
26
26
|
id,
|
|
27
27
|
username: "username",
|
|
@@ -46,7 +46,7 @@ describe("FlinkJwtAuthPlugin", () => {
|
|
|
46
46
|
it("should fail auth if token is invalid provided", async () => {
|
|
47
47
|
const plugin = jwtAuthPlugin({
|
|
48
48
|
secret: "secret",
|
|
49
|
-
getUser: async (id: string) => {
|
|
49
|
+
getUser: async (id: string, req) => {
|
|
50
50
|
fail(); // Should not invoke this
|
|
51
51
|
return {
|
|
52
52
|
id,
|
|
@@ -79,7 +79,7 @@ describe("FlinkJwtAuthPlugin", () => {
|
|
|
79
79
|
|
|
80
80
|
const plugin = jwtAuthPlugin({
|
|
81
81
|
secret,
|
|
82
|
-
getUser: async ({ id }: { id: string }) => {
|
|
82
|
+
getUser: async ({ id }: { id: string }, req) => {
|
|
83
83
|
expect(id).toBe(userId);
|
|
84
84
|
return {
|
|
85
85
|
id,
|
|
@@ -106,7 +106,7 @@ describe("FlinkJwtAuthPlugin", () => {
|
|
|
106
106
|
const secret = "secret";
|
|
107
107
|
const plugin = jwtAuthPlugin({
|
|
108
108
|
secret,
|
|
109
|
-
getUser: async (id: string) => {
|
|
109
|
+
getUser: async (id: string, req) => {
|
|
110
110
|
fail(); // Should not invoke this
|
|
111
111
|
return {
|
|
112
112
|
id,
|
|
@@ -126,4 +126,691 @@ describe("FlinkJwtAuthPlugin", () => {
|
|
|
126
126
|
|
|
127
127
|
expect(decoded.id).toBe("123");
|
|
128
128
|
});
|
|
129
|
+
|
|
130
|
+
describe("tokenExtractor", () => {
|
|
131
|
+
it("should use custom token extractor when it returns a string", async () => {
|
|
132
|
+
const secret = "secret";
|
|
133
|
+
const userId = "123";
|
|
134
|
+
const customToken = jwtSimple.encode(
|
|
135
|
+
{ id: userId, roles: ["user"] },
|
|
136
|
+
secret
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
const plugin = jwtAuthPlugin({
|
|
140
|
+
secret,
|
|
141
|
+
getUser: async ({ id }: { id: string }, req) => {
|
|
142
|
+
expect(id).toBe(userId);
|
|
143
|
+
return {
|
|
144
|
+
id,
|
|
145
|
+
username: "username",
|
|
146
|
+
};
|
|
147
|
+
},
|
|
148
|
+
rolePermissions: {
|
|
149
|
+
user: ["*"],
|
|
150
|
+
},
|
|
151
|
+
tokenExtractor: (req) => {
|
|
152
|
+
// Extract from query param
|
|
153
|
+
return req.query?.token as string;
|
|
154
|
+
},
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
const mockRequest = {
|
|
158
|
+
headers: {},
|
|
159
|
+
query: {
|
|
160
|
+
token: customToken,
|
|
161
|
+
},
|
|
162
|
+
} as unknown as FlinkRequest;
|
|
163
|
+
|
|
164
|
+
const authenticated = await plugin.authenticateRequest(mockRequest, "foo");
|
|
165
|
+
|
|
166
|
+
expect(authenticated).toBeTruthy();
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it("should fail auth when tokenExtractor returns null", async () => {
|
|
170
|
+
const plugin = jwtAuthPlugin({
|
|
171
|
+
secret: "secret",
|
|
172
|
+
getUser: async (id: string, req) => {
|
|
173
|
+
fail(); // Should not be called
|
|
174
|
+
return {
|
|
175
|
+
id,
|
|
176
|
+
username: "username",
|
|
177
|
+
};
|
|
178
|
+
},
|
|
179
|
+
rolePermissions: {
|
|
180
|
+
user: ["*"],
|
|
181
|
+
},
|
|
182
|
+
tokenExtractor: (req) => {
|
|
183
|
+
// Explicitly no token for this route
|
|
184
|
+
return null;
|
|
185
|
+
},
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
const mockRequest = {
|
|
189
|
+
headers: {
|
|
190
|
+
authorization: "Bearer some-valid-token",
|
|
191
|
+
},
|
|
192
|
+
} as FlinkRequest;
|
|
193
|
+
|
|
194
|
+
const authenticated = await plugin.authenticateRequest(mockRequest, "foo");
|
|
195
|
+
|
|
196
|
+
expect(authenticated).toBeFalse();
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it("should fall back to Bearer token when tokenExtractor returns undefined", async () => {
|
|
200
|
+
const secret = "secret";
|
|
201
|
+
const userId = "123";
|
|
202
|
+
const encodedToken = jwtSimple.encode(
|
|
203
|
+
{ id: userId, roles: ["user"] },
|
|
204
|
+
secret
|
|
205
|
+
);
|
|
206
|
+
|
|
207
|
+
const plugin = jwtAuthPlugin({
|
|
208
|
+
secret,
|
|
209
|
+
getUser: async ({ id }: { id: string }, req) => {
|
|
210
|
+
expect(id).toBe(userId);
|
|
211
|
+
return {
|
|
212
|
+
id,
|
|
213
|
+
username: "username",
|
|
214
|
+
};
|
|
215
|
+
},
|
|
216
|
+
rolePermissions: {
|
|
217
|
+
user: ["*"],
|
|
218
|
+
},
|
|
219
|
+
tokenExtractor: (req) => {
|
|
220
|
+
// Return undefined to fall back to default
|
|
221
|
+
return undefined;
|
|
222
|
+
},
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
const mockRequest = {
|
|
226
|
+
headers: {
|
|
227
|
+
authorization: "Bearer " + encodedToken,
|
|
228
|
+
},
|
|
229
|
+
} as FlinkRequest;
|
|
230
|
+
|
|
231
|
+
const authenticated = await plugin.authenticateRequest(mockRequest, "foo");
|
|
232
|
+
|
|
233
|
+
expect(authenticated).toBeTruthy();
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
it("should support conditional extraction based on path", async () => {
|
|
237
|
+
const secret = "secret";
|
|
238
|
+
const userId = "123";
|
|
239
|
+
const queryToken = jwtSimple.encode(
|
|
240
|
+
{ id: userId, roles: ["user"] },
|
|
241
|
+
secret
|
|
242
|
+
);
|
|
243
|
+
const bearerToken = jwtSimple.encode(
|
|
244
|
+
{ id: userId, roles: ["user"] },
|
|
245
|
+
secret
|
|
246
|
+
);
|
|
247
|
+
|
|
248
|
+
const plugin = jwtAuthPlugin({
|
|
249
|
+
secret,
|
|
250
|
+
getUser: async ({ id }: { id: string }, req) => {
|
|
251
|
+
return {
|
|
252
|
+
id,
|
|
253
|
+
username: "username",
|
|
254
|
+
};
|
|
255
|
+
},
|
|
256
|
+
rolePermissions: {
|
|
257
|
+
user: ["*"],
|
|
258
|
+
},
|
|
259
|
+
tokenExtractor: (req) => {
|
|
260
|
+
// Use query param for public API routes only
|
|
261
|
+
if (req.path?.startsWith("/api/public/")) {
|
|
262
|
+
return req.query?.token as string || null;
|
|
263
|
+
}
|
|
264
|
+
// Fall back to Bearer for other routes
|
|
265
|
+
return undefined;
|
|
266
|
+
},
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
// Test public route with query param
|
|
270
|
+
const publicRequest = {
|
|
271
|
+
path: "/api/public/data",
|
|
272
|
+
headers: {},
|
|
273
|
+
query: {
|
|
274
|
+
token: queryToken,
|
|
275
|
+
},
|
|
276
|
+
} as unknown as FlinkRequest;
|
|
277
|
+
|
|
278
|
+
const publicAuth = await plugin.authenticateRequest(publicRequest, "foo");
|
|
279
|
+
expect(publicAuth).toBeTruthy();
|
|
280
|
+
|
|
281
|
+
// Test non-public route with Bearer token
|
|
282
|
+
const privateRequest = {
|
|
283
|
+
path: "/api/private/data",
|
|
284
|
+
headers: {
|
|
285
|
+
authorization: "Bearer " + bearerToken,
|
|
286
|
+
},
|
|
287
|
+
} as unknown as FlinkRequest;
|
|
288
|
+
|
|
289
|
+
const privateAuth = await plugin.authenticateRequest(privateRequest, "foo");
|
|
290
|
+
expect(privateAuth).toBeTruthy();
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
it("should use default Bearer extraction when no tokenExtractor provided", async () => {
|
|
294
|
+
const secret = "secret";
|
|
295
|
+
const userId = "123";
|
|
296
|
+
const encodedToken = jwtSimple.encode(
|
|
297
|
+
{ id: userId, roles: ["user"] },
|
|
298
|
+
secret
|
|
299
|
+
);
|
|
300
|
+
|
|
301
|
+
const plugin = jwtAuthPlugin({
|
|
302
|
+
secret,
|
|
303
|
+
getUser: async ({ id }: { id: string }, req) => {
|
|
304
|
+
return {
|
|
305
|
+
id,
|
|
306
|
+
username: "username",
|
|
307
|
+
};
|
|
308
|
+
},
|
|
309
|
+
rolePermissions: {
|
|
310
|
+
user: ["*"],
|
|
311
|
+
},
|
|
312
|
+
// No tokenExtractor provided
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
const mockRequest = {
|
|
316
|
+
headers: {
|
|
317
|
+
authorization: "Bearer " + encodedToken,
|
|
318
|
+
},
|
|
319
|
+
} as FlinkRequest;
|
|
320
|
+
|
|
321
|
+
const authenticated = await plugin.authenticateRequest(mockRequest, "foo");
|
|
322
|
+
|
|
323
|
+
expect(authenticated).toBeTruthy();
|
|
324
|
+
});
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
describe("checkPermissions callback", () => {
|
|
328
|
+
it("should use custom permission checker when provided", async () => {
|
|
329
|
+
const secret = "secret";
|
|
330
|
+
const userId = "123";
|
|
331
|
+
const encodedToken = jwtSimple.encode(
|
|
332
|
+
{ id: userId, roles: ["user"] },
|
|
333
|
+
secret
|
|
334
|
+
);
|
|
335
|
+
|
|
336
|
+
const plugin = jwtAuthPlugin({
|
|
337
|
+
secret,
|
|
338
|
+
getUser: async ({ id }: { id: string }, req) => {
|
|
339
|
+
return {
|
|
340
|
+
id,
|
|
341
|
+
username: "testuser",
|
|
342
|
+
permissions: ["read", "write", "delete"],
|
|
343
|
+
};
|
|
344
|
+
},
|
|
345
|
+
rolePermissions: {
|
|
346
|
+
user: [], // Empty - custom checker will handle this
|
|
347
|
+
},
|
|
348
|
+
checkPermissions: async (user, routePermissions) => {
|
|
349
|
+
// Check if user has all required permissions
|
|
350
|
+
return routePermissions.every((perm) =>
|
|
351
|
+
user.permissions?.includes(perm)
|
|
352
|
+
);
|
|
353
|
+
},
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
const mockRequest = {
|
|
357
|
+
headers: {
|
|
358
|
+
authorization: "Bearer " + encodedToken,
|
|
359
|
+
},
|
|
360
|
+
} as FlinkRequest;
|
|
361
|
+
|
|
362
|
+
const authenticated = await plugin.authenticateRequest(mockRequest, [
|
|
363
|
+
"read",
|
|
364
|
+
"write",
|
|
365
|
+
]);
|
|
366
|
+
|
|
367
|
+
expect(authenticated).toBeTruthy();
|
|
368
|
+
expect(mockRequest.user?.permissions).toEqual([
|
|
369
|
+
"read",
|
|
370
|
+
"write",
|
|
371
|
+
"delete",
|
|
372
|
+
]);
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
it("should fail auth when custom checker returns false", async () => {
|
|
376
|
+
const secret = "secret";
|
|
377
|
+
const userId = "123";
|
|
378
|
+
const encodedToken = jwtSimple.encode(
|
|
379
|
+
{ id: userId, roles: ["user"] },
|
|
380
|
+
secret
|
|
381
|
+
);
|
|
382
|
+
|
|
383
|
+
const plugin = jwtAuthPlugin({
|
|
384
|
+
secret,
|
|
385
|
+
getUser: async ({ id }: { id: string }, req) => {
|
|
386
|
+
return {
|
|
387
|
+
id,
|
|
388
|
+
username: "testuser",
|
|
389
|
+
permissions: ["read"], // Only has read
|
|
390
|
+
};
|
|
391
|
+
},
|
|
392
|
+
rolePermissions: {},
|
|
393
|
+
checkPermissions: async (user, routePermissions) => {
|
|
394
|
+
return routePermissions.every((perm) =>
|
|
395
|
+
user.permissions?.includes(perm)
|
|
396
|
+
);
|
|
397
|
+
},
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
const mockRequest = {
|
|
401
|
+
headers: {
|
|
402
|
+
authorization: "Bearer " + encodedToken,
|
|
403
|
+
},
|
|
404
|
+
} as FlinkRequest;
|
|
405
|
+
|
|
406
|
+
// Route requires write, but user only has read
|
|
407
|
+
const authenticated = await plugin.authenticateRequest(mockRequest, [
|
|
408
|
+
"read",
|
|
409
|
+
"write",
|
|
410
|
+
]);
|
|
411
|
+
|
|
412
|
+
expect(authenticated).toBeFalse();
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
it("should use static rolePermissions when checkPermissions not provided (backward compat)", async () => {
|
|
416
|
+
const secret = "secret";
|
|
417
|
+
const userId = "123";
|
|
418
|
+
const encodedToken = jwtSimple.encode(
|
|
419
|
+
{ id: userId, roles: ["admin"] },
|
|
420
|
+
secret
|
|
421
|
+
);
|
|
422
|
+
|
|
423
|
+
const plugin = jwtAuthPlugin({
|
|
424
|
+
secret,
|
|
425
|
+
getUser: async ({ id }: { id: string }, req) => {
|
|
426
|
+
return {
|
|
427
|
+
id,
|
|
428
|
+
username: "admin",
|
|
429
|
+
};
|
|
430
|
+
},
|
|
431
|
+
rolePermissions: {
|
|
432
|
+
admin: ["read", "write", "delete"],
|
|
433
|
+
},
|
|
434
|
+
// No checkPermissions provided - uses static
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
const mockRequest = {
|
|
438
|
+
headers: {
|
|
439
|
+
authorization: "Bearer " + encodedToken,
|
|
440
|
+
},
|
|
441
|
+
} as FlinkRequest;
|
|
442
|
+
|
|
443
|
+
const authenticated = await plugin.authenticateRequest(
|
|
444
|
+
mockRequest,
|
|
445
|
+
"write"
|
|
446
|
+
);
|
|
447
|
+
|
|
448
|
+
expect(authenticated).toBeTruthy();
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
it("should support synchronous permission checker", async () => {
|
|
452
|
+
const secret = "secret";
|
|
453
|
+
const userId = "123";
|
|
454
|
+
const encodedToken = jwtSimple.encode(
|
|
455
|
+
{ id: userId, roles: ["user"] },
|
|
456
|
+
secret
|
|
457
|
+
);
|
|
458
|
+
|
|
459
|
+
const plugin = jwtAuthPlugin({
|
|
460
|
+
secret,
|
|
461
|
+
getUser: async ({ id }: { id: string }, req) => {
|
|
462
|
+
return {
|
|
463
|
+
id,
|
|
464
|
+
username: "testuser",
|
|
465
|
+
permissions: ["read"],
|
|
466
|
+
};
|
|
467
|
+
},
|
|
468
|
+
rolePermissions: {},
|
|
469
|
+
// Synchronous checker (not async)
|
|
470
|
+
checkPermissions: (user, routePermissions) => {
|
|
471
|
+
return routePermissions.every((perm) =>
|
|
472
|
+
user.permissions?.includes(perm)
|
|
473
|
+
);
|
|
474
|
+
},
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
const mockRequest = {
|
|
478
|
+
headers: {
|
|
479
|
+
authorization: "Bearer " + encodedToken,
|
|
480
|
+
},
|
|
481
|
+
} as FlinkRequest;
|
|
482
|
+
|
|
483
|
+
const authenticated = await plugin.authenticateRequest(
|
|
484
|
+
mockRequest,
|
|
485
|
+
"read"
|
|
486
|
+
);
|
|
487
|
+
|
|
488
|
+
expect(authenticated).toBeTruthy();
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
it("should pass when route has no permissions and custom checker provided", async () => {
|
|
492
|
+
const secret = "secret";
|
|
493
|
+
const userId = "123";
|
|
494
|
+
const encodedToken = jwtSimple.encode(
|
|
495
|
+
{ id: userId, roles: ["user"] },
|
|
496
|
+
secret
|
|
497
|
+
);
|
|
498
|
+
|
|
499
|
+
let checkerCalled = false;
|
|
500
|
+
|
|
501
|
+
const plugin = jwtAuthPlugin({
|
|
502
|
+
secret,
|
|
503
|
+
getUser: async ({ id }: { id: string }, req) => {
|
|
504
|
+
return { id, username: "testuser" };
|
|
505
|
+
},
|
|
506
|
+
rolePermissions: {},
|
|
507
|
+
checkPermissions: async (user, routePermissions) => {
|
|
508
|
+
checkerCalled = true;
|
|
509
|
+
return true;
|
|
510
|
+
},
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
const mockRequest = {
|
|
514
|
+
headers: {
|
|
515
|
+
authorization: "Bearer " + encodedToken,
|
|
516
|
+
},
|
|
517
|
+
} as FlinkRequest;
|
|
518
|
+
|
|
519
|
+
// Empty permissions (public route)
|
|
520
|
+
const authenticated = await plugin.authenticateRequest(mockRequest, []);
|
|
521
|
+
|
|
522
|
+
expect(authenticated).toBeTruthy();
|
|
523
|
+
expect(checkerCalled).toBeFalse(); // Checker should not be called for public routes
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
it("should handle database-fetched permissions in getUser", async () => {
|
|
527
|
+
const secret = "secret";
|
|
528
|
+
const userId = "123";
|
|
529
|
+
const encodedToken = jwtSimple.encode(
|
|
530
|
+
{ id: userId, roles: ["user"] },
|
|
531
|
+
secret
|
|
532
|
+
);
|
|
533
|
+
|
|
534
|
+
// Simulate DB permissions
|
|
535
|
+
const dbPermissions: { [key: string]: string[] } = {
|
|
536
|
+
"123": ["read", "write", "custom_permission"],
|
|
537
|
+
};
|
|
538
|
+
|
|
539
|
+
const plugin = jwtAuthPlugin({
|
|
540
|
+
secret,
|
|
541
|
+
getUser: async ({ id }: { id: string }, req) => {
|
|
542
|
+
// Simulate fetching permissions from DB
|
|
543
|
+
const permissions = dbPermissions[id] || [];
|
|
544
|
+
return {
|
|
545
|
+
id,
|
|
546
|
+
username: "testuser",
|
|
547
|
+
permissions,
|
|
548
|
+
};
|
|
549
|
+
},
|
|
550
|
+
rolePermissions: {},
|
|
551
|
+
checkPermissions: async (user, routePermissions) => {
|
|
552
|
+
return routePermissions.every((perm) =>
|
|
553
|
+
user.permissions?.includes(perm)
|
|
554
|
+
);
|
|
555
|
+
},
|
|
556
|
+
});
|
|
557
|
+
|
|
558
|
+
const mockRequest = {
|
|
559
|
+
headers: {
|
|
560
|
+
authorization: "Bearer " + encodedToken,
|
|
561
|
+
},
|
|
562
|
+
} as FlinkRequest;
|
|
563
|
+
|
|
564
|
+
const authenticated = await plugin.authenticateRequest(mockRequest, [
|
|
565
|
+
"custom_permission",
|
|
566
|
+
]);
|
|
567
|
+
|
|
568
|
+
expect(authenticated).toBeTruthy();
|
|
569
|
+
expect(mockRequest.user?.permissions).toContain("custom_permission");
|
|
570
|
+
});
|
|
571
|
+
});
|
|
572
|
+
|
|
573
|
+
describe("useDynamicRoles", () => {
|
|
574
|
+
it("should use roles from user object when useDynamicRoles is true", async () => {
|
|
575
|
+
const secret = "secret";
|
|
576
|
+
const userId = "123";
|
|
577
|
+
// Token has one set of roles
|
|
578
|
+
const encodedToken = jwtSimple.encode(
|
|
579
|
+
{ id: userId, roles: ["guest"] }, // Token says guest
|
|
580
|
+
secret
|
|
581
|
+
);
|
|
582
|
+
|
|
583
|
+
const plugin = jwtAuthPlugin({
|
|
584
|
+
secret,
|
|
585
|
+
useDynamicRoles: true,
|
|
586
|
+
getUser: async ({ id }: { id: string }, req) => {
|
|
587
|
+
// Simulate fetching org-specific role based on header
|
|
588
|
+
const orgId = req.headers["x-organization-id"];
|
|
589
|
+
const role = orgId === "org1" ? "admin" : "user";
|
|
590
|
+
|
|
591
|
+
return {
|
|
592
|
+
id,
|
|
593
|
+
username: "testuser",
|
|
594
|
+
roles: [role], // Dynamic role from database/context
|
|
595
|
+
};
|
|
596
|
+
},
|
|
597
|
+
rolePermissions: {
|
|
598
|
+
admin: ["read", "write", "delete"],
|
|
599
|
+
user: ["read", "write"],
|
|
600
|
+
guest: ["read"],
|
|
601
|
+
},
|
|
602
|
+
});
|
|
603
|
+
|
|
604
|
+
const mockRequest = {
|
|
605
|
+
headers: {
|
|
606
|
+
authorization: "Bearer " + encodedToken,
|
|
607
|
+
"x-organization-id": "org1",
|
|
608
|
+
},
|
|
609
|
+
} as unknown as FlinkRequest;
|
|
610
|
+
|
|
611
|
+
// Should pass because user is admin in org1 (despite token saying guest)
|
|
612
|
+
const authenticated = await plugin.authenticateRequest(mockRequest, ["delete"]);
|
|
613
|
+
|
|
614
|
+
expect(authenticated).toBeTruthy();
|
|
615
|
+
expect(mockRequest.user?.roles).toEqual(["admin"]);
|
|
616
|
+
});
|
|
617
|
+
|
|
618
|
+
it("should fail when dynamic role doesn't have required permission", async () => {
|
|
619
|
+
const secret = "secret";
|
|
620
|
+
const userId = "123";
|
|
621
|
+
const encodedToken = jwtSimple.encode(
|
|
622
|
+
{ id: userId, roles: ["admin"] }, // Token says admin
|
|
623
|
+
secret
|
|
624
|
+
);
|
|
625
|
+
|
|
626
|
+
const plugin = jwtAuthPlugin({
|
|
627
|
+
secret,
|
|
628
|
+
useDynamicRoles: true,
|
|
629
|
+
getUser: async ({ id }: { id: string }, req) => {
|
|
630
|
+
const orgId = req.headers["x-organization-id"];
|
|
631
|
+
const role = orgId === "org2" ? "guest" : "admin";
|
|
632
|
+
|
|
633
|
+
return {
|
|
634
|
+
id,
|
|
635
|
+
username: "testuser",
|
|
636
|
+
roles: [role],
|
|
637
|
+
};
|
|
638
|
+
},
|
|
639
|
+
rolePermissions: {
|
|
640
|
+
admin: ["read", "write", "delete"],
|
|
641
|
+
guest: ["read"],
|
|
642
|
+
},
|
|
643
|
+
});
|
|
644
|
+
|
|
645
|
+
const mockRequest = {
|
|
646
|
+
headers: {
|
|
647
|
+
authorization: "Bearer " + encodedToken,
|
|
648
|
+
"x-organization-id": "org2", // In org2, user is guest
|
|
649
|
+
},
|
|
650
|
+
} as unknown as FlinkRequest;
|
|
651
|
+
|
|
652
|
+
// Should fail because user is only guest in org2
|
|
653
|
+
const authenticated = await plugin.authenticateRequest(mockRequest, ["delete"]);
|
|
654
|
+
|
|
655
|
+
expect(authenticated).toBeFalse();
|
|
656
|
+
});
|
|
657
|
+
|
|
658
|
+
it("should use token roles when useDynamicRoles is false (default)", async () => {
|
|
659
|
+
const secret = "secret";
|
|
660
|
+
const userId = "123";
|
|
661
|
+
const encodedToken = jwtSimple.encode(
|
|
662
|
+
{ id: userId, roles: ["admin"] },
|
|
663
|
+
secret
|
|
664
|
+
);
|
|
665
|
+
|
|
666
|
+
const plugin = jwtAuthPlugin({
|
|
667
|
+
secret,
|
|
668
|
+
// useDynamicRoles not set (defaults to false)
|
|
669
|
+
getUser: async ({ id }: { id: string }, req) => {
|
|
670
|
+
return {
|
|
671
|
+
id,
|
|
672
|
+
username: "testuser",
|
|
673
|
+
roles: ["guest"], // This should be ignored
|
|
674
|
+
};
|
|
675
|
+
},
|
|
676
|
+
rolePermissions: {
|
|
677
|
+
admin: ["read", "write", "delete"],
|
|
678
|
+
guest: ["read"],
|
|
679
|
+
},
|
|
680
|
+
});
|
|
681
|
+
|
|
682
|
+
const mockRequest = {
|
|
683
|
+
headers: {
|
|
684
|
+
authorization: "Bearer " + encodedToken,
|
|
685
|
+
},
|
|
686
|
+
} as FlinkRequest;
|
|
687
|
+
|
|
688
|
+
// Should pass because token has admin role (user.roles ignored)
|
|
689
|
+
const authenticated = await plugin.authenticateRequest(mockRequest, ["delete"]);
|
|
690
|
+
|
|
691
|
+
expect(authenticated).toBeTruthy();
|
|
692
|
+
});
|
|
693
|
+
|
|
694
|
+
it("should support multi-tenant scenario with organization context", async () => {
|
|
695
|
+
const secret = "secret";
|
|
696
|
+
const userId = "user123";
|
|
697
|
+
const encodedToken = jwtSimple.encode(
|
|
698
|
+
{ id: userId, roles: [] }, // No roles in token
|
|
699
|
+
secret
|
|
700
|
+
);
|
|
701
|
+
|
|
702
|
+
// Simulate database of org memberships
|
|
703
|
+
const orgMemberships: { [key: string]: { [userId: string]: string } } = {
|
|
704
|
+
org1: { user123: "admin" },
|
|
705
|
+
org2: { user123: "user" },
|
|
706
|
+
org3: { user123: "guest" },
|
|
707
|
+
};
|
|
708
|
+
|
|
709
|
+
const plugin = jwtAuthPlugin({
|
|
710
|
+
secret,
|
|
711
|
+
useDynamicRoles: true,
|
|
712
|
+
getUser: async ({ id }: { id: string }, req) => {
|
|
713
|
+
const orgId = req.headers["x-organization-id"] as string;
|
|
714
|
+
const role = orgMemberships[orgId]?.[id] || "guest";
|
|
715
|
+
|
|
716
|
+
return {
|
|
717
|
+
id,
|
|
718
|
+
username: "multitenantuser",
|
|
719
|
+
organizationId: orgId,
|
|
720
|
+
roles: [role],
|
|
721
|
+
};
|
|
722
|
+
},
|
|
723
|
+
rolePermissions: {
|
|
724
|
+
admin: ["read", "write", "delete", "manage"],
|
|
725
|
+
user: ["read", "write"],
|
|
726
|
+
guest: ["read"],
|
|
727
|
+
},
|
|
728
|
+
});
|
|
729
|
+
|
|
730
|
+
// Test as admin in org1
|
|
731
|
+
const org1Request = {
|
|
732
|
+
headers: {
|
|
733
|
+
authorization: "Bearer " + encodedToken,
|
|
734
|
+
"x-organization-id": "org1",
|
|
735
|
+
},
|
|
736
|
+
} as unknown as FlinkRequest;
|
|
737
|
+
|
|
738
|
+
const org1Auth = await plugin.authenticateRequest(org1Request, ["manage"]);
|
|
739
|
+
expect(org1Auth).toBeTruthy();
|
|
740
|
+
expect(org1Request.user?.roles).toEqual(["admin"]);
|
|
741
|
+
|
|
742
|
+
// Test as user in org2
|
|
743
|
+
const org2Request = {
|
|
744
|
+
headers: {
|
|
745
|
+
authorization: "Bearer " + encodedToken,
|
|
746
|
+
"x-organization-id": "org2",
|
|
747
|
+
},
|
|
748
|
+
} as unknown as FlinkRequest;
|
|
749
|
+
|
|
750
|
+
const org2AuthWrite = await plugin.authenticateRequest(org2Request, ["write"]);
|
|
751
|
+
expect(org2AuthWrite).toBeTruthy();
|
|
752
|
+
|
|
753
|
+
const org2AuthManage = await plugin.authenticateRequest(org2Request, ["manage"]);
|
|
754
|
+
expect(org2AuthManage).toBeFalse(); // User can't manage in org2
|
|
755
|
+
|
|
756
|
+
// Test as guest in org3
|
|
757
|
+
const org3Request = {
|
|
758
|
+
headers: {
|
|
759
|
+
authorization: "Bearer " + encodedToken,
|
|
760
|
+
"x-organization-id": "org3",
|
|
761
|
+
},
|
|
762
|
+
} as unknown as FlinkRequest;
|
|
763
|
+
|
|
764
|
+
const org3AuthRead = await plugin.authenticateRequest(org3Request, ["read"]);
|
|
765
|
+
expect(org3AuthRead).toBeTruthy();
|
|
766
|
+
|
|
767
|
+
const org3AuthWrite = await plugin.authenticateRequest(org3Request, ["write"]);
|
|
768
|
+
expect(org3AuthWrite).toBeFalse(); // Guest can't write in org3
|
|
769
|
+
});
|
|
770
|
+
|
|
771
|
+
it("should have access to request properties in getUser callback", async () => {
|
|
772
|
+
const secret = "secret";
|
|
773
|
+
const userId = "123";
|
|
774
|
+
const encodedToken = jwtSimple.encode({ id: userId }, secret);
|
|
775
|
+
|
|
776
|
+
let capturedPath: string | undefined;
|
|
777
|
+
let capturedMethod: string | undefined;
|
|
778
|
+
let capturedHeaders: any;
|
|
779
|
+
|
|
780
|
+
const plugin = jwtAuthPlugin({
|
|
781
|
+
secret,
|
|
782
|
+
useDynamicRoles: true,
|
|
783
|
+
getUser: async ({ id }: { id: string }, req) => {
|
|
784
|
+
// Capture request properties to verify they're accessible
|
|
785
|
+
capturedPath = req.path;
|
|
786
|
+
capturedMethod = req.method;
|
|
787
|
+
capturedHeaders = req.headers;
|
|
788
|
+
|
|
789
|
+
return {
|
|
790
|
+
id,
|
|
791
|
+
username: "testuser",
|
|
792
|
+
roles: ["user"],
|
|
793
|
+
};
|
|
794
|
+
},
|
|
795
|
+
rolePermissions: {
|
|
796
|
+
user: ["read"],
|
|
797
|
+
},
|
|
798
|
+
});
|
|
799
|
+
|
|
800
|
+
const mockRequest = {
|
|
801
|
+
path: "/api/users",
|
|
802
|
+
method: "GET",
|
|
803
|
+
headers: {
|
|
804
|
+
authorization: "Bearer " + encodedToken,
|
|
805
|
+
"x-custom-header": "custom-value",
|
|
806
|
+
},
|
|
807
|
+
} as unknown as FlinkRequest;
|
|
808
|
+
|
|
809
|
+
await plugin.authenticateRequest(mockRequest, ["read"]);
|
|
810
|
+
|
|
811
|
+
expect(capturedPath).toBe("/api/users");
|
|
812
|
+
expect(capturedMethod).toBe("GET");
|
|
813
|
+
expect(capturedHeaders["x-custom-header"]).toBe("custom-value");
|
|
814
|
+
});
|
|
815
|
+
});
|
|
129
816
|
});
|