@friggframework/core 2.0.0--canary.461.651659d.0 → 2.0.0--canary.461.0b53aff.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,368 @@
1
+ const { AuthenticateUser } = require('../../user/use-cases/authenticate-user');
2
+ const { GetUserFromBearerToken } = require('../../user/use-cases/get-user-from-bearer-token');
3
+ const { GetUserFromXFriggHeaders } = require('../../user/use-cases/get-user-from-x-frigg-headers');
4
+ const { GetUserFromAdopterJwt } = require('../../user/use-cases/get-user-from-adopter-jwt');
5
+ const { User } = require('../../user/user');
6
+ const Boom = require('@hapi/boom');
7
+
8
+ describe('AuthenticateUser - Multi-Mode Authentication', () => {
9
+ let authenticateUser;
10
+ let mockGetUserFromBearerToken;
11
+ let mockGetUserFromXFriggHeaders;
12
+ let mockGetUserFromAdopterJwt;
13
+ let mockUserConfig;
14
+ let mockUser;
15
+
16
+ beforeEach(() => {
17
+ mockUser = new User(
18
+ { id: 'user-123', username: 'testuser' },
19
+ null,
20
+ false,
21
+ 'individual',
22
+ true,
23
+ false
24
+ );
25
+
26
+ mockGetUserFromBearerToken = {
27
+ execute: jest.fn().mockResolvedValue(mockUser),
28
+ };
29
+
30
+ mockGetUserFromXFriggHeaders = {
31
+ execute: jest.fn().mockResolvedValue(mockUser),
32
+ };
33
+
34
+ mockGetUserFromAdopterJwt = {
35
+ execute: jest.fn().mockResolvedValue(mockUser),
36
+ };
37
+
38
+ mockUserConfig = {
39
+ authModes: {
40
+ friggToken: true,
41
+ xFriggHeaders: true,
42
+ adopterJwt: false,
43
+ },
44
+ };
45
+
46
+ authenticateUser = new AuthenticateUser({
47
+ getUserFromBearerToken: mockGetUserFromBearerToken,
48
+ getUserFromXFriggHeaders: mockGetUserFromXFriggHeaders,
49
+ getUserFromAdopterJwt: mockGetUserFromAdopterJwt,
50
+ userConfig: mockUserConfig,
51
+ });
52
+ });
53
+
54
+ describe('Priority 1: X-Frigg Headers (Backend-to-Backend)', () => {
55
+ it('should authenticate with x-frigg-appUserId header', async () => {
56
+ const mockReq = {
57
+ headers: {
58
+ 'x-frigg-appuserid': 'app-user-123',
59
+ },
60
+ };
61
+
62
+ const result = await authenticateUser.execute(mockReq);
63
+
64
+ expect(result).toBe(mockUser);
65
+ expect(mockGetUserFromXFriggHeaders.execute).toHaveBeenCalledWith(
66
+ 'app-user-123',
67
+ undefined
68
+ );
69
+ expect(mockGetUserFromBearerToken.execute).not.toHaveBeenCalled();
70
+ });
71
+
72
+ it('should authenticate with x-frigg-appOrgId header', async () => {
73
+ const mockReq = {
74
+ headers: {
75
+ 'x-frigg-apporgid': 'app-org-456',
76
+ },
77
+ };
78
+
79
+ const result = await authenticateUser.execute(mockReq);
80
+
81
+ expect(result).toBe(mockUser);
82
+ expect(mockGetUserFromXFriggHeaders.execute).toHaveBeenCalledWith(
83
+ undefined,
84
+ 'app-org-456'
85
+ );
86
+ expect(mockGetUserFromBearerToken.execute).not.toHaveBeenCalled();
87
+ });
88
+
89
+ it('should authenticate with both x-frigg headers when they match', async () => {
90
+ const mockReq = {
91
+ headers: {
92
+ 'x-frigg-appuserid': 'app-user-123',
93
+ 'x-frigg-apporgid': 'app-org-456',
94
+ },
95
+ };
96
+
97
+ const result = await authenticateUser.execute(mockReq);
98
+
99
+ expect(result).toBe(mockUser);
100
+ expect(mockGetUserFromXFriggHeaders.execute).toHaveBeenCalledWith(
101
+ 'app-user-123',
102
+ 'app-org-456'
103
+ );
104
+ });
105
+
106
+ it('should reject conflicting x-frigg headers (delegated to use case)', async () => {
107
+ const mockReq = {
108
+ headers: {
109
+ 'x-frigg-appuserid': 'app-user-123',
110
+ 'x-frigg-apporgid': 'app-org-999',
111
+ },
112
+ };
113
+
114
+ const conflictError = Boom.badRequest('User ID mismatch');
115
+ mockGetUserFromXFriggHeaders.execute.mockRejectedValue(conflictError);
116
+
117
+ await expect(authenticateUser.execute(mockReq)).rejects.toThrow(
118
+ conflictError
119
+ );
120
+ });
121
+
122
+ it('should skip x-frigg headers when authModes.xFriggHeaders is false', async () => {
123
+ mockUserConfig.authModes.xFriggHeaders = false;
124
+
125
+ const mockReq = {
126
+ headers: {
127
+ 'x-frigg-appuserid': 'app-user-123',
128
+ authorization: 'Bearer frigg-token-xyz',
129
+ },
130
+ };
131
+
132
+ await authenticateUser.execute(mockReq);
133
+
134
+ expect(mockGetUserFromXFriggHeaders.execute).not.toHaveBeenCalled();
135
+ expect(mockGetUserFromBearerToken.execute).toHaveBeenCalledWith(
136
+ 'Bearer frigg-token-xyz'
137
+ );
138
+ });
139
+ });
140
+
141
+ describe('Priority 2: Adopter JWT', () => {
142
+ beforeEach(() => {
143
+ mockUserConfig.authModes.adopterJwt = true;
144
+ });
145
+
146
+ it('should try JWT when enabled and Bearer token is 3-part format', async () => {
147
+ const mockReq = {
148
+ headers: {
149
+ authorization: 'Bearer eyJhbGci.eyJzdWIi.signature',
150
+ },
151
+ };
152
+
153
+ await authenticateUser.execute(mockReq);
154
+
155
+ expect(mockGetUserFromAdopterJwt.execute).toHaveBeenCalledWith(
156
+ 'eyJhbGci.eyJzdWIi.signature'
157
+ );
158
+ expect(mockGetUserFromBearerToken.execute).not.toHaveBeenCalled();
159
+ });
160
+
161
+ it('should fall back to Frigg token when Bearer token is not JWT format', async () => {
162
+ const mockReq = {
163
+ headers: {
164
+ authorization: 'Bearer simple-token',
165
+ },
166
+ };
167
+
168
+ await authenticateUser.execute(mockReq);
169
+
170
+ expect(mockGetUserFromAdopterJwt.execute).not.toHaveBeenCalled();
171
+ expect(mockGetUserFromBearerToken.execute).toHaveBeenCalledWith(
172
+ 'Bearer simple-token'
173
+ );
174
+ });
175
+
176
+ it('should not try JWT when authModes.adopterJwt is false', async () => {
177
+ mockUserConfig.authModes.adopterJwt = false;
178
+
179
+ const mockReq = {
180
+ headers: {
181
+ authorization: 'Bearer eyJhbGci.eyJzdWIi.signature',
182
+ },
183
+ };
184
+
185
+ await authenticateUser.execute(mockReq);
186
+
187
+ expect(mockGetUserFromAdopterJwt.execute).not.toHaveBeenCalled();
188
+ expect(mockGetUserFromBearerToken.execute).toHaveBeenCalledWith(
189
+ 'Bearer eyJhbGci.eyJzdWIi.signature'
190
+ );
191
+ });
192
+ });
193
+
194
+ describe('Priority 3: Frigg Native Token (Fallback)', () => {
195
+ it('should fall back to Frigg token when no x-frigg headers', async () => {
196
+ const mockReq = {
197
+ headers: {
198
+ authorization: 'Bearer frigg-token-123',
199
+ },
200
+ };
201
+
202
+ const result = await authenticateUser.execute(mockReq);
203
+
204
+ expect(result).toBe(mockUser);
205
+ expect(mockGetUserFromBearerToken.execute).toHaveBeenCalledWith(
206
+ 'Bearer frigg-token-123'
207
+ );
208
+ expect(mockGetUserFromXFriggHeaders.execute).not.toHaveBeenCalled();
209
+ });
210
+
211
+ it('should skip Frigg token when authModes.friggToken is false', async () => {
212
+ mockUserConfig.authModes.friggToken = false;
213
+
214
+ const mockReq = {
215
+ headers: {
216
+ authorization: 'Bearer frigg-token-123',
217
+ },
218
+ };
219
+
220
+ await expect(authenticateUser.execute(mockReq)).rejects.toThrow(
221
+ Boom.unauthorized().message
222
+ );
223
+
224
+ expect(mockGetUserFromBearerToken.execute).not.toHaveBeenCalled();
225
+ });
226
+ });
227
+
228
+ describe('Priority Ordering', () => {
229
+ it('should prioritize x-frigg headers over bearer token', async () => {
230
+ const mockReq = {
231
+ headers: {
232
+ 'x-frigg-appuserid': 'app-user-123',
233
+ authorization: 'Bearer frigg-token-xyz',
234
+ },
235
+ };
236
+
237
+ await authenticateUser.execute(mockReq);
238
+
239
+ expect(mockGetUserFromXFriggHeaders.execute).toHaveBeenCalledWith(
240
+ 'app-user-123',
241
+ undefined
242
+ );
243
+ expect(mockGetUserFromBearerToken.execute).not.toHaveBeenCalled();
244
+ });
245
+
246
+ it('should try JWT before Frigg token when JWT enabled', async () => {
247
+ mockUserConfig.authModes.adopterJwt = true;
248
+
249
+ const mockReq = {
250
+ headers: {
251
+ authorization: 'Bearer part1.part2.part3',
252
+ },
253
+ };
254
+
255
+ await authenticateUser.execute(mockReq);
256
+
257
+ expect(mockGetUserFromAdopterJwt.execute).toHaveBeenCalledWith(
258
+ 'part1.part2.part3'
259
+ );
260
+ expect(mockGetUserFromBearerToken.execute).not.toHaveBeenCalled();
261
+ });
262
+ });
263
+
264
+ describe('Auth Mode Configuration', () => {
265
+ it('should use default friggToken mode when authModes not configured', () => {
266
+ const authWithDefaults = new AuthenticateUser({
267
+ getUserFromBearerToken: mockGetUserFromBearerToken,
268
+ getUserFromXFriggHeaders: mockGetUserFromXFriggHeaders,
269
+ getUserFromAdopterJwt: mockGetUserFromAdopterJwt,
270
+ userConfig: {}, // No authModes
271
+ });
272
+
273
+ const mockReq = {
274
+ headers: {
275
+ authorization: 'Bearer token',
276
+ },
277
+ };
278
+
279
+ authWithDefaults.execute(mockReq);
280
+
281
+ expect(mockGetUserFromBearerToken.execute).toHaveBeenCalled();
282
+ });
283
+
284
+ it('should throw unauthorized when no valid authentication provided', async () => {
285
+ const mockReq = {
286
+ headers: {},
287
+ };
288
+
289
+ await expect(authenticateUser.execute(mockReq)).rejects.toThrow(
290
+ Boom.unauthorized().message
291
+ );
292
+
293
+ await expect(authenticateUser.execute(mockReq)).rejects.toThrow(
294
+ 'No valid authentication provided'
295
+ );
296
+ });
297
+
298
+ it('should throw unauthorized when all auth modes disabled', async () => {
299
+ mockUserConfig.authModes = {
300
+ friggToken: false,
301
+ xFriggHeaders: false,
302
+ adopterJwt: false,
303
+ };
304
+
305
+ const mockReq = {
306
+ headers: {
307
+ authorization: 'Bearer token',
308
+ },
309
+ };
310
+
311
+ await expect(authenticateUser.execute(mockReq)).rejects.toThrow(
312
+ 'No valid authentication provided'
313
+ );
314
+ });
315
+ });
316
+
317
+ describe('Error Handling', () => {
318
+ it('should propagate authentication errors from x-frigg headers', async () => {
319
+ const mockReq = {
320
+ headers: {
321
+ 'x-frigg-appuserid': 'invalid-user',
322
+ },
323
+ };
324
+
325
+ const customError = Boom.badRequest('Invalid user ID');
326
+ mockGetUserFromXFriggHeaders.execute.mockRejectedValue(customError);
327
+
328
+ await expect(authenticateUser.execute(mockReq)).rejects.toThrow(
329
+ customError
330
+ );
331
+ });
332
+
333
+ it('should propagate authentication errors from bearer token', async () => {
334
+ const mockReq = {
335
+ headers: {
336
+ authorization: 'Bearer invalid-token',
337
+ },
338
+ };
339
+
340
+ const customError = Boom.unauthorized('Invalid token');
341
+ mockGetUserFromBearerToken.execute.mockRejectedValue(customError);
342
+
343
+ await expect(authenticateUser.execute(mockReq)).rejects.toThrow(
344
+ customError
345
+ );
346
+ });
347
+
348
+ it('should propagate not implemented error from JWT', async () => {
349
+ mockUserConfig.authModes.adopterJwt = true;
350
+
351
+ const mockReq = {
352
+ headers: {
353
+ authorization: 'Bearer part1.part2.part3',
354
+ },
355
+ };
356
+
357
+ const notImplementedError = Boom.notImplemented('JWT not implemented');
358
+ mockGetUserFromAdopterJwt.execute.mockRejectedValue(
359
+ notImplementedError
360
+ );
361
+
362
+ await expect(authenticateUser.execute(mockReq)).rejects.toThrow(
363
+ notImplementedError
364
+ );
365
+ });
366
+ });
367
+ });
368
+
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@friggframework/core",
3
3
  "prettier": "@friggframework/prettier-config",
