@mimik/oauth-helper 1.9.6 → 1.10.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 CHANGED
@@ -108,7 +108,7 @@ Validate the request by inspecting the token. To be used for `system` and `user`
108
108
  | request | <code>object</code> | The request with a token in the header. |
109
109
  | definitions | <code>object</code> | Swagger security definitions to compare with. |
110
110
  | scopes | <code>Array.&lt;string&gt;</code> | List of swagger scopes associated whith the requested endpoint. |
111
- | next | [<code>requestCallback</code>](#requestCallback) | The callback that handles the response. The validation adds the following properties to the request: - in case of 'system' token: 'clientId', 'tokenType', 'customer', 'onBehlaf'. If the token is a cluster token the property 'cluster' is set to true. - in case of 'user' token: userId, appId, onBehalfId, 'onBehalf'. |
111
+ | next | [<code>requestCallback</code>](#requestCallback) | The callback that handles the response. The validation adds the following properties to the request: - in case of 'system' token: 'claims', clientId', 'tokenType', 'customer', 'onBehlaf'. If the token is a cluster token the property 'cluster' is set to true. - in case of 'user' token: claims, userId, appId, onBehalfId, 'onBehalf'. The claims array contains the claims that are included on the scope of the recieved token. It there is no claims in the scope the claims property in request is an emtpy array. The scope must have the followimg format: `<action>:<resourceName>::<claims>`` where claims is a set of claims separatedby `,`. The is an exception for onBelf scope where the format is `onbBehalf:<action><resourceName>::<claims>`. The claims will be validated looking at the definitions property of the swagger file, by looking at at the top level properties of a definition named: `<resourceName>Claims`. If there is multiple scopes to use, the claims array will contain a concatenation of all the claims if present. |
112
112
 
113
113
  <a name="module_oauth-helper..apiTokenAdminSecurityHelper"></a>
114
114
 
@@ -123,7 +123,7 @@ Validate the request by inspecting the token. To be used for `admin` token.
123
123
  | request | <code>object</code> | The request with a token in the header. |
124
124
  | definitions | <code>object</code> | Swagger security definitions to compare with. |
125
125
  | scopes | <code>Array.&lt;string&gt;</code> | List of swagger scopes associated whith the requested endpoint. |
126
- | next | [<code>requestCallback</code>](#requestCallback) | The callback that handles the response. The validation adds the following properties to the request: 'clientId', tokenType', 'customer'. |
126
+ | next | [<code>requestCallback</code>](#requestCallback) | The callback that handles the response. The validation adds the following properties to the request: 'claims', clientId', tokenType', 'customer'. The claims array contains the claims that are included on the scope of the recieved token. It there is no claims in the scope the claims property in request is an emtpy array. The scope must have the followimg format: `<action>:<resourceName>::<claims>`` where claims is a set of claims separatedby `,`. The is an exception for onBelf scope where the format is `onbBehalf:<action><resourceName>::<claims>`. The claims will be validated looking at the definitions property of the swagger file, by looking at at the top level properties of a definition named: `<resourceName>Claims`. If there is multiple scopes to use, the claims array will contain a concatenation of all the claims if present. |
127
127
 
128
128
  <a name="requestCallback"></a>
129
129
 
package/index.js CHANGED
@@ -26,6 +26,11 @@ const CLIENT = '@clients';
26
26
  const AUTHORIZATION = 'authorization';
27
27
  const HEADER = 'header';
28
28
  const CLUSTER = 'cluster';
29
+ const SCOPES_SEPARATOR = ' ';
30
+ const CLAIMS_SEPARATOR = ',';
31
+ const RESOURCE_SEPARATOR = ':';
32
+ const SCOPE_CLAIMS_SEPARATOR = '::';
33
+ const CLAIMS_DEFINITION = 'Claims';
29
34
 
30
35
  const tokens = {};
31
36
 
@@ -87,24 +92,55 @@ module.exports = (config) => {
87
92
  }
88
93
  return auth[1];
89
94
  };
90
- const checkScopes = (tokenScopes, defScopes) => {
95
+ const checkScopes = (tokenScopes, defScopes, swagger) => {
91
96
  if (!tokenScopes) {
92
97
  throw new Error('no scope in authorization token');
93
98
  }
94
- const currentScopes = tokenScopes.split(' ');
99
+ let claims = [];
100
+ let onBehalf = false;
95
101
 
96
102
  if (defScopes && defScopes.length !== 0) {
97
- const intersects = _.intersection(currentScopes, defScopes);
103
+ const currentScopes = tokenScopes.split(SCOPES_SEPARATOR);
104
+ const intersects = [];
105
+ let resourceIndex = 1;
106
+
107
+ currentScopes.forEach((currentScope) => {
108
+ const analyzedScope = currentScope.split(SCOPE_CLAIMS_SEPARATOR);
109
+ const analyzedResource = analyzedScope[0].split(RESOURCE_SEPARATOR);
110
+ if (analyzedResource[0] === ON_BEHALF) {
111
+ onBehalf = true;
112
+ resourceIndex = 2; // legacy handling
113
+ }
114
+
115
+ if (defScopes.includes(analyzedScope[0])) {
116
+ if (analyzedScope[1]) {
117
+ const includedDefinitionName = `${analyzedResource[resourceIndex]}${CLAIMS_DEFINITION}`;
98
118
 
119
+ if (!swagger || !swagger.swaggerObject || !swagger.swaggerObject.definitions) {
120
+ throw new Error(`missing ${includedDefinitionName} definition: no definitions`);
121
+ }
122
+ const includedDefinition = swagger.swaggerObject.definitions[includedDefinitionName];
123
+
124
+ if (!includedDefinition) {
125
+ throw new Error(`missing ${includedDefinitionName} definition`);
126
+ }
127
+ const includedClaims = analyzedScope[1].split(CLAIMS_SEPARATOR);
128
+ const definitionClaims = Object.keys(includedDefinition);
129
+ const claimsIntersects = _.intersection(includedClaims, definitionClaims);
130
+
131
+ if (claimsIntersects.length !== includedClaims.length) {
132
+ throw new Error(`incorrect claims included: ${_.difference(includedClaims, claimsIntersects)}`);
133
+ }
134
+ claims = claims.concat(claimsIntersects);
135
+ }
136
+ intersects.push(analyzedScope[0]);
137
+ }
138
+ });
99
139
  if (intersects.length === 0) {
100
140
  throw new Error(`incorrect scopes: ${tokenScopes}`);
101
141
  }
102
-
103
- if (_.find(intersects, (scope) => scope.split(':')[0] === ON_BEHALF)) {
104
- return true;
105
- }
106
142
  }
107
- return false;
143
+ return { onBehalf, claims };
108
144
  };
109
145
 
110
146
  const getToken = (type, origin, options, correlationId) => {
@@ -246,8 +282,13 @@ module.exports = (config) => {
246
282
  * @param {requestCallback} next - The callback that handles the response.
247
283
  *
248
284
  * The validation adds the following properties to the request:
249
- * - in case of 'system' token: 'clientId', 'tokenType', 'customer', 'onBehlaf'. If the token is a cluster token the property 'cluster' is set to true.
250
- * - in case of 'user' token: userId, appId, onBehalfId, 'onBehalf'.
285
+ * - in case of 'system' token: 'claims', clientId', 'tokenType', 'customer', 'onBehlaf'. If the token is a cluster token the property 'cluster' is set to true.
286
+ * - in case of 'user' token: claims, userId, appId, onBehalfId, 'onBehalf'.
287
+ *
288
+ * The claims array contains the claims that are included on the scope of the recieved token. It there is no claims in the scope the claims property in request is an emtpy array.
289
+ * The scope must have the followimg format: `<action>:<resourceName>::<claims>`` where claims is a set of claims separatedby `,`. The is an exception for onBelf scope where the format is `onbBehalf:<action><resourceName>::<claims>`.
290
+ * The claims will be validated looking at the definitions property of the swagger file, by looking at at the top level properties of a definition named: `<resourceName>Claims`.
291
+ * If there is multiple scopes to use, the claims array will contain a concatenation of all the claims if present.
251
292
  */
252
293
  const apiTokenSecurityHelper = (request, definitions, scopes, next) => {
253
294
  let authToken;
@@ -282,14 +323,15 @@ module.exports = (config) => {
282
323
  return;
283
324
  }
284
325
  }
285
- let onBehalfOption = false;
326
+ let scopeResult;
286
327
 
287
- try { onBehalfOption = checkScopes(token.scope, scopes); }
328
+ try { scopeResult = checkScopes(token.scope, scopes, request.swagger); }
288
329
  catch (errScopes) {
289
330
  next(errScopes);
290
331
  return;
291
332
  }
292
- if (onBehalfOption) request[TOKEN_PARAMS.onBehalf] = true;
333
+ if (scopeResult.onBehalf) request[TOKEN_PARAMS.onBehalf] = true;
334
+ request[TOKEN_PARAMS.claims] = scopeResult.claims;
293
335
  if (definitions.flow === IMPLICIT) {
294
336
  if (token.sub) request[TOKEN_PARAMS.userId] = token.sub;
295
337
  if (token.azp) request[TOKEN_PARAMS.appId] = token.azp;
@@ -319,7 +361,12 @@ module.exports = (config) => {
319
361
  * @param {string[]} scopes - List of swagger scopes associated whith the requested endpoint.
320
362
  * @param {requestCallback} next - The callback that handles the response.
321
363
  *
322
- * The validation adds the following properties to the request: 'clientId', tokenType', 'customer'.
364
+ * The validation adds the following properties to the request: 'claims', clientId', tokenType', 'customer'.
365
+ *
366
+ * The claims array contains the claims that are included on the scope of the recieved token. It there is no claims in the scope the claims property in request is an emtpy array.
367
+ * The scope must have the followimg format: `<action>:<resourceName>::<claims>`` where claims is a set of claims separatedby `,`. The is an exception for onBelf scope where the format is `onbBehalf:<action><resourceName>::<claims>`.
368
+ * The claims will be validated looking at the definitions property of the swagger file, by looking at at the top level properties of a definition named: `<resourceName>Claims`.
369
+ * If there is multiple scopes to use, the claims array will contain a concatenation of all the claims if present.
323
370
  */
324
371
  const apiTokenAdminSecurityHelper = (request, definitions, scopes, next) => {
325
372
  let authToken;
@@ -341,6 +388,16 @@ module.exports = (config) => {
341
388
  next(new Error('invalid token: wrong type'));
342
389
  return;
343
390
  }
391
+ if (token.subType === SUB_ADMIN) {
392
+ if (!token.cust) {
393
+ next(new Error('invalid token: no customer'));
394
+ return;
395
+ }
396
+ }
397
+ else if (token.sub !== `${config.security.admin.externalId}${CLIENT}`) {
398
+ next(new Error(`jwt subject invalid: ${token.sub}`));
399
+ return;
400
+ }
344
401
  try {
345
402
  jwt.verify(authToken, keys(flow), jwtOptions(flow));
346
403
  }
@@ -357,21 +414,14 @@ module.exports = (config) => {
357
414
  return;
358
415
  }
359
416
  }
360
- try { checkScopes(token.scope, scopes); }
417
+ let scopeResult;
418
+
419
+ try { scopeResult = checkScopes(token.scope, scopes, request.swagger); }
361
420
  catch (errScopes) {
362
421
  next(errScopes);
363
422
  return;
364
423
  }
365
- if (token.subType === SUB_ADMIN) {
366
- if (!token.cust) {
367
- next(new Error('invalid token: no customer'));
368
- return;
369
- }
370
- }
371
- else if (token.sub !== `${config.security.admin.externalId}${CLIENT}`) {
372
- next(new Error(`jwt subject invalid: ${token.sub}`));
373
- return;
374
- }
424
+ request[TOKEN_PARAMS.claims] = scopeResult.claims;
375
425
  if (token.subType) request[TOKEN_PARAMS.tokenType] = token.subType;
376
426
  if (token.sub) request[TOKEN_PARAMS.clientId] = token.sub;
377
427
  if (token.cust) request[TOKEN_PARAMS.customer] = token.cust;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mimik/oauth-helper",
3
- "version": "1.9.6",
3
+ "version": "1.10.0",
4
4
  "description": "Oauth helper for mimik microservices",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -31,33 +31,33 @@
31
31
  "@mimik/request-retry": "^2.0.6",
32
32
  "@mimik/response-helper": "^2.6.0",
33
33
  "@mimik/sumologic-winston-logger": "^1.6.6",
34
- "@mimik/swagger-helper": "^2.5.0",
34
+ "@mimik/swagger-helper": "^2.5.1",
35
35
  "bluebird": "3.7.2",
36
36
  "jsonwebtoken": "8.5.1",
37
37
  "lodash": "4.17.21"
38
38
  },
39
39
  "devDependencies": {
40
40
  "@mimik/eslint-plugin-dependencies": "^2.4.1",
41
- "@mimik/eslint-plugin-document-env": "^1.0.0",
41
+ "@mimik/eslint-plugin-document-env": "^1.0.1",
42
42
  "@mimik/request-helper": "^1.7.3",
43
- "body-parser": "1.19.1",
44
- "chai": "4.3.4",
45
- "eslint": "8.4.1",
43
+ "body-parser": "1.19.2",
44
+ "chai": "4.3.6",
45
+ "eslint": "8.10.0",
46
46
  "eslint-config-airbnb": "18.2.1",
47
- "eslint-plugin-import": "2.25.3",
47
+ "eslint-plugin-import": "2.25.4",
48
48
  "eslint-plugin-jsx-a11y": "6.5.1",
49
49
  "eslint-plugin-react": "7.27.1",
50
50
  "eslint-plugin-react-hooks": "4.3.0",
51
- "express": "4.17.1",
52
- "fancy-log": "1.3.3",
51
+ "express": "4.17.3",
52
+ "fancy-log": "2.0.0",
53
53
  "gulp": "4.0.2",
54
54
  "gulp-eslint": "6.0.0",
55
55
  "gulp-git": "2.10.1",
56
56
  "gulp-spawn-mocha": "6.0.0",
57
57
  "husky": "7.0.4",
58
- "jsdoc-to-markdown": "7.1.0",
59
- "mocha": "9.1.3",
60
- "mochawesome": "7.0.1",
58
+ "jsdoc-to-markdown": "7.1.1",
59
+ "mocha": "9.2.1",
60
+ "mochawesome": "7.1.0",
61
61
  "nyc": "15.1.0"
62
62
  }
63
63
  }
@@ -40,6 +40,7 @@ const adminTokenAppWrongId = jwt.sign(payload.admin, config.appGeneric.security.
40
40
  const adminTokenAppNoScope = jwt.sign(payload.adminNoScope, config.appGeneric.security.generic.key, options.adminApp);
41
41
  const subAdminTokenApp = jwt.sign(payload.subAdmin, config.appGeneric.security.generic.key, options.adminApp);
42
42
  const subAdminTokenAppNoCustomer = jwt.sign(payload.subAdminNoCustomer, config.appGeneric.security.generic.key, options.adminApp);
43
+ const tokenWithClaims = jwt.sign(payload.withClaims, config.implImplicit.security.implicit.key, options.implImplicit);
43
44
 
44
45
  describe('OauthHelper Unit Tests', () => {
45
46
  before(() => {
@@ -137,6 +138,107 @@ describe('OauthHelper Unit Tests', () => {
137
138
  expect(err.message).to.equal(`incorrect scopes: ${scope.regular}`);
138
139
  });
139
140
  });
141
+ it('should generate an error missing definition: no definition', () => {
142
+ oauthImplImplicit.apiTokenSecurityHelper({ headers: { authorization: `Bearer ${tokenWithClaims}` } }, definition.implicit, scope.forClaims, (err) => {
143
+ if (!err) throw new Error('cannot be successful');
144
+ expect(err.message).to.equal('missing userClaims definition: no definitions');
145
+ });
146
+ });
147
+ it('should generate an error missing definition', () => {
148
+ oauthImplImplicit.apiTokenSecurityHelper({
149
+ headers: { authorization: `Bearer ${tokenWithClaims}` },
150
+ swagger: {
151
+ swaggerObject: {
152
+ definitions: {
153
+ testClaims: {
154
+ test: 'test',
155
+ },
156
+ },
157
+ },
158
+ },
159
+ }, definition.implicit, scope.forClaims, (err) => {
160
+ if (!err) throw new Error('cannot be successful');
161
+ expect(err.message).to.equal('missing userClaims definition');
162
+ });
163
+ });
164
+ it('should generate an error incorrect claims included', () => {
165
+ oauthImplImplicit.apiTokenSecurityHelper({
166
+ headers: { authorization: `Bearer ${tokenWithClaims}` },
167
+ swagger: {
168
+ swaggerObject: {
169
+ definitions: {
170
+ userClaims: {
171
+ test: 'test',
172
+ },
173
+ },
174
+ },
175
+ },
176
+ }, definition.implicit, scope.forClaims, (err) => {
177
+ if (!err) throw new Error('cannot be successful');
178
+ expect(err.message).to.equal('incorrect claims included: lastName,firstName');
179
+ });
180
+ });
181
+ it('should generate an error incorrect claims included', () => {
182
+ oauthImplImplicit.apiTokenSecurityHelper({
183
+ headers: { authorization: `Bearer ${tokenWithClaims}` },
184
+ swagger: {
185
+ swaggerObject: {
186
+ definitions: {
187
+ userClaims: {
188
+ lastName: 'test',
189
+ test: 'test',
190
+ },
191
+ },
192
+ },
193
+ },
194
+ }, definition.implicit, scope.forClaims, (err) => {
195
+ if (!err) throw new Error('cannot be successful');
196
+ expect(err.message).to.equal('incorrect claims included: firstName');
197
+ });
198
+ });
199
+ it('should return a token and request contains claims', () => {
200
+ const request = {
201
+ headers: { authorization: `Bearer ${tokenWithClaims}` },
202
+ swagger: {
203
+ swaggerObject: {
204
+ definitions: {
205
+ userClaims: {
206
+ lastName: 'test',
207
+ firstName: 'test',
208
+ test: 'test',
209
+ },
210
+ },
211
+ },
212
+ },
213
+ };
214
+ oauthImplImplicit.apiTokenSecurityHelper(request, definition.implicit, scope.forClaims, (err) => {
215
+ if (err) throw err;
216
+ expect(request.claims).to.deep.equal(['lastName', 'firstName']);
217
+ });
218
+ });
219
+ it('should return a token and request contains multiple claims', () => {
220
+ const request = {
221
+ headers: { authorization: `Bearer ${tokenWithClaims}` },
222
+ swagger: {
223
+ swaggerObject: {
224
+ definitions: {
225
+ userClaims: {
226
+ lastName: 'test',
227
+ firstName: 'test',
228
+ test: 'test',
229
+ },
230
+ usersClaims: {
231
+ email: 'test',
232
+ },
233
+ },
234
+ },
235
+ },
236
+ };
237
+ oauthImplImplicit.apiTokenSecurityHelper(request, definition.implicit, scope.forClaimsMultiple, (err) => {
238
+ if (err) throw err;
239
+ expect(request.claims).to.deep.equal(['lastName', 'firstName', 'email']);
240
+ });
241
+ });
140
242
  it('should have update request implicit with authorization', () => {
141
243
  const request = {
142
244
  headers: {
@@ -216,9 +216,12 @@ const config = {
216
216
  const scope = {
217
217
  regular: 'test:1 test:2 test:3',
218
218
  onBehalf: 'test:1 onBehalf:1',
219
+ withClaims: 'get:user::lastName,firstName get:users::email',
219
220
  scopes: ['test:1', 'test:2', 'test:3'],
220
221
  otherScopes: ['test:4', 'test:5'],
221
222
  onBehalfScopes: ['onBehalf:1', 'test:6'],
223
+ forClaims: ['get:user'],
224
+ forClaimsMultiple: ['get:user', 'get:users'],
222
225
  };
223
226
  const cust = 'testCustomer';
224
227
  const subType = 'testType';
@@ -251,6 +254,12 @@ const payload = {
251
254
  subType,
252
255
  cust,
253
256
  },
257
+ withClaims: {
258
+ scope: scope.withClaims,
259
+ subType,
260
+ azp,
261
+ cust,
262
+ },
254
263
  exchange: {
255
264
  scope: scope.regular,
256
265
  subType,