@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.
@@ -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
  });