@flink-app/jwt-auth-plugin 0.12.1-alpha.3 → 0.12.1-alpha.33

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.
Files changed (2) hide show
  1. package/package.json +4 -4
  2. package/readme.md +604 -18
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "@flink-app/jwt-auth-plugin",
3
- "version": "0.12.1-alpha.3",
3
+ "version": "0.12.1-alpha.33",
4
4
  "description": "Flink plugin for JWT auth",
5
5
  "scripts": {
6
6
  "test": "node --preserve-symlinks -r ts-node/register -- node_modules/jasmine/bin/jasmine --config=./spec/support/jasmine.json",
7
7
  "test:watch": "nodemon --ext ts --exec 'jasmine-ts --config=./spec/support/jasmine.json'",
8
- "prepublish": "tsc --project tsconfig.dist.json",
8
+ "prepare": "tsc --project tsconfig.dist.json",
9
9
  "watch": "tsc-watch --project tsconfig.dist.json"
10
10
  },
11
11
  "author": "joel@frost.se",
@@ -20,7 +20,7 @@
20
20
  "jwt-simple": "^0.5.6"
21
21
  },
22
22
  "devDependencies": {
23
- "@flink-app/flink": "^0.12.1-alpha.3",
23
+ "@flink-app/flink": "^0.12.1-alpha.23",
24
24
  "@types/bcrypt": "^5.0.0",
25
25
  "@types/jasmine": "^3.7.1",
26
26
  "@types/node": "22.13.10",
@@ -31,5 +31,5 @@
31
31
  "tsc-watch": "^4.2.9",
32
32
  "typescript": "5.4.5"
33
33
  },
34
- "gitHead": "51b6524e233acb2430953ffaed6382909f34db8e"
34
+ "gitHead": "eec3a22c21db0e7fec84190bf7a74c1e430e5ec4"
35
35
  }
package/readme.md CHANGED
@@ -1,33 +1,619 @@
1
- # Flink API Docs
1
+ # JWT Authentication Plugin
2
2
 
3
- **WORK IN PROGRESS 👷‍♀️👷🏻‍♂️**
3
+ A Flink authentication plugin that provides JWT (JSON Web Token) based authentication with role-based permissions, password hashing, and token management.
4
4
 
5
- A FLINK plugin that generates a VERY simple documentation based on the apps
6
- registered routes and schemas.
5
+ ## Features
7
6
 
8
- ## Usage
7
+ - JWT token generation and validation
8
+ - Password hashing using bcrypt
9
+ - Role-based access control with permissions
10
+ - Configurable password policies
11
+ - Token expiration support
12
+ - Bearer token authentication
9
13
 
10
- Install plugin to your flink app project:
14
+ ## Installation
11
15
 
