@sustaina/iam-middleware 1.0.3 → 1.1.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 +97 -4
- package/dist/auth/AuthMiddleware.d.ts +36 -5
- package/dist/auth/AuthMiddleware.d.ts.map +1 -1
- package/dist/auth/AuthMiddleware.js +187 -109
- package/dist/auth/AuthMiddleware.js.map +1 -1
- package/dist/auth/AuthMiddleware.test.d.ts +2 -0
- package/dist/auth/AuthMiddleware.test.d.ts.map +1 -0
- package/dist/auth/AuthMiddleware.test.js +712 -0
- package/dist/auth/AuthMiddleware.test.js.map +1 -0
- package/dist/auth/ImplementModeMiddleware.test.d.ts +2 -0
- package/dist/auth/ImplementModeMiddleware.test.d.ts.map +1 -0
- package/dist/auth/ImplementModeMiddleware.test.js +249 -0
- package/dist/auth/ImplementModeMiddleware.test.js.map +1 -0
- package/dist/types/AuthTypes.d.ts +14 -4
- package/dist/types/AuthTypes.d.ts.map +1 -1
- package/package.json +12 -7
|
@@ -0,0 +1,712 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const jsonwebtoken_1 = __importDefault(require("jsonwebtoken"));
|
|
7
|
+
// Mock dependencies
|
|
8
|
+
jest.mock("jsonwebtoken");
|
|
9
|
+
jest.mock("@opentelemetry/api", () => ({
|
|
10
|
+
trace: {
|
|
11
|
+
getTracer: () => ({
|
|
12
|
+
startSpan: () => ({
|
|
13
|
+
setAttributes: jest.fn(),
|
|
14
|
+
recordException: jest.fn(),
|
|
15
|
+
end: jest.fn(),
|
|
16
|
+
}),
|
|
17
|
+
}),
|
|
18
|
+
},
|
|
19
|
+
}));
|
|
20
|
+
jest.mock("pino", () => {
|
|
21
|
+
return jest.fn(() => ({
|
|
22
|
+
info: jest.fn(),
|
|
23
|
+
warn: jest.fn(),
|
|
24
|
+
error: jest.fn(),
|
|
25
|
+
debug: jest.fn(),
|
|
26
|
+
}));
|
|
27
|
+
});
|
|
28
|
+
// Mock Redis client - defined before jest.mock
|
|
29
|
+
const mockRedisClient = {
|
|
30
|
+
get: jest.fn(),
|
|
31
|
+
multi: jest.fn(() => ({
|
|
32
|
+
exec: jest.fn(),
|
|
33
|
+
})),
|
|
34
|
+
isOpen: true,
|
|
35
|
+
connect: jest.fn(),
|
|
36
|
+
};
|
|
37
|
+
// Mock BaseMiddleware's Redis setup - use factory function to avoid hoisting issues
|
|
38
|
+
jest.mock("../infrastructure/RedisClient", () => ({
|
|
39
|
+
RedisClient: {
|
|
40
|
+
getInstance: jest.fn().mockResolvedValue({
|
|
41
|
+
get: jest.fn(),
|
|
42
|
+
multi: jest.fn(() => ({
|
|
43
|
+
exec: jest.fn(),
|
|
44
|
+
})),
|
|
45
|
+
isOpen: true,
|
|
46
|
+
connect: jest.fn(),
|
|
47
|
+
}),
|
|
48
|
+
},
|
|
49
|
+
}));
|
|
50
|
+
// Import after mocks are set up
|
|
51
|
+
const AuthMiddleware_1 = require("./AuthMiddleware");
|
|
52
|
+
describe("AuthMiddleware", () => {
|
|
53
|
+
let authMiddleware;
|
|
54
|
+
const mockJwtSecret = "test-secret";
|
|
55
|
+
const mockToken = "valid.jwt.token";
|
|
56
|
+
// Valid user payload
|
|
57
|
+
const validUserPayload = {
|
|
58
|
+
id: "user-123",
|
|
59
|
+
name: "Test User",
|
|
60
|
+
email: "test@example.com",
|
|
61
|
+
tenantId: "tenant-456",
|
|
62
|
+
type: "user",
|
|
63
|
+
tenantLocale: "en-US",
|
|
64
|
+
passwordPolicy: "standard",
|
|
65
|
+
isDenyPasswordChange: false,
|
|
66
|
+
isPasswordSendEmail: true,
|
|
67
|
+
jti: "jti-token-123",
|
|
68
|
+
};
|
|
69
|
+
// Valid admin payload
|
|
70
|
+
const validAdminPayload = {
|
|
71
|
+
id: "admin-123",
|
|
72
|
+
name: "Test Admin",
|
|
73
|
+
email: "admin@example.com",
|
|
74
|
+
isPlatformAdmin: true,
|
|
75
|
+
};
|
|
76
|
+
// Mock request factory
|
|
77
|
+
const createMockRequest = (authHeader) => ({
|
|
78
|
+
headers: {
|
|
79
|
+
authorization: authHeader,
|
|
80
|
+
},
|
|
81
|
+
url: "/test-url",
|
|
82
|
+
});
|
|
83
|
+
// Mock reply factory
|
|
84
|
+
const createMockReply = () => {
|
|
85
|
+
const reply = {
|
|
86
|
+
status: jest.fn().mockReturnThis(),
|
|
87
|
+
send: jest.fn().mockReturnThis(),
|
|
88
|
+
};
|
|
89
|
+
return reply;
|
|
90
|
+
};
|
|
91
|
+
beforeEach(() => {
|
|
92
|
+
jest.clearAllMocks();
|
|
93
|
+
// Setup AuthMiddleware with mocked redis client
|
|
94
|
+
authMiddleware = new AuthMiddleware_1.AuthMiddleware({
|
|
95
|
+
jwtSecret: mockJwtSecret,
|
|
96
|
+
redisClient: mockRedisClient,
|
|
97
|
+
});
|
|
98
|
+
// Default mock implementations
|
|
99
|
+
jsonwebtoken_1.default.verify.mockReturnValue(validUserPayload);
|
|
100
|
+
mockRedisClient.get.mockResolvedValue(validUserPayload.jti);
|
|
101
|
+
});
|
|
102
|
+
describe("authenticate", () => {
|
|
103
|
+
it("should authenticate successfully with valid token", async () => {
|
|
104
|
+
const request = createMockRequest(`Bearer ${mockToken}`);
|
|
105
|
+
const reply = createMockReply();
|
|
106
|
+
await authMiddleware.authenticate(request, reply);
|
|
107
|
+
expect(request.isAuthenticated).toBe(true);
|
|
108
|
+
expect(request.user).toEqual({
|
|
109
|
+
id: validUserPayload.id,
|
|
110
|
+
name: validUserPayload.name,
|
|
111
|
+
email: validUserPayload.email,
|
|
112
|
+
type: validUserPayload.type,
|
|
113
|
+
});
|
|
114
|
+
expect(request.payload).toEqual(validUserPayload);
|
|
115
|
+
});
|
|
116
|
+
it("should return 401 when no authorization header is provided", async () => {
|
|
117
|
+
const request = createMockRequest();
|
|
118
|
+
const reply = createMockReply();
|
|
119
|
+
await authMiddleware.authenticate(request, reply);
|
|
120
|
+
expect(reply.status).toHaveBeenCalledWith(401);
|
|
121
|
+
expect(reply.send).toHaveBeenCalledWith({
|
|
122
|
+
error: "Authentication Error",
|
|
123
|
+
message: "Authorization token is required",
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
it("should return 401 when authorization header does not start with Bearer", async () => {
|
|
127
|
+
const request = createMockRequest("Basic sometoken");
|
|
128
|
+
const reply = createMockReply();
|
|
129
|
+
await authMiddleware.authenticate(request, reply);
|
|
130
|
+
expect(reply.status).toHaveBeenCalledWith(401);
|
|
131
|
+
expect(reply.send).toHaveBeenCalledWith({
|
|
132
|
+
error: "Authentication Error",
|
|
133
|
+
message: "Authorization token is required",
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
it("should return 401 when JWT verification fails", async () => {
|
|
137
|
+
jsonwebtoken_1.default.verify.mockImplementation(() => {
|
|
138
|
+
throw new Error("Invalid token");
|
|
139
|
+
});
|
|
140
|
+
const request = createMockRequest(`Bearer ${mockToken}`);
|
|
141
|
+
const reply = createMockReply();
|
|
142
|
+
await authMiddleware.authenticate(request, reply);
|
|
143
|
+
expect(reply.status).toHaveBeenCalledWith(401);
|
|
144
|
+
expect(reply.send).toHaveBeenCalledWith({
|
|
145
|
+
error: "Authentication Error",
|
|
146
|
+
message: "Invalid or expired token",
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
it("should return 401 when payload is incomplete", async () => {
|
|
150
|
+
const incompletePayload = {
|
|
151
|
+
id: "user-123",
|
|
152
|
+
name: "Test User",
|
|
153
|
+
// missing other required fields
|
|
154
|
+
};
|
|
155
|
+
jsonwebtoken_1.default.verify.mockReturnValue(incompletePayload);
|
|
156
|
+
const request = createMockRequest(`Bearer ${mockToken}`);
|
|
157
|
+
const reply = createMockReply();
|
|
158
|
+
await authMiddleware.authenticate(request, reply);
|
|
159
|
+
expect(reply.status).toHaveBeenCalledWith(401);
|
|
160
|
+
expect(reply.send).toHaveBeenCalledWith({
|
|
161
|
+
error: "Authentication Error",
|
|
162
|
+
message: "Invalid token payload",
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
it("should return 401 when token is not in allowlist", async () => {
|
|
166
|
+
mockRedisClient.get.mockResolvedValue("different-jti");
|
|
167
|
+
const request = createMockRequest(`Bearer ${mockToken}`);
|
|
168
|
+
const reply = createMockReply();
|
|
169
|
+
await authMiddleware.authenticate(request, reply);
|
|
170
|
+
expect(reply.status).toHaveBeenCalledWith(401);
|
|
171
|
+
expect(reply.send).toHaveBeenCalledWith({
|
|
172
|
+
error: "Authentication Error",
|
|
173
|
+
message: "Token is not in allow list",
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
it("should return 401 when jti is missing in token", async () => {
|
|
177
|
+
const payloadWithoutJti = { ...validUserPayload };
|
|
178
|
+
delete payloadWithoutJti.jti;
|
|
179
|
+
jsonwebtoken_1.default.verify.mockReturnValue(payloadWithoutJti);
|
|
180
|
+
const request = createMockRequest(`Bearer ${mockToken}`);
|
|
181
|
+
const reply = createMockReply();
|
|
182
|
+
await authMiddleware.authenticate(request, reply);
|
|
183
|
+
expect(reply.status).toHaveBeenCalledWith(401);
|
|
184
|
+
expect(reply.send).toHaveBeenCalledWith({
|
|
185
|
+
error: "Authentication Error",
|
|
186
|
+
message: "Token is not in allow list",
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
it("should return 500 when Redis check fails", async () => {
|
|
190
|
+
mockRedisClient.get.mockRejectedValue(new Error("Redis connection error"));
|
|
191
|
+
const request = createMockRequest(`Bearer ${mockToken}`);
|
|
192
|
+
const reply = createMockReply();
|
|
193
|
+
await authMiddleware.authenticate(request, reply);
|
|
194
|
+
expect(reply.status).toHaveBeenCalledWith(500);
|
|
195
|
+
expect(reply.send).toHaveBeenCalledWith({
|
|
196
|
+
error: "Internal Server Error",
|
|
197
|
+
message: "Error while checking token validity",
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
});
|
|
201
|
+
describe("optionalAuthenticate", () => {
|
|
202
|
+
it("should authenticate successfully with valid token", async () => {
|
|
203
|
+
const request = createMockRequest(`Bearer ${mockToken}`);
|
|
204
|
+
const reply = createMockReply();
|
|
205
|
+
await authMiddleware.optionalAuthenticate(request, reply);
|
|
206
|
+
expect(request.isAuthenticated).toBe(true);
|
|
207
|
+
expect(request.user).toEqual({
|
|
208
|
+
id: validUserPayload.id,
|
|
209
|
+
name: validUserPayload.name,
|
|
210
|
+
email: validUserPayload.email,
|
|
211
|
+
type: validUserPayload.type,
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
it("should not return error when no authorization header is provided", async () => {
|
|
215
|
+
const request = createMockRequest();
|
|
216
|
+
const reply = createMockReply();
|
|
217
|
+
const result = await authMiddleware.optionalAuthenticate(request, reply);
|
|
218
|
+
expect(result).toBeUndefined();
|
|
219
|
+
expect(request.isAuthenticated).toBeUndefined();
|
|
220
|
+
});
|
|
221
|
+
it("should not return error when JWT verification fails", async () => {
|
|
222
|
+
jsonwebtoken_1.default.verify.mockImplementation(() => {
|
|
223
|
+
throw new Error("Invalid token");
|
|
224
|
+
});
|
|
225
|
+
const request = createMockRequest(`Bearer ${mockToken}`);
|
|
226
|
+
const reply = createMockReply();
|
|
227
|
+
const result = await authMiddleware.optionalAuthenticate(request, reply);
|
|
228
|
+
expect(result).toBeUndefined();
|
|
229
|
+
});
|
|
230
|
+
it("should not return error when payload is incomplete", async () => {
|
|
231
|
+
const incompletePayload = {
|
|
232
|
+
id: "user-123",
|
|
233
|
+
name: "Test User",
|
|
234
|
+
};
|
|
235
|
+
jsonwebtoken_1.default.verify.mockReturnValue(incompletePayload);
|
|
236
|
+
const request = createMockRequest(`Bearer ${mockToken}`);
|
|
237
|
+
const reply = createMockReply();
|
|
238
|
+
const result = await authMiddleware.optionalAuthenticate(request, reply);
|
|
239
|
+
expect(result).toBeUndefined();
|
|
240
|
+
});
|
|
241
|
+
it("should not return error when token is not in allowlist", async () => {
|
|
242
|
+
mockRedisClient.get.mockResolvedValue("different-jti");
|
|
243
|
+
const request = createMockRequest(`Bearer ${mockToken}`);
|
|
244
|
+
const reply = createMockReply();
|
|
245
|
+
const result = await authMiddleware.optionalAuthenticate(request, reply);
|
|
246
|
+
expect(result).toBeUndefined();
|
|
247
|
+
});
|
|
248
|
+
it("should still return 500 on Redis error (server errors are not optional)", async () => {
|
|
249
|
+
mockRedisClient.get.mockRejectedValue(new Error("Redis connection error"));
|
|
250
|
+
const request = createMockRequest(`Bearer ${mockToken}`);
|
|
251
|
+
const reply = createMockReply();
|
|
252
|
+
await authMiddleware.optionalAuthenticate(request, reply);
|
|
253
|
+
expect(reply.status).toHaveBeenCalledWith(500);
|
|
254
|
+
expect(reply.send).toHaveBeenCalledWith({
|
|
255
|
+
error: "Internal Server Error",
|
|
256
|
+
message: "Error while checking token validity",
|
|
257
|
+
});
|
|
258
|
+
});
|
|
259
|
+
});
|
|
260
|
+
describe("authenticateAdmin", () => {
|
|
261
|
+
beforeEach(() => {
|
|
262
|
+
jsonwebtoken_1.default.verify.mockReturnValue(validAdminPayload);
|
|
263
|
+
});
|
|
264
|
+
it("should authenticate admin successfully with valid token", async () => {
|
|
265
|
+
const request = createMockRequest(`Bearer ${mockToken}`);
|
|
266
|
+
const reply = createMockReply();
|
|
267
|
+
await authMiddleware.authenticateAdmin(request, reply);
|
|
268
|
+
expect(request.isAuthenticatedAdmin).toBe(true);
|
|
269
|
+
expect(request.admin).toEqual({
|
|
270
|
+
id: validAdminPayload.id,
|
|
271
|
+
name: validAdminPayload.name,
|
|
272
|
+
email: validAdminPayload.email,
|
|
273
|
+
});
|
|
274
|
+
expect(request.adminPayload).toEqual(validAdminPayload);
|
|
275
|
+
});
|
|
276
|
+
it("should return 401 when no authorization header is provided", async () => {
|
|
277
|
+
const request = createMockRequest();
|
|
278
|
+
const reply = createMockReply();
|
|
279
|
+
await authMiddleware.authenticateAdmin(request, reply);
|
|
280
|
+
expect(reply.status).toHaveBeenCalledWith(401);
|
|
281
|
+
expect(reply.send).toHaveBeenCalledWith({
|
|
282
|
+
error: "Authentication Error",
|
|
283
|
+
message: "Authorization token is required",
|
|
284
|
+
});
|
|
285
|
+
});
|
|
286
|
+
it("should return 401 when authorization header does not start with Bearer", async () => {
|
|
287
|
+
const request = createMockRequest("Basic sometoken");
|
|
288
|
+
const reply = createMockReply();
|
|
289
|
+
await authMiddleware.authenticateAdmin(request, reply);
|
|
290
|
+
expect(reply.status).toHaveBeenCalledWith(401);
|
|
291
|
+
expect(reply.send).toHaveBeenCalledWith({
|
|
292
|
+
error: "Authentication Error",
|
|
293
|
+
message: "Authorization token is required",
|
|
294
|
+
});
|
|
295
|
+
});
|
|
296
|
+
it("should return 401 when JWT verification fails", async () => {
|
|
297
|
+
jsonwebtoken_1.default.verify.mockImplementation(() => {
|
|
298
|
+
throw new Error("Invalid token");
|
|
299
|
+
});
|
|
300
|
+
const request = createMockRequest(`Bearer ${mockToken}`);
|
|
301
|
+
const reply = createMockReply();
|
|
302
|
+
await authMiddleware.authenticateAdmin(request, reply);
|
|
303
|
+
expect(reply.status).toHaveBeenCalledWith(401);
|
|
304
|
+
expect(reply.send).toHaveBeenCalledWith({
|
|
305
|
+
error: "Authentication Error",
|
|
306
|
+
message: "Invalid or expired token",
|
|
307
|
+
});
|
|
308
|
+
});
|
|
309
|
+
it("should return 401 when admin payload is incomplete", async () => {
|
|
310
|
+
const incompletePayload = {
|
|
311
|
+
id: "admin-123",
|
|
312
|
+
name: "Test Admin",
|
|
313
|
+
// missing email and isPlatformAdmin
|
|
314
|
+
};
|
|
315
|
+
jsonwebtoken_1.default.verify.mockReturnValue(incompletePayload);
|
|
316
|
+
const request = createMockRequest(`Bearer ${mockToken}`);
|
|
317
|
+
const reply = createMockReply();
|
|
318
|
+
await authMiddleware.authenticateAdmin(request, reply);
|
|
319
|
+
expect(reply.status).toHaveBeenCalledWith(401);
|
|
320
|
+
expect(reply.send).toHaveBeenCalledWith({
|
|
321
|
+
error: "Authentication Error",
|
|
322
|
+
message: "Invalid token payload",
|
|
323
|
+
});
|
|
324
|
+
});
|
|
325
|
+
it("should return 401 when isPlatformAdmin is false", async () => {
|
|
326
|
+
const nonAdminPayload = {
|
|
327
|
+
...validAdminPayload,
|
|
328
|
+
isPlatformAdmin: false,
|
|
329
|
+
};
|
|
330
|
+
jsonwebtoken_1.default.verify.mockReturnValue(nonAdminPayload);
|
|
331
|
+
const request = createMockRequest(`Bearer ${mockToken}`);
|
|
332
|
+
const reply = createMockReply();
|
|
333
|
+
await authMiddleware.authenticateAdmin(request, reply);
|
|
334
|
+
expect(reply.status).toHaveBeenCalledWith(401);
|
|
335
|
+
expect(reply.send).toHaveBeenCalledWith({
|
|
336
|
+
error: "Authentication Error",
|
|
337
|
+
message: "Invalid token payload",
|
|
338
|
+
});
|
|
339
|
+
});
|
|
340
|
+
});
|
|
341
|
+
describe("optionalAuthenticateAdmin", () => {
|
|
342
|
+
beforeEach(() => {
|
|
343
|
+
jsonwebtoken_1.default.verify.mockReturnValue(validAdminPayload);
|
|
344
|
+
});
|
|
345
|
+
it("should authenticate admin successfully with valid token", async () => {
|
|
346
|
+
const request = createMockRequest(`Bearer ${mockToken}`);
|
|
347
|
+
const reply = createMockReply();
|
|
348
|
+
await authMiddleware.optionalAuthenticateAdmin(request, reply);
|
|
349
|
+
expect(request.isAuthenticatedAdmin).toBe(true);
|
|
350
|
+
expect(request.admin).toEqual({
|
|
351
|
+
id: validAdminPayload.id,
|
|
352
|
+
name: validAdminPayload.name,
|
|
353
|
+
email: validAdminPayload.email,
|
|
354
|
+
});
|
|
355
|
+
expect(request.adminPayload).toEqual(validAdminPayload);
|
|
356
|
+
});
|
|
357
|
+
it("should not return error when no authorization header is provided", async () => {
|
|
358
|
+
const request = createMockRequest();
|
|
359
|
+
const reply = createMockReply();
|
|
360
|
+
const result = await authMiddleware.optionalAuthenticateAdmin(request, reply);
|
|
361
|
+
expect(result).toBeUndefined();
|
|
362
|
+
expect(request.isAuthenticatedAdmin).toBeUndefined();
|
|
363
|
+
});
|
|
364
|
+
it("should not return error when JWT verification fails", async () => {
|
|
365
|
+
jsonwebtoken_1.default.verify.mockImplementation(() => {
|
|
366
|
+
throw new Error("Invalid token");
|
|
367
|
+
});
|
|
368
|
+
const request = createMockRequest(`Bearer ${mockToken}`);
|
|
369
|
+
const reply = createMockReply();
|
|
370
|
+
const result = await authMiddleware.optionalAuthenticateAdmin(request, reply);
|
|
371
|
+
expect(result).toBeUndefined();
|
|
372
|
+
expect(request.isAuthenticatedAdmin).toBeUndefined();
|
|
373
|
+
});
|
|
374
|
+
it("should not return error when admin payload is incomplete", async () => {
|
|
375
|
+
const incompletePayload = {
|
|
376
|
+
id: "admin-123",
|
|
377
|
+
name: "Test Admin",
|
|
378
|
+
// missing email and isPlatformAdmin
|
|
379
|
+
};
|
|
380
|
+
jsonwebtoken_1.default.verify.mockReturnValue(incompletePayload);
|
|
381
|
+
const request = createMockRequest(`Bearer ${mockToken}`);
|
|
382
|
+
const reply = createMockReply();
|
|
383
|
+
const result = await authMiddleware.optionalAuthenticateAdmin(request, reply);
|
|
384
|
+
expect(result).toBeUndefined();
|
|
385
|
+
expect(request.isAuthenticatedAdmin).toBeUndefined();
|
|
386
|
+
});
|
|
387
|
+
it("should not return error when isPlatformAdmin is false", async () => {
|
|
388
|
+
const nonAdminPayload = {
|
|
389
|
+
...validAdminPayload,
|
|
390
|
+
isPlatformAdmin: false,
|
|
391
|
+
};
|
|
392
|
+
jsonwebtoken_1.default.verify.mockReturnValue(nonAdminPayload);
|
|
393
|
+
const request = createMockRequest(`Bearer ${mockToken}`);
|
|
394
|
+
const reply = createMockReply();
|
|
395
|
+
const result = await authMiddleware.optionalAuthenticateAdmin(request, reply);
|
|
396
|
+
expect(result).toBeUndefined();
|
|
397
|
+
expect(request.isAuthenticatedAdmin).toBeUndefined();
|
|
398
|
+
});
|
|
399
|
+
});
|
|
400
|
+
describe("permissionGuard", () => {
|
|
401
|
+
beforeEach(() => {
|
|
402
|
+
jsonwebtoken_1.default.verify.mockReturnValue(validUserPayload);
|
|
403
|
+
mockRedisClient.get.mockResolvedValue(validUserPayload.jti);
|
|
404
|
+
});
|
|
405
|
+
const createAuthenticatedRequest = () => {
|
|
406
|
+
const request = createMockRequest(`Bearer ${mockToken}`);
|
|
407
|
+
request.user = {
|
|
408
|
+
id: validUserPayload.id,
|
|
409
|
+
name: validUserPayload.name,
|
|
410
|
+
email: validUserPayload.email,
|
|
411
|
+
type: validUserPayload.type,
|
|
412
|
+
};
|
|
413
|
+
request.payload = validUserPayload;
|
|
414
|
+
return request;
|
|
415
|
+
};
|
|
416
|
+
it("should return 401 when user is not authenticated", async () => {
|
|
417
|
+
const request = createMockRequest(`Bearer ${mockToken}`);
|
|
418
|
+
const reply = createMockReply();
|
|
419
|
+
const guard = authMiddleware.permissionGuard({ access: "canRead", subProgram: "test-program" });
|
|
420
|
+
await guard(request, reply);
|
|
421
|
+
expect(reply.status).toHaveBeenCalledWith(401);
|
|
422
|
+
expect(reply.send).toHaveBeenCalledWith({
|
|
423
|
+
error: "Authentication Error",
|
|
424
|
+
message: "User not authenticated",
|
|
425
|
+
});
|
|
426
|
+
});
|
|
427
|
+
it("should pass when user is administrator", async () => {
|
|
428
|
+
const request = createAuthenticatedRequest();
|
|
429
|
+
request.payload = { ...validUserPayload, type: "administrator" };
|
|
430
|
+
const reply = createMockReply();
|
|
431
|
+
const guard = authMiddleware.permissionGuard({ access: "canRead", subProgram: "test-program" });
|
|
432
|
+
await guard(request, reply);
|
|
433
|
+
expect(reply.status).not.toHaveBeenCalled();
|
|
434
|
+
});
|
|
435
|
+
it("should pass when user has canRead permission", async () => {
|
|
436
|
+
const request = createAuthenticatedRequest();
|
|
437
|
+
const reply = createMockReply();
|
|
438
|
+
mockRedisClient.get.mockImplementation((key) => {
|
|
439
|
+
if (key.startsWith("auth:permission:")) {
|
|
440
|
+
return Promise.resolve(JSON.stringify({ canRead: true }));
|
|
441
|
+
}
|
|
442
|
+
return Promise.resolve(validUserPayload.jti);
|
|
443
|
+
});
|
|
444
|
+
const guard = authMiddleware.permissionGuard({ access: "canRead", subProgram: "test-program" });
|
|
445
|
+
await guard(request, reply);
|
|
446
|
+
expect(reply.status).not.toHaveBeenCalledWith(403);
|
|
447
|
+
});
|
|
448
|
+
it("should return 403 when user does not have canRead permission", async () => {
|
|
449
|
+
const request = createAuthenticatedRequest();
|
|
450
|
+
const reply = createMockReply();
|
|
451
|
+
mockRedisClient.get.mockImplementation((key) => {
|
|
452
|
+
if (key.startsWith("auth:permission:")) {
|
|
453
|
+
return Promise.resolve(JSON.stringify({ canRead: false }));
|
|
454
|
+
}
|
|
455
|
+
return Promise.resolve(validUserPayload.jti);
|
|
456
|
+
});
|
|
457
|
+
const guard = authMiddleware.permissionGuard({ access: "canRead", subProgram: "test-program" });
|
|
458
|
+
await guard(request, reply);
|
|
459
|
+
expect(reply.status).toHaveBeenCalledWith(403);
|
|
460
|
+
expect(reply.send).toHaveBeenCalledWith({
|
|
461
|
+
error: "PermissionDenied",
|
|
462
|
+
message: "Permission denied: cannot read",
|
|
463
|
+
});
|
|
464
|
+
});
|
|
465
|
+
it("should pass when user has canCreate permission", async () => {
|
|
466
|
+
const request = createAuthenticatedRequest();
|
|
467
|
+
const reply = createMockReply();
|
|
468
|
+
mockRedisClient.get.mockImplementation((key) => {
|
|
469
|
+
if (key.startsWith("auth:permission:")) {
|
|
470
|
+
return Promise.resolve(JSON.stringify({ canCreate: true }));
|
|
471
|
+
}
|
|
472
|
+
return Promise.resolve(validUserPayload.jti);
|
|
473
|
+
});
|
|
474
|
+
const guard = authMiddleware.permissionGuard({ access: "canCreate", subProgram: "test-program" });
|
|
475
|
+
await guard(request, reply);
|
|
476
|
+
expect(reply.status).not.toHaveBeenCalledWith(403);
|
|
477
|
+
});
|
|
478
|
+
it("should return 403 when user does not have canCreate permission", async () => {
|
|
479
|
+
const request = createAuthenticatedRequest();
|
|
480
|
+
const reply = createMockReply();
|
|
481
|
+
mockRedisClient.get.mockImplementation((key) => {
|
|
482
|
+
if (key.startsWith("auth:permission:")) {
|
|
483
|
+
return Promise.resolve(JSON.stringify({ canCreate: false }));
|
|
484
|
+
}
|
|
485
|
+
return Promise.resolve(validUserPayload.jti);
|
|
486
|
+
});
|
|
487
|
+
const guard = authMiddleware.permissionGuard({ access: "canCreate", subProgram: "test-program" });
|
|
488
|
+
await guard(request, reply);
|
|
489
|
+
expect(reply.status).toHaveBeenCalledWith(403);
|
|
490
|
+
expect(reply.send).toHaveBeenCalledWith({
|
|
491
|
+
error: "PermissionDenied",
|
|
492
|
+
message: "Permission denied: cannot create",
|
|
493
|
+
});
|
|
494
|
+
});
|
|
495
|
+
it("should pass when user has canEdit permission", async () => {
|
|
496
|
+
const request = createAuthenticatedRequest();
|
|
497
|
+
const reply = createMockReply();
|
|
498
|
+
mockRedisClient.get.mockImplementation((key) => {
|
|
499
|
+
if (key.startsWith("auth:permission:")) {
|
|
500
|
+
return Promise.resolve(JSON.stringify({ canEdit: true }));
|
|
501
|
+
}
|
|
502
|
+
return Promise.resolve(validUserPayload.jti);
|
|
503
|
+
});
|
|
504
|
+
const guard = authMiddleware.permissionGuard({ access: "canEdit", subProgram: "test-program" });
|
|
505
|
+
await guard(request, reply);
|
|
506
|
+
expect(reply.status).not.toHaveBeenCalledWith(403);
|
|
507
|
+
});
|
|
508
|
+
it("should return 403 when user does not have canEdit permission", async () => {
|
|
509
|
+
const request = createAuthenticatedRequest();
|
|
510
|
+
const reply = createMockReply();
|
|
511
|
+
mockRedisClient.get.mockImplementation((key) => {
|
|
512
|
+
if (key.startsWith("auth:permission:")) {
|
|
513
|
+
return Promise.resolve(JSON.stringify({ canEdit: false }));
|
|
514
|
+
}
|
|
515
|
+
return Promise.resolve(validUserPayload.jti);
|
|
516
|
+
});
|
|
517
|
+
const guard = authMiddleware.permissionGuard({ access: "canEdit", subProgram: "test-program" });
|
|
518
|
+
await guard(request, reply);
|
|
519
|
+
expect(reply.status).toHaveBeenCalledWith(403);
|
|
520
|
+
expect(reply.send).toHaveBeenCalledWith({
|
|
521
|
+
error: "PermissionDenied",
|
|
522
|
+
message: "Permission denied: cannot edit",
|
|
523
|
+
});
|
|
524
|
+
});
|
|
525
|
+
it("should pass when user has canDelete permission", async () => {
|
|
526
|
+
const request = createAuthenticatedRequest();
|
|
527
|
+
const reply = createMockReply();
|
|
528
|
+
mockRedisClient.get.mockImplementation((key) => {
|
|
529
|
+
if (key.startsWith("auth:permission:")) {
|
|
530
|
+
return Promise.resolve(JSON.stringify({ canDelete: true }));
|
|
531
|
+
}
|
|
532
|
+
return Promise.resolve(validUserPayload.jti);
|
|
533
|
+
});
|
|
534
|
+
const guard = authMiddleware.permissionGuard({ access: "canDelete", subProgram: "test-program" });
|
|
535
|
+
await guard(request, reply);
|
|
536
|
+
expect(reply.status).not.toHaveBeenCalledWith(403);
|
|
537
|
+
});
|
|
538
|
+
it("should return 403 when user does not have canDelete permission", async () => {
|
|
539
|
+
const request = createAuthenticatedRequest();
|
|
540
|
+
const reply = createMockReply();
|
|
541
|
+
mockRedisClient.get.mockImplementation((key) => {
|
|
542
|
+
if (key.startsWith("auth:permission:")) {
|
|
543
|
+
return Promise.resolve(JSON.stringify({ canDelete: false }));
|
|
544
|
+
}
|
|
545
|
+
return Promise.resolve(validUserPayload.jti);
|
|
546
|
+
});
|
|
547
|
+
const guard = authMiddleware.permissionGuard({ access: "canDelete", subProgram: "test-program" });
|
|
548
|
+
await guard(request, reply);
|
|
549
|
+
expect(reply.status).toHaveBeenCalledWith(403);
|
|
550
|
+
expect(reply.send).toHaveBeenCalledWith({
|
|
551
|
+
error: "PermissionDenied",
|
|
552
|
+
message: "Permission denied: cannot delete",
|
|
553
|
+
});
|
|
554
|
+
});
|
|
555
|
+
it("should pass when user has canNotify permission", async () => {
|
|
556
|
+
const request = createAuthenticatedRequest();
|
|
557
|
+
const reply = createMockReply();
|
|
558
|
+
mockRedisClient.get.mockImplementation((key) => {
|
|
559
|
+
if (key.startsWith("auth:permission:")) {
|
|
560
|
+
return Promise.resolve(JSON.stringify({ canNotify: true }));
|
|
561
|
+
}
|
|
562
|
+
return Promise.resolve(validUserPayload.jti);
|
|
563
|
+
});
|
|
564
|
+
const guard = authMiddleware.permissionGuard({ access: "canNotify", subProgram: "test-program" });
|
|
565
|
+
await guard(request, reply);
|
|
566
|
+
expect(reply.status).not.toHaveBeenCalledWith(403);
|
|
567
|
+
});
|
|
568
|
+
it("should return 403 when user does not have canNotify permission", async () => {
|
|
569
|
+
const request = createAuthenticatedRequest();
|
|
570
|
+
const reply = createMockReply();
|
|
571
|
+
mockRedisClient.get.mockImplementation((key) => {
|
|
572
|
+
if (key.startsWith("auth:permission:")) {
|
|
573
|
+
return Promise.resolve(JSON.stringify({ canNotify: false }));
|
|
574
|
+
}
|
|
575
|
+
return Promise.resolve(validUserPayload.jti);
|
|
576
|
+
});
|
|
577
|
+
const guard = authMiddleware.permissionGuard({ access: "canNotify", subProgram: "test-program" });
|
|
578
|
+
await guard(request, reply);
|
|
579
|
+
expect(reply.status).toHaveBeenCalledWith(403);
|
|
580
|
+
expect(reply.send).toHaveBeenCalledWith({
|
|
581
|
+
error: "PermissionDenied",
|
|
582
|
+
message: "Permission denied: cannot notify",
|
|
583
|
+
});
|
|
584
|
+
});
|
|
585
|
+
it("should pass when user has canCreateDraft permission", async () => {
|
|
586
|
+
const request = createAuthenticatedRequest();
|
|
587
|
+
const reply = createMockReply();
|
|
588
|
+
mockRedisClient.get.mockImplementation((key) => {
|
|
589
|
+
if (key.startsWith("auth:permission:")) {
|
|
590
|
+
return Promise.resolve(JSON.stringify({ canCreateDraft: true }));
|
|
591
|
+
}
|
|
592
|
+
return Promise.resolve(validUserPayload.jti);
|
|
593
|
+
});
|
|
594
|
+
const guard = authMiddleware.permissionGuard({ access: "canCreateDraft", subProgram: "test-program" });
|
|
595
|
+
await guard(request, reply);
|
|
596
|
+
expect(reply.status).not.toHaveBeenCalledWith(403);
|
|
597
|
+
});
|
|
598
|
+
it("should return 403 when user does not have canCreateDraft permission", async () => {
|
|
599
|
+
const request = createAuthenticatedRequest();
|
|
600
|
+
const reply = createMockReply();
|
|
601
|
+
mockRedisClient.get.mockImplementation((key) => {
|
|
602
|
+
if (key.startsWith("auth:permission:")) {
|
|
603
|
+
return Promise.resolve(JSON.stringify({ canCreateDraft: false }));
|
|
604
|
+
}
|
|
605
|
+
return Promise.resolve(validUserPayload.jti);
|
|
606
|
+
});
|
|
607
|
+
const guard = authMiddleware.permissionGuard({ access: "canCreateDraft", subProgram: "test-program" });
|
|
608
|
+
await guard(request, reply);
|
|
609
|
+
expect(reply.status).toHaveBeenCalledWith(403);
|
|
610
|
+
expect(reply.send).toHaveBeenCalledWith({
|
|
611
|
+
error: "PermissionDenied",
|
|
612
|
+
message: "Permission denied: cannot create draft",
|
|
613
|
+
});
|
|
614
|
+
});
|
|
615
|
+
it("should return 403 when permission not found in Redis", async () => {
|
|
616
|
+
const request = createAuthenticatedRequest();
|
|
617
|
+
const reply = createMockReply();
|
|
618
|
+
mockRedisClient.get.mockImplementation((key) => {
|
|
619
|
+
if (key.startsWith("auth:permission:")) {
|
|
620
|
+
return Promise.resolve(null);
|
|
621
|
+
}
|
|
622
|
+
return Promise.resolve(validUserPayload.jti);
|
|
623
|
+
});
|
|
624
|
+
const guard = authMiddleware.permissionGuard({ access: "canRead", subProgram: "test-program" });
|
|
625
|
+
await guard(request, reply);
|
|
626
|
+
expect(reply.status).toHaveBeenCalledWith(403);
|
|
627
|
+
expect(reply.send).toHaveBeenCalledWith({
|
|
628
|
+
error: "PermissionNotFound",
|
|
629
|
+
message: "Permission not found for subprogram test-program",
|
|
630
|
+
});
|
|
631
|
+
});
|
|
632
|
+
it("should return 500 when Redis error occurs", async () => {
|
|
633
|
+
const request = createAuthenticatedRequest();
|
|
634
|
+
const reply = createMockReply();
|
|
635
|
+
mockRedisClient.get.mockImplementation((key) => {
|
|
636
|
+
if (key.startsWith("auth:permission:")) {
|
|
637
|
+
return Promise.reject(new Error("Redis error"));
|
|
638
|
+
}
|
|
639
|
+
return Promise.resolve(validUserPayload.jti);
|
|
640
|
+
});
|
|
641
|
+
const guard = authMiddleware.permissionGuard({ access: "canRead", subProgram: "test-program" });
|
|
642
|
+
await guard(request, reply);
|
|
643
|
+
expect(reply.status).toHaveBeenCalledWith(500);
|
|
644
|
+
expect(reply.send).toHaveBeenCalledWith({
|
|
645
|
+
error: "Internal Server Error",
|
|
646
|
+
message: "Redis error while checking permissions",
|
|
647
|
+
});
|
|
648
|
+
});
|
|
649
|
+
it("should handle array of permission checks", async () => {
|
|
650
|
+
const request = createAuthenticatedRequest();
|
|
651
|
+
const reply = createMockReply();
|
|
652
|
+
mockRedisClient.get.mockImplementation((key) => {
|
|
653
|
+
if (key.startsWith("auth:permission:")) {
|
|
654
|
+
return Promise.resolve(JSON.stringify({ canRead: true, canCreate: true }));
|
|
655
|
+
}
|
|
656
|
+
return Promise.resolve(validUserPayload.jti);
|
|
657
|
+
});
|
|
658
|
+
const guard = authMiddleware.permissionGuard([
|
|
659
|
+
{ access: "canRead", subProgram: "program1" },
|
|
660
|
+
{ access: "canCreate", subProgram: "program2" },
|
|
661
|
+
]);
|
|
662
|
+
await guard(request, reply);
|
|
663
|
+
expect(reply.status).not.toHaveBeenCalledWith(403);
|
|
664
|
+
});
|
|
665
|
+
it("should strip spaces from subProgram name in cache key", async () => {
|
|
666
|
+
const request = createAuthenticatedRequest();
|
|
667
|
+
const reply = createMockReply();
|
|
668
|
+
mockRedisClient.get.mockImplementation((key) => {
|
|
669
|
+
// Verify the key has spaces removed
|
|
670
|
+
if (key === `auth:permission:testprogram:${validUserPayload.id}`) {
|
|
671
|
+
return Promise.resolve(JSON.stringify({ canRead: true }));
|
|
672
|
+
}
|
|
673
|
+
if (key.startsWith("auth:session:")) {
|
|
674
|
+
return Promise.resolve(validUserPayload.jti);
|
|
675
|
+
}
|
|
676
|
+
return Promise.resolve(null);
|
|
677
|
+
});
|
|
678
|
+
const guard = authMiddleware.permissionGuard({ access: "canRead", subProgram: "test program" });
|
|
679
|
+
await guard(request, reply);
|
|
680
|
+
expect(mockRedisClient.get).toHaveBeenCalledWith(`auth:permission:testprogram:${validUserPayload.id}`);
|
|
681
|
+
});
|
|
682
|
+
});
|
|
683
|
+
describe("constructor", () => {
|
|
684
|
+
it("should use provided jwtSecret", () => {
|
|
685
|
+
const customSecret = "custom-secret";
|
|
686
|
+
const middleware = new AuthMiddleware_1.AuthMiddleware({
|
|
687
|
+
jwtSecret: customSecret,
|
|
688
|
+
redisClient: mockRedisClient,
|
|
689
|
+
});
|
|
690
|
+
expect(middleware).toBeDefined();
|
|
691
|
+
});
|
|
692
|
+
it("should use environment variable for jwtSecret when not provided", () => {
|
|
693
|
+
const originalEnv = process.env.IAM_JWT_SECRET;
|
|
694
|
+
process.env.IAM_JWT_SECRET = "env-secret";
|
|
695
|
+
const middleware = new AuthMiddleware_1.AuthMiddleware({
|
|
696
|
+
redisClient: mockRedisClient,
|
|
697
|
+
});
|
|
698
|
+
expect(middleware).toBeDefined();
|
|
699
|
+
process.env.IAM_JWT_SECRET = originalEnv;
|
|
700
|
+
});
|
|
701
|
+
it("should use default secret when neither option nor env is provided", () => {
|
|
702
|
+
const originalEnv = process.env.IAM_JWT_SECRET;
|
|
703
|
+
delete process.env.IAM_JWT_SECRET;
|
|
704
|
+
const middleware = new AuthMiddleware_1.AuthMiddleware({
|
|
705
|
+
redisClient: mockRedisClient,
|
|
706
|
+
});
|
|
707
|
+
expect(middleware).toBeDefined();
|
|
708
|
+
process.env.IAM_JWT_SECRET = originalEnv;
|
|
709
|
+
});
|
|
710
|
+
});
|
|
711
|
+
});
|
|
712
|
+
//# sourceMappingURL=AuthMiddleware.test.js.map
|