@xenterprises/fastify-xauth-jwks 1.1.2 → 1.2.1

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/LICENSE ADDED
@@ -0,0 +1,100 @@
1
+ PROPRIETARY SOFTWARE LICENSE
2
+
3
+ Copyright (c) 2024-2026 X Enterprises LLC. All Rights Reserved.
4
+
5
+ This software and associated documentation files (the "Software") are the
6
+ exclusive property of X Enterprises LLC, a Washington limited liability
7
+ company ("X Enterprises"). The Software is distributed through public
8
+ package registries (including npm) for operational convenience only; such
9
+ distribution does not grant any rights beyond those expressly stated below.
10
+
11
+ TERMS AND CONDITIONS
12
+
13
+ 1. OWNERSHIP
14
+ All rights, title, and interest in and to the Software, including all
15
+ intellectual property rights, are and shall remain the exclusive property
16
+ of X Enterprises. No rights are granted except as expressly set forth in
17
+ this License.
18
+
19
+ 2. PERMITTED USE
20
+ Subject to the restrictions in Section 3, you are permitted to download,
21
+ install, and execute the Software solely as a dependency of:
22
+
23
+ (a) software developed, owned, or operated by X Enterprises;
24
+
25
+ (b) software that X Enterprises has developed, delivered, or licensed to
26
+ a third party ("Client") under a written engagement agreement with
27
+ X Enterprises, when such use is performed by or on behalf of that
28
+ Client; or
29
+
30
+ (c) end-user access to, or consumption of, a product or service described
31
+ in (a) or (b), provided that such access does not involve
32
+ redistribution, modification, or separate use of the Software.
33
+
34
+ Permitted Use includes automated installation and execution by continuous
35
+ integration systems, container builds, hosting platforms, and similar
36
+ infrastructure, to the extent necessary to support (a), (b), or (c).
37
+
38
+ 3. RESTRICTIONS
39
+ Except as expressly permitted in Section 2, and without the prior written
40
+ consent of X Enterprises, you may not:
41
+
42
+ (a) copy, modify, adapt, translate, or create derivative works of the
43
+ Software for any purpose outside the scope of Section 2;
44
+
45
+ (b) redistribute, republish, sublicense, sell, lease, rent, or otherwise
46
+ transfer the Software, in whole or in part, whether standalone or
47
+ bundled with other software;
48
+
49
+ (c) reverse engineer, decompile, disassemble, or attempt to derive the
50
+ source code or underlying ideas, algorithms, structure, or
51
+ organization of the Software, except to the extent such activity is
52
+ expressly permitted by applicable law notwithstanding this
53
+ restriction;
54
+
55
+ (d) use the Software, in whole or in part, to develop, operate, or
56
+ provide any product or service that competes with or substitutes for
57
+ any X Enterprises product or service;
58
+
59
+ (e) remove, obscure, or alter any copyright, trademark, license, or other
60
+ proprietary notice contained in or on the Software; or
61
+
62
+ (f) use the Software in violation of any applicable law or regulation.
63
+
64
+ 4. NO WARRANTY
65
+ THE SOFTWARE IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
66
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
67
+ FITNESS FOR A PARTICULAR PURPOSE, AND NONINFRINGEMENT. IN NO EVENT SHALL
68
+ X ENTERPRISES BE LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER LIABILITY,
69
+ WHETHER IN AN ACTION OF CONTRACT, TORT, OR OTHERWISE, ARISING FROM, OUT
70
+ OF, OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
71
+ THE SOFTWARE.
72
+
73
+ 5. LIMITATION OF LIABILITY
74
+ IN NO EVENT SHALL X ENTERPRISES BE LIABLE FOR ANY INDIRECT, INCIDENTAL,
75
+ SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
76
+ TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
77
+ PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
78
+ LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
79
+ NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
80
+ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
81
+
82
+ 6. GOVERNING LAW
83
+ This License shall be governed by and construed in accordance with the
84
+ laws of the State of Washington, United States, without regard to its
85
+ conflict of law provisions. Exclusive jurisdiction for any dispute
86
+ arising out of this License shall lie in the state or federal courts
87
+ located in King County, Washington.
88
+
89
+ 7. TERMINATION
90
+ This License is effective until terminated. Your rights under this
91
+ License will terminate automatically and without notice if you fail to
92
+ comply with any term herein. Upon termination, you must cease all use of
93
+ the Software and destroy all copies in your possession or control.
94
+ Sections 1, 3, 4, 5, 6, and 7 survive termination.
95
+
96
+ For licensing inquiries, contact: legal@x.enterprises
97
+
98
+ ---
99
+ X Enterprises LLC
100
+ Bothell, Washington, United States
package/README.md CHANGED
@@ -1,20 +1,6 @@
1
- # xAuthJWSK
1
+ # @xenterprises/fastify-xauth-jwks
2
2
 
