@villedemontreal/jwt-validator 5.9.3 → 5.10.1

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.
Files changed (43) hide show
  1. package/dist/scripts/showCoverage.js.map +1 -1
  2. package/dist/scripts/testUnits.js.map +1 -1
  3. package/dist/scripts/watch.js.map +1 -1
  4. package/dist/src/config/configs.js.map +1 -1
  5. package/dist/src/config/init.js +2 -3
  6. package/dist/src/config/init.js.map +1 -1
  7. package/dist/src/index.d.ts +1 -0
  8. package/dist/src/index.js +1 -0
  9. package/dist/src/index.js.map +1 -1
  10. package/dist/src/jwtValidator.js.map +1 -1
  11. package/dist/src/jwtValidator.test.js.map +1 -1
  12. package/dist/src/middleware/jwtMiddleware.js.map +1 -1
  13. package/dist/src/middleware/tokenTransformationMiddleware.js.map +1 -1
  14. package/dist/src/models/customError.js +2 -3
  15. package/dist/src/models/customError.js.map +1 -1
  16. package/dist/src/models/gluuUserType.js +1 -1
  17. package/dist/src/models/gluuUserType.js.map +1 -1
  18. package/dist/src/models/identities.d.ts +523 -0
  19. package/dist/src/models/identities.js +57 -0
  20. package/dist/src/models/identities.js.map +1 -0
  21. package/dist/src/models/publicKey.d.ts +0 -1
  22. package/dist/src/models/publicKey.js +1 -1
  23. package/dist/src/models/publicKey.js.map +1 -1
  24. package/dist/src/repositories/cachedPublicKeyRepository.js.map +1 -1
  25. package/dist/src/repositories/publicKeyRepository.js.map +1 -1
  26. package/dist/src/userValidator.js.map +1 -1
  27. package/dist/src/userValidator.test.js.map +1 -1
  28. package/dist/src/utils/createIdentityFromJwt.d.ts +39 -0
  29. package/dist/src/utils/createIdentityFromJwt.js +464 -0
  30. package/dist/src/utils/createIdentityFromJwt.js.map +1 -0
  31. package/dist/src/utils/createIdentityFromJwt.test.d.ts +1 -0
  32. package/dist/src/utils/createIdentityFromJwt.test.js +1433 -0
  33. package/dist/src/utils/createIdentityFromJwt.test.js.map +1 -0
  34. package/dist/src/utils/jwtMock.js.map +1 -1
  35. package/dist/src/utils/logger.js +2 -3
  36. package/dist/src/utils/logger.js.map +1 -1
  37. package/dist/src/utils/testingConfigurations.js +1 -2
  38. package/dist/src/utils/testingConfigurations.js.map +1 -1
  39. package/package.json +30 -30
  40. package/src/index.ts +1 -0
  41. package/src/models/identities.ts +621 -0
  42. package/src/utils/createIdentityFromJwt.test.ts +1595 -0
  43. package/src/utils/createIdentityFromJwt.ts +540 -0
