@flink-app/jwt-auth-plugin 2.0.0-alpha.112 → 2.0.0-alpha.114

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,27 @@
1
1
  # @flink-app/jwt-auth-plugin
2
2
 
3
+ ## 2.0.0-alpha.114
4
+
5
+ ### Minor Changes
6
+
7
+ - 5f11719: Response headers from a successful authentication. `FlinkAuthPlugin.authenticateRequest` may now return `{ authenticated: true, headers }` (in addition to `true | false | FlinkResponse`) to proceed to the handler _and_ contribute headers to the response — the seat auth plugins previously lacked for cross-cutting concerns discovered during authentication, such as re-issuing a sliding session cookie. The headers are applied to whatever response Flink ends up sending, including handler and framework errors, and a handler's own `FlinkResponse.headers` win on conflict. Core also exports `isAuthSuccess()` and `getAuthSuccessHeaders()` for code that consumes an auth result itself.
8
+
9
+ The jwt-auth-plugin exposes this as `onAuthSuccess(user, req, ctx)`, the counterpart to `onAuthFailure`: called only on successful authentication, just before the handler runs — return `{ headers }` to add them to the response, or void to leave it untouched.
10
+
11
+ Fully backward compatible: plugins returning `true` behave exactly as before.
12
+
13
+ ### Patch Changes
14
+
15
+ - Updated dependencies [5f11719]
16
+ - @flink-app/flink@2.0.0-alpha.114
17
+
18
+ ## 2.0.0-alpha.113
19
+
20
+ ### Patch Changes
21
+
22
+ - Updated dependencies [4a8b81a]
23
+ - @flink-app/flink@2.0.0-alpha.113
24
+
3
25
  ## 2.0.0-alpha.112
4
26
 
5
27
  ### Patch Changes
@@ -45,6 +45,27 @@ export interface AuthFailure {
45
45
  * instead of the default `401 Unauthorized`. Return void/undefined to keep the default.
46
46
  */
47
47
  export type AuthFailureHandler = (failure: AuthFailure, req: FlinkRequest, ctx?: any) => FlinkResponse | void | Promise<FlinkResponse | void>;
48
+ /**
49
+ * What an auth-success callback may contribute to the response.
50
+ */
51
+ export interface AuthSuccessResult {
52
+ /**
53
+ * Headers to merge into whatever response Flink ends up sending for this
54
+ * request, e.g. a `Set-Cookie` for a renewed session. Headers set by the
55
+ * handler itself win on conflict.
56
+ */
57
+ headers?: Record<string, string>;
58
+ }
59
+ /**
60
+ * Custom auth-success callback, the counterpart to `onAuthFailure`.
61
+ *
62
+ * Called only when authentication succeeded, right before the handler runs, with
63
+ * the user resolved by `getUser`. Return `{ headers }` to have Flink apply those
64
+ * headers to the response - the seat for cross-cutting concerns discovered during
65
+ * authentication, such as re-issuing a sliding session cookie. Return void to
66
+ * leave the response untouched.
67
+ */
68
+ export type AuthSuccessHandler = (user: FlinkAuthUser, req: FlinkRequest, ctx?: any) => AuthSuccessResult | void | Promise<AuthSuccessResult | void>;
48
69
  /**
49
70
  * Custom token TTL resolver callback.
50
71
  *
@@ -134,6 +155,25 @@ export interface JwtAuthPluginOptions {
134
155
  * ```
135
156
  */
136
157
  onAuthFailure?: AuthFailureHandler;
158
+ /**
159
+ * Optional callback invoked on successful authentication.
160
+ *
161
+ * Called with the authenticated user just before the handler runs, and never
162
+ * for rejected requests. Return `{ headers }` to contribute headers to the
163
+ * response, e.g. a re-signed session cookie when the presented token is
164
+ * running low on lifetime. Headers set by the handler win on conflict.
165
+ *
166
+ * Example:
167
+ * ```typescript
168
+ * onAuthSuccess: (user, req) => {
169
+ * const renewed = renewCookieIfExpiring(req);
170
+ * if (renewed) {
171
+ * return { headers: { "set-cookie": renewed } };
172
+ * }
173
+ * }
174
+ * ```
175
+ */
176
+ onAuthSuccess?: AuthSuccessHandler;
137
177
  /**
138
178
  * Optional dynamic token TTL resolver.
139
179
  *
@@ -173,4 +213,4 @@ export interface JwtAuthPlugin extends FlinkAuthPlugin {
173
213
  /**
174
214
  * Configures and creates authentication plugin.
175
215
  */
176
- export declare function jwtAuthPlugin({ secret, getUser, rolePermissions, algo, passwordPolicy, tokenTTL, tokenExtractor, checkPermissions, useDynamicRoles, resolveTokenTTL, onAuthFailure, }: JwtAuthPluginOptions): JwtAuthPlugin;
216
+ export declare function jwtAuthPlugin({ secret, getUser, rolePermissions, algo, passwordPolicy, tokenTTL, tokenExtractor, checkPermissions, useDynamicRoles, resolveTokenTTL, onAuthFailure, onAuthSuccess, }: JwtAuthPluginOptions): JwtAuthPlugin;
@@ -65,7 +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, tokenTTL = _a.tokenTTL, tokenExtractor = _a.tokenExtractor, checkPermissions = _a.checkPermissions, _d = _a.useDynamicRoles, useDynamicRoles = _d === void 0 ? false : _d, resolveTokenTTL = _a.resolveTokenTTL, onAuthFailure = _a.onAuthFailure;
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, onAuthFailure = _a.onAuthFailure, onAuthSuccess = _a.onAuthSuccess;
69
69
  return {
70
70
  authenticateRequest: function (req, permissions, ctx) { return __awaiter(_this, void 0, void 0, function () {
71
71
  return __generator(this, function (_a) {
@@ -77,6 +77,7 @@ function jwtAuthPlugin(_a) {
77
77
  checkPermissions: checkPermissions,
78
78
  useDynamicRoles: useDynamicRoles,
79
79
  onAuthFailure: onAuthFailure,
80
+ onAuthSuccess: onAuthSuccess,
80
81
  })];
81
82
  });
82
83
  }); },
