@flink-app/jwt-auth-plugin 2.0.0-alpha.75 → 2.0.0-alpha.77

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 CHANGED
@@ -1,5 +1,21 @@
1
1
  # @flink-app/jwt-auth-plugin
2
2
 
3
+ ## 2.0.0-alpha.77
4
+
5
+ ### Patch Changes
6
+
7
+ - @flink-app/flink@2.0.0-alpha.77
8
+
9
+ ## 2.0.0-alpha.76
10
+
11
+ ### Minor Changes
12
+
13
+ - Add resolveTokenTTL callback to jwt-auth-plugin for dynamic token TTL based on role. Fix renames in email-plugin and inbound-email-plugin. Fix tsconfig in generic-auth-plugin.
14
+
15
+ ### Patch Changes
16
+
17
+ - @flink-app/flink@2.0.0-alpha.76
18
+
3
19
  ## 2.0.0-alpha.75
4
20
 
5
21
  ### Patch Changes
@@ -20,6 +20,16 @@ export type TokenExtractor = (req: FlinkRequest) => string | null | undefined;
20
20
  * @returns true if user has required permissions, false otherwise
21
21
  */
22
22
  export type PermissionChecker = (user: FlinkAuthUser, routePermissions: string[]) => Promise<boolean> | boolean;
