@xenterprises/fastify-xauth-local 1.0.0

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/.gitlab-ci.yml ADDED
@@ -0,0 +1,27 @@
1
+ image: node:20
2
+
3
+ stages:
4
+ - test
5
+ - publish
6
+
7
+ cache:
8
+ paths:
9
+ - node_modules/
10
+
11
+ test:
12
+ stage: test
13
+ script:
14
+ - npm install
15
+ - npm test
16
+ rules:
17
+ - if: $CI_PIPELINE_SOURCE == "merge_request_event"
18
+ - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
19
+
20
+ publish:
21
+ stage: publish
22
+ script:
23
+ - echo "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" > .npmrc
24
+ - npm publish --access public
25
+ rules:
26
+ - if: $CI_COMMIT_TAG =~ /^v\d+\.\d+\.\d+$/
27
+ when: manual
package/README.md ADDED
@@ -0,0 +1,453 @@
1
+ # xAuthLocal
2
+
3
+ Fastify 5 plugin for JWT authentication with role-based access control, supporting multiple authentication configurations for different route prefixes.
4
+
5
+ ## Features
6
+
7
+ - **Multiple Auth Configs**: Separate authentication for different route prefixes (e.g., `/api`, `/admin`, `/portal`)
8
+ - **JWT Authentication**: RS256 (RSA keys) or HS256 (symmetric secret) support per config
9
+ - **Route Exclusions**: Express-jwt compatible `.unless()` style patterns
10
+ - **Role-Based Access Control**: Support for multiple roles via `scope` claim
11
+ - **Local Auth Routes**: Built-in login, register, me, and password-reset endpoints per config
12
+ - **Skip User Lookup**: Option to use token data only for `/me` endpoint (no database call)
13
+ - **Backwards Compatible**: Uses `request.auth` pattern familiar from express-jwt
14
+
15
+ ## Installation
16
+
17
+ ```bash
18
+ npm install xauthlocal jsonwebtoken bcryptjs
19
+ ```
20
+
21
+ ## Quick Start
22
+
23
+ ### Single Config
24
+
25
+ ```javascript
26
+ import Fastify from "fastify";
27
+ import xAuthLocal from "xauthlocal";
28
+
29
+ const fastify = Fastify();
30
+
31
+ await fastify.register(xAuthLocal, {
32
+ configs: [
33
+ {
34
+ name: "api",
35
+ prefix: "/api",
36
+ secret: process.env.JWT_SECRET,
37
+ excludedPaths: ["/api/public", "/api/health"],
38
+ },
39
+ ],
40
+ });
41
+
42
+ // Protected route
43
+ fastify.get("/api/users", async (request) => {
44
+ // request.auth contains the decoded JWT payload
45
+ return { userId: request.auth.id };
46
+ });
47
+
48
+ await fastify.listen({ port: 3000 });
49
+ ```
50
+
51
+ ### Multiple Configs
52
+
53
+ ```javascript
54
+ await fastify.register(xAuthLocal, {
55
+ configs: [
56
+ {
57
+ name: "api",
58
+ prefix: "/api",
59
+ secret: process.env.API_SECRET,
60
+ local: {
61
+ enabled: true,
62
+ userLookup: async (email) => db.users.findByEmail(email),
63
+ createUser: async (userData) => db.users.create(userData),
64
+ skipUserLookup: true, // /me returns token data, no DB call
65
+ },
66
+ },
67
+ {
68
+ name: "admin",
69
+ prefix: "/admin",
70
+ secret: process.env.ADMIN_SECRET,
71
+ local: {
72
+ enabled: true,
73
+ userLookup: async (email) => db.admins.findByEmail(email),
74
+ skipUserLookup: false, // /me fetches fresh data from DB
75
+ },
76
+ },
77
+ {
78
+ name: "portal",
79
+ prefix: "/portal",
80
+ publicKey: "./keys/portal-public.pem",
81
+ privateKey: "./keys/portal-private.pem",
82
+ },
83
+ ],
84
+ });
85
+
86
+ // Each config protects routes under its prefix
87
+ // Tokens are config-specific (API token won't work for admin routes)
88
+ ```
89
+
90
+ ## Configuration Options
91
+
92
+ ### Plugin Options
93
+
94
+ | Option | Type | Required | Default | Description |
95
+ |--------|------|----------|---------|-------------|
96
+ | `configs` | Array | Yes | - | Array of auth configurations |
97
+ | `basePath` | string | No | `process.cwd()` | Base path for relative key paths |
98
+ | `active` | boolean | No | `true` | Enable/disable the plugin |
99
+
100
+ ### Config Options
101
+
102
+ Each config in the `configs` array supports:
103
+
104
+ | Option | Type | Required | Default | Description |
105
+ |--------|------|----------|---------|-------------|
106
+ | `name` | string | Yes | - | Unique identifier for this config |
107
+ | `prefix` | string | Yes | - | Route prefix to protect (e.g., `/api`) |
108
+ | `secret` | string | Yes* | - | Symmetric secret for HS256 |
109
+ | `publicKey` | string | Yes* | - | Public key content or path for RS256 |
110
+ | `privateKey` | string | Yes* | - | Private key content or path for RS256 |
111
+ | `algorithm` | string | No | Auto | `'RS256'` for keys, `'HS256'` for secret |
112
+ | `expiresIn` | string | No | `'4d'` | Default token expiration |
113
+ | `audience` | string | No | - | JWT audience claim |
114
+ | `issuer` | string | No | - | JWT issuer claim |
115
+ | `excludedPaths` | Array | No | `[]` | Paths to exclude from auth |
116
+ | `requestProperty` | string | No | `'auth'` | Property to attach decoded token |
117
+ | `credentialsRequired` | boolean | No | `true` | Whether token is required |
118
+ | `getToken` | Function | No | - | Custom token extraction function |
119
+ | `local` | Object | No | - | Local routes configuration |
120
+
121
+ *One of `secret` or `publicKey`/`privateKey` is required per config.
122
+
123
+ ### Local Route Options
124
+
125
+ | Option | Type | Required | Default | Description |
126
+ |--------|------|----------|---------|-------------|
127
+ | `enabled` | boolean | No | `false` | Enable local auth routes |
128
+ | `loginPath` | string | No | `{prefix}/local` | Login route path |
129
+ | `mePath` | string | No | `{loginPath}/me` | Me route path |
130
+ | `skipUserLookup` | boolean | No | `false` | Use token data only for /me |
131
+ | `userLookup` | Function | Yes** | - | Function to lookup user by email |
132
+ | `createUser` | Function | No | - | Function to create new user |
133
+ | `passwordReset` | Function | No | - | Function to handle password reset |
134
+ | `saltRounds` | number | No | `10` | bcrypt salt rounds |
135
+
136
+ **Required if local routes are enabled.
137
+
138
+ ## Route Exclusions
139
+
140
+ Exclude routes from authentication using patterns compatible with express-jwt:
141
+
142
+ ```javascript
143
+ await fastify.register(xAuthLocal, {
144
+ configs: [
145
+ {
146
+ name: "api",
147
+ prefix: "/api",
148
+ secret: process.env.JWT_SECRET,
149
+ excludedPaths: [
150
+ // String prefix match
151
+ "/api/public",
152
+ "/api/health",
153
+
154
+ // Regex match
155
+ /^\/api\/v\d+\/public/,
156
+
157
+ // Object with URL and optional methods
158
+ { url: "/api/webhook", methods: ["POST"] },
159
+ { url: /^\/api\/callback/, methods: ["GET", "POST"] },
160
+
161
+ // Object without methods (matches all methods)
162
+ { url: "/api/status" },
163
+ ],
164
+ },
165
+ ],
166
+ });
167
+ ```
168
+
169
+ ## Role-Based Access Control
170
+
171
+ Use the `requireRole` helper to protect routes by role:
172
+
173
+ ```javascript
174
+ const apiConfig = fastify.xauthlocal.get("api");
175
+
176
+ // Single role required
177
+ fastify.get(
178
+ "/api/admin/dashboard",
179
+ { preHandler: [apiConfig.requireRole("admin")] },
180
+ async (request) => ({ dashboard: true })
181
+ );
182
+
183
+ // Multiple roles (any match)
184
+ fastify.get(
185
+ "/api/manage/users",
186
+ { preHandler: [apiConfig.requireRole(["admin", "manager"])] },
187
+ async (request) => ({ users: [] })
188
+ );
189
+ ```
190
+
191
+ Roles are read from the `scope` claim in the JWT payload:
192
+
193
+ ```javascript
194
+ const apiConfig = fastify.xauthlocal.get("api");
195
+ const token = apiConfig.jwt.sign({
196
+ id: 1,
197
+ email: "admin@example.com",
198
+ scope: ["admin", "user"], // Array of roles
199
+ });
200
+ ```
201
+
202
+ ## Local Auth Routes
203
+
204
+ When `local.enabled` is true, the following endpoints are registered:
205
+
206
+ | Method | Path | Auth Required | Description |
207
+ |--------|------|---------------|-------------|
208
+ | POST | `{prefix}/local` | No | Login with email/password |
209
+ | GET | `{prefix}/local/me` | Yes | Get current user |
210
+ | POST | `{prefix}/local/register` | No | Register new user |
211
+ | POST | `{prefix}/local/password-reset` | No | Request password reset |
212
+ | PUT | `{prefix}/local/password-reset` | No | Complete password reset |
213
+
214
+ ### skipUserLookup Option
215
+
216
+ Control whether `/me` endpoint makes a database call:
217
+
218
+ ```javascript
219
+ {
220
+ name: "api",
221
+ prefix: "/api",
222
+ secret: process.env.JWT_SECRET,
223
+ local: {
224
+ enabled: true,
225
+ userLookup: async (email) => db.users.findByEmail(email),
226
+ skipUserLookup: true, // Returns token data directly, no DB call
227
+ },
228
+ }
229
+ ```
230
+
231
+ - **`skipUserLookup: true`**: Returns user data stored in the JWT token (faster, no DB call)
232
+ - **`skipUserLookup: false`**: Fetches fresh user data from database via `userLookup` (default)
233
+
234
+ ### Login Request/Response
235
+
236
+ ```javascript
237
+ // POST /api/local
238
+ // Request
239
+ {
240
+ "email": "user@example.com",
241
+ "password": "password123"
242
+ }
243
+
244
+ // Response
245
+ {
246
+ "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
247
+ "user": {
248
+ "id": 1,
249
+ "email": "user@example.com",
250
+ "first_name": "John",
251
+ "last_name": "Doe",
252
+ "admin": false,
253
+ "scope": ["user"]
254
+ }
255
+ }
256
+ ```
257
+
258
+ ## API
259
+
260
+ After registration, access the plugin via `fastify.xauthlocal`:
261
+
262
+ ```javascript
263
+ // Get all configs
264
+ fastify.xauthlocal.configs; // { api: {...}, admin: {...} }
265
+
266
+ // Get specific config by name
267
+ const apiConfig = fastify.xauthlocal.get("api");
268
+
269
+ // JWT Service (per config)
270
+ apiConfig.jwt.sign(payload, options);
271
+ apiConfig.jwt.verify(token, options);
272
+ apiConfig.jwt.decode(token);
273
+
274
+ // Role middleware factory (per config)
275
+ apiConfig.requireRole("admin");
276
+ apiConfig.requireRole(["admin", "manager"]);
277
+
278
+ // Custom middleware factory (per config)
279
+ apiConfig.createMiddleware({
280
+ excludedPaths: ["/special"],
281
+ credentialsRequired: false,
282
+ });
283
+
284
+ // Check if route is excluded (per config)
285
+ apiConfig.isExcluded("/api/public/health", "GET"); // true
286
+
287
+ // Config info (per config)
288
+ apiConfig.name; // "api"
289
+ apiConfig.prefix; // "/api"
290
+ apiConfig.hasLocalRoutes; // true
291
+ apiConfig.localPrefix; // "/api/local"
292
+ apiConfig.mePath; // "/api/local/me"
293
+
294
+ // Password utilities (global)
295
+ await fastify.xauthlocal.password.hash("password123");
296
+ await fastify.xauthlocal.password.compare("password123", hash);
297
+
298
+ // Summary config (read-only)
299
+ fastify.xauthlocal.config;
300
+ // {
301
+ // configCount: 2,
302
+ // configNames: ["api", "admin"]
303
+ // }
304
+ ```
305
+
306
+ ## Using RSA Keys
307
+
308
+ Generate keys:
309
+
310
+ ```bash
311
+ # Generate private key
312
+ openssl genrsa -out private.pem 2048
313
+
314
+ # Generate public key
315
+ openssl rsa -in private.pem -pubout -out public.pem
316
+ ```
317
+
318
+ Configure with key paths or content:
319
+
320
+ ```javascript
321
+ // Using file paths
322
+ await fastify.register(xAuthLocal, {
323
+ configs: [
324
+ {
325
+ name: "api",
326
+ prefix: "/api",
327
+ publicKey: "./keys/public.pem",
328
+ privateKey: "./keys/private.pem",
329
+ },
330
+ ],
331
+ });
332
+
333
+ // Using key content (e.g., from environment)
334
+ await fastify.register(xAuthLocal, {
335
+ configs: [
336
+ {
337
+ name: "api",
338
+ prefix: "/api",
339
+ publicKey: process.env.JWT_PUBLIC_KEY,
340
+ privateKey: process.env.JWT_PRIVATE_KEY,
341
+ },
342
+ ],
343
+ });
344
+ ```
345
+
346
+ ## Optional Authentication
347
+
348
+ Allow routes to work with or without authentication:
349
+
350
+ ```javascript
351
+ await fastify.register(xAuthLocal, {
352
+ configs: [
353
+ {
354
+ name: "api",
355
+ prefix: "/api",
356
+ secret: process.env.JWT_SECRET,
357
+ credentialsRequired: false,
358
+ },
359
+ ],
360
+ });
361
+
362
+ fastify.get("/api/posts", async (request) => {
363
+ if (request.auth) {
364
+ // Authenticated user - return personalized content
365
+ return getPersonalizedPosts(request.auth.id);
366
+ }
367
+ // Anonymous user - return public content
368
+ return getPublicPosts();
369
+ });
370
+ ```
371
+
372
+ ## Complete Example
373
+
374
+ ```javascript
375
+ import Fastify from "fastify";
376
+ import xAuthLocal from "xauthlocal";
377
+
378
+ const fastify = Fastify({ logger: true });
379
+
380
+ // In-memory users for demo
381
+ const users = new Map();
382
+ const admins = new Map();
383
+
384
+ await fastify.register(xAuthLocal, {
385
+ configs: [
386
+ {
387
+ name: "api",
388
+ prefix: "/api",
389
+ secret: process.env.API_SECRET || "api-development-secret",
390
+ expiresIn: "7d",
391
+ excludedPaths: ["/api/health", { url: "/api/public", methods: ["GET"] }],
392
+ local: {
393
+ enabled: true,
394
+ userLookup: async (email) => users.get(email),
395
+ createUser: async (userData) => {
396
+ const user = { id: Date.now(), ...userData, scope: ["user"] };
397
+ users.set(userData.email, user);
398
+ return user;
399
+ },
400
+ passwordReset: async (email) => {
401
+ console.log(`Password reset requested for ${email}`);
402
+ },
403
+ skipUserLookup: true, // Fast /me endpoint
404
+ },
405
+ },
406
+ {
407
+ name: "admin",
408
+ prefix: "/admin",
409
+ secret: process.env.ADMIN_SECRET || "admin-development-secret",
410
+ expiresIn: "1d",
411
+ local: {
412
+ enabled: true,
413
+ userLookup: async (email) => admins.get(email),
414
+ skipUserLookup: false, // Always fetch fresh admin data
415
+ },
416
+ },
417
+ ],
418
+ });
419
+
420
+ // Public routes
421
+ fastify.get("/api/health", async () => ({ status: "ok" }));
422
+ fastify.get("/api/public/info", async () => ({ version: "1.0.0" }));
423
+
424
+ // Protected API routes
425
+ fastify.get("/api/profile", async (request) => ({
426
+ id: request.auth.id,
427
+ email: request.auth.email,
428
+ }));
429
+
430
+ // Admin-only route
431
+ const adminConfig = fastify.xauthlocal.get("admin");
432
+ fastify.get(
433
+ "/admin/stats",
434
+ { preHandler: [adminConfig.requireRole("admin")] },
435
+ async () => ({ users: users.size, admins: admins.size })
436
+ );
437
+
438
+ await fastify.listen({ port: 3000 });
439
+
440
+ console.log("Server running on http://localhost:3000");
441
+ console.log("API: POST /api/local to login, POST /api/local/register to create account");
442
+ console.log("Admin: POST /admin/local to login");
443
+ ```
444
+
445
+ ## Testing
446
+
447
+ ```bash
448
+ npm test
449
+ ```
450
+
451
+ ## License
452
+
453
+ ISC
package/package.json ADDED
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "@xenterprises/fastify-xauth-local",
3
+ "version": "1.0.0",
4
+ "description": "Fastify plugin for JWT authentication with role-based access control - compatible with Express JWT patterns",
5
+ "type": "module",
6
+ "main": "src/xAuthLocal.js",
7
+ "exports": {
8
+ ".": "./src/xAuthLocal.js"
9
+ },
10
+ "scripts": {
11
+ "test": "node --test test/xAuthLocal.test.js"
12
+ },
13
+ "keywords": [
14
+ "fastify",
15
+ "fastify-plugin",
16
+ "jwt",
17
+ "authentication",
18
+ "authorization",
19
+ "roles",
20
+ "rbac",
21
+ "local-auth"
22
+ ],
23
+ "author": "",
24
+ "license": "ISC",
25
+ "dependencies": {
26
+ "bcryptjs": "^2.4.3",
27
+ "fastify-plugin": "^5.0.1",
28
+ "jsonwebtoken": "^9.0.2"
29
+ },
30
+ "peerDependencies": {
31
+ "fastify": "^5.0.0"
32
+ },
33
+ "devDependencies": {
34
+ "fastify": "^5.0.0"
35
+ },
36
+ "engines": {
37
+ "node": ">=20.0.0"
38
+ }
39
+ }