@@ -87,9 +88,9 @@ function jwtAuthPlugin(_a) {
87
88
  }
88
89
  function authenticateRequest(req_1, routePermissions_1, rolePermissions_1, ctx_1, _a) {
89
90
  return __awaiter(this, arguments, void 0, function (req, routePermissions, rolePermissions, ctx, _b) {
90
- var fail, token, decodedToken, decodeError, permissionsArr, validPerms, user, validPerms, hasPermission;
91
+ var fail, token, decodedToken, decodeError, permissionsArr, validPerms, user, validPerms, hasPermission, successRes;
91
92
  var _this = this;
92
- var secret = _b.secret, algo = _b.algo, getUser = _b.getUser, tokenExtractor = _b.tokenExtractor, checkPermissions = _b.checkPermissions, useDynamicRoles = _b.useDynamicRoles, onAuthFailure = _b.onAuthFailure;
93
+ var secret = _b.secret, algo = _b.algo, getUser = _b.getUser, tokenExtractor = _b.tokenExtractor, checkPermissions = _b.checkPermissions, useDynamicRoles = _b.useDynamicRoles, onAuthFailure = _b.onAuthFailure, onAuthSuccess = _b.onAuthSuccess;
93
94
  return __generator(this, function (_c) {
94
95
  switch (_c.label) {
95
96
  case 0:
@@ -183,7 +184,15 @@ function authenticateRequest(req_1, routePermissions_1, rolePermissions_1, ctx_1
183
184
  rolePermissions: rolePermissions,
184
185
  checkPermissions: checkPermissions,
185
186
  });
186
- return [2 /*return*/, true];
187
+ if (!onAuthSuccess) return [3 /*break*/, 5];
188
+ return [4 /*yield*/, onAuthSuccess(user, req, ctx)];
189
+ case 4:
190
+ successRes = _c.sent();
191
+ if (successRes === null || successRes === void 0 ? void 0 : successRes.headers) {
192
+ return [2 /*return*/, { authenticated: true, headers: successRes.headers }];
193
+ }
194
+ _c.label = 5;
195
+ case 5: return [2 /*return*/, true];
187
196
  }
188
197
  });
189
198
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flink-app/jwt-auth-plugin",
3
- "version": "2.0.0-alpha.112",
3
+ "version": "2.0.0-alpha.114",
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.112"
22
+ "@flink-app/flink": "2.0.0-alpha.114"
23
23
  },
24
24
  "peerDependencies": {
25
- "@flink-app/flink": ">=2.0.0-alpha.112"
25
+ "@flink-app/flink": ">=2.0.0-alpha.114"
26
26
  },
27
27
  "gitHead": "4243e3b3cd6d4e1ca001a61baa8436bf2bbe4113",
28
28
  "scripts": {
package/readme.md CHANGED
@@ -74,6 +74,7 @@ start();
74
74
  | `tokenExtractor` | `(req: FlinkRequest) => string \| null \| undefined` | No | - | Custom token extraction function. Return `string` for token, `null` for no token, `undefined` to fallback to Bearer |
75
75
  | `checkPermissions` | `(user: FlinkAuthUser, routePermissions: string[]) => Promise<boolean> \| boolean` | No | - | Custom permission validator for dynamic permissions from database. Replaces static `rolePermissions` when provided |
76
76
  | `useDynamicRoles` | `boolean` | No | `false` | When `true`, uses roles from the user object returned by `getUser` instead of roles from the token. Useful for multi-tenant scenarios where user roles vary by organization |
77
+ | `onAuthSuccess` | `(user: FlinkAuthUser, req: FlinkRequest, ctx?: any) => AuthSuccessResult \| void` | No | - | Called on successful authentication, before the handler runs. Return `{ headers }` to add response headers, e.g. a renewed session cookie |
77
78
 
78
79
  ### Default Password Policy
79
80
 
@@ -481,6 +482,46 @@ jwtAuthPlugin({
481
482
 
482
483
  **Solution**: Clear permission cache or reduce cache TTL
483
484
 
485
+ ## `onAuthSuccess` - Response Headers From The Auth Step
486
+
487
+ The counterpart to `onAuthFailure`. It is called **only** when authentication succeeded, right before
488
+ the handler runs, with the user resolved by `getUser`. Return `{ headers }` and Flink applies them to
489
+ whatever response it ends up sending for that request.
490
+
491
+ This is the seat for cross-cutting concerns the *authentication step* discovers - the canonical one
492
+ being a sliding session: re-sign the cookie JWT when the presented token is running low, and let the
493
+ new cookie ride out on the same response.
494
+
495
+ ```typescript
496
+ jwtAuthPlugin({
497
+ // ...
498
+ onAuthSuccess: (user, req, ctx) => {
499
+ const { exp } = (req.token as { exp?: number }) ?? {};
500
+ if (!exp) return;
501
+
502
+ const now = Math.floor(Date.now() / 1000);
503
+ if (exp - now > 10 * 60) return; // still fresh, leave the response alone
504
+
505
+ const renewed = jwtSimple.encode({ ...req.token, iat: now, exp: now + 30 * 60 }, secret);
506
+
507
+ return { headers: { "set-cookie": `jwt=${renewed}; Path=/; HttpOnly; Secure; SameSite=Lax` } };
508
+ },
509
+ });
510
+ ```
511
+
512
+ **Notes:**
513
+
514
+ - Never called for a rejected request - missing/invalid token, `no_user` and permission denials all go
515
+ to `onAuthFailure` instead.
516
+ - Headers apply to every response for that request, including handler errors and framework errors
517
+ (400 validation, 500 handler exceptions) - the session really was rotated server side.
518
+ - A handler's own `FlinkResponse.headers` are applied afterwards and **win on conflict** for the same
519
+ header name. Headers that don't collide are kept.
520
+ - Return void (or omit `headers`) to leave the response untouched - the plugin then returns plain
521
+ `true` exactly as before.
522
+ - Custom auth plugins get the same primitive: `authenticateRequest` may return
523
+ `{ authenticated: true, headers }` in addition to `true | false | FlinkResponse`.
524
+
484
525
  ## `req.token` & `req.rawToken` - Accessing Token Claims
485
526
 
486
527
  After successful authentication the plugin attaches the verified token to the request:
@@ -1097,3 +1097,77 @@ describe("FlinkJwtAuthPlugin onAuthFailure", () => {
1097
1097
  expect(failures).toEqual([]);
1098
1098
  });
1099
1099
  });
1100
+
1101
+ describe("FlinkJwtAuthPlugin onAuthSuccess", () => {
1102
+ const secret = "secret";
1103
+ const renewedCookie = "jwt=renewed; path=/; HttpOnly;";
1104
+
1105
+ const makePlugin = (onAuthSuccess: any, overrides: any = {}) =>
1106
+ jwtAuthPlugin({
1107
+ secret,
1108
+ getUser: async ({ id }: { id: string }) => ({ id, username: "username" }),
1109
+ rolePermissions: { user: ["foo"] },
1110
+ onAuthSuccess,
1111
+ ...overrides,
1112
+ });
1113
+
1114
+ const reqWith = (authorization: string) =>
1115
+ ({ headers: { authorization } } as FlinkRequest);
1116
+
1117
+ const validToken = () => jwtSimple.encode({ id: "123", roles: ["user"] }, secret);
1118
+
1119
+ it("returns the headers from onAuthSuccess as an authenticated result", async () => {
1120
+ const seen: any[] = [];
1121
+ const plugin = makePlugin((user: any, req: FlinkRequest) => {
1122
+ seen.push({ user, hasReq: !!req });
1123
+ return { headers: { "set-cookie": renewedCookie } };
1124
+ });
1125
+
1126
+ const res: any = await plugin.authenticateRequest(reqWith("Bearer " + validToken()), "foo");
1127
+
1128
+ expect(res).toEqual({ authenticated: true, headers: { "set-cookie": renewedCookie } });
1129
+ expect(seen).toEqual([{ user: { id: "123", username: "username" }, hasReq: true }]);
1130
+ });
1131
+
1132
+ it("returns plain true when onAuthSuccess returns void", async () => {
1133
+ const plugin = makePlugin(() => {});
1134
+
1135
+ const res = await plugin.authenticateRequest(reqWith("Bearer " + validToken()), "foo");
1136
+
1137
+ expect(res).toBeTrue();
1138
+ });
1139
+
1140
+ it("supports an async onAuthSuccess", async () => {
1141
+ const plugin = makePlugin(async () => ({ headers: { "set-cookie": renewedCookie } }));
1142
+
1143
+ const res: any = await plugin.authenticateRequest(reqWith("Bearer " + validToken()), "foo");
1144
+
1145
+ expect(res.authenticated).toBeTrue();
1146
+ expect(res.headers["set-cookie"]).toBe(renewedCookie);
1147
+ });
1148
+
1149
+ it("does not invoke onAuthSuccess for a rejected request", async () => {
1150
+ const calls: any[] = [];
1151
+ const plugin = makePlugin(() => {
1152
+ calls.push("called");
1153
+ return { headers: { "set-cookie": renewedCookie } };
1154
+ });
1155
+
1156
+ // missing token, invalid token and permission denied
1157
+ expect(await plugin.authenticateRequest(reqWith(""), "foo")).toBeFalse();
1158
+ expect(await plugin.authenticateRequest(reqWith("Bearer garbage"), "foo")).toBeFalse();
1159
+ expect(await plugin.authenticateRequest(reqWith("Bearer " + validToken()), "bar")).toBeFalse();
1160
+
1161
+ expect(calls).toEqual([]);
1162
+ });
1163
+
1164
+ it("keeps returning plain true when no onAuthSuccess is configured", async () => {
1165
+ const plugin = jwtAuthPlugin({
1166
+ secret,
1167
+ getUser: async ({ id }: { id: string }) => ({ id, username: "username" }),
1168
+ rolePermissions: { user: ["foo"] },
1169
+ });
1170
+
1171
+ expect(await plugin.authenticateRequest(reqWith("Bearer " + validToken()), "foo")).toBeTrue();
1172
+ });
1173
+ });
@@ -1,4 +1,4 @@
1
- import { FlinkAuthPlugin, FlinkAuthUser, FlinkRequest, FlinkResponse, log } from "@flink-app/flink";
1
+ import { FlinkAuthPlugin, FlinkAuthResult, FlinkAuthUser, FlinkRequest, FlinkResponse, log } from "@flink-app/flink";
2
2
  import jwtSimple from "jwt-simple";
3
3
  import { encrypt, genSalt } from "./BcryptUtils";
4
4
  import { hasValidPermissions } from "./PermissionValidator";
@@ -66,6 +66,33 @@ export type AuthFailureHandler = (
66
66
  ctx?: any
67
67
  ) => FlinkResponse | void | Promise<FlinkResponse | void>;
68
68
 
69
+ /**
70
+ * What an auth-success callback may contribute to the response.
71
+ */
72
+ export interface AuthSuccessResult {
73
+ /**
74
+ * Headers to merge into whatever response Flink ends up sending for this
75
+ * request, e.g. a `Set-Cookie` for a renewed session. Headers set by the
76
+ * handler itself win on conflict.
77
+ */
78
+ headers?: Record<string, string>;
79
+ }
80
+
81
+ /**
82
+ * Custom auth-success callback, the counterpart to `onAuthFailure`.
83
+ *
84
+ * Called only when authentication succeeded, right before the handler runs, with
85
+ * the user resolved by `getUser`. Return `{ headers }` to have Flink apply those
86
+ * headers to the response - the seat for cross-cutting concerns discovered during
87
+ * authentication, such as re-issuing a sliding session cookie. Return void to
88
+ * leave the response untouched.
89
+ */
90
+ export type AuthSuccessHandler = (
91
+ user: FlinkAuthUser,
92
+ req: FlinkRequest,
93
+ ctx?: any
94
+ ) => AuthSuccessResult | void | Promise<AuthSuccessResult | void>;
95
+
69
96
  /**
70
97
  * Custom token TTL resolver callback.
71
98
  *
@@ -156,6 +183,25 @@ export interface JwtAuthPluginOptions {
156
183
  * ```
157
184
  */
158
185
  onAuthFailure?: AuthFailureHandler;
186
+ /**
187
+ * Optional callback invoked on successful authentication.
188
+ *
189
+ * Called with the authenticated user just before the handler runs, and never
190
+ * for rejected requests. Return `{ headers }` to contribute headers to the
191
+ * response, e.g. a re-signed session cookie when the presented token is
192
+ * running low on lifetime. Headers set by the handler win on conflict.
193
+ *
194
+ * Example:
195
+ * ```typescript
196
+ * onAuthSuccess: (user, req) => {
197
+ * const renewed = renewCookieIfExpiring(req);
198
+ * if (renewed) {
199
+ * return { headers: { "set-cookie": renewed } };
200
+ * }
201
+ * }
202
+ * ```
203
+ */
204
+ onAuthSuccess?: AuthSuccessHandler;
159
205
  /**
160
206
  * Optional dynamic token TTL resolver.
161
207
  *
@@ -207,6 +253,7 @@ export function jwtAuthPlugin({
207
253
  useDynamicRoles = false,
208
254
  resolveTokenTTL,
209
255
  onAuthFailure,
256
+ onAuthSuccess,
210
257
  }: JwtAuthPluginOptions): JwtAuthPlugin {
211
258
  return {
212
259
  authenticateRequest: async (req, permissions, ctx) =>
@@ -218,6 +265,7 @@ export function jwtAuthPlugin({
218
265
  checkPermissions,
219
266
  useDynamicRoles,
220
267
  onAuthFailure,
268
+ onAuthSuccess,
221
269
  }),
222
270
  createToken: (payload, roles) => createToken({ ...payload, roles }, { algo, secret, tokenTTL, resolveTokenTTL }, roles),
223
271
  createPasswordHashAndSalt: (password: string) => createPasswordHashAndSalt(password, passwordPolicy),
@@ -230,16 +278,23 @@ async function authenticateRequest(
230
278
  routePermissions: string | string[],
231
279
  rolePermissions: { [x: string]: string[] },
232
280
  ctx: any,
233
- { secret, algo, getUser, tokenExtractor, checkPermissions, useDynamicRoles, onAuthFailure }: Pick<
281
+ { secret, algo, getUser, tokenExtractor, checkPermissions, useDynamicRoles, onAuthFailure, onAuthSuccess }: Pick<
234
282
  JwtAuthPluginOptions,
235
- "algo" | "secret" | "getUser" | "tokenExtractor" | "checkPermissions" | "useDynamicRoles" | "onAuthFailure"
283
+ | "algo"
284
+ | "secret"
285
+ | "getUser"
286
+ | "tokenExtractor"
287
+ | "checkPermissions"
288
+ | "useDynamicRoles"
289
+ | "onAuthFailure"
290
+ | "onAuthSuccess"
236
291
  >
237
- ): Promise<boolean | FlinkResponse> {
292
+ ): Promise<FlinkAuthResult> {
238
293
  /**
239
294
  * Resolves a rejected authentication: lets `onAuthFailure` shape the response,
240
295
  * falls back to `false` (default 401) when no handler or no response is returned.
241
296
  */
242
- const fail = async (reason: AuthFailureReason, error?: unknown): Promise<boolean | FlinkResponse> => {
297
+ const fail = async (reason: AuthFailureReason, error?: unknown): Promise<FlinkAuthResult> => {
243
298
  if (onAuthFailure) {
244
299
  const failureRes = await onAuthFailure({ reason, error }, req, ctx);
245
300
  if (failureRes) {
@@ -338,6 +393,17 @@ async function authenticateRequest(
338
393
  checkPermissions,
339
394
  });
340
395
 
396
+ // Success hook runs only here, i.e. never for a rejected request, and may
397
+ // contribute headers (typically a renewed session cookie) that Flink applies
398
+ // to the response.
399
+ if (onAuthSuccess) {
400
+ const successRes = await onAuthSuccess(user, req, ctx);
401
+
402
+ if (successRes?.headers) {
403
+ return { authenticated: true, headers: successRes.headers };
404
+ }
405
+ }
406
+
341
407
  return true;
342
408
  }
343
409
  }