@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,259 @@
1
+ // test/integration.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("Protected route requires authentication", 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
+ fastify.get("/admin/protected", async (request) => {
19
+ return { user: request.user };
20
+ });
21
+ const response = await fastify.inject({
22
+ method: "GET",
23
+ url: "/admin/protected",
24
+ });
25
+ assert.equal(response.statusCode, 401);
26
+ const body = JSON.parse(response.payload);
27
+ assert.equal(body.error, "Access token required");
28
+ assert.equal(body.path, "admin");
29
+ } finally {
30
+ try { await fastify.close(); } catch {}
31
+ }
32
+ });
33
+
34
+ test("Request with invalid token fails", async () => {
35
+ const fastify = Fastify({ logger: false });
36
+ try {
37
+ await fastify.register(xAuth, {
38
+ paths: {
39
+ admin: {
40
+ pathPattern: "/admin",
41
+ jwksUrl: "https://example.com/.well-known/jwks.json",
42
+ },
43
+ },
44
+ });
45
+ fastify.get("/admin/protected", async (request) => {
46
+ return { user: request.user };
47
+ });
48
+ const response = await fastify.inject({
49
+ method: "GET",
50
+ url: "/admin/protected",
51
+ headers: {
52
+ authorization: "Bearer invalid_token",
53
+ },
54
+ });
55
+ assert.equal(response.statusCode, 401);
56
+ const body = JSON.parse(response.payload);
57
+ assert.equal(body.error, "Invalid token");
58
+ } finally {
59
+ try { await fastify.close(); } catch {}
60
+ }
61
+ });
62
+
63
+ test("Excluded paths do not require authentication", async () => {
64
+ const fastify = Fastify({ logger: false });
65
+ try {
66
+ await fastify.register(xAuth, {
67
+ paths: {
68
+ admin: {
69
+ pathPattern: "/admin",
70
+ jwksUrl: "https://example.com/.well-known/jwks.json",
71
+ excludedPaths: ["/health", "/status"],
72
+ },
73
+ },
74
+ });
75
+ fastify.get("/admin/health", async () => {
76
+ return { status: "healthy" };
77
+ });
78
+ fastify.get("/admin/status", async () => {
79
+ return { status: "ok" };
80
+ });
81
+ const healthRes = await fastify.inject({
82
+ method: "GET",
83
+ url: "/admin/health",
84
+ });
85
+ assert.equal(healthRes.statusCode, 200);
86
+ const statusRes = await fastify.inject({
87
+ method: "GET",
88
+ url: "/admin/status",
89
+ });
90
+ assert.equal(statusRes.statusCode, 200);
91
+ } finally {
92
+ try { await fastify.close(); } catch {}
93
+ }
94
+ });
95
+
96
+ test("Different path patterns protect different routes", async () => {
97
+ const fastify = Fastify({ logger: false });
98
+ try {
99
+ await fastify.register(xAuth, {
100
+ paths: {
101
+ admin: {
102
+ pathPattern: "/admin",
103
+ jwksUrl: "https://example.com/admin/.well-known/jwks.json",
104
+ },
105
+ portal: {
106
+ pathPattern: "/portal",
107
+ jwksUrl: "https://example.com/portal/.well-known/jwks.json",
108
+ },
109
+ },
110
+ });
111
+ fastify.get("/admin/data", async () => {
112
+ return { data: "admin" };
113
+ });
114
+ fastify.get("/portal/data", async () => {
115
+ return { data: "portal" };
116
+ });
117
+ fastify.get("/public/data", async () => {
118
+ return { data: "public" };
119
+ });
120
+ const adminRes = await fastify.inject({
121
+ method: "GET",
122
+ url: "/admin/data",
123
+ });
124
+ assert.equal(adminRes.statusCode, 401);
125
+ const portalRes = await fastify.inject({
126
+ method: "GET",
127
+ url: "/portal/data",
128
+ });
129
+ assert.equal(portalRes.statusCode, 401);
130
+ const publicRes = await fastify.inject({
131
+ method: "GET",
132
+ url: "/public/data",
133
+ });
134
+ assert.equal(publicRes.statusCode, 200);
135
+ } finally {
136
+ try { await fastify.close(); } catch {}
137
+ }
138
+ });
139
+
140
+ test("Multiple excluded paths work correctly", async () => {
141
+ const fastify = Fastify({ logger: false });
142
+ try {
143
+ await fastify.register(xAuth, {
144
+ paths: {
145
+ admin: {
146
+ pathPattern: "/admin",
147
+ jwksUrl: "https://example.com/.well-known/jwks.json",
148
+ excludedPaths: ["/public", "/docs", "/health"],
149
+ },
150
+ },
151
+ });
152
+ fastify.get("/admin/public/info", async () => ({ message: "public" }));
153
+ fastify.get("/admin/docs/api", async () => ({ message: "docs" }));
154
+ fastify.get("/admin/health", async () => ({ message: "health" }));
155
+ fastify.get("/admin/dashboard", async () => ({ message: "dashboard" }));
156
+ const publicRes = await fastify.inject({ url: "/admin/public/info" });
157
+ assert.equal(publicRes.statusCode, 200);
158
+ const docsRes = await fastify.inject({ url: "/admin/docs/api" });
159
+ assert.equal(docsRes.statusCode, 200);
160
+ const healthRes = await fastify.inject({ url: "/admin/health" });
161
+ assert.equal(healthRes.statusCode, 200);
162
+ const dashboardRes = await fastify.inject({ url: "/admin/dashboard" });
163
+ assert.equal(dashboardRes.statusCode, 401);
164
+ } finally {
165
+ try { await fastify.close(); } catch {}
166
+ }
167
+ });
168
+
169
+ test("Nested protected routes work correctly", async () => {
170
+ const fastify = Fastify({ logger: false });
171
+ try {
172
+ await fastify.register(xAuth, {
173
+ paths: {
174
+ admin: {
175
+ pathPattern: "/admin",
176
+ jwksUrl: "https://example.com/.well-known/jwks.json",
177
+ },
178
+ },
179
+ });
180
+ fastify.get("/admin/users/profile", async () => ({
181
+ user: "profile",
182
+ }));
183
+ fastify.get("/admin/users/:id", async () => ({
184
+ user: "by_id",
185
+ }));
186
+ fastify.get("/admin/settings/theme", async () => ({
187
+ theme: "dark",
188
+ }));
189
+ const profileRes = await fastify.inject({ url: "/admin/users/profile" });
190
+ assert.equal(profileRes.statusCode, 401);
191
+ const userRes = await fastify.inject({ url: "/admin/users/123" });
192
+ assert.equal(userRes.statusCode, 401);
193
+ const themeRes = await fastify.inject({ url: "/admin/settings/theme" });
194
+ assert.equal(themeRes.statusCode, 401);
195
+ } finally {
196
+ try { await fastify.close(); } catch {}
197
+ }
198
+ });
199
+
200
+ test("Token extraction works with Bearer prefix", async () => {
201
+ const fastify = Fastify({ logger: false });
202
+ try {
203
+ await fastify.register(xAuth, {
204
+ paths: {
205
+ api: {
206
+ pathPattern: "/api",
207
+ jwksUrl: "https://example.com/.well-known/jwks.json",
208
+ },
209
+ },
210
+ });
211
+ fastify.get("/api/test", async (request) => {
212
+ return { received: !!request.user };
213
+ });
214
+ const response1 = await fastify.inject({
215
+ method: "GET",
216
+ url: "/api/test",
217
+ headers: {
218
+ authorization: "invalid_token",
219
+ },
220
+ });
221
+ assert.equal(response1.statusCode, 401);
222
+ const body1 = JSON.parse(response1.payload);
223
+ assert.equal(body1.error, "Access token required");
224
+ const response2 = await fastify.inject({
225
+ method: "GET",
226
+ url: "/api/test",
227
+ headers: {
228
+ authorization: "Bearer some_token",
229
+ },
230
+ });
231
+ assert.equal(response2.statusCode, 401);
232
+ const body2 = JSON.parse(response2.payload);
233
+ assert.equal(body2.error, "Invalid token");
234
+ } finally {
235
+ try { await fastify.close(); } catch {}
236
+ }
237
+ });
238
+
239
+ test("Case sensitivity of path patterns", async () => {
240
+ const fastify = Fastify({ logger: false });
241
+ try {
242
+ await fastify.register(xAuth, {
243
+ paths: {
244
+ admin: {
245
+ pathPattern: "/Admin",
246
+ jwksUrl: "https://example.com/.well-known/jwks.json",
247
+ },
248
+ },
249
+ });
250
+ fastify.get("/Admin/data", async () => ({ data: "admin" }));
251
+ fastify.get("/admin/data", async () => ({ data: "public" }));
252
+ const adminRes = await fastify.inject({ url: "/Admin/data" });
253
+ assert.equal(adminRes.statusCode, 401);
254
+ const pubRes = await fastify.inject({ url: "/admin/data" });
255
+ assert.equal(pubRes.statusCode, 200);
256
+ } finally {
257
+ try { await fastify.close(); } catch {}
258
+ }
259
+ });
@@ -0,0 +1,195 @@
1
+ // test/utils.test.js
2
+ import { test } from "node:test";
3
+ import assert from "node:assert";
4
+ import {
5
+ extractToken,
6
+ hasRole,
7
+ hasPermission,
8
+ getUserId,
9
+ getAuthEndpoint,
10
+ } from "../src/utils/index.js";
11
+
12
+ test("extractToken - valid Bearer token", () => {
13
+ const request = {
14
+ headers: {
15
+ authorization: "Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
16
+ },
17
+ };
18
+
19
+ const token = extractToken(request);
20
+ assert.equal(token, "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...");
21
+ });
22
+
23
+ test("extractToken - no authorization header", () => {
24
+ const request = { headers: {} };
25
+ const token = extractToken(request);
26
+ assert.equal(token, null);
27
+ });
28
+
29
+ test("extractToken - invalid format", () => {
30
+ const request = {
31
+ headers: {
32
+ authorization: "Basic dXNlcjpwYXNz",
33
+ },
34
+ };
35
+
36
+ const token = extractToken(request);
37
+ assert.equal(token, null);
38
+ });
39
+
40
+ test("hasRole - user has single role", () => {
41
+ const user = { roles: ["admin"] };
42
+
43
+ assert.ok(hasRole(user, "admin"));
44
+ assert.ok(!hasRole(user, "super_admin"));
45
+ });
46
+
47
+ test("hasRole - user has multiple roles", () => {
48
+ const user = { roles: ["admin", "manager", "editor"] };
49
+
50
+ assert.ok(hasRole(user, "admin"));
51
+ assert.ok(hasRole(user, "manager"));
52
+ assert.ok(!hasRole(user, "owner"));
53
+ });
54
+
55
+ test("hasRole - check against array of roles (any match)", () => {
56
+ const user = { roles: ["manager"] };
57
+
58
+ assert.ok(hasRole(user, ["admin", "manager", "editor"]));
59
+ assert.ok(!hasRole(user, ["admin", "owner"]));
60
+ });
61
+
62
+ test("hasRole - user has no roles", () => {
63
+ const user = {};
64
+
65
+ assert.ok(!hasRole(user, "admin"));
66
+ });
67
+
68
+ test("hasRole - roles as string instead of array", () => {
69
+ const user = { roles: "admin" };
70
+
71
+ assert.ok(hasRole(user, "admin"));
72
+ assert.ok(!hasRole(user, "manager"));
73
+ });
74
+
75
+ test("hasPermission - user has single permission", () => {
76
+ const user = { permissions: ["users:read"] };
77
+
78
+ assert.ok(hasPermission(user, "users:read"));
79
+ assert.ok(!hasPermission(user, "users:write"));
80
+ });
81
+
82
+ test("hasPermission - user has multiple permissions", () => {
83
+ const user = { permissions: ["users:read", "users:write", "posts:read"] };
84
+
85
+ assert.ok(hasPermission(user, "users:read"));
86
+ assert.ok(hasPermission(user, "users:write"));
87
+ assert.ok(!hasPermission(user, "users:delete"));
88
+ });
89
+
90
+ test("hasPermission - check against array of permissions (any match)", () => {
91
+ const user = { permissions: ["posts:read"] };
92
+
93
+ assert.ok(hasPermission(user, ["users:read", "posts:read"]));
94
+ assert.ok(!hasPermission(user, ["users:read", "users:write"]));
95
+ });
96
+
97
+ test("hasPermission - user has no permissions", () => {
98
+ const user = {};
99
+
100
+ assert.ok(!hasPermission(user, "users:read"));
101
+ });
102
+
103
+ test("getUserId - from auth.userId", () => {
104
+ const request = {
105
+ auth: { userId: "user_123" },
106
+ };
107
+
108
+ const userId = getUserId(request);
109
+ assert.equal(userId, "user_123");
110
+ });
111
+
112
+ test("getUserId - from user.sub", () => {
113
+ const request = {
114
+ user: { sub: "user_456" },
115
+ };
116
+
117
+ const userId = getUserId(request);
118
+ assert.equal(userId, "user_456");
119
+ });
120
+
121
+ test("getUserId - prioritizes auth.userId over user.sub", () => {
122
+ const request = {
123
+ auth: { userId: "user_123" },
124
+ user: { sub: "user_456" },
125
+ };
126
+
127
+ const userId = getUserId(request);
128
+ assert.equal(userId, "user_123");
129
+ });
130
+
131
+ test("getUserId - returns null when not found", () => {
132
+ const request = {};
133
+
134
+ const userId = getUserId(request);
135
+ assert.equal(userId, null);
136
+ });
137
+
138
+ test("getAuthEndpoint - returns path name", () => {
139
+ const request = {
140
+ auth: { endpoint: "admin" },
141
+ };
142
+
143
+ const endpoint = getAuthEndpoint(request);
144
+ assert.equal(endpoint, "admin");
145
+ });
146
+
147
+ test("getAuthEndpoint - returns null when not found", () => {
148
+ const request = {};
149
+
150
+ const endpoint = getAuthEndpoint(request);
151
+ assert.equal(endpoint, null);
152
+ });
153
+
154
+ test("requireRole - creates preHandler function", async () => {
155
+ const { requireRole } = await import("../src/utils/index.js");
156
+
157
+ const handler = requireRole("admin");
158
+ assert.ok(typeof handler === "function");
159
+ });
160
+
161
+ test("requirePermission - creates preHandler function", async () => {
162
+ const { requirePermission } = await import("../src/utils/index.js");
163
+
164
+ const handler = requirePermission("users:write");
165
+ assert.ok(typeof handler === "function");
166
+ });
167
+
168
+ test("requireEndpoint - creates preHandler function", async () => {
169
+ const { requireEndpoint } = await import("../src/utils/index.js");
170
+
171
+ const handler = requireEndpoint("portal");
172
+ assert.ok(typeof handler === "function");
173
+ });
174
+
175
+ test("decodeToken - decodes JWT without verification", async () => {
176
+ const { decodeToken } = await import("../src/utils/index.js");
177
+
178
+ // Sample JWT (unsigned)
179
+ const token = "eyJhbGciOiJub25lIn0.eyJzdWIiOiJ1c2VyXzEyMyIsIm5hbWUiOiJKb2huIERvZSIsImlhdCI6MTY5MjEwMzAwMH0.";
180
+ const payload = decodeToken(token);
181
+
182
+ assert.equal(payload.sub, "user_123");
183
+ assert.equal(payload.name, "John Doe");
184
+ });
185
+
186
+ test("decodeHeader - decodes JWT header", async () => {
187
+ const { decodeHeader } = await import("../src/utils/index.js");
188
+
189
+ const token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6InRlc3Rfa2V5In0.eyJzdWIiOiJ1c2VyXzEyMyJ9.signature";
190
+ const header = decodeHeader(token);
191
+
192
+ assert.equal(header.alg, "HS256");
193
+ assert.equal(header.typ, "JWT");
194
+ assert.equal(header.kid, "test_key");
195
+ });