16
+ Install the plugin in your Flink app project:
17
+
18
+ ```bash
19
+ npm install @flink-app/jwt-auth-plugin
20
+ ```
21
+
22
+ ## Basic Setup
23
+
24
+ ```typescript
25
+ import { FlinkApp } from "@flink-app/flink";
26
+ import { jwtAuthPlugin } from "@flink-app/jwt-auth-plugin";
27
+ import { Ctx } from "./Ctx";
28
+
29
+ function start() {
30
+ const app = new FlinkApp<Ctx>({
31
+ name: "My Flink App",
32
+ auth: jwtAuthPlugin({
33
+ secret: process.env.JWT_SECRET || "your-secret-key",
34
+ getUser: async (tokenData) => {
35
+ // Retrieve user from database using token data
36
+ const user = await ctx.repos.userRepo.findById(tokenData.userId);
37
+ return {
38
+ id: user._id,
39
+ username: user.username,
40
+ roles: user.roles,
41
+ };
42
+ },
43
+ rolePermissions: {
44
+ admin: ["read", "write", "delete", "manage_users"],
45
+ user: ["read", "write"],
46
+ guest: ["read"],
47
+ },
48
+ }),
49
+ db: {
50
+ uri: "mongodb://localhost:27017/my-app",
51
+ },
52
+ });
53
+
54
+ app.start();
55
+ }
56
+
57
+ start();
58
+ ```
59
+
60
+ ## Configuration Options
61
+
62
+ ### `JwtAuthPluginOptions`
63
+
64
+ | Option | Type | Required | Default | Description |
65
+ |--------|------|----------|---------|-------------|
66
+ | `secret` | `string` | Yes | - | Secret key used to sign and verify JWT tokens. Keep this secure! |
67
+ | `getUser` | `(tokenData: any) => Promise<FlinkAuthUser>` | Yes | - | Async function that retrieves user data from token payload |
68
+ | `rolePermissions` | `{ [role: string]: string[] }` | Yes | - | Maps roles to their allowed permissions |
69
+ | `algo` | `jwtSimple.TAlgorithm` | No | `"HS256"` | JWT signing algorithm |
70
+ | `passwordPolicy` | `RegExp` | No | `/^(?=.*[A-Za-z])(?=.*\d)[A-Za-z\d]{8,}$/` | Regex to validate password strength |
71
+ | `tokenTTL` | `number` | No | `1000 * 60 * 60 * 24 * 365 * 100` (100 years) | Token time-to-live in milliseconds |
72
+
73
+ ### Default Password Policy
74
+
75
+ The default password policy requires:
76
+ - Minimum 8 characters
77
+ - At least one letter (A-Z or a-z)
78
+ - At least one number (0-9)
79
+
80
+ You can customize this by providing your own regex:
81
+
82
+ ```typescript
83
+ jwtAuthPlugin({
84
+ secret: "your-secret",
85
+ getUser: async (tokenData) => { /* ... */ },
86
+ rolePermissions: { /* ... */ },
87
+ passwordPolicy: /^(?=.*[A-Za-z])(?=.*\d)(?=.*[@$!%*#?&])[A-Za-z\d@$!%*#?&]{12,}$/,
88
+ // Requires: 12+ chars, 1 letter, 1 number, 1 special character
89
+ })
90
+ ```
91
+
92
+ ## Context API
93
+
94
+ Once configured, the plugin provides the following methods via the `auth` context:
95
+
96
+ ### `createToken(payload: any, roles: string[]): Promise<string>`
97
+
98
+ Creates a JWT token with the provided payload and roles.
99
+
100
+ ```typescript
101
+ const token = await ctx.auth.createToken(
102
+ { userId: user._id, username: user.username },
103
+ ["user"]
104
+ );
105
+ ```
106
+
107
+ **Parameters:**
108
+ - `payload`: Any data to encode in the token (typically user ID and username)
109
+ - `roles`: Array of role names assigned to the user
110
+
111
+ **Returns:** JWT token string
112
+
113
+ **Example:**
114
+ ```typescript
115
+ // In a login handler
116
+ const handler: Handler<Ctx, LoginReq, LoginRes> = async ({ ctx, req }) => {
117
+ const user = await ctx.repos.userRepo.findOne({ username: req.body.username });
118
+
119
+ if (!user) {
120
+ return { status: 401, error: { code: "invalid_credentials" } };
121
+ }
122
+
123
+ const token = await ctx.auth.createToken(
124
+ { userId: user._id, username: user.username },
125
+ user.roles
126
+ );
127
+
128
+ return {
129
+ data: {
130
+ token,
131
+ user: {
132
+ id: user._id,
133
+ username: user.username,
134
+ },
135
+ },
136
+ };
137
+ };
138
+ ```
139
+
140
+ ### `createPasswordHashAndSalt(password: string): Promise<{ hash: string; salt: string } | null>`
141
+
142
+ Generates a secure password hash and salt using bcrypt.
143
+
144
+ ```typescript
145
+ const result = await ctx.auth.createPasswordHashAndSalt("mypassword123");
146
+ if (result) {
147
+ const { hash, salt } = result;
148
+ // Save hash and salt to database
149
+ }
150
+ ```
151
+
152
+ **Parameters:**
153
+ - `password`: The plain text password to hash
154
+
155
+ **Returns:**
156
+ - Object with `hash` and `salt` if password meets policy
157
+ - `null` if password doesn't meet the configured password policy
158
+
159
+ **Security Note:** Both hash and salt must be stored in your database to validate passwords later.
160
+
161
+ **Example:**
162
+ ```typescript
163
+ // Creating a new user
164
+ const handler: Handler<Ctx, CreateUserReq, CreateUserRes> = async ({ ctx, req }) => {
165
+ const passwordData = await ctx.auth.createPasswordHashAndSalt(req.body.password);
166
+
167
+ if (!passwordData) {
168
+ return {
169
+ status: 400,
170
+ error: {
171
+ code: "weak_password",
172
+ message: "Password does not meet security requirements",
173
+ },
174
+ };
175
+ }
176
+
177
+ const user = await ctx.repos.userRepo.create({
178
+ username: req.body.username,
179
+ password: passwordData.hash,
180
+ salt: passwordData.salt,
181
+ roles: ["user"],
182
+ });
183
+
184
+ return { data: { userId: user._id } };
185
+ };
12
186
  ```
