@flink-app/jwt-auth-plugin 2.0.0-alpha.74 → 2.0.0-alpha.76
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 +16 -0
- package/dist/FlinkJwtAuthPlugin.d.ts +28 -2
- package/dist/FlinkJwtAuthPlugin.js +11 -8
- package/package.json +3 -3
- package/spec/FlinkJwtAuthPlugin.spec.ts +106 -0
- package/src/FlinkJwtAuthPlugin.ts +40 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# @flink-app/jwt-auth-plugin
|
|
2
2
|
|
|
3
|
+
## 2.0.0-alpha.76
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 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.
|
|
8
|
+
|
|
9
|
+
### Patch Changes
|
|
10
|
+
|
|
11
|
+
- @flink-app/flink@2.0.0-alpha.76
|
|
12
|
+
|
|
13
|
+
## 2.0.0-alpha.75
|
|
14
|
+
|
|
15
|
+
### Patch Changes
|
|
16
|
+
|
|
17
|
+
- @flink-app/flink@2.0.0-alpha.75
|
|
18
|
+
|
|
3
19
|
## 2.0.0-alpha.74
|
|
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,
|
|
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,
|
|
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
|
|
175
|
-
|
|
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
|
-
|
|
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.
|
|
3
|
+
"version": "2.0.0-alpha.76",
|
|
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.
|
|
22
|
+
"@flink-app/flink": "2.0.0-alpha.76"
|
|
23
23
|
},
|
|
24
24
|
"peerDependencies": {
|
|
25
|
-
"@flink-app/flink": ">=2.0.0-alpha.
|
|
25
|
+
"@flink-app/flink": ">=2.0.0-alpha.76"
|
|
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
|
|
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(
|
|
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
|
-
|
|
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) {
|