@opengovsg/mockpass 3.1.2 → 3.1.3
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/.eslintrc.json +1 -1
- package/lib/auth-code.js +2 -2
- package/lib/express/sgid.js +80 -34
- package/package.json +1 -1
package/.eslintrc.json
CHANGED
package/lib/auth-code.js
CHANGED
|
@@ -4,9 +4,9 @@ const crypto = require('crypto')
|
|
|
4
4
|
const AUTH_CODE_TIMEOUT = 5 * 60 * 1000
|
|
5
5
|
const profileAndNonceStore = new ExpiryMap(AUTH_CODE_TIMEOUT)
|
|
6
6
|
|
|
7
|
-
const generateAuthCode = ({ profile, nonce }) => {
|
|
7
|
+
const generateAuthCode = ({ profile, scopes, nonce }) => {
|
|
8
8
|
const authCode = crypto.randomBytes(45).toString('base64')
|
|
9
|
-
profileAndNonceStore.set(authCode, { profile, nonce })
|
|
9
|
+
profileAndNonceStore.set(authCode, { profile, scopes, nonce })
|
|
10
10
|
return authCode
|
|
11
11
|
}
|
|
12
12
|
|
package/lib/express/sgid.js
CHANGED
|
@@ -35,11 +35,13 @@ function config(app, { showLoginPage, serviceProvider }) {
|
|
|
35
35
|
|
|
36
36
|
app.get(`${PATH_PREFIX}/authorize`, (req, res) => {
|
|
37
37
|
const { redirect_uri: redirectURI, state, nonce } = req.query
|
|
38
|
+
const scopes = req.query.scope ?? 'openid'
|
|
39
|
+
console.info(`Requested scope ${scopes}`)
|
|
38
40
|
if (showLoginPage(req)) {
|
|
39
41
|
const values = profiles
|
|
40
42
|
.filter((profile) => assertions.myinfo.v3.personas[profile.nric])
|
|
41
43
|
.map((profile) => {
|
|
42
|
-
const authCode = generateAuthCode({ profile, nonce })
|
|
44
|
+
const authCode = generateAuthCode({ profile, scopes, nonce })
|
|
43
45
|
const assertURL = buildAssertURL(redirectURI, authCode, state)
|
|
44
46
|
const id = idGenerator.singPass(profile)
|
|
45
47
|
return { id, assertURL }
|
|
@@ -48,9 +50,9 @@ function config(app, { showLoginPage, serviceProvider }) {
|
|
|
48
50
|
res.send(response)
|
|
49
51
|
} else {
|
|
50
52
|
const profile = defaultProfile
|
|
51
|
-
const authCode = generateAuthCode({ profile, nonce })
|
|
53
|
+
const authCode = generateAuthCode({ profile, scopes, nonce })
|
|
52
54
|
const assertURL = buildAssertURL(redirectURI, authCode, state)
|
|
53
|
-
console.
|
|
55
|
+
console.info(
|
|
54
56
|
`Redirecting login from ${req.query.client_id} to ${assertURL}`,
|
|
55
57
|
)
|
|
56
58
|
res.redirect(assertURL)
|
|
@@ -65,13 +67,16 @@ function config(app, { showLoginPage, serviceProvider }) {
|
|
|
65
67
|
console.log(req.body)
|
|
66
68
|
const { client_id: aud, code: authCode } = req.body
|
|
67
69
|
|
|
68
|
-
console.
|
|
70
|
+
console.info(
|
|
69
71
|
`Received auth code ${authCode} from ${aud} and ${req.body.redirect_uri}`,
|
|
70
72
|
)
|
|
71
|
-
try {
|
|
72
|
-
const { profile, nonce } = lookUpByAuthCode(authCode)
|
|
73
73
|
|
|
74
|
-
|
|
74
|
+
try {
|
|
75
|
+
const { profile, scopes, nonce } = lookUpByAuthCode(authCode)
|
|
76
|
+
console.info(
|
|
77
|
+
`Profile ${JSON.stringify(profile)} with token scope ${scopes}`,
|
|
78
|
+
)
|
|
79
|
+
const accessToken = authCode
|
|
75
80
|
const iss = `${req.protocol}://${req.get('host')}`
|
|
76
81
|
|
|
77
82
|
const { idTokenClaims, refreshToken } = assertions.oidc.create.singPass(
|
|
@@ -97,7 +102,7 @@ function config(app, { showLoginPage, serviceProvider }) {
|
|
|
97
102
|
access_token: accessToken,
|
|
98
103
|
refresh_token: refreshToken,
|
|
99
104
|
expires_in: 24 * 60 * 60,
|
|
100
|
-
scope:
|
|
105
|
+
scope: scopes,
|
|
101
106
|
token_type: 'Bearer',
|
|
102
107
|
id_token: idToken,
|
|
103
108
|
})
|
|
@@ -109,45 +114,44 @@ function config(app, { showLoginPage, serviceProvider }) {
|
|
|
109
114
|
)
|
|
110
115
|
|
|
111
116
|
app.get(`${PATH_PREFIX}/userinfo`, async (req, res) => {
|
|
112
|
-
const
|
|
117
|
+
const authCode = (
|
|
113
118
|
req.headers.authorization || req.headers.Authorization
|
|
114
119
|
).replace('Bearer ', '')
|
|
120
|
+
// eslint-disable-next-line no-unused-vars
|
|
121
|
+
const { profile, scopes, unused } = lookUpByAuthCode(authCode)
|
|
122
|
+
const uuid = profile.uuid
|
|
115
123
|
const nric = assertions.oidc.singPass.find((p) => p.uuid === uuid).nric
|
|
116
124
|
const persona = assertions.myinfo.v3.personas[nric]
|
|
117
|
-
const name = persona.name.value
|
|
118
|
-
const dateOfBirth = persona.dob.value
|
|
119
125
|
|
|
126
|
+
console.info(`userinfo scopes ${scopes}`)
|
|
120
127
|
const payloadKey = await jose.JWK.createKey('oct', 256, {
|
|
121
128
|
alg: 'A256GCM',
|
|
122
129
|
})
|
|
123
130
|
|
|
124
|
-
const
|
|
125
|
-
{ format: 'compact' },
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
.update(nric)
|
|
129
|
-
.final()
|
|
130
|
-
const encryptedName = await jose.JWE.createEncrypt(
|
|
131
|
-
{ format: 'compact' },
|
|
132
|
-
payloadKey,
|
|
133
|
-
)
|
|
134
|
-
.update(name)
|
|
135
|
-
.final()
|
|
136
|
-
const encryptedDateOfBirth = await jose.JWE.createEncrypt(
|
|
137
|
-
{ format: 'compact' },
|
|
138
|
-
payloadKey,
|
|
139
|
-
)
|
|
140
|
-
.update(dateOfBirth)
|
|
141
|
-
.final()
|
|
142
|
-
const data = {
|
|
143
|
-
'myinfo.nric_number': encryptedNric,
|
|
144
|
-
'myinfo.name': encryptedName,
|
|
145
|
-
'myinfo.date_of_birth': encryptedDateOfBirth,
|
|
131
|
+
const encryptPayload = async (field) => {
|
|
132
|
+
return await jose.JWE.createEncrypt({ format: 'compact' }, payloadKey)
|
|
133
|
+
.update(field)
|
|
134
|
+
.final()
|
|
146
135
|
}
|
|
136
|
+
const encryptedNric = await encryptPayload(nric)
|
|
137
|
+
const scopesArr = scopes
|
|
138
|
+
.split(' ')
|
|
139
|
+
.filter((field) => field !== 'openid' && field !== 'myinfo.nric_number')
|
|
140
|
+
console.info(`userinfo scopesArr ${scopesArr}`)
|
|
141
|
+
const myInfoFields = await Promise.all(
|
|
142
|
+
scopesArr.map((scope) =>
|
|
143
|
+
encryptPayload(sgIDScopeToMyInfoField(persona, scope)),
|
|
144
|
+
),
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
const data = {}
|
|
148
|
+
scopesArr.forEach((name, index) => {
|
|
149
|
+
data[name] = myInfoFields[index]
|
|
150
|
+
})
|
|
151
|
+
data['myinfo.nric_number'] = encryptedNric
|
|
147
152
|
const encryptionKey = await jose.JWK.asKey(serviceProvider.pubKey, 'pem')
|
|
148
153
|
|
|
149
154
|
const plaintextPayloadKey = JSON.stringify(payloadKey.toJSON(true))
|
|
150
|
-
console.log(plaintextPayloadKey)
|
|
151
155
|
const encryptedPayloadKey = await jose.JWE.createEncrypt(
|
|
152
156
|
{ format: 'compact' },
|
|
153
157
|
encryptionKey,
|
|
@@ -179,6 +183,9 @@ function config(app, { showLoginPage, serviceProvider }) {
|
|
|
179
183
|
jwks_uri: `${issuer}/.well-known/jwks.json`,
|
|
180
184
|
response_types_supported: ['code'],
|
|
181
185
|
grant_types_supported: ['authorization_code'],
|
|
186
|
+
// Note: some of these scopes are not yet officially documented
|
|
187
|
+
// in https://docs.id.gov.sg/data-catalog
|
|
188
|
+
// So they are not officially supported yet.
|
|
182
189
|
scopes_supported: [
|
|
183
190
|
'openid',
|
|
184
191
|
'myinfo.nric_number',
|
|
@@ -203,4 +210,43 @@ function config(app, { showLoginPage, serviceProvider }) {
|
|
|
203
210
|
})
|
|
204
211
|
}
|
|
205
212
|
|
|
213
|
+
const concatMyInfoRegAddr = (regadd) => {
|
|
214
|
+
const line1 =
|
|
215
|
+
!!regadd.block.value || !!regadd.street.value
|
|
216
|
+
? `${regadd.block.value} ${regadd.street.value}`
|
|
217
|
+
: ''
|
|
218
|
+
const line2 =
|
|
219
|
+
!!regadd.floor.value || !!regadd.unit.value
|
|
220
|
+
? `#${regadd.floor.value}-${regadd.unit.value}`
|
|
221
|
+
: ''
|
|
222
|
+
const line3 =
|
|
223
|
+
!!regadd.country.desc || !!regadd.postal.value
|
|
224
|
+
? `${regadd.country.desc} ${regadd.postal.value}`
|
|
225
|
+
: ''
|
|
226
|
+
return `${line1}\n${line2}\n${line3}`
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Refer to https://docs.id.gov.sg/data-catalog
|
|
230
|
+
const sgIDScopeToMyInfoField = (persona, scope) => {
|
|
231
|
+
switch (scope) {
|
|
232
|
+
// No NRIC as that is always returned by default
|
|
233
|
+
case 'myinfo.name':
|
|
234
|
+
return persona.name.value
|
|
235
|
+
case 'myinfo.email':
|
|
236
|
+
return persona.email.value
|
|
237
|
+
case 'myinfo.mobile_number':
|
|
238
|
+
return persona.mobileno.nbr.value
|
|
239
|
+
case 'myinfo.registered_address':
|
|
240
|
+
return concatMyInfoRegAddr(persona.regadd)
|
|
241
|
+
case 'myinfo.date_of_birth':
|
|
242
|
+
return persona.dob.value
|
|
243
|
+
case 'myinfo.passport_number':
|
|
244
|
+
return persona.passportnumber.value
|
|
245
|
+
case 'myinfo.passport_expiry_date':
|
|
246
|
+
return persona.passportexpirydate.value
|
|
247
|
+
default:
|
|
248
|
+
return ''
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
206
252
|
module.exports = config
|