13
- npm i -S @flink-app/api-docs-plugin
187
+
188
+ ### `validatePassword(password: string, passwordHash: string, salt: string): Promise<boolean>`
189
+
190
+ Validates a password against a stored hash and salt.
191
+
192
+ ```typescript
193
+ const isValid = await ctx.auth.validatePassword(
194
+ "mypassword123",
195
+ user.password,
196
+ user.salt
197
+ );
14
198
  ```
15
199
 
16
- Add and configure plugin in your app startup (probable the `index.ts` in root project):
200
+ **Parameters:**
201
+ - `password`: Plain text password to validate
202
+ - `passwordHash`: Stored password hash from database
203
+ - `salt`: Stored salt from database
204
+
205
+ **Returns:** `true` if password matches, `false` otherwise
17
206
 
207
+ **Example:**
208
+ ```typescript
209
+ // In a login handler
210
+ const handler: Handler<Ctx, LoginReq, LoginRes> = async ({ ctx, req }) => {
211
+ const user = await ctx.repos.userRepo.findOne({ username: req.body.username });
212
+
213
+ if (!user) {
214
+ return { status: 401, error: { code: "invalid_credentials" } };
215
+ }
216
+
217
+ const isValidPassword = await ctx.auth.validatePassword(
218
+ req.body.password,
219
+ user.password,
220
+ user.salt
221
+ );
222
+
223
+ if (!isValidPassword) {
224
+ return { status: 401, error: { code: "invalid_credentials" } };
225
+ }
226
+
227
+ const token = await ctx.auth.createToken(
228
+ { userId: user._id, username: user.username },
229
+ user.roles
230
+ );
231
+
232
+ return {
233
+ data: {
234
+ token,
235
+ user: { id: user._id, username: user.username },
236
+ },
237
+ };
238
+ };
18
239
  ```