4
- "version": "2.0.0--canary.461.651659d.0",
4
+ "version": "2.0.0--canary.461.0b53aff.0",
5
5
  "dependencies": {
6
6
  "@aws-sdk/client-apigatewaymanagementapi": "^3.588.0",
7
7
  "@aws-sdk/client-kms": "^3.588.0",
@@ -37,9 +37,9 @@
37
37
  }
38
38
  },
39
39
  "devDependencies": {
40
- "@friggframework/eslint-config": "2.0.0--canary.461.651659d.0",
41
- "@friggframework/prettier-config": "2.0.0--canary.461.651659d.0",
42
- "@friggframework/test": "2.0.0--canary.461.651659d.0",
40
+ "@friggframework/eslint-config": "2.0.0--canary.461.0b53aff.0",
41
+ "@friggframework/prettier-config": "2.0.0--canary.461.0b53aff.0",
42
+ "@friggframework/test": "2.0.0--canary.461.0b53aff.0",
43
43
  "@prisma/client": "^6.17.0",
44
44
  "@types/lodash": "4.17.15",
45
45
  "@typescript-eslint/eslint-plugin": "^8.0.0",
@@ -79,5 +79,5 @@
79
79
  "publishConfig": {
80
80
  "access": "public"
81
81
  },
82
- "gitHead": "651659d3282b05bcecf571281d5a1a7e19c156bf"
82
+ "gitHead": "0b53aff86541a1114582f0448544e9ff6f0302b5"
83
83
  }
