@mimik/api-helper 2.0.7 → 2.0.8

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,557 @@
1
+ import esmock from 'esmock';
2
+ import { expect } from 'chai';
3
+ import jwtReal from 'jsonwebtoken';
4
+
5
+ const TOKEN_PARAMS = {
6
+ userId: 'userId',
7
+ onBehalfId: 'onBehalfId',
8
+ appId: 'appId',
9
+ clientId: 'clientId',
10
+ customer: 'customer',
11
+ onBehalf: 'onBehalf',
12
+ tokenType: 'tokenType',
13
+ cluster: 'cluster',
14
+ claims: 'claims',
15
+ };
16
+
17
+ const SECRET = 'test-secret';
18
+
19
+ const makeToken = payload => jwtReal.sign(payload, SECRET);
20
+
21
+ const makeConfig = (overrides = {}) => ({
22
+ security: {
23
+ server: { audience: 'test-audience', issuer: 'test-issuer', accessKey: SECRET },
24
+ generic: { audience: 'noGeneric', key: 'noGeneric' },
25
+ admin: { externalId: 'admin-ext-id' },
26
+ apiKeys: ['valid-key-1', 'valid-key-2'],
27
+ ...overrides,
28
+ },
29
+ serverSettings: { id: 'test-server' },
30
+ });
31
+
32
+ const makeConReq = (token, securityType, scopes = []) => {
33
+ const request = {};
34
+ const con = {
35
+ request,
36
+ operation: { security: [{ [securityType]: scopes }] },
37
+ api: { definition: { components: { schemas: {} } } },
38
+ };
39
+ const req = {
40
+ headers: { Authorization: `Bearer ${token}` },
41
+ };
42
+
43
+ return { con, req, request };
44
+ };
45
+
46
+ let securityLib;
47
+
48
+ describe('securityHandlers', () => {
49
+ before(async () => {
50
+ const mod = await esmock('../lib/securityHandlers.js', {
51
+ '@mimik/swagger-helper': { TOKEN_PARAMS },
52
+ });
53
+
54
+ ({ securityLib } = mod);
55
+ });
56
+
57
+ describe('AdminSecurity', () => {
58
+ describe('mock', () => {
59
+ it('should set dummy values on req and request and return true', () => {
60
+ const { AdminSecurity } = securityLib(makeConfig());
61
+ const request = {};
62
+ const con = { request };
63
+ const req = {};
64
+
65
+ const result = AdminSecurity.mock(con, req);
66
+ expect(result).to.equal(true);
67
+ expect(req.claims).to.deep.equal(['dummyClaims']);
68
+ expect(req.tokenType).to.equal('admin');
69
+ expect(req.clientId).to.equal('dummyClientId');
70
+ expect(req.customer).to.equal('dummyCustomer');
71
+ expect(request.claims).to.deep.equal(['dummyClaims']);
72
+ });
73
+ });
74
+
75
+ describe('regular', () => {
76
+ it('should throw 401 when headers are missing', () => {
77
+ const { AdminSecurity } = securityLib(makeConfig());
78
+ const con = { request: {} };
79
+ const req = {};
80
+
81
+ expect(() => AdminSecurity.regular(con, req)).to.throw('missing header');
82
+ });
83
+
84
+ it('should throw 401 when Authorization header is missing', () => {
85
+ const { AdminSecurity } = securityLib(makeConfig());
86
+ const con = { request: {} };
87
+ const req = { headers: {} };
88
+
89
+ expect(() => AdminSecurity.regular(con, req)).to.throw('missing authorization header');
90
+ });
91
+
92
+ it('should throw 401 when bearer format is wrong', () => {
93
+ const { AdminSecurity } = securityLib(makeConfig());
94
+ const con = { request: {} };
95
+ const req = { headers: { Authorization: 'Basic abc123' } };
96
+
97
+ expect(() => AdminSecurity.regular(con, req)).to.throw('authorization type incorrect');
98
+ });
99
+
100
+ it('should throw 403 when token subType is not admin or subAdmin', () => {
101
+ const token = makeToken({ subType: 'user', sub: 'test', scope: 'read' });
102
+ const { con, req } = makeConReq(token, 'AdminSecurity');
103
+ const { AdminSecurity } = securityLib(makeConfig());
104
+
105
+ expect(() => AdminSecurity.regular(con, req)).to.throw('invalid token: wrong type');
106
+ });
107
+
108
+ it('should throw 403 when subAdmin token has no customer', () => {
109
+ const token = makeToken({ subType: 'subAdmin', sub: 'test', scope: 'read' });
110
+ const { con, req } = makeConReq(token, 'AdminSecurity');
111
+ const { AdminSecurity } = securityLib(makeConfig());
112
+
113
+ expect(() => AdminSecurity.regular(con, req)).to.throw('invalid token: no customer');
114
+ });
115
+
116
+ it('should throw 403 when admin subject does not match', () => {
117
+ const token = makeToken({ subType: 'admin', sub: 'wrong@clients', scope: 'read' });
118
+ const { con, req } = makeConReq(token, 'AdminSecurity');
119
+ const { AdminSecurity } = securityLib(makeConfig());
120
+
121
+ expect(() => AdminSecurity.regular(con, req)).to.throw('jwt subject invalid');
122
+ });
123
+
124
+ it('should succeed for valid admin token', () => {
125
+ const token = makeToken({
126
+ subType: 'admin',
127
+ sub: 'admin-ext-id@clients',
128
+ scope: 'test:resource',
129
+ aud: 'test-audience',
130
+ iss: 'test-issuer',
131
+ });
132
+ const { con, req } = makeConReq(token, 'AdminSecurity', ['test:resource']);
133
+ const { AdminSecurity } = securityLib(makeConfig());
134
+
135
+ const result = AdminSecurity.regular(con, req);
136
+ expect(result).to.equal(true);
137
+ expect(req.tokenType).to.equal('admin');
138
+ expect(req.clientId).to.equal('admin-ext-id@clients');
139
+ });
140
+
141
+ it('should succeed for valid subAdmin token with customer', () => {
142
+ const token = makeToken({
143
+ subType: 'subAdmin',
144
+ sub: 'sub-admin@clients',
145
+ cust: 'customer-123',
146
+ scope: 'test:resource',
147
+ aud: 'test-audience',
148
+ iss: 'test-issuer',
149
+ });
150
+ const { con, req } = makeConReq(token, 'AdminSecurity', ['test:resource']);
151
+ const { AdminSecurity } = securityLib(makeConfig());
152
+
153
+ const result = AdminSecurity.regular(con, req);
154
+ expect(result).to.equal(true);
155
+ expect(req.customer).to.equal('customer-123');
156
+ expect(req.tokenType).to.equal('subAdmin');
157
+ });
158
+ });
159
+ });
160
+
161
+ describe('SystemSecurity', () => {
162
+ describe('mock', () => {
163
+ it('should set dummy values and return true', () => {
164
+ const { SystemSecurity } = securityLib(makeConfig());
165
+ const request = {};
166
+ const con = { request };
167
+ const req = {};
168
+
169
+ const result = SystemSecurity.mock(con, req);
170
+ expect(result).to.equal(true);
171
+ expect(req.tokenType).to.equal('dummyServiceType');
172
+ expect(req.clientId).to.equal('dummyClientId');
173
+ });
174
+ });
175
+
176
+ describe('regular', () => {
177
+ it('should throw 403 when token subType is admin', () => {
178
+ const token = makeToken({ subType: 'admin', sub: 'test', scope: 'read' });
179
+ const { con, req } = makeConReq(token, 'SystemSecurity');
180
+ const { SystemSecurity } = securityLib(makeConfig());
181
+
182
+ expect(() => SystemSecurity.regular(con, req)).to.throw('invalid token: wrong type');
183
+ });
184
+
185
+ it('should throw 403 when token subType is subAdmin', () => {
186
+ const token = makeToken({ subType: 'subAdmin', sub: 'test', scope: 'read' });
187
+ const { con, req } = makeConReq(token, 'SystemSecurity');
188
+ const { SystemSecurity } = securityLib(makeConfig());
189
+
190
+ expect(() => SystemSecurity.regular(con, req)).to.throw('invalid token: wrong type');
191
+ });
192
+
193
+ it('should succeed for valid system token', () => {
194
+ const token = makeToken({
195
+ subType: 'service',
196
+ sub: 'service@clients',
197
+ scope: 'system:resource',
198
+ aud: 'test-audience',
199
+ iss: 'test-issuer',
200
+ });
201
+ const { con, req } = makeConReq(token, 'SystemSecurity', ['system:resource']);
202
+ const { SystemSecurity } = securityLib(makeConfig());
203
+
204
+ const result = SystemSecurity.regular(con, req);
205
+ expect(result).to.equal(true);
206
+ expect(req.tokenType).to.equal('service');
207
+ expect(req.clientId).to.equal('service@clients');
208
+ });
209
+
210
+ it('should set cluster flag when token type is cluster', () => {
211
+ const token = makeToken({
212
+ subType: 'service',
213
+ type: 'cluster',
214
+ sub: 'service@clients',
215
+ scope: 'system:resource',
216
+ aud: 'test-audience',
217
+ iss: 'test-issuer',
218
+ });
219
+ const { con, req } = makeConReq(token, 'SystemSecurity', ['system:resource']);
220
+ const { SystemSecurity } = securityLib(makeConfig());
221
+
222
+ SystemSecurity.regular(con, req);
223
+ expect(req.cluster).to.equal(true);
224
+ });
225
+
226
+ it('should set onBehalf when scope starts with onBehalf', () => {
227
+ const token = makeToken({
228
+ subType: 'service',
229
+ sub: 'service@clients',
230
+ scope: 'onBehalf:system:resource',
231
+ aud: 'test-audience',
232
+ iss: 'test-issuer',
233
+ });
234
+ const { con, req } = makeConReq(token, 'SystemSecurity', ['onBehalf:system:resource']);
235
+ const { SystemSecurity } = securityLib(makeConfig());
236
+
237
+ SystemSecurity.regular(con, req);
238
+ expect(req.onBehalf).to.equal(true);
239
+ });
240
+ });
241
+ });
242
+
243
+ describe('UserSecurity', () => {
244
+ describe('mock', () => {
245
+ it('should set dummy values and return true', () => {
246
+ const { UserSecurity } = securityLib(makeConfig());
247
+ const request = {};
248
+ const con = { request };
249
+ const req = {};
250
+
251
+ const result = UserSecurity.mock(con, req);
252
+ expect(result).to.equal(true);
253
+ expect(req.userId).to.equal('dummyUserId');
254
+ expect(req.appId).to.equal('dummyAppId');
255
+ expect(req.tokenType).to.equal('user');
256
+ });
257
+ });
258
+
259
+ describe('regular', () => {
260
+ it('should succeed for valid user token', () => {
261
+ const token = makeToken({
262
+ sub: 'user-123',
263
+ azp: 'app-456',
264
+ scope: 'user:resource',
265
+ aud: 'test-audience',
266
+ iss: 'test-issuer',
267
+ });
268
+ const { con, req } = makeConReq(token, 'UserSecurity', ['user:resource']);
269
+ const { UserSecurity } = securityLib(makeConfig());
270
+
271
+ const result = UserSecurity.regular(con, req);
272
+ expect(result).to.equal(true);
273
+ expect(req.tokenType).to.equal('user');
274
+ expect(req.userId).to.equal('user-123');
275
+ expect(req.appId).to.equal('app-456');
276
+ });
277
+
278
+ it('should handle may_act delegation', () => {
279
+ const token = makeToken({
280
+ sub: 'delegate-user',
281
+ // eslint-disable-next-line camelcase
282
+ may_act: { sub: 'actual-user' },
283
+ scope: 'user:resource',
284
+ aud: 'test-audience',
285
+ iss: 'test-issuer',
286
+ });
287
+ const { con, req } = makeConReq(token, 'UserSecurity', ['user:resource']);
288
+ const { UserSecurity } = securityLib(makeConfig());
289
+
290
+ UserSecurity.regular(con, req);
291
+ expect(req.onBehalfId).to.equal('delegate-user');
292
+ expect(req.userId).to.equal('actual-user');
293
+ });
294
+ });
295
+ });
296
+
297
+ describe('ApiKeySecurity', () => {
298
+ describe('mock', () => {
299
+ it('should set dummy apiKey and return true', () => {
300
+ const { ApiKeySecurity } = securityLib(makeConfig());
301
+ const request = {};
302
+ const con = { request };
303
+ const req = {};
304
+
305
+ const result = ApiKeySecurity.mock(con, req);
306
+ expect(result).to.equal(true);
307
+ expect(req.apiKey).to.equal('dummyApiKey');
308
+ expect(request.apiKey).to.equal('dummyApiKey');
309
+ });
310
+ });
311
+
312
+ describe('regular', () => {
313
+ it('should accept a valid API key', () => {
314
+ const { ApiKeySecurity } = securityLib(makeConfig());
315
+ const request = {};
316
+ const con = { request };
317
+ const req = { headers: { apikey: 'valid-key-1' } };
318
+
319
+ const result = ApiKeySecurity.regular(con, req);
320
+ expect(result).to.equal(true);
321
+ expect(req.apiKey).to.equal('valid-key-1');
322
+ });
323
+
324
+ it('should throw 401 for invalid API key', () => {
325
+ const { ApiKeySecurity } = securityLib(makeConfig());
326
+ const request = {};
327
+ const con = { request };
328
+ const req = { headers: { apikey: 'bad-key' } };
329
+
330
+ expect(() => ApiKeySecurity.regular(con, req)).to.throw('invalid API key');
331
+ });
332
+
333
+ it('should throw 401 when headers are missing', () => {
334
+ const { ApiKeySecurity } = securityLib(makeConfig());
335
+ const request = {};
336
+ const con = { request };
337
+ const req = {};
338
+
339
+ expect(() => ApiKeySecurity.regular(con, req)).to.throw('invalid API key');
340
+ });
341
+ });
342
+ });
343
+
344
+ describe('token verification', () => {
345
+ it('should throw 401 for invalid token (cannot decode)', () => {
346
+ const { SystemSecurity } = securityLib(makeConfig());
347
+ const con = { request: {} };
348
+ const req = { headers: { Authorization: 'Bearer not-a-jwt' } };
349
+
350
+ expect(() => SystemSecurity.regular(con, req)).to.throw('invalid token');
351
+ });
352
+
353
+ it('should use generic key when not noGeneric', () => {
354
+ const genericKey = 'generic-secret';
355
+
356
+ const genericToken = jwtReal.sign({
357
+ subType: 'service',
358
+ sub: 'svc@clients',
359
+ scope: 'system:resource',
360
+ aud: 'generic-audience',
361
+ iss: 'test-issuer',
362
+ }, genericKey);
363
+
364
+ const config = makeConfig({
365
+ generic: { audience: 'generic-audience', key: genericKey },
366
+ });
367
+ const { SystemSecurity } = securityLib(config);
368
+ const { con, req } = makeConReq(genericToken, 'SystemSecurity', ['system:resource']);
369
+ const result = SystemSecurity.regular(con, req);
370
+ expect(result).to.equal(true);
371
+ });
372
+
373
+ it('should fall back to previousKey on verify failure', () => {
374
+ const previousKey = 'old-secret';
375
+ const token = jwtReal.sign({
376
+ subType: 'service',
377
+ sub: 'svc@clients',
378
+ scope: 'system:resource',
379
+ aud: 'test-audience',
380
+ iss: 'test-issuer',
381
+ }, previousKey);
382
+
383
+ const config = makeConfig({
384
+ generic: { audience: 'noGeneric', key: 'noGeneric', previousKey },
385
+ });
386
+ const { SystemSecurity } = securityLib(config);
387
+ const { con, req } = makeConReq(token, 'SystemSecurity', ['system:resource']);
388
+ const result = SystemSecurity.regular(con, req);
389
+ expect(result).to.equal(true);
390
+ });
391
+
392
+ it('should throw 403 when both keys fail', () => {
393
+ const token = jwtReal.sign({
394
+ subType: 'service',
395
+ sub: 'svc@clients',
396
+ scope: 'system:resource',
397
+ }, 'wrong-key');
398
+
399
+ const config = makeConfig({
400
+ generic: { audience: 'noGeneric', key: 'noGeneric', previousKey: 'also-wrong' },
401
+ });
402
+ const { SystemSecurity } = securityLib(config);
403
+ const { con, req } = makeConReq(token, 'SystemSecurity', ['system:resource']);
404
+ try {
405
+ SystemSecurity.regular(con, req);
406
+ expect.fail('should have thrown');
407
+ }
408
+ catch (err) {
409
+ expect(err.statusCode).to.equal(403);
410
+ }
411
+ });
412
+ });
413
+
414
+ describe('scope checking', () => {
415
+ it('should throw 401 when token has no scope', () => {
416
+ const token = makeToken({
417
+ subType: 'service',
418
+ sub: 'svc@clients',
419
+ aud: 'test-audience',
420
+ iss: 'test-issuer',
421
+ });
422
+ const { con, req } = makeConReq(token, 'SystemSecurity', ['some:scope']);
423
+ const { SystemSecurity } = securityLib(makeConfig());
424
+
425
+ try {
426
+ SystemSecurity.regular(con, req);
427
+ expect.fail('should have thrown');
428
+ }
429
+ catch (err) {
430
+ expect(err.statusCode).to.equal(401);
431
+ expect(err.message).to.equal('no scope in authorization token');
432
+ }
433
+ });
434
+
435
+ it('should throw 403 when scopes do not intersect', () => {
436
+ const token = makeToken({
437
+ subType: 'service',
438
+ sub: 'svc@clients',
439
+ scope: 'other:resource',
440
+ aud: 'test-audience',
441
+ iss: 'test-issuer',
442
+ });
443
+ const { con, req } = makeConReq(token, 'SystemSecurity', ['required:scope']);
444
+ const { SystemSecurity } = securityLib(makeConfig());
445
+
446
+ try {
447
+ SystemSecurity.regular(con, req);
448
+ expect.fail('should have thrown');
449
+ }
450
+ catch (err) {
451
+ expect(err.statusCode).to.equal(403);
452
+ expect(err.message).to.include('incorrect scopes');
453
+ }
454
+ });
455
+
456
+ it('should validate claims against definition schemas', () => {
457
+ const token = makeToken({
458
+ subType: 'service',
459
+ sub: 'svc@clients',
460
+ scope: 'system:resource::name,email',
461
+ aud: 'test-audience',
462
+ iss: 'test-issuer',
463
+ });
464
+ const { con, req } = makeConReq(token, 'SystemSecurity', ['system:resource']);
465
+
466
+ con.api.definition.components.schemas.resourceClaims = {
467
+ name: { type: 'string' },
468
+ email: { type: 'string' },
469
+ phone: { type: 'string' },
470
+ };
471
+ const { SystemSecurity } = securityLib(makeConfig());
472
+
473
+ const result = SystemSecurity.regular(con, req);
474
+ expect(result).to.equal(true);
475
+ expect(req.claims).to.deep.equal(['name', 'email']);
476
+ });
477
+
478
+ it('should throw 403 when claims are not in definition', () => {
479
+ const token = makeToken({
480
+ subType: 'service',
481
+ sub: 'svc@clients',
482
+ scope: 'system:resource::badClaim',
483
+ aud: 'test-audience',
484
+ iss: 'test-issuer',
485
+ });
486
+ const { con, req } = makeConReq(token, 'SystemSecurity', ['system:resource']);
487
+
488
+ con.api.definition.components.schemas.resourceClaims = {
489
+ name: { type: 'string' },
490
+ };
491
+ const { SystemSecurity } = securityLib(makeConfig());
492
+
493
+ try {
494
+ SystemSecurity.regular(con, req);
495
+ expect.fail('should have thrown');
496
+ }
497
+ catch (err) {
498
+ expect(err.statusCode).to.equal(403);
499
+ expect(err.message).to.include('incorrect claims');
500
+ }
501
+ });
502
+
503
+ it('should throw 500 when claims definition is missing', () => {
504
+ const token = makeToken({
505
+ subType: 'service',
506
+ sub: 'svc@clients',
507
+ scope: 'system:resource::name',
508
+ aud: 'test-audience',
509
+ iss: 'test-issuer',
510
+ });
511
+ const { con, req } = makeConReq(token, 'SystemSecurity', ['system:resource']);
512
+
513
+ con.api.definition = {};
514
+ const { SystemSecurity } = securityLib(makeConfig());
515
+
516
+ try {
517
+ SystemSecurity.regular(con, req);
518
+ expect.fail('should have thrown');
519
+ }
520
+ catch (err) {
521
+ expect(err.statusCode).to.equal(500);
522
+ expect(err.message).to.include('no definition');
523
+ }
524
+ });
525
+ });
526
+
527
+ describe('header checking', () => {
528
+ it('should handle case-insensitive Authorization header', () => {
529
+ const token = makeToken({
530
+ subType: 'service',
531
+ sub: 'svc@clients',
532
+ scope: 'system:resource',
533
+ aud: 'test-audience',
534
+ iss: 'test-issuer',
535
+ });
536
+ const { SystemSecurity } = securityLib(makeConfig());
537
+ const request = {};
538
+ const con = {
539
+ request,
540
+ operation: { security: [{ SystemSecurity: ['system:resource'] }] },
541
+ api: { definition: { components: { schemas: {} } } },
542
+ };
543
+ const req = { headers: { authorization: `Bearer ${token}` } };
544
+
545
+ const result = SystemSecurity.regular(con, req);
546
+ expect(result).to.equal(true);
547
+ });
548
+
549
+ it('should throw for duplicated authorization headers', () => {
550
+ const { SystemSecurity } = securityLib(makeConfig());
551
+ const con = { request: {} };
552
+ const req = { headers: { Authorization: 'Bearer x', authorization: 'Bearer y' } };
553
+
554
+ expect(() => SystemSecurity.regular(con, req)).to.throw('duplicated authorization header');
555
+ });
556
+ });
557
+ });
package/.nycrc DELETED
@@ -1,4 +0,0 @@
1
- {
2
- "exclude": ["gulpfile.js"],
3
- "reporter": ["lcov", "text"]
4
- }