@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.
- package/lib/express/oidc/v2-ndi.js +59 -23
- package/lib/express/sgid.js +66 -8
- package/package.json +2 -2
|
@@ -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
|
|
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 ===
|
|
68
|
-
(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 {
|
|
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
|
|
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
|
|
120
|
+
return encryptionKey
|
|
84
121
|
}
|
|
85
122
|
if (!encryptionKey) {
|
|
86
|
-
encryptionKey = jwks
|
|
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
|
|
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
|
|
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:
|
|
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(
|
|
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({
|
package/lib/express/sgid.js
CHANGED
|
@@ -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
|
|
270
|
+
return defaultUndefinedToNA(persona.uuid?.value)
|
|
241
271
|
case 'myinfo.name':
|
|
242
|
-
return persona.name
|
|
272
|
+
return defaultUndefinedToNA(persona.name?.value)
|
|
243
273
|
case 'myinfo.email':
|
|
244
|
-
return persona.email
|
|
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
|
|
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
|
|
284
|
+
return defaultUndefinedToNA(persona.dob?.value)
|
|
251
285
|
case 'myinfo.passport_number':
|
|
252
|
-
return persona.passportnumber
|
|
286
|
+
return defaultUndefinedToNA(persona.passportnumber?.value)
|
|
253
287
|
case 'myinfo.passport_expiry_date':
|
|
254
|
-
return persona.passportexpirydate
|
|
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.
|
|
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": "^
|
|
62
|
+
"lint-staged": "^14.0.1",
|
|
63
63
|
"nodemon": "^3.0.1",
|
|
64
64
|
"pinst": "^3.0.0",
|
|
65
65
|
"prettier": "^2.0.5"
|