@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.
@@ -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,222 @@ 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) => 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;
26
69
  }
27
70
 
28
71
  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>;
72
+ /**
73
+ * Encodes and returns JWT token that includes provided payload.
74
+ *
75
+ * The payload can by anything but should in most cases be and object that
76
+ * holds user information including an identifier such as the username or id.
77
+ */
78
+ createToken: (payload: any, roles: string[]) => Promise<string>;
79
+
80
+ /**
81
+ * Generates new password hash and salt for provided password.
82
+ *
83
+ * This method should be used when setting a new password. Both hash and salt needs
84
+ * to be saved in database as both are needed to validate the password.
85
+ *
86
+ * Returns null if password does not match configured `passwordPolicy`.
87
+ */
88
+ createPasswordHashAndSalt: (password: string) => Promise<{ hash: string; salt: string } | null>;
89
+
90
+ /**
91
+ * Validates that provided `password` is same as provided `hash`.
92
+ */
93
+ validatePassword: (password: string, passwordHash: string, salt: string) => Promise<boolean>;
57
94
  }
58
95
 
59
96
  /**
60
97
  * Configures and creates authentication plugin.
61
98
  */
62
99
  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
100
+ secret,
101
+ getUser,
102
+ rolePermissions,
103
+ algo = "HS256",
104
+ passwordPolicy = defaultPasswordPolicy,
105
+ tokenTTL = 1000 * 60 * 60 * 24 * 365 * 100, //Defaults to hundred year
106
+ tokenExtractor,
107
+ checkPermissions,
69
108
  }: 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
- };
109
+ return {
110
+ authenticateRequest: async (req, permissions) =>
111
+ authenticateRequest(req, permissions, rolePermissions, {
112
+ algo,
113
+ secret,
114
+ getUser,
115
+ tokenExtractor,
116
+ checkPermissions,
117
+ }),
118
+ createToken: (payload, roles) => createToken({ ...payload, roles }, { algo, secret, tokenTTL }),
119
+ createPasswordHashAndSalt: (password: string) => createPasswordHashAndSalt(password, passwordPolicy),
120
+ validatePassword,
121
+ };
83
122
  }
84
123
 
85
124
  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">
125
+ req: FlinkRequest,
126
+ routePermissions: string | string[],
127
+ rolePermissions: { [x: string]: string[] },
128
+ { secret, algo, getUser, tokenExtractor, checkPermissions }: Pick<
129
+ JwtAuthPluginOptions,
130
+ "algo" | "secret" | "getUser" | "tokenExtractor" | "checkPermissions"
131
+ >
94
132
  ) {
95
- const token = getTokenFromReq(req);
133
+ let token: string | null | undefined;
96
134
 
97
- if (token) {
98
- let decodedToken;
135
+ if (tokenExtractor) {
136
+ token = tokenExtractor(req);
99
137
 
100
- try {
101
- decodedToken = jwtSimple.decode(token, secret, false, algo);
102
- } catch (err) {
103
- log.debug(`Failed to decode token: ${err}`);
104
- decodedToken = null;
138
+ // If tokenExtractor returns undefined, fall back to default
139
+ if (token === undefined) {
140
+ token = getTokenFromReq(req);
141
+ }
142
+ // If it returns null, token stays null (no default fallback)
143
+ // If it returns string, token is the string
144
+ } else {
145
+ // No custom extractor, use default
146
+ token = getTokenFromReq(req);
105
147
  }
106
148
 
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
- );
149
+ if (token) {
150
+ let decodedToken;
118
151
 
119
- if (!validPerms) {
120
- return false;
152
+ try {
153
+ decodedToken = jwtSimple.decode(token, secret, false, algo);
154
+ } catch (err) {
155
+ log.debug(`[JWT AUTH PLUGIN] Failed to decode token: ${err}`);
156
+ decodedToken = null;
121
157
  }
122
- }
123
158
 
124
- const user = await getUser(decodedToken);
159
+ if (decodedToken) {
160
+ const permissionsArr = Array.isArray(routePermissions) ? routePermissions : [routePermissions];
125
161
 
162
+ // Static permission check - only if custom checker NOT provided
163
+ if (!checkPermissions && permissionsArr && permissionsArr.length > 0) {
164
+ const validPerms = hasValidPermissions(decodedToken.roles || [], rolePermissions, permissionsArr);
126
165
 
127
- req.user = user;
128
- return true;
166
+ if (!validPerms) {
167
+ return false;
168
+ }
169
+ }
170
+
171
+ const user = await getUser(decodedToken);
172
+
173
+ if (!user) {
174
+ log.debug("[JWT AUTH PLUGIN] User not returned from getUser callback");
175
+ return false;
176
+ }
177
+
178
+ // Custom permission check - only if provided
179
+ if (checkPermissions && permissionsArr && permissionsArr.length > 0) {
180
+ const hasPermission = await checkPermissions(user, permissionsArr);
181
+
182
+ if (!hasPermission) {
183
+ log.debug("[JWT AUTH PLUGIN] Custom permission check failed");
184
+ return false;
185
+ }
186
+ }
187
+
188
+ req.user = user;
189
+ return true;
190
+ }
129
191
  }
130
- }
131
- return false;
192
+ return false;
132
193
  }
133
194
 
134
195
  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;
196
+ const authHeader = req.headers.authorization;
197
+ if (authHeader) {
198
+ const [, token] = authHeader.split("Bearer ");
199
+ return token;
200
+ }
201
+ return;
141
202
  }
142
203
 
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
- }
204
+ async function createToken(payload: any, { secret, algo, tokenTTL }: Pick<JwtAuthPluginOptions, "algo" | "secret" | "tokenTTL">) {
205
+ if (!payload) {
206
+ throw new Error("Cannot create token - payload is missing");
207
+ }
150
208
 
151
- return jwtSimple.encode({exp : _calculateExpiration(tokenTTL || 1000 * 60 * 60 * 24 * 365 * 100), ...payload}, secret, algo);
209
+ return jwtSimple.encode({ exp: _calculateExpiration(tokenTTL || 1000 * 60 * 60 * 24 * 365 * 100), ...payload }, secret, algo);
152
210
  }
153
211
 
154
- function _calculateExpiration(expiresInMs : number) {
212
+ function _calculateExpiration(expiresInMs: number) {
155
213
  return Math.floor((Date.now() + expiresInMs) / 1000);
156
214
  }
157
215
 
216
+ async function createPasswordHashAndSalt(password: string, passwordPolicy: RegExp) {
217
+ if (!passwordPolicy.test(password)) {
218
+ log.debug(`Password does not match password policy '${passwordPolicy}'`);
219
+ return null;
220
+ }
158
221
 
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 };
222
+ const salt = await genSalt(10);
223
+ const hash = await encrypt(password, salt);
224
+ return { salt, hash };
171
225
  }
172
226
 
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;
227
+ async function validatePassword(password: string, passwordHash: string, salt: string) {
228
+ const hashCandidate = await encrypt(password, salt);
229
+ return hashCandidate === passwordHash;
180
230
  }