@flink-app/jwt-auth-plugin 0.12.1-alpha.40 → 0.12.1-alpha.43

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