23
+ /**
24
+ * Custom token TTL resolver callback.
25
+ *
26
+ * Called during token creation to determine expiration dynamically based on roles.
27
+ *
28
+ * Return values:
29
+ * - `number`: TTL in milliseconds to use for this token
30
+ * - `undefined`: Fall back to static `tokenTTL` option (or default ~100 years)
31
+ */
32
+ export type TokenTTLResolver = (roles: string[], payload: any) => number | undefined;
23
33
  export interface JwtAuthPluginOptions {
24
34
  secret: string;
25
35
  algo?: jwtSimple.TAlgorithm;
@@ -73,6 +83,23 @@ export interface JwtAuthPluginOptions {
73
83
  * ```
74
84
  */
75
85
  useDynamicRoles?: boolean;
86
+ /**
87
+ * Optional dynamic token TTL resolver.
88
+ *
89
+ * When provided, called during token creation with the user's roles and payload.
90
+ * Return a TTL in milliseconds to override the static `tokenTTL`, or `undefined`
91
+ * to fall back to `tokenTTL` (or the default).
92
+ *
93
+ * Example:
94
+ * ```typescript
95
+ * resolveTokenTTL: (roles) => {
96
+ * if (roles.includes("service-account")) return 1000 * 60 * 60 * 24 * 365; // 1 year
97
+ * if (roles.includes("admin")) return 1000 * 60 * 60 * 8; // 8 hours
98
+ * return 1000 * 60 * 60; // 1 hour default
99
+ * }
100
+ * ```
101
+ */
102
+ resolveTokenTTL?: TokenTTLResolver;
76
103
  }
77
104
  export interface JwtAuthPlugin extends FlinkAuthPlugin {
78
105
  /**
@@ -95,5 +122,4 @@ export interface JwtAuthPlugin extends FlinkAuthPlugin {
95
122
  /**
96
123
  * Configures and creates authentication plugin.
97
124
  */
98
- export declare function jwtAuthPlugin({ secret, getUser, rolePermissions, algo, passwordPolicy, tokenTTL, //Defaults to hundred year
99
- tokenExtractor, checkPermissions, useDynamicRoles, }: JwtAuthPluginOptions): JwtAuthPlugin;
125
+ export declare function jwtAuthPlugin({ secret, getUser, rolePermissions, algo, passwordPolicy, tokenTTL, tokenExtractor, checkPermissions, useDynamicRoles, resolveTokenTTL, }: JwtAuthPluginOptions): JwtAuthPlugin;
@@ -65,8 +65,7 @@ var defaultPasswordPolicy = /^(?=.*[A-Za-z])(?=.*\d)[A-Za-z\d]{8,}$/;
65
65
  */
66
66
  function jwtAuthPlugin(_a) {
67
67
  var _this = this;
68
- var secret = _a.secret, getUser = _a.getUser, rolePermissions = _a.rolePermissions, _b = _a.algo, algo = _b === void 0 ? "HS256" : _b, _c = _a.passwordPolicy, passwordPolicy = _c === void 0 ? defaultPasswordPolicy : _c, _d = _a.tokenTTL, tokenTTL = _d === void 0 ? 1000 * 60 * 60 * 24 * 365 * 100 : _d, //Defaults to hundred year
69
- tokenExtractor = _a.tokenExtractor, checkPermissions = _a.checkPermissions, _e = _a.useDynamicRoles, useDynamicRoles = _e === void 0 ? false : _e;
68
+ var secret = _a.secret, getUser = _a.getUser, rolePermissions = _a.rolePermissions, _b = _a.algo, algo = _b === void 0 ? "HS256" : _b, _c = _a.passwordPolicy, passwordPolicy = _c === void 0 ? defaultPasswordPolicy : _c, tokenTTL = _a.tokenTTL, tokenExtractor = _a.tokenExtractor, checkPermissions = _a.checkPermissions, _d = _a.useDynamicRoles, useDynamicRoles = _d === void 0 ? false : _d, resolveTokenTTL = _a.resolveTokenTTL;
70
69
  return {
71
70
  authenticateRequest: function (req, permissions) { return __awaiter(_this, void 0, void 0, function () {
72
71
  return __generator(this, function (_a) {
@@ -80,7 +79,7 @@ function jwtAuthPlugin(_a) {
80
79
  })];
81
80
  });
82
81
  }); },
83
- createToken: function (payload, roles) { return createToken(__assign(__assign({}, payload), { roles: roles }), { algo: algo, secret: secret, tokenTTL: tokenTTL }); },
82
+ createToken: function (payload, roles) { return createToken(__assign(__assign({}, payload), { roles: roles }), { algo: algo, secret: secret, tokenTTL: tokenTTL, resolveTokenTTL: resolveTokenTTL }, roles); },
84
83
  createPasswordHashAndSalt: function (password) { return createPasswordHashAndSalt(password, passwordPolicy); },
85
84
  validatePassword: validatePassword,
86
85
  };
@@ -169,14 +168,18 @@ function getTokenFromReq(req) {
169
168
  }
170
169
  return;
171
170
  }
172
- function createToken(payload_1, _a) {
173
- return __awaiter(this, arguments, void 0, function (payload, _b) {
174
- var secret = _b.secret, algo = _b.algo, tokenTTL = _b.tokenTTL;
175
- return __generator(this, function (_c) {
171
+ function createToken(payload_1, _a, roles_1) {
172
+ return __awaiter(this, arguments, void 0, function (payload, _b, roles) {
173
+ var resolvedTTL, effectiveTTL;
174
+ var _c;
175
+ var secret = _b.secret, algo = _b.algo, tokenTTL = _b.tokenTTL, resolveTokenTTL = _b.resolveTokenTTL;
176
+ return __generator(this, function (_d) {
176
177
  if (!payload) {
177
178
  throw new Error("Cannot create token - payload is missing");
178
179
  }
179
- return [2 /*return*/, jwt_simple_1.default.encode(__assign({ exp: _calculateExpiration(tokenTTL || 1000 * 60 * 60 * 24 * 365 * 100) }, payload), secret, algo)];
180
+ resolvedTTL = resolveTokenTTL === null || resolveTokenTTL === void 0 ? void 0 : resolveTokenTTL(roles, payload);
181
+ effectiveTTL = (_c = resolvedTTL !== null && resolvedTTL !== void 0 ? resolvedTTL : tokenTTL) !== null && _c !== void 0 ? _c : 1000 * 60 * 60 * 24 * 365 * 100;
182
+ return [2 /*return*/, jwt_simple_1.default.encode(__assign({ exp: _calculateExpiration(effectiveTTL) }, payload), secret, algo)];
180
183
  });
181
184
  });
182
185
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flink-app/jwt-auth-plugin",
3
- "version": "2.0.0-alpha.75",
3
+ "version": "2.0.0-alpha.77",
4
4
  "description": "Flink plugin for JWT auth",
5
5
  "author": "joel@frost.se",
6
6
  "license": "MIT",
@@ -19,10 +19,10 @@
19
19
  "@types/node": "22.13.10",
20
20
  "ts-node": "^10.9.2",
21
21
  "tsc-watch": "^4.2.9",
22
- "@flink-app/flink": "2.0.0-alpha.75"
22
+ "@flink-app/flink": "2.0.0-alpha.77"
23
23
  },
24
24
  "peerDependencies": {
25
- "@flink-app/flink": ">=2.0.0-alpha.75"
25
+ "@flink-app/flink": ">=2.0.0-alpha.77"
26
26
  },
27
27
  "gitHead": "4243e3b3cd6d4e1ca001a61baa8436bf2bbe4113",
28
28
  "scripts": {
@@ -813,4 +813,110 @@ describe("FlinkJwtAuthPlugin", () => {
813
813
  expect(capturedHeaders["x-custom-header"]).toBe("custom-value");
814
814
  });
815
815
  });
816
+
817
+ describe("resolveTokenTTL", () => {
818
+ it("should use resolved TTL when callback returns a number", async () => {
819
+ const secret = "secret";
820
+ const ttl = 1000 * 60 * 60 * 8; // 8 hours
821
+
822
+ const plugin = jwtAuthPlugin({
823
+ secret,
824
+ getUser: async () => ({ id: "1", username: "u" }),
825
+ rolePermissions: {},
826
+ resolveTokenTTL: () => ttl,
827
+ });
828
+
829
+ const token = await plugin.createToken({ id: "1" }, ["admin"]);
830
+ const decoded = jwtSimple.decode(token, secret);
831
+ const expectedExp = Math.floor((Date.now() + ttl) / 1000);
832
+
833
+ expect(decoded.exp).toBeCloseTo(expectedExp, -1);
834
+ });
835
+
836
+ it("should fall back to static tokenTTL when callback returns undefined", async () => {
837
+ const secret = "secret";
838
+ const staticTTL = 1000 * 60 * 60 * 2; // 2 hours
839
+
840
+ const plugin = jwtAuthPlugin({
841
+ secret,
842
+ getUser: async () => ({ id: "1", username: "u" }),
843
+ rolePermissions: {},
844
+ tokenTTL: staticTTL,
845
+ resolveTokenTTL: () => undefined,
846
+ });
847
+
848
+ const token = await plugin.createToken({ id: "1" }, ["user"]);
849
+ const decoded = jwtSimple.decode(token, secret);
850
+ const expectedExp = Math.floor((Date.now() + staticTTL) / 1000);
851
+
852
+ expect(decoded.exp).toBeCloseTo(expectedExp, -1);
853
+ });
854
+
855
+ it("should fall back to default TTL when neither option is provided", async () => {
856
+ const secret = "secret";
857
+ const hundredYears = 1000 * 60 * 60 * 24 * 365 * 100;
858
+
859
+ const plugin = jwtAuthPlugin({
860
+ secret,
861
+ getUser: async () => ({ id: "1", username: "u" }),
862
+ rolePermissions: {},
863
+ });
864
+
865
+ const token = await plugin.createToken({ id: "1" }, ["user"]);
866
+ const decoded = jwtSimple.decode(token, secret);
867
+ const expectedExp = Math.floor((Date.now() + hundredYears) / 1000);
868
+
869
+ expect(decoded.exp).toBeCloseTo(expectedExp, -1);
870
+ });
871
+
872
+ it("should receive roles and payload in callback", async () => {
873
+ const secret = "secret";
874
+ let capturedRoles: string[] | undefined;
875
+ let capturedPayload: any;
876
+
877
+ const plugin = jwtAuthPlugin({
878
+ secret,
879
+ getUser: async () => ({ id: "1", username: "u" }),
880
+ rolePermissions: {},
881
+ resolveTokenTTL: (roles, payload) => {
882
+ capturedRoles = roles;
883
+ capturedPayload = payload;
884
+ return 60000;
885
+ },
886
+ });
887
+
888
+ await plugin.createToken({ id: "user-1", email: "a@b.com" }, ["admin", "editor"]);
889
+
890
+ expect(capturedRoles).toEqual(["admin", "editor"]);
891
+ expect(capturedPayload?.id).toBe("user-1");
892
+ expect(capturedPayload?.email).toBe("a@b.com");
893
+ });
894
+
895
+ it("should produce different TTLs for different roles", async () => {
896
+ const secret = "secret";
897
+
898
+ const plugin = jwtAuthPlugin({
899
+ secret,
900
+ getUser: async () => ({ id: "1", username: "u" }),
901
+ rolePermissions: {},
902
+ resolveTokenTTL: (roles) => {
903
+ if (roles.includes("admin")) return 1000 * 60 * 60; // 1 hour
904
+ return 1000 * 60 * 60 * 24 * 30; // 30 days
905
+ },
906
+ });
907
+
908
+ const adminToken = await plugin.createToken({ id: "1" }, ["admin"]);
909
+ const userToken = await plugin.createToken({ id: "2" }, ["user"]);
910
+
911
+ const adminDecoded = jwtSimple.decode(adminToken, secret);
912
+ const userDecoded = jwtSimple.decode(userToken, secret);
913
+
914
+ // User token should expire much later than admin token
915
+ expect(userDecoded.exp).toBeGreaterThan(adminDecoded.exp);
916
+
917
+ // Difference should be roughly 30 days minus 1 hour ≈ 29.96 days in seconds
918
+ const diffSeconds = userDecoded.exp - adminDecoded.exp;
919
+ expect(diffSeconds).toBeGreaterThan(29 * 24 * 60 * 60);
920
+ });
921
+ });
816
922
  });
@@ -34,6 +34,17 @@ export type PermissionChecker = (
34
34
  routePermissions: string[]
35
35
  ) => Promise<boolean> | boolean;
36
36
 
37
+ /**
38
+ * Custom token TTL resolver callback.
39
+ *
40
+ * Called during token creation to determine expiration dynamically based on roles.
41
+ *
42
+ * Return values:
43
+ * - `number`: TTL in milliseconds to use for this token
44
+ * - `undefined`: Fall back to static `tokenTTL` option (or default ~100 years)
45
+ */
46
+ export type TokenTTLResolver = (roles: string[], payload: any) => number | undefined;
47
+
37
48
  export interface JwtAuthPluginOptions {
38
49
  secret: string;
39
50
  algo?: jwtSimple.TAlgorithm;
@@ -87,6 +98,23 @@ export interface JwtAuthPluginOptions {
87
98
  * ```
88
99
  */
89
100
  useDynamicRoles?: boolean;
101
+ /**
102
+ * Optional dynamic token TTL resolver.
103
+ *
104
+ * When provided, called during token creation with the user's roles and payload.
105
+ * Return a TTL in milliseconds to override the static `tokenTTL`, or `undefined`
106
+ * to fall back to `tokenTTL` (or the default).
107
+ *
108
+ * Example:
109
+ * ```typescript
110
+ * resolveTokenTTL: (roles) => {
111
+ * if (roles.includes("service-account")) return 1000 * 60 * 60 * 24 * 365; // 1 year
112
+ * if (roles.includes("admin")) return 1000 * 60 * 60 * 8; // 8 hours
113
+ * return 1000 * 60 * 60; // 1 hour default
114
+ * }
115
+ * ```
116
+ */
117
+ resolveTokenTTL?: TokenTTLResolver;
90
118
  }
91
119
 
92
120
  export interface JwtAuthPlugin extends FlinkAuthPlugin {
@@ -115,10 +143,11 @@ export function jwtAuthPlugin({
115
143
  rolePermissions,
116
144
  algo = "HS256",
117
145
  passwordPolicy = defaultPasswordPolicy,
118
- tokenTTL = 1000 * 60 * 60 * 24 * 365 * 100, //Defaults to hundred year
146
+ tokenTTL,
119
147
  tokenExtractor,
120
148
  checkPermissions,
121
149
  useDynamicRoles = false,
150
+ resolveTokenTTL,
122
151
  }: JwtAuthPluginOptions): JwtAuthPlugin {
123
152
  return {
124
153
  authenticateRequest: async (req, permissions) =>
@@ -130,7 +159,7 @@ export function jwtAuthPlugin({
130
159
  checkPermissions,
131
160
  useDynamicRoles,
132
161
  }),
133
- createToken: (payload, roles) => createToken({ ...payload, roles }, { algo, secret, tokenTTL }),
162
+ createToken: (payload, roles) => createToken({ ...payload, roles }, { algo, secret, tokenTTL, resolveTokenTTL }, roles),
134
163
  createPasswordHashAndSalt: (password: string) => createPasswordHashAndSalt(password, passwordPolicy),
135
164
  validatePassword,
136
165
  };
@@ -234,12 +263,19 @@ function getTokenFromReq(req: FlinkRequest) {
234
263
  return;
235
264
  }
236
265
 
237
- async function createToken(payload: any, { secret, algo, tokenTTL }: Pick<JwtAuthPluginOptions, "algo" | "secret" | "tokenTTL">) {
266
+ async function createToken(
267
+ payload: any,
268
+ { secret, algo, tokenTTL, resolveTokenTTL }: Pick<JwtAuthPluginOptions, "algo" | "secret" | "tokenTTL" | "resolveTokenTTL">,
269
+ roles: string[]
270
+ ) {
238
271
  if (!payload) {
239
272
  throw new Error("Cannot create token - payload is missing");
240
273
  }
241
274
 
242
- return jwtSimple.encode({ exp: _calculateExpiration(tokenTTL || 1000 * 60 * 60 * 24 * 365 * 100), ...payload }, secret, algo);
275
+ const resolvedTTL = resolveTokenTTL?.(roles, payload);
276
+ const effectiveTTL = resolvedTTL ?? tokenTTL ?? 1000 * 60 * 60 * 24 * 365 * 100;
277
+
278
+ return jwtSimple.encode({ exp: _calculateExpiration(effectiveTTL), ...payload }, secret, algo);
243
279
  }
244
280
 
245
281
  function _calculateExpiration(expiresInMs: number) {