@@ -0,0 +1,113 @@
1
+ const Boom = require('@hapi/boom');
2
+ const { GetUserFromAdopterJwt } = require('../../use-cases/get-user-from-adopter-jwt');
3
+
4
+ describe('GetUserFromAdopterJwt', () => {
5
+ let getUserFromAdopterJwt;
6
+ let mockUserRepository;
7
+ let mockUserConfig;
8
+
9
+ beforeEach(() => {
10
+ mockUserRepository = {
11
+ findIndividualUserByAppUserId: jest.fn(),
12
+ findOrganizationUserByAppOrgId: jest.fn(),
13
+ createIndividualUser: jest.fn(),
14
+ createOrganizationUser: jest.fn(),
15
+ };
16
+
17
+ mockUserConfig = {
18
+ usePassword: false,
19
+ primary: 'individual',
20
+ individualUserRequired: true,
21
+ organizationUserRequired: false,
22
+ authModes: {
23
+ adopterJwt: true,
24
+ },
25
+ jwtConfig: {
26
+ secret: 'test-secret',
27
+ userIdClaim: 'sub',
28
+ orgIdClaim: 'org_id',
29
+ algorithm: 'HS256',
30
+ },
31
+ };
32
+
33
+ getUserFromAdopterJwt = new GetUserFromAdopterJwt({
34
+ userRepository: mockUserRepository,
35
+ userConfig: mockUserConfig,
36
+ });
37
+ });
38
+
39
+ describe('Stub Behavior', () => {
40
+ it('should throw 501 Not Implemented error', async () => {
41
+ const jwtToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyMTIzIiwib3JnX2lkIjoib3JnNDU2In0.signature';
42
+
43
+ await expect(
44
+ getUserFromAdopterJwt.execute(jwtToken)
45
+ ).rejects.toThrow(Boom.notImplemented().message);
46
+ });
47
+
48
+ it('should provide helpful error message about alternative auth modes', async () => {
49
+ const jwtToken = 'test.jwt.token';
50
+
51
+ try {
52
+ await getUserFromAdopterJwt.execute(jwtToken);
53
+ fail('Should have thrown error');
54
+ } catch (error) {
55
+ expect(error.message).toContain('not yet implemented');
56
+ expect(error.message).toContain('friggToken');
57
+ expect(error.message).toContain('xFriggHeaders');
58
+ }
59
+ });
60
+
61
+ it('should throw 501 error with any token format', async () => {
62
+ await expect(
63
+ getUserFromAdopterJwt.execute('simple-token')
64
+ ).rejects.toThrow(Boom.notImplemented().message);
65
+
66
+ await expect(
67
+ getUserFromAdopterJwt.execute('part1.part2.part3')
68
+ ).rejects.toThrow(Boom.notImplemented().message);
69
+
70
+ await expect(getUserFromAdopterJwt.execute('')).rejects.toThrow(
71
+ Boom.notImplemented().message
72
+ );
73
+ });
74
+ });
75
+
76
+ describe('Initialization', () => {
77
+ it('should initialize successfully with valid configuration', () => {
78
+ expect(getUserFromAdopterJwt).toBeDefined();
79
+ expect(getUserFromAdopterJwt.userRepository).toBe(
80
+ mockUserRepository
81
+ );
82
+ expect(getUserFromAdopterJwt.userConfig).toBe(mockUserConfig);
83
+ });
84
+
85
+ it('should initialize without jwtConfig (will fail on execute)', () => {
86
+ const configWithoutJwt = {
87
+ usePassword: false,
88
+ primary: 'individual',
89
+ };
90
+
91
+ const instance = new GetUserFromAdopterJwt({
92
+ userRepository: mockUserRepository,
93
+ userConfig: configWithoutJwt,
94
+ });
95
+
96
+ expect(instance).toBeDefined();
97
+ });
98
+ });
99
+
100
+ describe('Future Implementation Notes', () => {
101
+ it('should have documented todos for JWT implementation', () => {
102
+ const useCaseFileContent = require('fs').readFileSync(
103
+ require.resolve('../../use-cases/get-user-from-adopter-jwt.js'),
104
+ 'utf-8'
105
+ );
106
+
107
+ expect(useCaseFileContent).toContain('@todo');
108
+ expect(useCaseFileContent).toContain('jsonwebtoken');
109
+ expect(useCaseFileContent).toContain('FUTURE IMPLEMENTATION');
110
+ });
111
+ });
112
+ });
113
+