@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.
@@ -0,0 +1,439 @@
1
+ // test/xAuth.test.js
2
+ import { test } from "node:test";
3
+ import assert from "node:assert";
4
+ import Fastify from "fastify";
5
+ import xAuth from "../src/xAuth.js";
6
+
7
+ test("xAuth plugin registration with single path", async () => {
8
+ const fastify = Fastify({ logger: false });
9
+ try {
10
+ await fastify.register(xAuth, {
11
+ paths: {
12
+ admin: {
13
+ pathPattern: "/admin",
14
+ jwksUrl: "https://example.com/.well-known/jwks.json",
15
+ },
16
+ },
17
+ });
18
+ assert.ok(fastify.xAuth);
19
+ assert.ok(fastify.xAuth.validators);
20
+ assert.ok(fastify.xAuth.validators.admin);
21
+ } finally {
22
+ try { await fastify.close(); } catch {}
23
+ }
24
+ });
25
+
26
+ test("Multiple path validators registration", async () => {
27
+ const fastify = Fastify({ logger: false });
28
+ try {
29
+ await fastify.register(xAuth, {
30
+ paths: {
31
+ admin: {
32
+ pathPattern: "/admin",
33
+ jwksUrl: "https://example.com/admin/.well-known/jwks.json",
34
+ },
35
+ portal: {
36
+ pathPattern: "/portal",
37
+ jwksUrl: "https://example.com/portal/.well-known/jwks.json",
38
+ },
39
+ },
40
+ });
41
+ assert.equal(Object.keys(fastify.xAuth.validators).length, 2);
42
+ assert.ok(fastify.xAuth.validators.admin);
43
+ assert.ok(fastify.xAuth.validators.portal);
44
+ } finally {
45
+ try { await fastify.close(); } catch {}
46
+ }
47
+ });
48
+
49
+ test("Validator has required properties", async () => {
50
+ const fastify = Fastify({ logger: false });
51
+ try {
52
+ const jwksUrl = "https://example.com/.well-known/jwks.json";
53
+ await fastify.register(xAuth, {
54
+ paths: {
55
+ api: {
56
+ pathPattern: "/api",
57
+ jwksUrl,
58
+ },
59
+ },
60
+ });
61
+ const validator = fastify.xAuth.validators.api;
62
+ assert.ok(typeof validator.verifyJWT === "function");
63
+ assert.equal(validator.name, "api");
64
+ assert.equal(validator.pathPattern, "/api");
65
+ assert.equal(validator.jwksUrl, jwksUrl);
66
+ } finally {
67
+ try { await fastify.close(); } catch {}
68
+ }
69
+ });
70
+
71
+ test("Inactive path should not be registered", async () => {
72
+ const fastify = Fastify({ logger: false });
73
+ try {
74
+ await fastify.register(xAuth, {
75
+ paths: {
76
+ active: {
77
+ pathPattern: "/active",
78
+ jwksUrl: "https://example.com/active/.well-known/jwks.json",
79
+ },
80
+ inactive: {
81
+ active: false,
82
+ pathPattern: "/inactive",
83
+ jwksUrl: "https://example.com/inactive/.well-known/jwks.json",
84
+ },
85
+ },
86
+ });
87
+ assert.ok(fastify.xAuth.validators.active);
88
+ assert.equal(fastify.xAuth.validators.inactive, undefined);
89
+ } finally {
90
+ try { await fastify.close(); } catch {}
91
+ }
92
+ });
93
+
94
+ test("Custom pathPattern configuration", async () => {
95
+ const fastify = Fastify({ logger: false });
96
+ try {
97
+ await fastify.register(xAuth, {
98
+ paths: {
99
+ api: {
100
+ pathPattern: "/api/v1/admin",
101
+ jwksUrl: "https://example.com/.well-known/jwks.json",
102
+ },
103
+ },
104
+ });
105
+ const validator = fastify.xAuth.validators.api;
106
+ assert.equal(validator.pathPattern, "/api/v1/admin");
107
+ } finally {
108
+ try { await fastify.close(); } catch {}
109
+ }
110
+ });
111
+
112
+ test("Different paths have different JWKS URLs", async () => {
113
+ const fastify = Fastify({ logger: false });
114
+ try {
115
+ await fastify.register(xAuth, {
116
+ paths: {
117
+ admin: {
118
+ pathPattern: "/admin",
119
+ jwksUrl: "https://example.com/admin/.well-known/jwks.json",
120
+ },
121
+ portal: {
122
+ pathPattern: "/portal",
123
+ jwksUrl: "https://example.com/portal/.well-known/jwks.json",
124
+ },
125
+ },
126
+ });
127
+ const adminJwks = fastify.xAuth.validators.admin.jwksUrl;
128
+ const portalJwks = fastify.xAuth.validators.portal.jwksUrl;
129
+ assert.notEqual(adminJwks, portalJwks);
130
+ } finally {
131
+ try { await fastify.close(); } catch {}
132
+ }
133
+ });
134
+
135
+ test("Error when no paths configured", async () => {
136
+ const fastify = Fastify({ logger: false });
137
+ try {
138
+ await assert.rejects(
139
+ async () => {
140
+ await fastify.register(xAuth, { paths: {} });
141
+ },
142
+ {
143
+ message: "At least one protected path configuration is required",
144
+ }
145
+ );
146
+ } finally {
147
+ try { await fastify.close(); } catch {}
148
+ }
149
+ });
150
+
151
+ test("Error when missing both jwksUrl and jwksData", async () => {
152
+ const fastify = Fastify({ logger: false });
153
+ try {
154
+ await assert.rejects(
155
+ async () => {
156
+ await fastify.register(xAuth, {
157
+ paths: {
158
+ api: {
159
+ pathPattern: "/api",
160
+ },
161
+ },
162
+ });
163
+ },
164
+ {
165
+ message: /Either jwksUrl or jwksData is required/,
166
+ }
167
+ );
168
+ } finally {
169
+ try { await fastify.close(); } catch {}
170
+ }
171
+ });
172
+
173
+ test("Local JWKS data configuration - full JWKS object", async () => {
174
+ const fastify = Fastify({ logger: false });
175
+ try {
176
+ const localJwks = {
177
+ keys: [
178
+ {
179
+ kty: "RSA",
180
+ use: "sig",
181
+ kid: "test-key",
182
+ n: "test-n-value",
183
+ e: "AQAB",
184
+ },
185
+ ],
186
+ };
187
+ await fastify.register(xAuth, {
188
+ paths: {
189
+ api: {
190
+ pathPattern: "/api",
191
+ jwksData: localJwks,
192
+ },
193
+ },
194
+ });
195
+ const validator = fastify.xAuth.validators.api;
196
+ assert.ok(validator);
197
+ assert.ok(typeof validator.verifyJWT === "function");
198
+ } finally {
199
+ try { await fastify.close(); } catch {}
200
+ }
201
+ });
202
+
203
+ test("Local JWK data configuration - single key (for signing and validation)", async () => {
204
+ const fastify = Fastify({ logger: false });
205
+ try {
206
+ const singleJwk = {
207
+ kty: "RSA",
208
+ use: "sig",
209
+ kid: "single-key",
210
+ n: "test-n-value",
211
+ e: "AQAB",
212
+ };
213
+ await fastify.register(xAuth, {
214
+ paths: {
215
+ api: {
216
+ pathPattern: "/api",
217
+ jwksData: singleJwk, // Single key, not wrapped in keys array
218
+ },
219
+ },
220
+ });
221
+ const validator = fastify.xAuth.validators.api;
222
+ assert.ok(validator);
223
+ assert.ok(typeof validator.verifyJWT === "function");
224
+ } finally {
225
+ try { await fastify.close(); } catch {}
226
+ }
227
+ });
228
+
229
+ test("Error when both jwksUrl and jwksData specified", async () => {
230
+ const fastify = Fastify({ logger: false });
231
+ try {
232
+ await assert.rejects(
233
+ async () => {
234
+ await fastify.register(xAuth, {
235
+ paths: {
236
+ api: {
237
+ pathPattern: "/api",
238
+ jwksUrl: "https://example.com/.well-known/jwks.json",
239
+ jwksData: { keys: [] },
240
+ },
241
+ },
242
+ });
243
+ },
244
+ {
245
+ message: /Cannot specify both jwksUrl and jwksData/,
246
+ }
247
+ );
248
+ } finally {
249
+ try { await fastify.close(); } catch {}
250
+ }
251
+ });
252
+
253
+ test("Excluded paths are not protected", async () => {
254
+ const fastify = Fastify({ logger: false });
255
+ try {
256
+ await fastify.register(xAuth, {
257
+ paths: {
258
+ admin: {
259
+ pathPattern: "/admin",
260
+ jwksUrl: "https://example.com/.well-known/jwks.json",
261
+ excludedPaths: ["/health", "/status"],
262
+ },
263
+ },
264
+ });
265
+ fastify.get("/admin/health", (request, reply) => {
266
+ reply.send({ status: "ok" });
267
+ });
268
+ fastify.get("/admin/status", (request, reply) => {
269
+ reply.send({ status: "ok" });
270
+ });
271
+ fastify.get("/admin/protected", (request, reply) => {
272
+ reply.send({ user: request.user || "no user" });
273
+ });
274
+ const healthResponse = await fastify.inject({
275
+ method: "GET",
276
+ url: "/admin/health",
277
+ });
278
+ assert.equal(healthResponse.statusCode, 200);
279
+ const protectedResponse = await fastify.inject({
280
+ method: "GET",
281
+ url: "/admin/protected",
282
+ });
283
+ assert.equal(protectedResponse.statusCode, 401);
284
+ } finally {
285
+ try { await fastify.close(); } catch {}
286
+ }
287
+ });
288
+
289
+ test("Request without token is rejected", async () => {
290
+ const fastify = Fastify({ logger: false });
291
+ try {
292
+ await fastify.register(xAuth, {
293
+ paths: {
294
+ api: {
295
+ pathPattern: "/api",
296
+ jwksUrl: "https://example.com/.well-known/jwks.json",
297
+ },
298
+ },
299
+ });
300
+ fastify.get("/api/data", (request, reply) => {
301
+ reply.send({ data: "secret" });
302
+ });
303
+ const response = await fastify.inject({
304
+ method: "GET",
305
+ url: "/api/data",
306
+ });
307
+ assert.equal(response.statusCode, 401);
308
+ const body = JSON.parse(response.body);
309
+ assert.equal(body.error, "Access token required");
310
+ assert.equal(body.path, "api");
311
+ } finally {
312
+ try { await fastify.close(); } catch {}
313
+ }
314
+ });
315
+
316
+ test("Request with invalid token format is rejected", async () => {
317
+ const fastify = Fastify({ logger: false });
318
+ try {
319
+ await fastify.register(xAuth, {
320
+ paths: {
321
+ api: {
322
+ pathPattern: "/api",
323
+ jwksUrl: "https://example.com/.well-known/jwks.json",
324
+ },
325
+ },
326
+ });
327
+ fastify.get("/api/data", (request, reply) => {
328
+ reply.send({ data: "secret" });
329
+ });
330
+ const response = await fastify.inject({
331
+ method: "GET",
332
+ url: "/api/data",
333
+ headers: {
334
+ authorization: "InvalidToken",
335
+ },
336
+ });
337
+ assert.equal(response.statusCode, 401);
338
+ } finally {
339
+ try { await fastify.close(); } catch {}
340
+ }
341
+ });
342
+
343
+ test("Unprotected paths allow access without token", async () => {
344
+ const fastify = Fastify({ logger: false });
345
+ try {
346
+ await fastify.register(xAuth, {
347
+ paths: {
348
+ admin: {
349
+ pathPattern: "/admin",
350
+ jwksUrl: "https://example.com/.well-known/jwks.json",
351
+ },
352
+ },
353
+ });
354
+ fastify.get("/public/data", (request, reply) => {
355
+ reply.send({ data: "public" });
356
+ });
357
+ const response = await fastify.inject({
358
+ method: "GET",
359
+ url: "/public/data",
360
+ });
361
+ assert.equal(response.statusCode, 200);
362
+ const body = JSON.parse(response.body);
363
+ assert.equal(body.data, "public");
364
+ } finally {
365
+ try { await fastify.close(); } catch {}
366
+ }
367
+ });
368
+
369
+ test("Payload cache is enabled by default", async () => {
370
+ const fastify = Fastify({ logger: false });
371
+ try {
372
+ await fastify.register(xAuth, {
373
+ paths: {
374
+ api: {
375
+ pathPattern: "/api",
376
+ jwksUrl: "https://example.com/.well-known/jwks.json",
377
+ },
378
+ },
379
+ });
380
+ const validator = fastify.xAuth.validators.api;
381
+ const stats = validator.getPayloadCacheStats();
382
+ assert.equal(stats.enabled, true);
383
+ assert.equal(stats.ttl, 300000); // 5 minutes default
384
+ assert.equal(stats.size, 0);
385
+ } finally {
386
+ try { await fastify.close(); } catch {}
387
+ }
388
+ });
389
+
390
+ test("Cache configuration is customizable", async () => {
391
+ const fastify = Fastify({ logger: false });
392
+ try {
393
+ await fastify.register(xAuth, {
394
+ paths: {
395
+ api: {
396
+ pathPattern: "/api",
397
+ jwksUrl: "https://example.com/.well-known/jwks.json",
398
+ enablePayloadCache: false,
399
+ payloadCacheTTL: 600000, // 10 minutes
400
+ jwksCooldownDuration: 60000,
401
+ jwksCacheMaxAge: 3600000,
402
+ },
403
+ },
404
+ });
405
+ const validator = fastify.xAuth.validators.api;
406
+ const stats = validator.getPayloadCacheStats();
407
+ assert.equal(stats.enabled, false);
408
+ assert.equal(stats.ttl, 600000);
409
+ assert.deepEqual(validator.config, {
410
+ jwksCooldownDuration: 60000,
411
+ jwksCacheMaxAge: 3600000,
412
+ enablePayloadCache: false,
413
+ payloadCacheTTL: 600000,
414
+ });
415
+ } finally {
416
+ try { await fastify.close(); } catch {}
417
+ }
418
+ });
419
+
420
+ test("Cache can be manually cleared", async () => {
421
+ const fastify = Fastify({ logger: false });
422
+ try {
423
+ await fastify.register(xAuth, {
424
+ paths: {
425
+ api: {
426
+ pathPattern: "/api",
427
+ jwksUrl: "https://example.com/.well-known/jwks.json",
428
+ },
429
+ },
430
+ });
431
+ const validator = fastify.xAuth.validators.api;
432
+ assert.ok(typeof validator.clearPayloadCache === "function");
433
+ validator.clearPayloadCache();
434
+ const stats = validator.getPayloadCacheStats();
435
+ assert.equal(stats.size, 0);
436
+ } finally {
437
+ try { await fastify.close(); } catch {}
438
+ }
439
+ });