@@ -0,0 +1,540 @@
1
+ import {
2
+ AccountProfile,
3
+ AnonymousIdentity,
4
+ CitizenIdentity,
5
+ EmployeeIdentity,
6
+ ExternalUserIdentity,
7
+ GenericUserIdentity,
8
+ GuestUserIdentity,
9
+ Identity,
10
+ ServiceAccountIdentity,
11
+ UnknownIdentity,
12
+ UnknownUserIdentity,
13
+ UserServiceAccountIdentity,
14
+ } from '../models/identities';
15
+
16
+ const usernameClaimName = 'userName';
17
+
18
+ /**
19
+ * creates a specific type of Identity from the submitted JWT.
20
+ * @param jwt specifies the JWT issued by TokenAPI
21
+ * @returns a specific type of Identity
22
+ * @description The purpose of this function is to classify the type of user behind the access token and select the right claim
23
+ * for the unique ID of the account, as we need the most stable ID as possible.
24
+ *
25
+ * There is a displayName attribute when you need to display the identity in a GUI.
26
+ *
27
+ * Also, we provide a toString() function to format a verbose representation of the identity, used to audit as much information as possible.
28
+ * @example
29
+ * const identity = createIdentityFromJwt(req.jwt);
30
+ * product.name = newName;
31
+ * product.modifiedBy = identity.id; // udoejo3
32
+ * @example
33
+ * const identity = createIdentityFromJwt(req.jwt);
34
+ * console.log(`Order created by "${identity}"`); // Order created by "user:employee:udoejo3:John DOE:john.doe@montreal.ca:100674051:421408000000:vdm"
35
+ * @example
36
+ * const identity = createIdentityFromJwt(req.jwt);
37
+ * res.send({ message: `Welcome back ${identity.displayName}`}); // { message: "Welcome back John DOE" }
38
+ * @example
39
+ * const identity = createIdentityFromJwt(req.jwt);
40
+ * if (identity.type === 'user' && identity.attributes.type === 'employee') {
41
+ * const employeeRecord = await employeeAPI.findEmployee(identity.attributes.registrationNumber);
42
+ * } else {
43
+ * throw new Error(`Expected an employee but received "${identity}"`);
44
+ * }
45
+ * @example
46
+ * const identity = createIdentityFromJwt(req.jwt);
47
+ * if (identity.type === 'user') {
48
+ * if (identity.attributes.email) {
49
+ * emailService.send(identity.attributes.email, 'Welcome', 'Bla bla...');
50
+ * } else {
51
+ * throw new Error(`User "${identity}" has no email`);
52
+ * }
53
+ * }
54
+ */
55
+ export function createIdentityFromJwt(jwt: any): Identity {
56
+ if (jwt === null || jwt === undefined) {
57
+ throw new Error('"jwt" parameter is required');
58
+ }
59
+ const issuer = getStringClaim(jwt, 'iss');
60
+ const realm = getStringClaim(jwt, 'realm');
61
+ const aud = getStringClaim(jwt, 'aud');
62
+ const sub = getStringClaim(jwt, 'sub');
63
+ const oid = getOptionalStringClaim(jwt, 'oid');
64
+ const env = getOptionalStringClaim(jwt, 'env');
65
+ const userType = getOptionalStringClaim(jwt, 'userType') ?? 'citizen';
66
+ const accessTokenIssuer = getOptionalStringClaim(jwt, 'accessTokenIssuer');
67
+ const isGenericUser = jwt.isGenericAccount;
68
+
69
+ //----------< Anonymous user >-----------------------------------------
70
+ if (userType === 'anonymous') {
71
+ const type = 'anonymous';
72
+ if (realm !== 'anonymous') {
73
+ throw new Error(`${type}: expected token to belong to the "anonymous" realm`);
74
+ }
75
+ const username = getStringClaim(jwt, usernameClaimName, type);
76
+ const result: AnonymousIdentity = {
77
+ type,
78
+ id: username,
79
+ displayName: getStringClaim(jwt, 'name', type),
80
+ attributes: {
81
+ type: 'anonymous',
82
+ username,
83
+ },
84
+ source: {
85
+ aud,
86
+ issuer,
87
+ accessTokenIssuer,
88
+ env,
89
+ realm,
90
+ claim: usernameClaimName,
91
+ internalId: sub,
92
+ },
93
+ toString(this: AnonymousIdentity) {
94
+ return encodeComponents(this.type, this.id, this.displayName);
95
+ },
96
+ };
97
+ return result;
98
+ }
99
+ //----------< Client Service account >-----------------------------------------
100
+ if (userType === 'client') {
101
+ const type = 'service-account';
102
+ const subType = 'client';
103
+ const result: ServiceAccountIdentity = {
104
+ type,
105
+ id: aud, // Gluu clientID or Azure appId
106
+ displayName: getStringClaim(jwt, 'displayName', type, subType),
107
+ attributes: {
108
+ type: subType,
109
+ },
110
+ source: {
111
+ aud,
112
+ issuer,
113
+ accessTokenIssuer,
114
+ env,
115
+ realm,
116
+ claim: 'aud',
117
+ internalId: oid ?? sub,
118
+ },
119
+ toString(this: ServiceAccountIdentity) {
120
+ return encodeComponents(this.type, this.attributes.type, this.id, this.displayName);
121
+ },
122
+ };
123
+ return result;
124
+ }
125
+ //----------< User Service account >-----------------------------------------
126
+ if (userType === 'serviceAccount') {
127
+ const type = 'service-account';
128
+ const subType = 'user';
129
+ const username = getStringClaim(jwt, usernameClaimName, type, subType);
130
+ const result: UserServiceAccountIdentity = {
131
+ type,
132
+ id: username,
133
+ displayName: getStringClaim(jwt, 'name', type, subType),
134
+ attributes: {
135
+ type: subType,
136
+ username,
137
+ },
138
+ source: {
139
+ aud,
140
+ issuer,
141
+ accessTokenIssuer,
142
+ env,
143
+ realm,
144
+ claim: usernameClaimName,
145
+ internalId: sub,
146
+ },
147
+ toString(this: UserServiceAccountIdentity) {
148
+ return encodeComponents(this.type, this.attributes.type, this.id, this.displayName);
149
+ },
150
+ };
151
+ return result;
152
+ }
153
+ //----------< Citizen >-----------------------------------------
154
+ if (userType === 'citizen') {
155
+ const type = 'user';
156
+ const subType = 'citizen';
157
+ if (realm !== 'citizens') {
158
+ throw new Error(`${type}:${subType}: expected token to belong to the "citizens" realm`);
159
+ }
160
+ const result: CitizenIdentity = {
161
+ type,
162
+ id: getStringClaim(jwt, 'mtlIdentityId', type, subType),
163
+ displayName: getStringClaim(jwt, 'name', type, subType),
164
+ attributes: {
165
+ type: subType,
166
+ username: getStringClaim(jwt, usernameClaimName, type, subType),
167
+ email: getStringClaim(jwt, 'email', type, subType),
168
+ firstName: getStringClaim(jwt, 'givenName', type, subType),
169
+ lastName: getStringClaim(jwt, 'familyName', type, subType),
170
+ },
171
+ source: {
172
+ aud,
173
+ issuer,
174
+ accessTokenIssuer,
175
+ env,
176
+ realm,
177
+ claim: 'mtlIdentityId',
178
+ internalId: oid ?? sub,
179
+ },
180
+ toString(this: CitizenIdentity) {
181
+ return encodeComponents(
182
+ this.type,
183
+ this.attributes.type,
184
+ this.id,
185
+ this.displayName,
186
+ this.attributes.email
187
+ );
188
+ },
189
+ };
190
+ return result;
191
+ }
192
+ //----------< Generic user >-----------------------------------------
193
+ if (isGenericUser === true) {
194
+ const type = 'user';
195
+ const subType = 'generic-user';
196
+ const username = getStringClaim(jwt, usernameClaimName, type, subType);
197
+ const result: GenericUserIdentity = {
198
+ type,
199
+ id: username,
200
+ displayName: getStringClaim(jwt, 'name', type, subType),
201
+ attributes: {
202
+ type: 'generic',
203
+ username,
204
+ email: getOptionalStringClaim(jwt, 'email'),
205
+ department: getOptionalStringClaim(jwt, 'department'),
206
+ firstName: getStringClaim(jwt, 'givenName', type, subType),
207
+ lastName: getStringClaim(jwt, 'familyName', type, subType),
208
+ accountProfile: getAccountProfile(jwt),
209
+ },
210
+ source: {
211
+ aud,
212
+ issuer,
213
+ accessTokenIssuer,
214
+ env,
215
+ realm,
216
+ claim: usernameClaimName,
217
+ internalId: oid ?? sub,
218
+ },
219
+ toString(this: GenericUserIdentity) {
220
+ return encodeComponents(
221
+ this.type,
222
+ this.attributes.type,
223
+ this.id,
224
+ this.displayName,
225
+ this.attributes.email,
226
+ this.attributes.department,
227
+ this.attributes.accountProfile
228
+ );
229
+ },
230
+ };
231
+ return result;
232
+ }
233
+ //----------< Employee >-----------------------------------------
234
+ if (userType === 'employee' && isEmployee(jwt)) {
235
+ const type = 'user';
236
+ const subType = 'employee';
237
+ if (realm !== 'employees') {
238
+ throw new Error(`${type}:${subType}: expected token to belong to the "employees" realm`);
239
+ }
240
+ const username = getStringClaim(jwt, usernameClaimName, type, subType);
241
+ const result: EmployeeIdentity = {
242
+ type,
243
+ id: username,
244
+ displayName: getStringClaim(jwt, 'name', type, subType),
245
+ attributes: {
246
+ type: subType,
247
+ email: getStringClaim(jwt, 'email', type, subType),
248
+ username,
249
+ registrationNumber: getStringClaim(jwt, 'employeeNumber', type, subType),
250
+ department: getStringClaim(jwt, 'department', type, subType),
251
+ firstName: getStringClaim(jwt, 'givenName', type, subType),
252
+ lastName: getStringClaim(jwt, 'familyName', type, subType),
253
+ accountProfile: getAccountProfile(jwt),
254
+ },
255
+ source: {
256
+ aud,
257
+ issuer,
258
+ accessTokenIssuer,
259
+ env,
260
+ realm,
261
+ claim: usernameClaimName,
262
+ internalId: oid ?? sub,
263
+ },
264
+ toString(this: EmployeeIdentity) {
265
+ return encodeComponents(
266
+ this.type,
267
+ this.attributes.type,
268
+ this.id,
269
+ this.displayName,
270
+ this.attributes.email,
271
+ this.attributes.registrationNumber,
272
+ this.attributes.department,
273
+ this.attributes.accountProfile
274
+ );
275
+ },
276
+ };
277
+ return result;
278
+ }
279
+ //----------< External user >-----------------------------------------
280
+ if (userType === 'employee' && isExternalUser(jwt)) {
281
+ const type = 'user';
282
+ const subType = 'external';
283
+ if (realm !== 'employees') {
284
+ throw new Error(`${type}:${subType}: expected token to belong to the "employees" realm`);
285
+ }
286
+ const username = getStringClaim(jwt, usernameClaimName, type, subType);
287
+ const result: ExternalUserIdentity = {
288
+ type,
289
+ id: username,
290
+ displayName: getStringClaim(jwt, 'name', type, subType),
291
+ attributes: {
292
+ type: subType,
293
+ email: getOptionalStringClaim(jwt, 'email'),
294
+ username,
295
+ department: getOptionalStringClaim(jwt, 'department'),
296
+ firstName: getStringClaim(jwt, 'givenName', type, subType),
297
+ lastName: getStringClaim(jwt, 'familyName', type, subType),
298
+ accountProfile: getAccountProfile(jwt),
299
+ },
300
+ source: {
301
+ aud,
302
+ issuer,
303
+ accessTokenIssuer,
304
+ env,
305
+ realm,
306
+ claim: usernameClaimName,
307
+ internalId: oid ?? sub,
308
+ },
309
+ toString(this: ExternalUserIdentity) {
310
+ return encodeComponents(
311
+ this.type,
312
+ this.attributes.type,
313
+ this.id,
314
+ this.displayName,
315
+ this.attributes.email,
316
+ this.attributes.department,
317
+ this.attributes.accountProfile
318
+ );
319
+ },
320
+ };
321
+ return result;
322
+ }
323
+ //----------< Guest user >-----------------------------------------
324
+ if (isGuestUser(jwt)) {
325
+ const type = 'user';
326
+ const subType = 'guest-user';
327
+ const username = getStringClaim(jwt, usernameClaimName, type, subType);
328
+ const result: GuestUserIdentity = {
329
+ type,
330
+ id: username,
331
+ displayName: getStringClaim(jwt, 'name', type, subType),
332
+ attributes: {
333
+ type: 'guest',
334
+ email: getStringClaim(jwt, 'email', type, subType),
335
+ username,
336
+ department: getOptionalStringClaim(jwt, 'department'),
337
+ firstName: getOptionalStringClaim(jwt, 'givenName'),
338
+ lastName: getOptionalStringClaim(jwt, 'familyName'),
339
+ accountProfile: getAccountProfile(jwt),
340
+ },
341
+ source: {
342
+ aud,
343
+ issuer,
344
+ accessTokenIssuer,
345
+ env,
346
+ realm,
347
+ claim: usernameClaimName,
348
+ internalId: oid ?? sub,
349
+ },
350
+ toString(this: GuestUserIdentity) {
351
+ return encodeComponents(
352
+ this.type,
353
+ this.attributes.type,
354
+ realm,
355
+ this.id,
356
+ this.displayName,
357
+ this.attributes.email
358
+ );
359
+ },
360
+ };
361
+ return result;
362
+ }
363
+ //----------< Unknown user type >-----------------------------------------
364
+ const username = getOptionalStringClaim(jwt, usernameClaimName);
365
+ const email = getOptionalStringClaim(jwt, 'email');
366
+ if (username || email) {
367
+ const type = 'user';
368
+ const subType = 'unknown';
369
+ const claim = username ? usernameClaimName : 'email';
370
+ const result: UnknownUserIdentity = {
371
+ type,
372
+ id: getStringClaim(jwt, claim, type, subType),
373
+ displayName: getOptionalStringClaim(jwt, 'name') ?? email ?? username,
374
+ attributes: {
375
+ type: subType,
376
+ email: getOptionalStringClaim(jwt, 'email'),
377
+ username,
378
+ registrationNumber: getOptionalStringClaim(jwt, 'employeeNumber'),
379
+ department: getOptionalStringClaim(jwt, 'department'),
380
+ firstName: getOptionalStringClaim(jwt, 'givenName'),
381
+ lastName: getOptionalStringClaim(jwt, 'familyName'),
382
+ accountProfile: getAccountProfile(jwt),
383
+ },
384
+ source: {
385
+ aud,
386
+ issuer,
387
+ accessTokenIssuer,
388
+ env,
389
+ realm,
390
+ claim: claim,
391
+ internalId: oid ?? sub,
392
+ },
393
+ toString(this: UnknownUserIdentity) {
394
+ return encodeComponents(
395
+ this.type,
396
+ this.attributes.type,
397
+ this.id,
398
+ this.displayName,
399
+ this.attributes.email,
400
+ this.attributes.registrationNumber,
401
+ this.attributes.department,
402
+ this.attributes.accountProfile
403
+ );
404
+ },
405
+ };
406
+ return result;
407
+ }
408
+ //----------< Unknown identity type >-----------------------------------------
409
+ const result: UnknownIdentity = {
410
+ type: 'unknown',
411
+ id: sub,
412
+ displayName: getOptionalStringClaim(jwt, 'name') ?? 'unknown',
413
+ attributes: {
414
+ type: 'unknown',
415
+ },
416
+ source: {
417
+ aud,
418
+ issuer,
419
+ accessTokenIssuer,
420
+ env,
421
+ realm,
422
+ claim: 'sub',
423
+ internalId: oid ?? sub,
424
+ },
425
+ toString(this: UnknownIdentity) {
426
+ return encodeComponents(this.type, this.id, this.displayName);
427
+ },
428
+ };
429
+ return result;
430
+ }
431
+
432
+ function getOptionalStringClaim(jwt: any, name: string): string | undefined {
433
+ const result = jwt[name];
434
+ if (result === undefined || result === null) {
435
+ return undefined;
436
+ }
437
+ if (typeof result !== 'string') {
438
+ throw new Error(`Expected claim '${name}' to contain a string but received: ${result}`);
439
+ }
440
+ return result;
441
+ }
442
+
443
+ function getStringClaim(
444
+ jwt: any,
445
+ name: string,
446
+ identityType?: string,
447
+ identitySubType?: string
448
+ ): string | undefined {
449
+ const result = getOptionalStringClaim(jwt, name);
450
+ if (!result) {
451
+ const subType = identitySubType ? `${identitySubType}: ` : '';
452
+ const prefix = (identityType ? `${identityType}: ` : '') + subType;
453
+ throw new Error(`${prefix}expected to find the "${name}" claim in the JWT`);
454
+ }
455
+ return result;
456
+ }
457
+
458
+ function encodeComponents(...components: string[]): string {
459
+ return components.map((x) => (x ?? '').replace(':', '_')).join(':');
460
+ }
461
+
462
+ function getAccountProfile(jwt: any): AccountProfile {
463
+ const email = getOptionalStringClaim(jwt, 'email');
464
+ if (email) {
465
+ if (email.endsWith('@spvm.qc.ca')) {
466
+ return 'spvm';
467
+ }
468
+ if (email.endsWith('.adm@lavilledemontreal.omnicrosoft.com')) {
469
+ return 'vdm-admin';
470
+ }
471
+ if (email.toLocaleLowerCase().endsWith('.adm@montrealville.omnicrosoft.com')) {
472
+ return 'vdm-admin';
473
+ }
474
+ }
475
+ return 'vdm';
476
+ }
477
+
478
+ function isValidCodeU(code: string): boolean {
479
+ const re = /^u[a-z0-9]{4,22}$/gi;
480
+ return re.test(code);
481
+ }
482
+
483
+ function isEmployee(jwt: any): boolean {
484
+ const username = getOptionalStringClaim(jwt, usernameClaimName);
485
+ if (!username) {
486
+ return false;
487
+ }
488
+ const employeeNumber = getOptionalStringClaim(jwt, 'employeeNumber');
489
+ const department = getOptionalStringClaim(jwt, 'department');
490
+ const name = getOptionalStringClaim(jwt, 'name');
491
+ const firstName = getOptionalStringClaim(jwt, 'givenName');
492
+ const lastName = getOptionalStringClaim(jwt, 'familyName');
493
+ const isCodeU = isValidCodeU(username);
494
+ const hasEmployeeNumber = !!employeeNumber;
495
+ const hasDepartment = !!department;
496
+ const hasNames = !!name && !!firstName && !!lastName;
497
+ return isCodeU && hasEmployeeNumber && hasDepartment && hasNames;
498
+ }
499
+
500
+ function isValidCodeX(code: string): boolean {
501
+ const re = /^x[a-z0-9]{4,22}$/gi;
502
+ return re.test(code);
503
+ }
504
+
505
+ function isExternalUser(jwt: any): boolean {
506
+ const username = getOptionalStringClaim(jwt, usernameClaimName);
507
+ if (!username) {
508
+ return false;
509
+ }
510
+ const name = getOptionalStringClaim(jwt, 'name');
511
+ const firstName = getOptionalStringClaim(jwt, 'givenName');
512
+ const lastName = getOptionalStringClaim(jwt, 'familyName');
513
+ const hasNames = !!name && !!firstName && !!lastName;
514
+ if (!hasNames) {
515
+ return false;
516
+ }
517
+ const email = getOptionalStringClaim(jwt, 'email');
518
+ const isCodeX = isValidCodeX(username);
519
+ const isExtEmail = email && email.toLocaleLowerCase().includes('.ext@');
520
+ return isCodeX || isExtEmail;
521
+ }
522
+
523
+ function isGuestUser(jwt: any): boolean {
524
+ const username = getOptionalStringClaim(jwt, usernameClaimName);
525
+ if (!username) {
526
+ return false;
527
+ }
528
+ const email = getOptionalStringClaim(jwt, 'email');
529
+ if (!email) {
530
+ return false;
531
+ }
532
+ const name = getOptionalStringClaim(jwt, 'name');
533
+ if (!name) {
534
+ return false;
535
+ }
536
+ return (
537
+ username.endsWith('#EXT#@lavilledemontreal.omnicrosoft.com') ||
538
+ username.endsWith('#EXT#@MontrealVille.onmicrosoft.com')
539
+ );
540
+ }