3
- **Lightweight, zero-config path-based JWT/JWKS validation for Fastify v5.**
4
-
5
- Protect multiple API paths with independent JWKS providers. Simple, fast, and production-ready.
6
-
7
- ## Features
8
-
9
- ✅ **Path-Based Protection** - Protect `/admin`, `/portal`, `/api` with different JWKS providers
10
- ✅ **Bearer Token Validation** - Automatic JWT validation against remote JWKS endpoints
11
- ✅ **Local or Remote JWKS** - Use remote URLs for production or local JWKS data for development
12
- ✅ **Dual-Level Caching** - JWKS cache (30 min) + JWT payload cache (5 min)
13
- ✅ **Excluded Paths** - Skip auth for health checks, docs, etc.
14
- ✅ **Zero Dependencies** - Only uses `jose` and `fastify-plugin`
15
- ✅ **Slim & Focused** - ~200 lines of core code, no bloat
16
- ✅ **Configurable** - All caching parameters customizable
17
- ✅ **Request Isolation** - Each path has separate validator & cache
3
+ Path-based JWT/JWKS validation for Fastify v5. Protect multiple API paths with independent JWKS providers.
18
4
 
19
5
  ## Installation
20
6
 
@@ -22,52 +8,203 @@ Protect multiple API paths with independent JWKS providers. Simple, fast, and pr
22
8
  npm install @xenterprises/fastify-xauth-jwks
23
9
  ```
24
10
 
25
- ## Quick Example
11
+ ## Quick Start
26
12
 
27
13
  ```javascript
28
- import Fastify from 'fastify';
29
- import xAuthJWSK from '@xenterprises/fastify-xauth-jwks';
14
+ import Fastify from "fastify";
15
+ import xAuth from "@xenterprises/fastify-xauth-jwks";
30
16
 
31
17
  const fastify = Fastify();
32
18
 
33
- await fastify.register(xAuthJWSK, {
19
+ await fastify.register(xAuth, {
34
20
  paths: {
35
21
  admin: {
36
22
  pathPattern: "/admin",
37
23
  jwksUrl: "https://your-auth.com/.well-known/jwks.json",
38
- }
39
- }
24
+ excludedPaths: ["/health"],
25
+ },
26
+ portal: {
27
+ pathPattern: "/portal",
28
+ jwksUrl: "https://your-auth.com/.well-known/jwks.json",
29
+ },
30
+ },
40
31
  });
41
32
 
