@flink-app/jwt-auth-plugin 0.12.1-alpha.4 → 0.12.1-alpha.41
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/dist/FlinkJwtAuthPlugin.d.ts +47 -3
- package/dist/FlinkJwtAuthPlugin.js +42 -18
- package/package.json +4 -4
- package/readme.md +968 -18
- package/spec/FlinkJwtAuthPlugin.spec.ts +443 -0
- package/src/FlinkJwtAuthPlugin.ts +180 -130
|
@@ -126,4 +126,447 @@ 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 }) => {
|
|
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) => {
|
|
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 }) => {
|
|
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 }) => {
|
|
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 }) => {
|
|
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 }) => {
|
|
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 }) => {
|
|
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 }) => {
|
|
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 }) => {
|
|
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 }) => {
|
|
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 }) => {
|
|
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
|
+
});
|
|
129
572
|
});
|