@opengovsg/mockpass 4.0.9 → 4.0.11

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.
@@ -59,47 +59,80 @@ const id_token_encryption_alg_values_supported = {
59
59
  corpPass: corppass_id_token_encryption_alg_values_supported,
60
60
  }
61
61
 
62
- function findEncryptionKey(jwks) {
62
+ function findEcdhEsEncryptionKey(jwks, crv, algs) {
63
63
  let encryptionKey = jwks.keys.find(
64
64
  (item) =>
65
65
  item.use === 'enc' &&
66
66
  item.kty === 'EC' &&
67
- item.crv === 'P-521' &&
68
- (item.alg === 'ECDH-ES+A256KW' || !item.alg),
67
+ item.crv === crv &&
68
+ (!item.alg ||
69
+ (item.alg === 'ECDH-ES+A256KW' &&
70
+ algs.some((alg) => alg === item.alg))),
69
71
  )
70
72
  if (encryptionKey) {
71
- return { ...encryptionKey, alg: 'ECDH-ES+A256KW' }
73
+ return {
74
+ ...encryptionKey,
75
+ ...(!encryptionKey.alg ? { alg: 'ECDH-ES+A256KW' } : {}),
76
+ }
77
+ }
78
+ encryptionKey = jwks.keys.find(
79
+ (item) =>
80
+ item.use === 'enc' &&
81
+ item.kty === 'EC' &&
82
+ item.crv === crv &&
83
+ (!item.alg ||
84
+ (item.alg === 'ECDH-ES+A192KW' &&
85
+ algs.some((alg) => alg === item.alg))),
86
+ )
87
+ if (encryptionKey) {
88
+ return {
89
+ ...encryptionKey,
90
+ ...(!encryptionKey.alg ? { alg: 'ECDH-ES+A256KW' } : {}),
91
+ }
92
+ }
93
+ encryptionKey = jwks.keys.find(
94
+ (item) =>
95
+ item.use === 'enc' &&
96
+ item.kty === 'EC' &&
97
+ item.crv === crv &&
98
+ (!item.alg ||
99
+ (item.alg === 'ECDH-ES+A128KW' &&
100
+ algs.some((alg) => alg === item.alg))),
101
+ )
102
+ if (encryptionKey) {
103
+ return {
104
+ ...encryptionKey,
105
+ ...(!encryptionKey.alg ? { alg: 'ECDH-ES+A256KW' } : {}),
106
+ }
107
+ }
108
+ return null
109
+ }
110
+
111
+ function findEncryptionKey(jwks, algs) {
112
+ let encryptionKey = findEcdhEsEncryptionKey(jwks, 'P-521', algs)
113
+ if (encryptionKey) {
114
+ return encryptionKey
72
115
  }
73
116
  if (!encryptionKey) {
74
- encryptionKey = jwks.keys.find(
75
- (item) =>
76
- item.use === 'enc' &&
77
- item.kty === 'EC' &&
78
- item.crv === 'P-384' &&
79
- (item.alg === 'ECDH-ES+A192KW' || !item.alg),
80
- )
117
+ encryptionKey = findEcdhEsEncryptionKey(jwks, 'P-384', algs)
81
118
  }
82
119
  if (encryptionKey) {
83
- return { ...encryptionKey, alg: 'ECDH-ES+A192KW' }
120
+ return encryptionKey
84
121
  }
85
122
  if (!encryptionKey) {
86
- encryptionKey = jwks.keys.find(
87
- (item) =>
88
- item.use === 'enc' &&
89
- item.kty === 'EC' &&
90
- item.crv === 'P-256' &&
91
- (item.alg === 'ECDH-ES+A128KW' || !item.alg),
92
- )
123
+ encryptionKey = findEcdhEsEncryptionKey(jwks, 'P-256', algs)
93
124
  }
94
125
  if (encryptionKey) {
95
- return { ...encryptionKey, alg: 'ECDH-ES+A128KW' }
126
+ return encryptionKey
96
127
  }
97
128
  if (!encryptionKey) {
98
129
  encryptionKey = jwks.keys.find(
99
130
  (item) =>
100
131
  item.use === 'enc' &&
101
132
  item.kty === 'RSA' &&
102
- (item.alg === 'RSA-OAEP-256' || !item.alg),
133
+ (!item.alg ||
134
+ (item.alg === 'RSA-OAEP-256' &&
135
+ algs.some((alg) => alg === item.alg))),
103
136
  )
104
137
  }
105
138
  if (encryptionKey) {
@@ -432,7 +465,7 @@ function config(app, { showLoginPage }) {
432
465
  const signedProtectedHeader = {
433
466
  alg: 'ES256',
434
467
  typ: 'JWT',
435
- kid: signingKey.kid,
468
+ kid: aspSigningKey.kid,
436
469
  }
437
470
  const signedIdToken = await new jose.CompactSign(
438
471
  new TextEncoder().encode(JSON.stringify(idTokenClaims)),
@@ -441,7 +474,10 @@ function config(app, { showLoginPage }) {
441
474
  .sign(signingKey)
442
475
 
443
476
  // Step 4: Encrypt ID token with RP encryption key
444
- const rpEncryptionKey = findEncryptionKey(rpKeysetJson)
477
+ const rpEncryptionKey = findEncryptionKey(
478
+ rpKeysetJson,
479
+ id_token_encryption_alg_values_supported[idp],
480
+ )
445
481
  if (!rpEncryptionKey) {
446
482
  console.error('No suitable encryption key found', rpKeysetJson.keys)
447
483
  return res.status(400).send({
@@ -208,6 +208,13 @@ function config(app, { showLoginPage, serviceProvider }) {
208
208
  'myinfo.residential',
209
209
  'myinfo.housingtype',
210
210
  'myinfo.hdbtype',
211
+ 'myinfo.birth_country',
212
+ 'myinfo.vehicles',
213
+ 'myinfo.name_of_employer',
214
+ 'myinfo.workpass_status',
215
+ 'myinfo.workpass_expiry_date',
216
+ 'myinfo.marital_status',
217
+ 'myinfo.mobile_number_with_country_code',
211
218
  ],
212
219
  id_token_signing_alg_values_supported: ['RS256'],
213
220
  subject_types_supported: ['pairwise'],
@@ -232,28 +239,79 @@ const concatMyInfoRegAddr = (regadd) => {
232
239
  return `${line1}\n${line2}\n${line3}`
233
240
  }
234
241
 
242
+ // Refer to sgid myinfo parser
243
+ const formatMobileNumberWithPrefix = (phone) => {
244
+ if (!phone || !phone.nbr?.value) {
245
+ return 'NA'
246
+ }
247
+ return phone.prefix?.value && phone.areacode?.value
248
+ ? `${phone.prefix?.value}${phone.areacode?.value} ${phone.nbr?.value}`
249
+ : phone.nbr?.value
250
+ }
251
+
252
+ // Refer to sgid myinfo parser
253
+ const formatVehicles = (vehicles) => {
254
+ const vehicleObjects =
255
+ vehicles?.map((vehicle) => ({
256
+ vehicle_number: vehicle.vehicleno?.value || 'NA',
257
+ })) || '[]'
258
+ return vehicleObjects
259
+ }
260
+
261
+ const defaultUndefinedToNA = (value) => {
262
+ return value || 'NA'
263
+ }
264
+
235
265
  // Refer to https://docs.id.gov.sg/data-catalog
236
266
  const sgIDScopeToMyInfoField = (persona, scope) => {
237
267
  switch (scope) {
238
268
  // No NRIC as that is always returned by default
239
269
  case 'openid':
240
- return persona.uuid.value
270
+ return defaultUndefinedToNA(persona.uuid?.value)
241
271
  case 'myinfo.name':
242
- return persona.name.value
272
+ return defaultUndefinedToNA(persona.name?.value)
243
273
  case 'myinfo.email':
244
- return persona.email.value
274
+ return defaultUndefinedToNA(persona.email?.value)
275
+ case 'myinfo.sex':
276
+ return defaultUndefinedToNA(persona.sex?.desc)
277
+ case 'myinfo.race':
278
+ return defaultUndefinedToNA(persona.race?.desc)
245
279
  case 'myinfo.mobile_number':
246
- return persona.mobileno.nbr.value
280
+ return defaultUndefinedToNA(persona.mobileno?.nbr?.value)
247
281
  case 'myinfo.registered_address':
248
282
  return concatMyInfoRegAddr(persona.regadd)
249
283
  case 'myinfo.date_of_birth':
250
- return persona.dob.value
284
+ return defaultUndefinedToNA(persona.dob?.value)
251
285
  case 'myinfo.passport_number':
252
- return persona.passportnumber.value
286
+ return defaultUndefinedToNA(persona.passportnumber?.value)
253
287
  case 'myinfo.passport_expiry_date':
254
- return persona.passportexpirydate.value
288
+ return defaultUndefinedToNA(persona.passportexpirydate?.value)
289
+ case 'myinfo.nationality':
290
+ return defaultUndefinedToNA(persona.nationality?.desc)
291
+ case 'myinfo.residentialstatus':
292
+ return defaultUndefinedToNA(persona.residentialstatus?.desc)
293
+ case 'myinfo.residential':
294
+ return defaultUndefinedToNA(persona.residential?.desc)
295
+ case 'myinfo.housingtype':
296
+ return defaultUndefinedToNA(persona.housingtype?.desc)
297
+ case 'myinfo.hdbtype':
298
+ return defaultUndefinedToNA(persona.hdbtype?.desc)
299
+ case 'myinfo.birth_country':
300
+ return defaultUndefinedToNA(persona.birthcountry?.desc)
301
+ case 'myinfo.vehicles':
302
+ return formatVehicles(persona.vehicles)
303
+ case 'myinfo.name_of_employer':
304
+ return defaultUndefinedToNA(persona.employment?.value)
305
+ case 'myinfo.workpass_status':
306
+ return defaultUndefinedToNA(persona.passstatus?.value)
307
+ case 'myinfo.workpass_expiry_date':
308
+ return defaultUndefinedToNA(persona.passexpirydate?.value)
309
+ case 'myinfo.marital_status':
310
+ return defaultUndefinedToNA(persona.marital?.desc)
311
+ case 'myinfo.mobile_number_with_country_code':
312
+ return formatMobileNumberWithPrefix(persona.mobileno)
255
313
  default:
256
- return ''
314
+ return 'NA'
257
315
  }
258
316
  }
259
317
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opengovsg/mockpass",
3
- "version": "4.0.9",
3
+ "version": "4.0.11",
4
4
  "description": "A mock SingPass/CorpPass server for dev purposes",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -59,7 +59,7 @@
59
59
  "eslint-config-prettier": "^8.3.0",
60
60
  "eslint-plugin-prettier": "^4.0.0",
61
61
  "husky": "^8.0.1",
62
- "lint-staged": "^13.0.3",
62
+ "lint-staged": "^14.0.1",
63
63
  "nodemon": "^3.0.1",
64
64
  "pinst": "^3.0.0",
65
65
  "prettier": "^2.0.5"