42
- fastify.get('/admin/users', (request) => {
43
- return { userId: request.auth.userId };
33
+ // Protected route - requires valid JWT
34
+ fastify.get("/admin/dashboard", (request) => {
35
+ return { userId: request.auth.userId, user: request.user };
44
36
  });
45
37
 
46
- fastify.listen({ port: 3000 });
38
+ // Excluded route - no auth required
39
+ fastify.get("/admin/health", () => ({ status: "ok" }));
40
+
41
+ // Public route - not under any protected path
42
+ fastify.get("/public/info", () => ({ info: "open" }));
43
+
44
+ await fastify.listen({ port: 3000 });
45
+ ```
46
+
47
+ ## Options
48
+
49
+ The plugin accepts a single `paths` object where each key is a path name and each value is a path configuration:
50
+
51
+ | Option | Type | Default | Required | Description |
52
+ |---|---|---|---|---|
53
+ | `paths` | `object` | — | Yes | Map of path name to path configuration. Must contain at least one entry. |
54
+
55
+ ### Path Configuration
56
+
57
+ | Option | Type | Default | Required | Description |
58
+ |---|---|---|---|---|
59
+ | `pathPattern` | `string` | `/<pathName>` | No | URL prefix to protect. All routes starting with this prefix require authentication. |
60
+ | `jwksUrl` | `string` | — | One of `jwksUrl` / `jwksData` | Remote JWKS endpoint URL for token verification. |
61
+ | `jwksData` | `object` | — | One of `jwksUrl` / `jwksData` | Local JWKS data (`{ keys: [...] }`) or a single JWK object for development/testing. |
62
+ | `active` | `boolean` | `true` | No | Set to `false` to skip registration of this path. |
63
+ | `excludedPaths` | `string[]` | `[]` | No | Sub-paths that bypass authentication (e.g., `["/health", "/docs"]`). |
64
+ | `jwksCooldownDuration` | `number` | `30000` | No | Minimum milliseconds between JWKS refetches (remote only). |
65
+ | `jwksCacheMaxAge` | `number` | `1800000` | No | Maximum age in milliseconds for cached JWKS keys (remote only). |
66
+ | `enablePayloadCache` | `boolean` | `true` | No | Cache verified JWT payloads in memory to avoid re-verification. |
67
+ | `payloadCacheTTL` | `number` | `300000` | No | Time-to-live in milliseconds for cached JWT payloads. |
68
+
69
+ ## Decorated Properties
70
+
71
+ After registration, the plugin decorates `fastify.xAuth`:
72
+
73
+ | Property | Type | Description |
74
+ |---|---|---|
75
+ | `fastify.xAuth.validators` | `object` | Map of path name to validator object. |
76
+
77
+ ### Validator Object
78
+
79
+ Each validator in `fastify.xAuth.validators.<name>` exposes:
80
+
81
+ | Property / Method | Type | Description |
82
+ |---|---|---|
83
+ | `name` | `string` | Path name (e.g., `"admin"`). |
84
+ | `pathPattern` | `string` | URL prefix being protected. |
85
+ | `jwksUrl` | `string \| undefined` | Remote JWKS URL if configured. |
86
+ | `config` | `object` | Caching configuration values. |
87
+ | `verifyJWT(token)` | `async function` | Verify a JWT string. Returns the payload object or `null`. |
88
+ | `clearPayloadCache()` | `function` | Clear the in-memory JWT payload cache. |
89
+ | `getPayloadCacheStats()` | `function` | Returns `{ size, enabled, ttl }` for monitoring. |
90
+
91
+ ## Request Properties
92
+
93
+ On authenticated requests, the plugin sets:
94
+
95
+ | Property | Type | Description |
96
+ |---|---|---|
97
+ | `request.user` | `object` | Full JWT payload (claims). |
98
+ | `request.auth.path` | `string` | Name of the path that authenticated this request. |
99
+ | `request.auth.userId` | `string` | The `sub` claim from the JWT. |
100
+ | `request.auth.payload` | `object` | Full JWT payload (same as `request.user`). |
101
+
102
+ ## Utility Exports
103
+
104
+ Import from `@xenterprises/fastify-xauth-jwks/utils`:
105
+
106
+ ```javascript
107
+ import {
108
+ extractToken,
109
+ hasRole,
110
+ hasPermission,
111
+ getUserId,
112
+ getAuthEndpoint,
113
+ requireRole,
114
+ requirePermission,
115
+ requireEndpoint,
116
+ decodeToken,
117
+ decodeHeader,
118
+ } from "@xenterprises/fastify-xauth-jwks/utils";
47
119
  ```
48
120
 
49
- ## Documentation
121
+ | Utility | Signature | Description |
122
+ |---|---|---|
123
+ | `extractToken(request)` | `(req) => string \| null` | Extract Bearer token from Authorization header. |
124
+ | `hasRole(user, roles)` | `(user, string \| string[]) => boolean` | Check if user has any of the specified roles (reads `user.roles`). |
125
+ | `hasPermission(user, perms)` | `(user, string \| string[]) => boolean` | Check if user has any of the specified permissions (reads `user.permissions`). |
126
+ | `getUserId(request)` | `(req) => string \| null` | Get user ID from `request.auth.userId` or `request.user.sub`. |
127
+ | `getAuthEndpoint(request)` | `(req) => string \| null` | Get the auth path name from `request.auth.path`. |
128
+ | `requireRole(roles)` | `(string \| string[]) => preHandler` | Fastify preHandler that returns 403 if user lacks the role. |
129
+ | `requirePermission(perms)` | `(string \| string[]) => preHandler` | Fastify preHandler that returns 403 if user lacks the permission. |
130
+ | `requireEndpoint(name)` | `(string) => preHandler` | Fastify preHandler that returns 403 if request was not authenticated by the named path. |
131
+ | `decodeToken(token)` | `(string) => object` | Decode JWT payload without verification (re-export of `jose.decodeJwt`). |
132
+ | `decodeHeader(token)` | `(string) => object` | Decode JWT header without verification (re-export of `jose.decodeProtectedHeader`). |
133
+
134
+ ### Usage with Route Hooks
135
+
136
+ ```javascript
137
+ import { requireRole, requirePermission } from "@xenterprises/fastify-xauth-jwks/utils";
138
+
139
+ fastify.get("/admin/settings", {
140
+ preHandler: requireRole("admin"),
141
+ handler: async (request) => ({ settings: "..." }),
142
+ });
143
+
144
+ fastify.delete("/admin/users/:id", {
145
+ preHandler: requirePermission("users:delete"),
146
+ handler: async (request) => ({ deleted: true }),
147
+ });
148
+ ```
149
+
150
+ ## Environment Variables
151
+
152
+ The plugin itself does not read environment variables. Your application should pass JWKS URLs from environment variables:
153
+
154
+ | Variable | Required | Description |
155
+ |---|---|---|
156
+ | `ADMIN_JWKS_URL` | Per path | JWKS endpoint for admin path validation. |
157
+ | `PORTAL_JWKS_URL` | Per path | JWKS endpoint for portal path validation. |
158
+
159
+ Example:
160
+
161
+ ```javascript
162
+ await fastify.register(xAuth, {
163
+ paths: {
164
+ admin: {
165
+ pathPattern: "/admin",
166
+ jwksUrl: process.env.ADMIN_JWKS_URL,
167
+ },
168
+ },
169
+ });
170
+ ```
171
+
172
+ ## Error Reference
173
+
174
+ | HTTP Status | Error | When |
175
+ |---|---|---|
176
+ | 401 | `Access token required` | No `Authorization: Bearer <token>` header present on a protected route. |
177
+ | 401 | `Invalid token` | Token failed JWKS verification, is expired, or missing `sub` claim. |
178
+ | 401 | `Authentication failed` | Unexpected error during authentication (logged server-side). |
179
+ | 403 | `Insufficient permissions` | `requireRole` or `requirePermission` check failed. |
180
+ | 403 | `Must authenticate via <name> endpoint` | `requireEndpoint` check failed. |
181
+
182
+ Startup errors (thrown during plugin registration):
183
+
184
+ | Error Message | Cause |
185
+ |---|---|
186
+ | `xAuth: options object is required` | No options passed to the plugin. |
187
+ | `xAuth: 'paths' option is required and must contain at least one path configuration` | Empty or missing `paths` option. |
188
+ | `<name>: Either jwksUrl or jwksData is required` | Path config missing both JWKS source options. |
189
+ | `<name>: Cannot specify both jwksUrl and jwksData` | Path config has both JWKS source options. |
190
+
191
+ ## How It Works
192
+
193
+ 1. **Registration**: For each entry in `paths`, the plugin creates a path validator. Remote JWKS endpoints are set up via `jose.createRemoteJWKSet` with configurable caching; local JWKS data uses `jose.createLocalJWKSet`.
50
194
 
51
- **Getting Started:**
52
- - **[QUICK_START.md](./QUICK_START.md)** - Get started in 5 minutes
53
- - **[CONFIGURATION.md](./CONFIGURATION.md)** - Complete configuration reference (all options + examples)
195
+ 2. **Request Hook**: An `onRequest` hook checks every incoming request URL against each registered `pathPattern` using `String.startsWith()`. If the URL matches a protected path and is not in `excludedPaths`, the hook extracts the Bearer token and verifies it against the path's JWKS.
54
196
 
55
- **Examples & Guides:**
56
- - **[AUTHENTICATION_EXAMPLE.md](./AUTHENTICATION_EXAMPLE.md)** - Email/password auth with JWT signing
57
- - **[DEVELOPMENT.md](./DEVELOPMENT.md)** - Local development with test tokens
58
- - **[KEYS_GENERATION.md](./KEYS_GENERATION.md)** - Generate JWKS keys and test tokens
197
+ 3. **Caching**: Two levels of caching are used. JWKS keys are cached by the `jose` library with configurable cooldown and max age. JWT payloads can optionally be cached in a `Map` keyed by the raw token string, with configurable TTL.
59
198
 
60
- **Advanced:**
61
- - **[CACHING.md](./CACHING.md)** - Configure caching for performance
62
- - **[JOSE_UTILITIES.md](./JOSE_UTILITIES.md)** - Advanced JWT inspection
199
+ 4. **Request Decoration**: On successful verification, `request.user` is set to the full JWT payload and `request.auth` is set with the path name, user ID, and payload for downstream route handlers.
63
200
 
64
201
  ## Tests
65
202
 
66
203
  ```bash
67
204
  npm test
68
- # 49/49 tests passing
205
+ # 63 tests passing
69
206
  ```
70
207
 
71
208
  ## License
72
209
 
73
- ISC
210
+ UNLICENSED
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@xenterprises/fastify-xauth-jwks",
3
3
  "type": "module",
4
- "version": "1.1.2",
4
+ "version": "1.2.1",
5
5
  "description": "Fastify plugin for path-based JWT/JWKS validation. Protect multiple paths with independent JWKS providers.",
6
6
  "main": "src/index.js",
7
7
  "exports": {
@@ -22,7 +22,7 @@
22
22
  "path-based"
23
23
  ],
24
24
  "author": "Tim Mushen",
25
- "license": "ISC",
25
+ "license": "SEE LICENSE IN LICENSE",
26
26
  "repository": {
27
27
  "type": "git",
28
28
  "url": "https://gitlab.com/x-enterprises/fastify-plugins/fastify-x-auth-jwks"
@@ -40,5 +40,8 @@
40
40
  },
41
41
  "peerDependencies": {
42
42
  "fastify": "^5.0.0"
43
+ },
44
+ "publishConfig": {
45
+ "access": "public"
43
46
  }
44
47
  }
@@ -67,7 +67,7 @@ export function getUserId(request) {
67
67
  * @returns {string|null}
68
68
  */
69
69
  export function getAuthEndpoint(request) {
70
- return request.auth?.endpoint || null;
70
+ return request.auth?.path || null;
71
71
  }
72
72
 
73
73
  /**
package/src/xAuth.js CHANGED
@@ -3,10 +3,16 @@ import fp from "fastify-plugin";
3
3
  import { createPathValidator } from "./services/pathValidator.js";
4
4
 
5
5
  async function xAuthPlugin(fastify, options) {
6
- const { paths = {} } = options;
6
+ if (!options || typeof options !== "object") {
7
+ throw new Error("xAuth: options object is required");
8
+ }
9
+
10
+ const { paths } = options;
7
11
 
8
- if (Object.keys(paths).length === 0) {
9
- throw new Error("At least one protected path configuration is required");
12
+ if (!paths || typeof paths !== "object" || Object.keys(paths).length === 0) {
13
+ throw new Error(
14
+ "xAuth: 'paths' option is required and must contain at least one path configuration"
15
+ );
10
16
  }
11
17
 
12
18
  // Create xAuth namespace
@@ -33,4 +39,5 @@ async function xAuthPlugin(fastify, options) {
33
39
 
34
40
  export default fp(xAuthPlugin, {
35
41
  name: "xAuth",
42
+ fastify: ">=5.0.0",
36
43
  });
package/.gitlab-ci.yml DELETED
@@ -1,45 +0,0 @@
1
- # ============================================================================
2
- # GitLab CI/CD Pipeline - xAuthJWSK
3
- # ============================================================================
4
- # Runs tests on merge requests and commits to main/master
5
-
6
- stages:
7
- - test
8
-
9
- variables:
10
- NODE_ENV: test
11
-
12
- # ============================================================================
13
- # Shared Configuration
14
- # ============================================================================
15
- .shared_rules: &shared_rules
16
- rules:
17
- - if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
18
- - if: '$CI_COMMIT_BRANCH == "main"'
19
- - if: '$CI_COMMIT_BRANCH == "master"'
20
- - if: '$CI_COMMIT_TAG'
21
-
22
- # ============================================================================
23
- # STAGE: TEST
24
- # ============================================================================
25
- test:
26
- stage: test
27
- image: node:20-alpine
28
- <<: *shared_rules
29
-
30
- cache:
31
- key: ${CI_COMMIT_REF_SLUG}
32
- paths:
33
- - node_modules/
34
-
35
- before_script:
36
- - npm ci
37
-
38
- script:
39
- - echo "Running xAuthJWSK tests..."
40
- - npm test
41
- - npm audit --audit-level=high || true
42
-
43
- retry:
44
- max: 2
45
- when: runner_system_failure