@flink-app/jwt-auth-plugin 0.12.1-alpha.9 → 0.13.1

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.
@@ -1,9 +1,4 @@
1
- import {
2
- FlinkAuthPlugin,
3
- FlinkAuthUser,
4
- FlinkRequest,
5
- log,
6
- } from "@flink-app/flink";
1
+ import { FlinkAuthPlugin, FlinkAuthUser, FlinkRequest, log } from "@flink-app/flink";
7
2
  import jwtSimple from "jwt-simple";
8
3
  import { encrypt, genSalt } from "./BcryptUtils";
9
4
  import { hasValidPermissions } from "./PermissionValidator";
@@ -14,167 +9,255 @@ import { hasValidPermissions } from "./PermissionValidator";
14
9
  */
15
10
  const defaultPasswordPolicy = /^(?=.*[A-Za-z])(?=.*\d)[A-Za-z\d]{8,}$/;
16
11
 
12
+ /**
13
+ * Custom token extraction callback.
14
+ *
15
+ * Return values:
16
+ * - `string`: Token found, use this token
17
+ * - `null`: No token found, authentication should fail
18
+ * - `undefined`: Skip custom extraction, use default Bearer token extraction
19
+ */
20
+ export type TokenExtractor = (req: FlinkRequest) => string | null | undefined;
21
+
22
+ /**
23
+ * Custom permission validation callback.
24
+ *
25
+ * Called after getUser to validate if the user has required permissions.
26
+ * Useful for dynamic permissions stored in database.
27
+ *
28
+ * @param user - The authenticated user object returned from getUser
29
+ * @param routePermissions - Array of permissions required by the route
30
+ * @returns true if user has required permissions, false otherwise
31
+ */
32
+ export type PermissionChecker = (
33
+ user: FlinkAuthUser,
34
+ routePermissions: string[]
35
+ ) => Promise<boolean> | boolean;
36
+
17
37
  export interface JwtAuthPluginOptions {
18
- secret: string;
19
- algo?: jwtSimple.TAlgorithm;
20
- getUser: (tokenData: any) => Promise<FlinkAuthUser>;
21
- passwordPolicy?: RegExp;
22
- tokenTTL? : number;
23
- rolePermissions: {
24
- [role: string]: string[];
25
- };
38
+ secret: string;
39
+ algo?: jwtSimple.TAlgorithm;
40
+ getUser: (tokenData: any, req: FlinkRequest) => Promise<FlinkAuthUser | null | undefined>;
41
+ passwordPolicy?: RegExp;
42
+ tokenTTL?: number;
43
+ rolePermissions: {
44
+ [role: string]: string[];
45
+ };
46
+ /**
47
+ * Optional custom token extraction callback.
48
+ *
49
+ * Allows conditional token extraction based on request properties (path, method, headers, etc.).
50
+ * Return `undefined` to fall back to default Bearer token extraction.
51
+ */
52
+ tokenExtractor?: TokenExtractor;
53
+ /**
54
+ * Optional custom permission checker for dynamic permissions.
55
+ *
56
+ * When provided, replaces static rolePermissions checking.
57
+ * Called after getUser with the full user object.
58
+ *
59
+ * Example:
60
+ * ```typescript
61
+ * checkPermissions: async (user, routePermissions) => {
62
+ * return routePermissions.every(perm =>
63
+ * user.permissions?.includes(perm)
64
+ * );
65
+ * }
66
+ * ```
67
+ */
68
+ checkPermissions?: PermissionChecker;
69
+ /**
70
+ * When true, uses roles from the user object returned by getUser
71
+ * instead of roles from the decoded token for static permission checking.
72
+ *
73
+ * Useful for multi-tenant scenarios where user roles vary by organization context.
74
+ * The organization context can be determined from request headers, subdomain, path, etc.
75
+ *
76
+ * Example:
77
+ * ```typescript
78
+ * useDynamicRoles: true,
79
+ * getUser: async (tokenData, req) => {
80
+ * const orgId = req.headers['x-organization-id'];
81
+ * const membership = await getOrgMembership(tokenData.userId, orgId);
82
+ * return {
83
+ * id: tokenData.userId,
84
+ * roles: [membership.role], // Org-specific role
85
+ * };
86
+ * }
87
+ * ```
88
+ */
89
+ useDynamicRoles?: boolean;
26
90
  }
