@flink-app/jwt-auth-plugin 2.0.0-alpha.96 → 2.0.0-alpha.98
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 +57 -0
- package/dist/FlinkJwtAuthPlugin.d.ts +9 -1
- package/dist/FlinkJwtAuthPlugin.js +9 -5
- package/package.json +3 -3
- package/readme.md +29 -0
- package/spec/FlinkJwtAuthPlugin.spec.ts +56 -0
- package/src/FlinkJwtAuthPlugin.ts +18 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,62 @@
|
|
|
1
1
|
# @flink-app/jwt-auth-plugin
|
|
2
2
|
|
|
3
|
+
## 2.0.0-alpha.98
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 2f4a132: feat: expose verified token on the request
|
|
8
|
+
|
|
9
|
+
The JWT auth plugin now populates `req.token` (the decoded, signature-verified
|
|
10
|
+
payload) and `req.rawToken` (the original token string) after successful
|
|
11
|
+
authentication. Handlers can read token claims such as `jti` directly instead of
|
|
12
|
+
re-extracting and re-decoding the bearer token with an unverified decode.
|
|
13
|
+
|
|
14
|
+
```ts
|
|
15
|
+
// Before
|
|
16
|
+
const authHeader = req.headers.authorization;
|
|
17
|
+
const bearer = authHeader?.startsWith("Bearer ") ? authHeader.slice(7) : undefined;
|
|
18
|
+
const decoded = bearer ? (jsonwebtoken.decode(bearer) as { jti?: string }) : null;
|
|
19
|
+
const sessionId = decoded?.jti ?? legacySessionId(req.user._id);
|
|
20
|
+
|
|
21
|
+
// After
|
|
22
|
+
const { jti } = (req.token as { jti?: string }) ?? {};
|
|
23
|
+
const sessionId = jti ?? legacySessionId(req.user._id);
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
`token` / `rawToken` are added as optional fields on the core `FlinkRequest`
|
|
27
|
+
type, populated by auth plugins that have a token concept.
|
|
28
|
+
|
|
29
|
+
### Patch Changes
|
|
30
|
+
|
|
31
|
+
- Updated dependencies [2f4a132]
|
|
32
|
+
- @flink-app/flink@2.0.0-alpha.98
|
|
33
|
+
|
|
34
|
+
## 2.0.0-alpha.97
|
|
35
|
+
|
|
36
|
+
### Minor Changes
|
|
37
|
+
|
|
38
|
+
- eed6bc1: Pass host application context to auth plugins.
|
|
39
|
+
|
|
40
|
+
`FlinkAuthPlugin.authenticateRequest` now receives the host app's context as an optional third argument. Flink core supplies it on every authenticated request, so auth plugins can access repos/services without the caller wiring up a lazy accessor or maintaining a module-level mutable reference.
|
|
41
|
+
|
|
42
|
+
For `@flink-app/jwt-auth-plugin`, this means `getUser` receives the context directly:
|
|
43
|
+
|
|
44
|
+
```typescript
|
|
45
|
+
jwtAuthPlugin({
|
|
46
|
+
getUser: async (tokenData, req, ctx) => {
|
|
47
|
+
const user = await ctx?.repos.userRepo.getById(tokenData._id);
|
|
48
|
+
return user ? { username: user.username, _id: user._id } : null;
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
The `getContext` option previously added in alpha.96 is no longer needed and has been removed — remove it from your `jwtAuthPlugin({ … })` call. The third `ctx` parameter on `getUser` is optional, so callbacks that ignore it are unaffected.
|
|
54
|
+
|
|
55
|
+
### Patch Changes
|
|
56
|
+
|
|
57
|
+
- Updated dependencies [eed6bc1]
|
|
58
|
+
- @flink-app/flink@2.0.0-alpha.97
|
|
59
|
+
|
|
3
60
|
## 2.0.0-alpha.96
|
|
4
61
|
|
|
5
62
|
### Patch Changes
|
|
@@ -33,7 +33,15 @@ export type TokenTTLResolver = (roles: string[], payload: any) => number | undef
|
|
|
33
33
|
export interface JwtAuthPluginOptions {
|
|
34
34
|
secret: string;
|
|
35
35
|
algo?: jwtSimple.TAlgorithm;
|
|
36
|
-
|
|
36
|
+
/**
|
|
37
|
+
* @param ctx The host application's context, supplied by Flink core on
|
|
38
|
+
* each authenticated request. Lets the callback access repos/services
|
|
39
|
+
* that don't exist when the plugin is constructed (e.g. before
|
|
40
|
+
* `FlinkApp.start()` resolves) without forcing the consumer to keep a
|
|
41
|
+
* module-level mutable reference. Undefined only when invoked outside
|
|
42
|
+
* the normal request pipeline (e.g. unit tests).
|
|
43
|
+
*/
|
|
44
|
+
getUser: (tokenData: any, req: FlinkRequest, ctx?: any) => Promise<FlinkAuthUser | null | undefined>;
|
|
37
45
|
passwordPolicy?: RegExp;
|
|
38
46
|
tokenTTL?: number;
|
|
39
47
|
rolePermissions: {
|
|
@@ -67,9 +67,9 @@ function jwtAuthPlugin(_a) {
|
|
|
67
67
|
var _this = this;
|
|
68
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;
|
|
69
69
|
return {
|
|
70
|
-
authenticateRequest: function (req, permissions) { return __awaiter(_this, void 0, void 0, function () {
|
|
70
|
+
authenticateRequest: function (req, permissions, ctx) { return __awaiter(_this, void 0, void 0, function () {
|
|
71
71
|
return __generator(this, function (_a) {
|
|
72
|
-
return [2 /*return*/, authenticateRequest(req, permissions, rolePermissions, {
|
|
72
|
+
return [2 /*return*/, authenticateRequest(req, permissions, rolePermissions, ctx, {
|
|
73
73
|
algo: algo,
|
|
74
74
|
secret: secret,
|
|
75
75
|
getUser: getUser,
|
|
@@ -84,8 +84,8 @@ function jwtAuthPlugin(_a) {
|
|
|
84
84
|
validatePassword: validatePassword,
|
|
85
85
|
};
|
|
86
86
|
}
|
|
87
|
-
function authenticateRequest(req_1, routePermissions_1, rolePermissions_1, _a) {
|
|
88
|
-
return __awaiter(this, arguments, void 0, function (req, routePermissions, rolePermissions, _b) {
|
|
87
|
+
function authenticateRequest(req_1, routePermissions_1, rolePermissions_1, ctx_1, _a) {
|
|
88
|
+
return __awaiter(this, arguments, void 0, function (req, routePermissions, rolePermissions, ctx, _b) {
|
|
89
89
|
var token, decodedToken, permissionsArr, validPerms, user, validPerms, hasPermission;
|
|
90
90
|
var secret = _b.secret, algo = _b.algo, getUser = _b.getUser, tokenExtractor = _b.tokenExtractor, checkPermissions = _b.checkPermissions, useDynamicRoles = _b.useDynamicRoles;
|
|
91
91
|
return __generator(this, function (_c) {
|
|
@@ -122,7 +122,7 @@ function authenticateRequest(req_1, routePermissions_1, rolePermissions_1, _a) {
|
|
|
122
122
|
return [2 /*return*/, false];
|
|
123
123
|
}
|
|
124
124
|
}
|
|
125
|
-
return [4 /*yield*/, getUser(decodedToken, req)];
|
|
125
|
+
return [4 /*yield*/, getUser(decodedToken, req, ctx)];
|
|
126
126
|
case 1:
|
|
127
127
|
user = _c.sent();
|
|
128
128
|
if (!user) {
|
|
@@ -148,6 +148,10 @@ function authenticateRequest(req_1, routePermissions_1, rolePermissions_1, _a) {
|
|
|
148
148
|
_c.label = 3;
|
|
149
149
|
case 3:
|
|
150
150
|
req.user = user;
|
|
151
|
+
// Expose the verified token so handlers can read claims (e.g. jti)
|
|
152
|
+
// without re-extracting and re-decoding (and without an unverified decode).
|
|
153
|
+
req.token = decodedToken;
|
|
154
|
+
req.rawToken = token;
|
|
151
155
|
// Populate userPermissions from resolved permissions
|
|
152
156
|
req.userPermissions = computeUserPermissions(user, decodedToken, {
|
|
153
157
|
useDynamicRoles: useDynamicRoles,
|
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.98",
|
|
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.98"
|
|
23
23
|
},
|
|
24
24
|
"peerDependencies": {
|
|
25
|
-
"@flink-app/flink": ">=2.0.0-alpha.
|
|
25
|
+
"@flink-app/flink": ">=2.0.0-alpha.98"
|
|
26
26
|
},
|
|
27
27
|
"gitHead": "4243e3b3cd6d4e1ca001a61baa8436bf2bbe4113",
|
|
28
28
|
"scripts": {
|
package/readme.md
CHANGED
|
@@ -481,6 +481,35 @@ jwtAuthPlugin({
|
|
|
481
481
|
|
|
482
482
|
**Solution**: Clear permission cache or reduce cache TTL
|
|
483
483
|
|
|
484
|
+
## `req.token` & `req.rawToken` - Accessing Token Claims
|
|
485
|
+
|
|
486
|
+
After successful authentication the plugin attaches the verified token to the request:
|
|
487
|
+
|
|
488
|
+
- **`req.token`** - the decoded, **signature-verified** token payload (claims). Same object passed to `getUser`.
|
|
489
|
+
- **`req.rawToken`** - the original token string that authenticated the request (Bearer header or custom `tokenExtractor` output).
|
|
490
|
+
|
|
491
|
+
This lets handlers read token claims (e.g. `jti`, custom session ids) without re-extracting and re-decoding the bearer token themselves - and without an unverified decode.
|
|
492
|
+
|
|
493
|
+
```typescript
|
|
494
|
+
const handler: Handler<Ctx, any, any> = async ({ req }) => {
|
|
495
|
+
// Read claims straight off the verified token
|
|
496
|
+
const { jti } = (req.token as { jti?: string }) ?? {};
|
|
497
|
+
const sessionId = jti ?? legacySessionId(req.user._id);
|
|
498
|
+
|
|
499
|
+
// Forward the raw token to a downstream service if needed
|
|
500
|
+
await downstream.call({ headers: { authorization: `Bearer ${req.rawToken}` } });
|
|
501
|
+
|
|
502
|
+
return { data: { sessionId } };
|
|
503
|
+
};
|
|
504
|
+
```
|
|
505
|
+
|
|
506
|
+
**Notes:**
|
|
507
|
+
|
|
508
|
+
- Both fields are only set once authentication **succeeds** (a rejected request never reaches the handler).
|
|
509
|
+
- `req.token` is the exact decoded payload, including framework-added claims like `iat` and `exp`.
|
|
510
|
+
- Signature is verified during decode; unlike a bare `jsonwebtoken.decode(...)`, you don't need to (and shouldn't) re-verify.
|
|
511
|
+
- `req.rawToken` reflects whatever `tokenExtractor` returned, so it works for cookie/query-based tokens too.
|
|
512
|
+
|
|
484
513
|
## `req.userPermissions` - AI Permissions Integration
|
|
485
514
|
|
|
486
515
|
Starting in **v1.1.0**, the JWT auth plugin automatically populates `req.userPermissions` with the resolved permissions array. This field is specifically designed for use with Flink's AI agents and tools system, providing a standardized way to handle permissions across handlers, agents, and tools.
|
|
@@ -102,6 +102,62 @@ describe("FlinkJwtAuthPlugin", () => {
|
|
|
102
102
|
expect(authenticated).toBeTruthy();
|
|
103
103
|
});
|
|
104
104
|
|
|
105
|
+
it("should expose decoded token on req.token and raw string on req.rawToken", async () => {
|
|
106
|
+
const secret = "secret";
|
|
107
|
+
const userId = "123";
|
|
108
|
+
const encodedToken = jwtSimple.encode(
|
|
109
|
+
{ id: userId, jti: "session-abc", roles: ["user"] },
|
|
110
|
+
secret
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
const plugin = jwtAuthPlugin({
|
|
114
|
+
secret,
|
|
115
|
+
getUser: async ({ id }: { id: string }, req) => ({ id, username: "username" }),
|
|
116
|
+
rolePermissions: {
|
|
117
|
+
user: ["*"],
|
|
118
|
+
},
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
const mockRequest = {
|
|
122
|
+
headers: {
|
|
123
|
+
authorization: "Bearer " + encodedToken,
|
|
124
|
+
},
|
|
125
|
+
} as FlinkRequest;
|
|
126
|
+
|
|
127
|
+
const authenticated = await plugin.authenticateRequest(mockRequest, "foo");
|
|
128
|
+
|
|
129
|
+
expect(authenticated).toBeTruthy();
|
|
130
|
+
expect(mockRequest.rawToken).toBe(encodedToken);
|
|
131
|
+
expect(mockRequest.token?.jti).toBe("session-abc");
|
|
132
|
+
expect(mockRequest.token?.id).toBe(userId);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it("should set req.rawToken to the tokenExtractor output, not just Bearer", async () => {
|
|
136
|
+
const secret = "secret";
|
|
137
|
+
const customToken = jwtSimple.encode(
|
|
138
|
+
{ id: "1", jti: "from-query", roles: ["user"] },
|
|
139
|
+
secret
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
const plugin = jwtAuthPlugin({
|
|
143
|
+
secret,
|
|
144
|
+
getUser: async ({ id }: { id: string }, req) => ({ id, username: "username" }),
|
|
145
|
+
rolePermissions: { user: ["*"] },
|
|
146
|
+
tokenExtractor: (req) => req.query?.token as string,
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
const mockRequest = {
|
|
150
|
+
headers: {},
|
|
151
|
+
query: { token: customToken },
|
|
152
|
+
} as unknown as FlinkRequest;
|
|
153
|
+
|
|
154
|
+
const authenticated = await plugin.authenticateRequest(mockRequest, "foo");
|
|
155
|
+
|
|
156
|
+
expect(authenticated).toBeTruthy();
|
|
157
|
+
expect(mockRequest.rawToken).toBe(customToken);
|
|
158
|
+
expect(mockRequest.token?.jti).toBe("from-query");
|
|
159
|
+
});
|
|
160
|
+
|
|
105
161
|
it("should generate token", async () => {
|
|
106
162
|
const secret = "secret";
|
|
107
163
|
const plugin = jwtAuthPlugin({
|
|
@@ -48,7 +48,15 @@ export type TokenTTLResolver = (roles: string[], payload: any) => number | undef
|
|
|
48
48
|
export interface JwtAuthPluginOptions {
|
|
49
49
|
secret: string;
|
|
50
50
|
algo?: jwtSimple.TAlgorithm;
|
|
51
|
-
|
|
51
|
+
/**
|
|
52
|
+
* @param ctx The host application's context, supplied by Flink core on
|
|
53
|
+
* each authenticated request. Lets the callback access repos/services
|
|
54
|
+
* that don't exist when the plugin is constructed (e.g. before
|
|
55
|
+
* `FlinkApp.start()` resolves) without forcing the consumer to keep a
|
|
56
|
+
* module-level mutable reference. Undefined only when invoked outside
|
|
57
|
+
* the normal request pipeline (e.g. unit tests).
|
|
58
|
+
*/
|
|
59
|
+
getUser: (tokenData: any, req: FlinkRequest, ctx?: any) => Promise<FlinkAuthUser | null | undefined>;
|
|
52
60
|
passwordPolicy?: RegExp;
|
|
53
61
|
tokenTTL?: number;
|
|
54
62
|
rolePermissions: {
|
|
@@ -150,8 +158,8 @@ export function jwtAuthPlugin({
|
|
|
150
158
|
resolveTokenTTL,
|
|
151
159
|
}: JwtAuthPluginOptions): JwtAuthPlugin {
|
|
152
160
|
return {
|
|
153
|
-
authenticateRequest: async (req, permissions) =>
|
|
154
|
-
authenticateRequest(req, permissions, rolePermissions, {
|
|
161
|
+
authenticateRequest: async (req, permissions, ctx) =>
|
|
162
|
+
authenticateRequest(req, permissions, rolePermissions, ctx, {
|
|
155
163
|
algo,
|
|
156
164
|
secret,
|
|
157
165
|
getUser,
|
|
@@ -169,6 +177,7 @@ async function authenticateRequest(
|
|
|
169
177
|
req: FlinkRequest,
|
|
170
178
|
routePermissions: string | string[],
|
|
171
179
|
rolePermissions: { [x: string]: string[] },
|
|
180
|
+
ctx: any,
|
|
172
181
|
{ secret, algo, getUser, tokenExtractor, checkPermissions, useDynamicRoles }: Pick<
|
|
173
182
|
JwtAuthPluginOptions,
|
|
174
183
|
"algo" | "secret" | "getUser" | "tokenExtractor" | "checkPermissions" | "useDynamicRoles"
|
|
@@ -212,7 +221,7 @@ async function authenticateRequest(
|
|
|
212
221
|
}
|
|
213
222
|
}
|
|
214
223
|
|
|
215
|
-
const user = await getUser(decodedToken, req);
|
|
224
|
+
const user = await getUser(decodedToken, req, ctx);
|
|
216
225
|
|
|
217
226
|
if (!user) {
|
|
218
227
|
log.debug("[JWT AUTH PLUGIN] User not returned from getUser callback");
|
|
@@ -241,6 +250,11 @@ async function authenticateRequest(
|
|
|
241
250
|
|
|
242
251
|
req.user = user;
|
|
243
252
|
|
|
253
|
+
// Expose the verified token so handlers can read claims (e.g. jti)
|
|
254
|
+
// without re-extracting and re-decoding (and without an unverified decode).
|
|
255
|
+
req.token = decodedToken;
|
|
256
|
+
req.rawToken = token;
|
|
257
|
+
|
|
244
258
|
// Populate userPermissions from resolved permissions
|
|
245
259
|
req.userPermissions = computeUserPermissions(user, decodedToken, {
|
|
246
260
|
useDynamicRoles,
|