@xenterprises/fastify-xauth-jwks 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/README.md ADDED
@@ -0,0 +1,73 @@
1
+ # xAuthJWSK
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
18
+
19
+ ## Installation
20
+
21
+ ```bash
22
+ npm install @xenterprises/fastify-xauth-jwks
23
+ ```
24
+
25
+ ## Quick Example
26
+
27
+ ```javascript
28
+ import Fastify from 'fastify';
29
+ import xAuthJWSK from '@xenterprises/fastify-xauth-jwks';
30
+
31
+ const fastify = Fastify();
32
+
33
+ await fastify.register(xAuthJWSK, {
34
+ paths: {
35
+ admin: {
36
+ pathPattern: "/admin",
37
+ jwksUrl: "https://your-auth.com/.well-known/jwks.json",
38
+ }
39
+ }
40
+ });
41
+
42
+ fastify.get('/admin/users', (request) => {
43
+ return { userId: request.auth.userId };
44
+ });
45
+
46
+ fastify.listen({ port: 3000 });
47
+ ```
48
+
49
+ ## Documentation
50
+
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)
54
+
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
59
+
60
+ **Advanced:**
61
+ - **[CACHING.md](./CACHING.md)** - Configure caching for performance
62
+ - **[JOSE_UTILITIES.md](./JOSE_UTILITIES.md)** - Advanced JWT inspection
63
+
64
+ ## Tests
65
+
66
+ ```bash
67
+ npm test
68
+ # 49/49 tests passing ✅
69
+ ```
70
+
71
+ ## License
72
+
73
+ ISC
package/package.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "@xenterprises/fastify-xauth-jwks",
3
+ "type": "module",
4
+ "version": "1.0.0",
5
+ "description": "Fastify plugin for path-based JWT/JWKS validation. Protect multiple paths with independent JWKS providers.",
6
+ "main": "src/index.js",
7
+ "exports": {
8
+ ".": "./src/index.js",
9
+ "./utils": "./src/utils/index.js"
10
+ },
11
+ "scripts": {
12
+ "start": "fastify start -l info server/app.js",
13
+ "dev": "fastify start -w -l info -P server/app.js",
14
+ "test": "node --test test/xAuth.test.js test/utils.test.js test/integration.test.js"
15
+ },
16
+ "keywords": [
17
+ "fastify",
18
+ "jwt",
19
+ "jwks",
20
+ "jose",
21
+ "validation",
22
+ "path-based"
23
+ ],
24
+ "author": "Tim Mushen",
25
+ "license": "ISC",
26
+ "repository": {
27
+ "type": "git",
28
+ "url": "https://github.com/xenterprises/fastify-xauth-jwks"
29
+ },
30
+ "bugs": {
31
+ "url": "https://github.com/xenterprises/fastify-xauth-jwks/issues"
32
+ },
33
+ "homepage": "https://github.com/xenterprises/fastify-xauth-jwks#readme",
34
+ "dependencies": {
35
+ "fastify-plugin": "^5.0.0",
36
+ "jose": "^5.9.6"
37
+ },
38
+ "devDependencies": {
39
+ "fastify": "^5.1.0"
40
+ },
41
+ "peerDependencies": {
42
+ "fastify": "^5.0.0"
43
+ }
44
+ }
package/server/app.js ADDED
@@ -0,0 +1,370 @@
1
+ /**
2
+ * xAuthJWSK Demo Server
3
+ *
4
+ * Shows path-based JWT/JWKS validation with flexible configuration:
5
+ * - Development: Local JWKS data (example-jwks.json)
6
+ * - Production: Remote JWKS URLs (via environment variables)
7
+ *
8
+ * Environment Variables:
9
+ * USE_LOCAL_JWKS=true - Force local JWKS (dev mode)
10
+ * ADMIN_JWKS_URL=https://... - Admin path JWKS endpoint
11
+ * PORTAL_JWKS_URL=https://... - Portal path JWKS endpoint
12
+ * PARTNER_JWKS_URL=https://... - Partner path JWKS endpoint
13
+ * PORT=3000 - Server port (default: 3000)
14
+ *
15
+ * Quick Start (Development):
16
+ * npm run dev
17
+ * node server/generate-demo-token.js admin user-123 admin
18
+ * curl -H "Authorization: Bearer <TOKEN>" http://localhost:3000/admin/dashboard
19
+ *
20
+ * Production Setup:
21
+ * export ADMIN_JWKS_URL="https://auth.example.com/.well-known/jwks.json"
22
+ * export PORTAL_JWKS_URL="https://auth.example.com/.well-known/jwks.json"
23
+ * npm start
24
+ */
25
+
26
+ import Fastify from "fastify";
27
+ import xAuthJWSK from "../src/xAuth.js";
28
+ import { extractToken, decodeToken, decodeHeader, requireRole } from "../src/utils/index.js";
29
+ import exampleJwks from "./example-jwks.json" assert { type: "json" };
30
+
31
+ const fastify = Fastify({
32
+ logger: true,
33
+ });
34
+
35
+ // ============================================================================
36
+ // Configuration Strategy
37
+ // ============================================================================
38
+ // Flexible configuration for both development and production:
39
+ // - Development: Use local JWKS (example-jwks.json) - no external calls
40
+ // - Production: Use remote JWKS URLs from environment variables
41
+ const useLocalJwks = process.env.USE_LOCAL_JWKS === "true" || !process.env.ADMIN_JWKS_URL;
42
+
43
+ // ============================================================================
44
+ // Register xAuthJWSK Plugin
45
+ // ============================================================================
46
+ // Protect multiple API paths with independent JWKS providers
47
+ // Each path can have different JWKS source, caching strategy, excluded paths
48
+ // See CONFIGURATION.md for complete reference
49
+ await fastify.register(xAuthJWSK, {
50
+ paths: {
51
+ // ========================================================================
52
+ // ADMIN API - Strict authentication (e.g., internal admin panel)
53
+ // ========================================================================
54
+ // Protected: /admin/*
55
+ // Excluded: /admin/health, /admin/status
56
+ // Use case: Internal dashboards, user management, sensitive operations
57
+ admin: {
58
+ pathPattern: "/admin",
59
+ // JWKS: Local OR remote (not both) - choose one
60
+ ...(useLocalJwks
61
+ ? { jwksData: exampleJwks } // Development: local file
62
+ : { jwksUrl: process.env.ADMIN_JWKS_URL || "https://example.com/admin/.well-known/jwks.json" }), // Production: env var
63
+ // Paths that don't require authentication (health checks, etc)
64
+ excludedPaths: ["/health", "/status"],
65
+ // Caching: Strict cache - refresh every 5 minutes
66
+ enablePayloadCache: true,
67
+ payloadCacheTTL: 300000, // 5 min token payload cache
68
+ jwksCacheMaxAge: 1800000, // 30 min JWKS cache (production keys rarely change)
69
+ },
70
+
71
+ // ========================================================================
72
+ // PORTAL API - User-facing (e.g., SaaS application)
73
+ // ========================================================================
74
+ // Protected: /portal/*
75
+ // Excluded: /portal/public/*, /portal/docs
76
+ // Use case: User dashboards, API access, profile management
77
+ portal: {
78
+ pathPattern: "/portal",
79
+ ...(useLocalJwks
80
+ ? { jwksData: exampleJwks }
81
+ : { jwksUrl: process.env.PORTAL_JWKS_URL || "https://example.com/portal/.well-known/jwks.json" }),
82
+ // Public endpoints (marketing pages, documentation)
83
+ excludedPaths: ["/public", "/docs"],
84
+ // Caching: Standard cache - balance performance and freshness
85
+ enablePayloadCache: true,
86
+ payloadCacheTTL: 300000, // 5 min
87
+ },
88
+
89
+ // ========================================================================
90
+ // PARTNER API - Integration endpoint (e.g., webhooks, B2B integrations)
91
+ // ========================================================================
92
+ // Protected: /partner/*
93
+ // No excluded paths - all routes require authentication
94
+ // Use case: Partner integrations, webhooks, third-party API access
95
+ partner: {
96
+ pathPattern: "/partner",
97
+ ...(useLocalJwks
98
+ ? { jwksData: exampleJwks }
99
+ : { jwksUrl: process.env.PARTNER_JWKS_URL || "https://example.com/partner/.well-known/jwks.json" }),
100
+ // All routes require authentication - no exclusions
101
+ // Longer cache TTL - partner tokens typically valid longer
102
+ enablePayloadCache: true,
103
+ payloadCacheTTL: 600000, // 10 min - partners have longer-lived tokens
104
+ },
105
+ },
106
+ });
107
+
108
+ // ============================================================================
109
+ // Public Routes (No Authentication Required)
110
+ // ============================================================================
111
+
112
+ fastify.get("/", async () => {
113
+ return {
114
+ service: "xAuthJWSK Demo",
115
+ version: "1.0.0",
116
+ protectedPaths: Object.keys(fastify.xAuth.validators),
117
+ docs: "https://github.com/xenterprises/fastify-xauth-jwks",
118
+ };
119
+ });
120
+
121
+ fastify.get("/health", async () => {
122
+ return { status: "ok", timestamp: new Date().toISOString() };
123
+ });
124
+
125
+ // ============================================================================
126
+ // Admin Routes (Protected by /admin)
127
+ // ============================================================================
128
+
129
+ fastify.get("/admin/health", async () => {
130
+ return { status: "healthy", service: "admin-api" };
131
+ });
132
+
133
+ fastify.get("/admin/status", async () => {
134
+ return { status: "operational", uptime: process.uptime() };
135
+ });
136
+
137
+ fastify.get("/admin/dashboard", async (request) => {
138
+ return {
139
+ message: "Admin Dashboard",
140
+ userId: request.auth.userId,
141
+ user: request.auth.payload,
142
+ authenticatedVia: request.auth.path,
143
+ };
144
+ });
145
+
146
+ fastify.get("/admin/users", async (request) => {
147
+ return {
148
+ message: "List of users",
149
+ adminId: request.auth.userId,
150
+ users: [
151
+ { id: "user_1", name: "John Doe", email: "john@example.com" },
152
+ { id: "user_2", name: "Jane Smith", email: "jane@example.com" },
153
+ ],
154
+ };
155
+ });
156
+
157
+ fastify.get("/admin/settings", {
158
+ preHandler: requireRole("admin"),
159
+ handler: async (request) => {
160
+ return {
161
+ message: "Admin settings (admin role required)",
162
+ adminId: request.auth.userId,
163
+ roles: request.user?.roles || [],
164
+ };
165
+ },
166
+ });
167
+
168
+ // Cache management endpoint
169
+ fastify.post("/admin/cache/clear", async (request) => {
170
+ const validator = fastify.xAuth.validators.admin;
171
+ validator.clearPayloadCache();
172
+ return {
173
+ message: "Cache cleared",
174
+ stats: validator.getPayloadCacheStats(),
175
+ };
176
+ });
177
+
178
+ fastify.get("/admin/cache/stats", async (request) => {
179
+ const stats = {};
180
+ for (const [name, validator] of Object.entries(fastify.xAuth.validators)) {
181
+ stats[name] = validator.getPayloadCacheStats();
182
+ }
183
+ return { cacheStats: stats };
184
+ });
185
+
186
+ // ============================================================================
187
+ // Portal Routes (Protected by /portal)
188
+ // ============================================================================
189
+
190
+ fastify.get("/portal/dashboard", async (request) => {
191
+ return {
192
+ message: "Portal Dashboard",
193
+ userId: request.auth.userId,
194
+ user: request.auth.payload,
195
+ };
196
+ });
197
+
198
+ fastify.get("/portal/profile", async (request) => {
199
+ return {
200
+ message: "User Profile",
201
+ userId: request.auth.userId,
202
+ email: request.user?.email,
203
+ name: request.user?.name,
204
+ };
205
+ });
206
+
207
+ fastify.get("/portal/public/docs", async () => {
208
+ return {
209
+ message: "API Documentation",
210
+ endpoints: ["GET /portal/dashboard", "GET /portal/profile", "POST /portal/update"],
211
+ };
212
+ });
213
+
214
+ fastify.get("/portal/public/pricing", async () => {
215
+ return {
216
+ plans: [
217
+ { tier: "free", price: 0 },
218
+ { tier: "pro", price: 29 },
219
+ { tier: "enterprise", price: 299 },
220
+ ],
221
+ };
222
+ });
223
+
224
+ // ============================================================================
225
+ // Partner Routes (Protected by /partner)
226
+ // ============================================================================
227
+
228
+ fastify.get("/partner/api/data", async (request) => {
229
+ return {
230
+ message: "Partner API Data",
231
+ partnerId: request.auth.userId,
232
+ data: { status: "connected", lastSync: new Date().toISOString() },
233
+ };
234
+ });
235
+
236
+ fastify.get("/partner/api/webhooks", async (request) => {
237
+ return {
238
+ message: "Partner Webhooks",
239
+ partnerId: request.auth.userId,
240
+ webhooks: [
241
+ { id: "wh_1", event: "user.created", url: "https://partner.example.com/webhooks" },
242
+ ],
243
+ };
244
+ });
245
+
246
+ // ============================================================================
247
+ // Token Inspection Endpoints (No Auth Required - for debugging)
248
+ // ============================================================================
249
+
250
+ fastify.post("/debug/decode-token", async (request, reply) => {
251
+ const { token } = request.body;
252
+
253
+ if (!token) {
254
+ return reply.code(400).send({ error: "Token required" });
255
+ }
256
+
257
+ try {
258
+ const header = decodeHeader(token);
259
+ const payload = decodeToken(token);
260
+
261
+ return {
262
+ header,
263
+ payload,
264
+ warning: "⚠️ This is UNVERIFIED token data. Only for debugging. Do NOT trust in production.",
265
+ };
266
+ } catch (error) {
267
+ return reply.code(400).send({ error: "Invalid token format" });
268
+ }
269
+ });
270
+
271
+ fastify.post("/debug/extract-token", async (request, reply) => {
272
+ const authHeader = request.headers.authorization;
273
+
274
+ if (!authHeader) {
275
+ return reply.code(400).send({ error: "Authorization header required" });
276
+ }
277
+
278
+ const token = extractToken(request);
279
+
280
+ if (!token) {
281
+ return reply.code(400).send({ error: "Invalid Bearer token format" });
282
+ }
283
+
284
+ try {
285
+ const payload = decodeToken(token);
286
+ return {
287
+ extracted: true,
288
+ userId: payload.sub,
289
+ expiresAt: new Date(payload.exp * 1000).toISOString(),
290
+ payload,
291
+ };
292
+ } catch (error) {
293
+ return reply.code(400).send({ error: "Failed to decode token" });
294
+ }
295
+ });
296
+
297
+ // ============================================================================
298
+ // Validator Inspection Endpoint
299
+ // ============================================================================
300
+
301
+ fastify.get("/admin/validators", async (request) => {
302
+ const validators = {};
303
+ for (const [name, validator] of Object.entries(fastify.xAuth.validators)) {
304
+ validators[name] = {
305
+ name: validator.name,
306
+ pathPattern: validator.pathPattern,
307
+ jwksUrl: validator.jwksUrl,
308
+ config: validator.config,
309
+ cacheStats: validator.getPayloadCacheStats(),
310
+ };
311
+ }
312
+ return { validators };
313
+ });
314
+
315
+ // ============================================================================
316
+ // Server Startup
317
+ // ============================================================================
318
+
319
+ const start = async () => {
320
+ try {
321
+ await fastify.listen({ port: process.env.PORT || 3000, host: "0.0.0.0" });
322
+
323
+ console.log("\n🚀 xAuthJWSK Demo Server Started!\n");
324
+ console.log(`Mode: ${useLocalJwks ? "🏠 LOCAL (Development)" : "☁️ REMOTE (Production)"}`);
325
+ if (useLocalJwks) {
326
+ console.log(" Using example-jwks.json - perfect for testing!");
327
+ console.log(" Generate test tokens: node server/generate-demo-token.js [path] [userId] [role]");
328
+ }
329
+ console.log("\nProtected Paths:");
330
+ for (const [name, validator] of Object.entries(fastify.xAuth.validators)) {
331
+ console.log(`\n 📍 ${name.toUpperCase()}`);
332
+ console.log(` Path: ${validator.pathPattern}`);
333
+ console.log(` JWKS: ${useLocalJwks ? "local (example-jwks.json)" : validator.jwksUrl}`);
334
+ console.log(` Cache: ${validator.config.enablePayloadCache ? "enabled" : "disabled"}`);
335
+ }
336
+
337
+ console.log("\n\nPublic Endpoints:");
338
+ console.log(" GET / - API info");
339
+ console.log(" GET /health - Health check");
340
+ console.log(" POST /debug/decode-token - Decode JWT (debug only)");
341
+ console.log(" POST /debug/extract-token - Extract token from header");
342
+ console.log(" GET /admin/validators - View validator configs");
343
+
344
+ console.log("\n\nProtected Endpoints (require Bearer token):");
345
+ console.log(" GET /admin/dashboard - Admin dashboard");
346
+ console.log(" GET /admin/users - List users");
347
+ console.log(" GET /admin/settings - Settings (requires 'admin' role)");
348
+ console.log(" POST /admin/cache/clear - Clear auth cache");
349
+ console.log(" GET /admin/cache/stats - View cache statistics");
350
+ console.log(" GET /portal/dashboard - Portal dashboard");
351
+ console.log(" GET /portal/profile - User profile");
352
+ console.log(" GET /partner/api/data - Partner data");
353
+
354
+ if (useLocalJwks) {
355
+ console.log("\n\n🧪 Testing with Local JWKS:");
356
+ console.log(" 1. Generate a test token:");
357
+ console.log(" node server/generate-demo-token.js admin user-123 admin");
358
+ console.log(" 2. Copy the token and test a protected endpoint:");
359
+ console.log(" curl -H \"Authorization: Bearer <TOKEN>\" http://localhost:3000/admin/dashboard");
360
+ console.log(" 3. For more info, see KEYS_GENERATION.md and QUICK_START.md");
361
+ }
362
+
363
+ console.log("\n💡 Tip: Add '-H \"Authorization: Bearer YOUR_TOKEN\"' to protected requests\n");
364
+ } catch (err) {
365
+ fastify.log.error(err);
366
+ process.exit(1);
367
+ }
368
+ };
369
+
370
+ start();
@@ -0,0 +1,12 @@
1
+ {
2
+ "keys": [
3
+ {
4
+ "kty": "RSA",
5
+ "use": "sig",
6
+ "alg": "RS256",
7
+ "kid": "demo-key-admin",
8
+ "n": "xjlCRBqkQWeBpaMWV2E2h6L1zcqmxm0W3Z5BbMwP9jfYEJ_ZHvMdV8fYaWDV8xzGqL7Z9fQaL7bXmVzYcPz0Xq5L_VmE8V7K0L1M2N3O4P5Q6R7S8T9U0V1W2X3Y4Z5A6B7C8D9E0F1G2H3I4J5K6L7M8N9O0P1Q2R3S4T5U6V7W8X9Y0Z1A2B3C4D5E6F7G8H9I0J1K2L3M4N5O6P7Q8R9S0T1U2V3W4X5Y6Z7A8B9C0D1E2F3G4H5I6J7K8L9M0N1O2P3Q4R5S6T7U8V9W0X1Y2Z3A4B5C6D7E8F9G0H1I2J3K4L5M6N7O8P9Q0R1S2T3U4V5W6X7Y8Z9",
9
+ "e": "AQAB"
10
+ }
11
+ ]
12
+ }