27
91
 
28
92
  export interface JwtAuthPlugin extends FlinkAuthPlugin {
29
- /**
30
- * Encodes and returns JWT token that includes provided payload.
31
- *
32
- * The payload can by anything but should in most cases be and object that
33
- * holds user information including an identifier such as the username or id.
34
- */
35
- createToken: (payload: any, roles: string[]) => Promise<string>;
36
-
37
- /**
38
- * Generates new password hash and salt for provided password.
39
- *
40
- * This method should be used when setting a new password. Both hash and salt needs
41
- * to be saved in database as both are needed to validate the password.
42
- *
43
- * Returns null if password does not match configured `passwordPolicy`.
44
- */
45
- createPasswordHashAndSalt: (
46
- password: string
47
- ) => Promise<{ hash: string; salt: string } | null>;
48
-
49
- /**
50
- * Validates that provided `password` is same as provided `hash`.
51
- */
52
- validatePassword: (
53
- password: string,
54
- passwordHash: string,
55
- salt: string
56
- ) => Promise<boolean>;
93
+ /**
94
+ * Encodes and returns JWT token that includes provided payload.
95
+ *
96
+ * The payload can by anything but should in most cases be and object that
97
+ * holds user information including an identifier such as the username or id.
98
+ */
99
+ createToken: (payload: any, roles: string[]) => Promise<string>;
100
+
101
+ /**
102
+ * Generates new password hash and salt for provided password.
103
+ *
104
+ * This method should be used when setting a new password. Both hash and salt needs
105
+ * to be saved in database as both are needed to validate the password.
106
+ *
107
+ * Returns null if password does not match configured `passwordPolicy`.
108
+ */
109
+ createPasswordHashAndSalt: (password: string) => Promise<{ hash: string; salt: string } | null>;
110
+
111
+ /**
112
+ * Validates that provided `password` is same as provided `hash`.
113
+ */
114
+ validatePassword: (password: string, passwordHash: string, salt: string) => Promise<boolean>;
57
115
  }
58
116
 
59
117
  /**
60
118
  * Configures and creates authentication plugin.
61
119
  */
62
120
  export function jwtAuthPlugin({
63
- secret,
64
- getUser,
65
- rolePermissions,
66
- algo = "HS256",
67
- passwordPolicy = defaultPasswordPolicy,
68
- tokenTTL = 1000 * 60 * 60 * 24 * 365 * 100, //Defaults to hundred year
121
+ secret,
122
+ getUser,
123
+ rolePermissions,
124
+ algo = "HS256",
125
+ passwordPolicy = defaultPasswordPolicy,
126
+ tokenTTL = 1000 * 60 * 60 * 24 * 365 * 100, //Defaults to hundred year
127
+ tokenExtractor,
128
+ checkPermissions,
129
+ useDynamicRoles = false,
69
130
  }: JwtAuthPluginOptions): JwtAuthPlugin {
70
- return {
71
- authenticateRequest: async (req, permissions) =>
72
- authenticateRequest(req, permissions, rolePermissions, {
73
- algo,
74
- secret,
75
- getUser,
76
- }),
77
- createToken: (payload, roles) =>
78
- createToken({ ...payload, roles }, { algo, secret, tokenTTL }),
79
- createPasswordHashAndSalt: (password: string) =>
80
- createPasswordHashAndSalt(password, passwordPolicy),
81
- validatePassword,
82
- };
131
+ return {
132
+ authenticateRequest: async (req, permissions) =>
133
+ authenticateRequest(req, permissions, rolePermissions, {
134
+ algo,
135
+ secret,
136
+ getUser,
137
+ tokenExtractor,
138
+ checkPermissions,
139
+ useDynamicRoles,
140
+ }),
141
+ createToken: (payload, roles) => createToken({ ...payload, roles }, { algo, secret, tokenTTL }),
142
+ createPasswordHashAndSalt: (password: string) => createPasswordHashAndSalt(password, passwordPolicy),
143
+ validatePassword,
144
+ };
83
145
  }