19
- import { apiDocPlugin } from "@flink-app/api-docs-plugin";
240
+
241
+ ### `authenticateRequest(req: FlinkRequest, permissions: string | string[]): Promise<boolean>`
242
+
243
+ Automatically called by Flink framework to authenticate requests. You typically don't call this directly.
244
+
245
+ ## Role-Based Access Control
246
+
247
+ ### Defining Roles and Permissions
248
+
249
+ ```typescript
250
+ jwtAuthPlugin({
251
+ secret: "your-secret",
252
+ getUser: async (tokenData) => { /* ... */ },
253
+ rolePermissions: {
254
+ // Admin role can do everything
255
+ admin: ["read", "write", "delete", "manage_users", "view_analytics"],
256
+
257
+ // Regular user has limited permissions
258
+ user: ["read", "write"],
259
+
260
+ // Guest can only read
261
+ guest: ["read"],
262
+ },
263
+ })
264
+ ```
265
+
266
+ ### Protecting Routes with Permissions
267
+
268
+ Use the `permission` property in your route configuration to restrict access:
269
+
270
+ ```typescript
271
+ // Only authenticated users (any role)
272
+ export const Route: RouteProps = {
273
+ path: "/api/profile",
274
+ permission: "read", // Must have "read" permission
275
+ };
276
+
277
+ // Only admins
278
+ export const Route: RouteProps = {
279
+ path: "/api/admin/users",
280
+ permission: "manage_users", // Must have "manage_users" permission
281
+ };
282
+
283
+ // Multiple permissions (user must have at least one)
284
+ export const Route: RouteProps = {
285
+ path: "/api/content",
286
+ permission: ["read", "write"], // Must have either "read" OR "write"
287
+ };
288
+ ```
289
+
290
+ ### Accessing User in Handlers
291
+
292
+ Once authenticated, the user object is available in `req.user`:
293
+
294
+ ```typescript
295
+ const handler: Handler<Ctx, any, any> = async ({ ctx, req }) => {
296
+ // Access authenticated user
297
+ const userId = req.user?.id;
298
+ const username = req.user?.username;
299
+ const roles = req.user?.roles;
300
+
301
+ // Use user data in your logic
302
+ const data = await ctx.repos.dataRepo.findByUserId(userId);
303
+
304
+ return { data };
305
+ };
306
+ ```
307
+
308
+ ## Making Authenticated Requests
309
+
310
+ Clients must include the JWT token in the `Authorization` header:
311
+
312
+ ```
313
+ Authorization: Bearer <your-jwt-token>
314
+ ```
315
+
316
+ ### Example with fetch
317
+
318
+ ```javascript
319
+ const response = await fetch('https://api.example.com/profile', {
320
+ method: 'GET',
321
+ headers: {
322
+ 'Authorization': `Bearer ${token}`,
323
+ 'Content-Type': 'application/json',
324
+ },
325
+ });
326
+ ```
327
+
328
+ ### Example with axios
329
+
330
+ ```javascript
331
+ const response = await axios.get('https://api.example.com/profile', {
332
+ headers: {
333
+ 'Authorization': `Bearer ${token}`,
334
+ },
335
+ });
336
+ ```
337
+
338
+ ## Complete Example
339
+
340
+ ```typescript
341
+ // index.ts
342
+ import { FlinkApp } from "@flink-app/flink";
343
+ import { jwtAuthPlugin } from "@flink-app/jwt-auth-plugin";
344
+ import { Ctx } from "./Ctx";
20
345
 
21
346
  function start() {
22
- new FlinkApp<AppContext>({
23
- name: "My app",
24
- plugins: [
25
- // Register plugin, customize options if needed to
26
- apiDocPlugin({
27
- title: "API Docs: My app"
28
- })
29
- ],
30
- }).start();
347
+ const app = new FlinkApp<Ctx>({
348
+ name: "My App",
349
+ auth: jwtAuthPlugin({
350
+ secret: process.env.JWT_SECRET!,
351
+ getUser: async (tokenData) => {
352
+ const user = await app.ctx.repos.userRepo.findById(tokenData.userId);
353
+ return {
354
+ id: user._id,
355
+ username: user.username,
356
+ roles: user.roles,
357
+ };
358
+ },
359
+ rolePermissions: {
360
+ admin: ["read", "write", "delete", "manage_users"],
361
+ user: ["read", "write"],
362
+ },
363
+ passwordPolicy: /^(?=.*[A-Za-z])(?=.*\d)[A-Za-z\d@$!%*?&]{10,}$/,
364
+ tokenTTL: 1000 * 60 * 60 * 24 * 7, // 7 days
365
+ }),
366
+ db: {
367
+ uri: process.env.MONGODB_URI!,
368
+ },
369
+ });
370
+
371
+ app.start();
31
372
  }
32
373
 
374
+ start();
375
+
376
+ // handlers/auth/PostLogin.ts
377
+ import { Handler, RouteProps } from "@flink-app/flink";
378
+ import { Ctx } from "../../Ctx";
379
+ import LoginReq from "../../schemas/LoginReq";
380
+ import LoginRes from "../../schemas/LoginRes";
381
+
382
+ export const Route: RouteProps = {
383
+ path: "/auth/login",
384
+ };
385
+
386
+ const PostLogin: Handler<Ctx, LoginReq, LoginRes> = async ({ ctx, req }) => {
387
+ const { username, password } = req.body;
388
+
389
+ // Find user
390
+ const user = await ctx.repos.userRepo.findOne({ username });
391
+ if (!user) {
392
+ return {
393
+ status: 401,
394
+ error: { code: "invalid_credentials", message: "Invalid username or password" },
395
+ };
396
+ }
397
+
398
+ // Validate password
399
+ const isValid = await ctx.auth.validatePassword(password, user.password, user.salt);
400
+ if (!isValid) {
401
+ return {
402
+ status: 401,
403
+ error: { code: "invalid_credentials", message: "Invalid username or password" },
404
+ };
405
+ }
406
+
407
+ // Create token
408
+ const token = await ctx.auth.createToken(
409
+ { userId: user._id, username: user.username },
410
+ user.roles
411
+ );
412
+
413
+ return {
414
+ data: {
415
+ token,
416
+ user: {
417
+ id: user._id,
418
+ username: user.username,
419
+ roles: user.roles,
420
+ },
421
+ },
422
+ };
423
+ };
424
+
425
+ export default PostLogin;
426
+
427
+ // handlers/users/PostUser.ts
428
+ import { Handler, RouteProps } from "@flink-app/flink";
429
+ import { Ctx } from "../../Ctx";
430
+ import CreateUserReq from "../../schemas/CreateUserReq";
431
+ import CreateUserRes from "../../schemas/CreateUserRes";
432
+
433
+ export const Route: RouteProps = {
434
+ path: "/users",
435
+ permission: "manage_users", // Only admins can create users
436
+ };
437
+
438
+ const PostUser: Handler<Ctx, CreateUserReq, CreateUserRes> = async ({ ctx, req }) => {
439
+ const { username, password, roles } = req.body;
440
+
441
+ // Check if user exists
442
+ const existingUser = await ctx.repos.userRepo.findOne({ username });
443
+ if (existingUser) {
444
+ return {
445
+ status: 409,
446
+ error: { code: "user_exists", message: "Username already taken" },
447
+ };
448
+ }
449
+
450
+ // Hash password
451
+ const passwordData = await ctx.auth.createPasswordHashAndSalt(password);
452
+ if (!passwordData) {
453
+ return {
454
+ status: 400,
455
+ error: {
456
+ code: "weak_password",
457
+ message: "Password does not meet security requirements",
458
+ },
459
+ };
460
+ }
461
+
462
+ // Create user
463
+ const user = await ctx.repos.userRepo.create({
464
+ username,
465
+ password: passwordData.hash,
466
+ salt: passwordData.salt,
467
+ roles: roles || ["user"],
468
+ createdAt: new Date(),
469
+ });
470
+
471
+ return {
472
+ data: {
473
+ id: user._id,
474
+ username: user.username,
475
+ roles: user.roles,
476
+ },
477
+ };
478
+ };
479
+
480
+ export default PostUser;
33
481
  ```