84
146
 
85
147
  async function authenticateRequest(
86
- req: FlinkRequest,
87
- routePermissions: string | string[],
88
- rolePermissions: { [x: string]: string[] },
89
- {
90
- secret,
91
- algo,
92
- getUser,
93
- }: Pick<JwtAuthPluginOptions, "algo" | "secret" | "getUser">
148
+ req: FlinkRequest,
149
+ routePermissions: string | string[],
150
+ rolePermissions: { [x: string]: string[] },
151
+ { secret, algo, getUser, tokenExtractor, checkPermissions, useDynamicRoles }: Pick<
152
+ JwtAuthPluginOptions,
153
+ "algo" | "secret" | "getUser" | "tokenExtractor" | "checkPermissions" | "useDynamicRoles"
154
+ >
94
155
  ) {
95
- const token = getTokenFromReq(req);
156
+ let token: string | null | undefined;
96
157
 
97
- if (token) {
98
- let decodedToken;
158
+ if (tokenExtractor) {
159
+ token = tokenExtractor(req);
99
160
 
100
- try {
101
- decodedToken = jwtSimple.decode(token, secret, false, algo);
102
- } catch (err) {
103
- log.debug(`Failed to decode token: ${err}`);
104
- decodedToken = null;
161
+ // If tokenExtractor returns undefined, fall back to default
162
+ if (token === undefined) {
163
+ token = getTokenFromReq(req);
164
+ }
165
+ // If it returns null, token stays null (no default fallback)
166
+ // If it returns string, token is the string
167
+ } else {
168
+ // No custom extractor, use default
169
+ token = getTokenFromReq(req);
105
170
  }
106
171
 
107
- if (decodedToken) {
108
- const permissionsArr = Array.isArray(routePermissions)
109
- ? routePermissions
110
- : [routePermissions];
111
-
112
- if (permissionsArr && permissionsArr.length > 0) {
113
- const validPerms = hasValidPermissions(
114
- decodedToken.roles || [],
115
- rolePermissions,
116
- permissionsArr
117
- );
172
+ if (token) {
173
+ let decodedToken;
118
174
 
119
- if (!validPerms) {
120
- return false;
175
+ try {
176
+ decodedToken = jwtSimple.decode(token, secret, false, algo);
177
+ } catch (err) {
178
+ log.debug(`[JWT AUTH PLUGIN] Failed to decode token: ${err}`);
179
+ decodedToken = null;
121
180
  }
122
- }
123
181
 
124
- const user = await getUser(decodedToken);
182
+ if (decodedToken) {
183
+ const permissionsArr = Array.isArray(routePermissions) ? routePermissions : [routePermissions];
125
184
 
185
+ // Static permission check - only if custom checker NOT provided AND not using dynamic roles
186
+ if (!checkPermissions && !useDynamicRoles && permissionsArr && permissionsArr.length > 0) {
187
+ const validPerms = hasValidPermissions(decodedToken.roles || [], rolePermissions, permissionsArr);
126
188
 
127
- req.user = user;
128
- return true;
189
+ if (!validPerms) {
190
+ return false;
191
+ }
192
+ }
193
+
194
+ const user = await getUser(decodedToken, req);
195
+
196
+ if (!user) {
197
+ log.debug("[JWT AUTH PLUGIN] User not returned from getUser callback");
198
+ return false;
199
+ }
200
+
201
+ // Dynamic roles: check permissions using roles from user object
202
+ if (!checkPermissions && useDynamicRoles && permissionsArr && permissionsArr.length > 0) {
203
+ const validPerms = hasValidPermissions(user.roles || [], rolePermissions, permissionsArr);
204
+
205
+ if (!validPerms) {
206
+ log.debug("[JWT AUTH PLUGIN] Dynamic role permission check failed");
207
+ return false;
208
+ }
209
+ }
210
+
211
+ // Custom permission check - only if provided
212
+ if (checkPermissions && permissionsArr && permissionsArr.length > 0) {
213
+ const hasPermission = await checkPermissions(user, permissionsArr);
214
+
215
+ if (!hasPermission) {
216
+ log.debug("[JWT AUTH PLUGIN] Custom permission check failed");
217
+ return false;
218
+ }
219
+ }
220
+
221
+ req.user = user;
222
+ return true;
223
+ }
129
224
  }
130
- }
131
- return false;
225
+ return false;
132
226
  }
133
227
 
134
228
  function getTokenFromReq(req: FlinkRequest) {
135
- const authHeader = req.headers.authorization;
136
- if (authHeader) {
137
- const [, token] = authHeader.split("Bearer ");
138
- return token;
139
- }
140
- return;
229
+ const authHeader = req.headers.authorization;
230
+ if (authHeader) {
231
+ const [, token] = authHeader.split("Bearer ");
232
+ return token;
233
+ }
234
+ return;
141
235
  }
142
236
 
143
- async function createToken(
144
- payload: any,
145
- { secret, algo, tokenTTL }: Pick<JwtAuthPluginOptions, "algo" | "secret" | "tokenTTL" >
146
- ) {
147
- if (!payload) {
148
- throw new Error("Cannot create token - payload is missing");
149
- }
237
+ async function createToken(payload: any, { secret, algo, tokenTTL }: Pick<JwtAuthPluginOptions, "algo" | "secret" | "tokenTTL">) {
238
+ if (!payload) {
239
+ throw new Error("Cannot create token - payload is missing");
240
+ }
150
241
 
151
- return jwtSimple.encode({exp : _calculateExpiration(tokenTTL || 1000 * 60 * 60 * 24 * 365 * 100), ...payload}, secret, algo);
242
+ return jwtSimple.encode({ exp: _calculateExpiration(tokenTTL || 1000 * 60 * 60 * 24 * 365 * 100), ...payload }, secret, algo);
152
243
  }
153
244
 
154
- function _calculateExpiration(expiresInMs : number) {
245
+ function _calculateExpiration(expiresInMs: number) {
155
246
  return Math.floor((Date.now() + expiresInMs) / 1000);
156
247
  }
157
248
 
249
+ async function createPasswordHashAndSalt(password: string, passwordPolicy: RegExp) {
250
+ if (!passwordPolicy.test(password)) {
251
+ log.debug(`Password does not match password policy '${passwordPolicy}'`);
252
+ return null;
253
+ }
158
254
 
159
- async function createPasswordHashAndSalt(
160
- password: string,
161
- passwordPolicy: RegExp
162
- ) {
163
- if (!passwordPolicy.test(password)) {
164
- log.debug(`Password does not match password policy '${passwordPolicy}'`);
165
- return null;
166
- }
167
-
168
- const salt = await genSalt(10);
169
- const hash = await encrypt(password, salt);
170
- return { salt, hash };
255
+ const salt = await genSalt(10);
256
+ const hash = await encrypt(password, salt);
257
+ return { salt, hash };
171
258
  }
172
259
 
173
- async function validatePassword(
174
- password: string,
175
- passwordHash: string,
176
- salt: string
177
- ) {
178
- const hashCandidate = await encrypt(password, salt);
179
- return hashCandidate === passwordHash;
260
+ async function validatePassword(password: string, passwordHash: string, salt: string) {
261
+ const hashCandidate = await encrypt(password, salt);
262
+ return hashCandidate === passwordHash;
180
263
  }
package/tsconfig.json CHANGED
@@ -15,7 +15,7 @@
15
15
  "noEmit": false,
16
16
  "declaration": true,
17
17
  "experimentalDecorators": true,
18
- "checkJs": true,
18
+ "checkJs": false,
19
19
  "outDir": "dist",
20
20
  "typeRoots": ["./node_modules/@types"]
21
21
  },