482
+
483
+ ## Security Best Practices
484
+
485
+ ### 1. Secret Key Management
486
+
487
+ Never hardcode your JWT secret. Use environment variables:
488
+
489
+ ```typescript
490
+ jwtAuthPlugin({
491
+ secret: process.env.JWT_SECRET!,
492
+ // ...
493
+ })
494
+ ```
495
+
496
+ Generate a strong secret:
497
+ ```bash
498
+ node -e "console.log(require('crypto').randomBytes(64).toString('hex'))"
499
+ ```
500
+
501
+ ### 2. Token Expiration
502
+
503
+ Set an appropriate TTL for your use case:
504
+
505
+ ```typescript
506
+ jwtAuthPlugin({
507
+ secret: process.env.JWT_SECRET!,
508
+ tokenTTL: 1000 * 60 * 60 * 24 * 7, // 7 days
509
+ // ...
510
+ })
511
+ ```
512
+
513
+ ### 3. Password Policies
514
+
515
+ Enforce strong password requirements:
516
+
517
+ ```typescript
518
+ jwtAuthPlugin({
519
+ secret: process.env.JWT_SECRET!,
520
+ // Require: 12+ chars, uppercase, lowercase, number, special char
521
+ passwordPolicy: /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{12,}$/,
522
+ // ...
523
+ })
524
+ ```
525
+
526
+ ### 4. HTTPS Only
527
+
528
+ Always use HTTPS in production to prevent token interception.
529
+
530
+ ### 5. Token Storage (Client-Side)
531
+
532
+ - Avoid `localStorage` (vulnerable to XSS)
533
+ - Prefer `httpOnly` cookies or secure session storage
534
+ - Implement token refresh mechanisms for long-lived sessions
535
+
536
+ ### 6. Rate Limiting
537
+
538
+ Implement rate limiting on authentication endpoints to prevent brute force attacks.
539
+
540
+ ## TypeScript Types
541
+
542
+ ```typescript
543
+ import { JwtAuthPlugin, JwtAuthPluginOptions } from "@flink-app/jwt-auth-plugin";
544
+
545
+ // Plugin options
546
+ interface JwtAuthPluginOptions {
547
+ secret: string;
548
+ algo?: jwtSimple.TAlgorithm;
549
+ getUser: (tokenData: any) => Promise<FlinkAuthUser>;
550
+ passwordPolicy?: RegExp;
551
+ tokenTTL?: number;
552
+ rolePermissions: {
553
+ [role: string]: string[];
554
+ };
555
+ }
556
+
557
+ // Plugin interface
558
+ interface JwtAuthPlugin extends FlinkAuthPlugin {
559
+ createToken: (payload: any, roles: string[]) => Promise<string>;
560
+ createPasswordHashAndSalt: (
561
+ password: string
562
+ ) => Promise<{ hash: string; salt: string } | null>;
563
+ validatePassword: (
564
+ password: string,
565
+ passwordHash: string,
566
+ salt: string
567
+ ) => Promise<boolean>;
568
+ }
569
+
570
+ // Authenticated user (from Flink framework)
571
+ interface FlinkAuthUser {
572
+ id: string;
573
+ username?: string;
574
+ roles?: string[];
575
+ [key: string]: any;
576
+ }
577
+ ```
578
+
579
+ ## Troubleshooting
580
+
581
+ ### Token Validation Fails
582
+
583
+ **Issue:** Requests return 401 Unauthorized
584
+
585
+ **Solutions:**
586
+ - Verify the token is being sent in the `Authorization` header
587
+ - Check the header format: `Authorization: Bearer <token>`
588
+ - Ensure the secret used to sign matches the secret used to verify
589
+ - Check if the token has expired (if TTL is configured)
590
+
591
+ ### Password Creation Returns Null
592
+
593
+ **Issue:** `createPasswordHashAndSalt` returns `null`
594
+
595
+ **Solution:** Password doesn't meet the configured `passwordPolicy`. Update the password or adjust the policy.
596
+
597
+ ### getUser Function Errors
598
+
599
+ **Issue:** Authentication fails with error from `getUser`
600
+
601
+ **Solution:** Ensure your `getUser` function properly handles missing users:
602
+
603
+ ```typescript
604
+ getUser: async (tokenData) => {
605
+ const user = await ctx.repos.userRepo.findById(tokenData.userId);
606
+ if (!user) {
607
+ throw new Error("User not found");
608
+ }
609
+ return {
610
+ id: user._id,
611
+ username: user.username,
612
+ roles: user.roles,
613
+ };
614
+ }
615
+ ```
616
+
617
+ ## License
618
+
619
+ MIT