@opengovsg/mockpass 4.0.6 → 4.0.8

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.
@@ -5,7 +5,7 @@ const path = require('path')
5
5
  const express = require('express')
6
6
  const { pick, partition } = require('lodash')
7
7
 
8
- const jose = require('node-jose')
8
+ const jose = require('jose')
9
9
  const jwt = require('jsonwebtoken')
10
10
 
11
11
  const assertions = require('../../assertions')
@@ -31,16 +31,27 @@ module.exports =
31
31
  }
32
32
 
33
33
  const encryptPersona = async (persona) => {
34
- const signedPersona = jwt.sign(persona, MOCKPASS_PRIVATE_KEY, {
35
- algorithm: 'RS256',
36
- })
37
- const serviceCertAsKey = await jose.JWK.asKey(serviceProvider.cert, 'pem')
38
- const encryptedAndSignedPersona = await jose.JWE.createEncrypt(
39
- { format: 'compact' },
40
- serviceCertAsKey,
34
+ /*
35
+ * We sign and encrypt the persona. It's important to note that although a signature is
36
+ * usually derived from the payload hash and is thus much smaller than the payload itself,
37
+ * we're specifically contructeding a JWT, which contains the original payload.
38
+ *
39
+ * We then construct a JWE and provide two headers specifying the encryption algorithms used.
40
+ * You can read about them here: https://www.rfc-editor.org/rfc/inline-errata/rfc7518.html
41
+ *
42
+ * These values weren't picked arbitrarily; they were the defaults used by a library we
43
+ * formerly used: node-jose. We opted to continue using them for backwards compatibility.
44
+ */
45
+ const privateKey = await jose.importPKCS8(MOCKPASS_PRIVATE_KEY.toString())
46
+ const sign = await new jose.SignJWT(persona)
47
+ .setProtectedHeader({ alg: 'RS256' })
48
+ .sign(privateKey)
49
+ const publicKey = await jose.importX509(serviceProvider.cert.toString())
50
+ const encryptedAndSignedPersona = await new jose.CompactEncrypt(
51
+ Buffer.from(sign),
41
52
  )
42
- .update(JSON.stringify(signedPersona))
43
- .final()
53
+ .setProtectedHeader({ alg: 'RSA-OAEP', enc: 'A128CBC-HS256' })
54
+ .encrypt(publicKey)
44
55
  return encryptedAndSignedPersona
45
56
  }
46
57
 
@@ -142,7 +153,6 @@ module.exports =
142
153
  redirect_uri,
143
154
  })
144
155
  : {}
145
-
146
156
  if (!tokenTemplate) {
147
157
  res.status(400).send({
148
158
  code: 400,
@@ -49,22 +49,40 @@ function config(app, { showLoginPage }) {
49
49
  } = req.query
50
50
 
51
51
  if (scope !== 'openid') {
52
- return res.status(400).send(`Unknown scope ${scope}`)
52
+ return res.status(400).send({
53
+ error: 'invalid_scope',
54
+ error_description: `Unknown scope ${scope}`,
55
+ })
53
56
  }
54
57
  if (response_type !== 'code') {
55
- return res.status(400).send(`Unknown response_type ${response_type}`)
58
+ return res.status(400).send({
59
+ error: 'unsupported_response_type',
60
+ error_description: `Unknown response_type ${response_type}`,
61
+ })
56
62
  }
57
63
  if (!client_id) {
58
- return res.status(400).send('Missing client_id')
64
+ return res.status(400).send({
65
+ error: 'invalid_request',
66
+ error_description: 'Missing client_id',
67
+ })
59
68
  }
60
69
  if (!redirectURI) {
61
- return res.status(400).send('Missing redirect_uri')
70
+ return res.status(400).send({
71
+ error: 'invalid_request',
72
+ error_description: 'Missing redirect_uri',
73
+ })
62
74
  }
63
75
  if (!nonce) {
64
- return res.status(400).send('Missing nonce')
76
+ return res.status(400).send({
77
+ error: 'invalid_request',
78
+ error_description: 'Missing nonce',
79
+ })
65
80
  }
66
81
  if (!state) {
67
- return res.status(400).send('Missing state')
82
+ return res.status(400).send({
83
+ error: 'invalid_request',
84
+ error_description: 'Missing state',
85
+ })
68
86
  }
69
87
 
70
88
  // Identical to OIDC v1
@@ -128,27 +146,43 @@ function config(app, { showLoginPage }) {
128
146
 
129
147
  // Only SP requires client_id
130
148
  if (idp === 'singPass' && !client_id) {
131
- return res.status(400).send('Missing client_id')
149
+ return res.status(400).send({
150
+ error: 'invalid_request',
151
+ error_description: 'Missing client_id',
152
+ })
132
153
  }
133
154
  if (!redirectURI) {
134
- return res.status(400).send('Missing redirect_uri')
155
+ return res.status(400).send({
156
+ error: 'invalid_request',
157
+ error_description: 'Missing redirect_uri',
158
+ })
135
159
  }
136
160
  if (grant_type !== 'authorization_code') {
137
- return res.status(400).send(`Unknown grant_type ${grant_type}`)
161
+ return res.status(400).send({
162
+ error: 'unsupported_grant_type',
163
+ error_description: `Unknown grant_type ${grant_type}`,
164
+ })
138
165
  }
139
166
  if (!authCode) {
140
- return res.status(400).send('Missing code')
167
+ return res.status(400).send({
168
+ error: 'invalid_request',
169
+ error_description: 'Missing code',
170
+ })
141
171
  }
142
172
  if (
143
173
  client_assertion_type !==
144
174
  'urn:ietf:params:oauth:client-assertion-type:jwt-bearer'
145
175
  ) {
146
- return res
147
- .status(400)
148
- .send(`Unknown client_assertion_type ${client_assertion_type}`)
176
+ return res.status(400).send({
177
+ error: 'invalid_request',
178
+ error_description: `Unknown client_assertion_type ${client_assertion_type}`,
179
+ })
149
180
  }
150
181
  if (!clientAssertion) {
151
- return res.status(400).send('Missing client_assertion')
182
+ return res.status(400).send({
183
+ error: 'invalid_request',
184
+ error_description: 'Missing client_assertion',
185
+ })
152
186
  }
153
187
 
154
188
  // Step 0: Get the RP keyset
@@ -165,11 +199,10 @@ function config(app, { showLoginPage }) {
165
199
  method: 'GET',
166
200
  }).then((response) => response.text())
167
201
  } catch (e) {
168
- return res
169
- .status(400)
170
- .send(
171
- `Failed to fetch RP JWKS from specified endpoint: ${e.message}`,
172
- )
202
+ return res.status(400).send({
203
+ error: 'invalid_client',
204
+ error_description: `Failed to fetch RP JWKS from specified endpoint: ${e.message}`,
205
+ })
173
206
  }
174
207
  } else {
175
208
  // If the endpoint is not defined, default to the sample keyset we provided.
@@ -180,7 +213,10 @@ function config(app, { showLoginPage }) {
180
213
  try {
181
214
  rpKeysetJson = JSON.parse(rpKeysetString)
182
215
  } catch (e) {
183
- return res.status(400).send(`Unable to parse RP keyset: ${e.message}`)
216
+ return res.status(400).send({
217
+ error: 'invalid_client',
218
+ error_description: `Unable to parse RP keyset: ${e.message}`,
219
+ })
184
220
  }
185
221
 
186
222
  const rpKeyset = await jose.JWK.asKeyStore(rpKeysetJson)
@@ -192,33 +228,37 @@ function config(app, { showLoginPage }) {
192
228
  rpKeyset,
193
229
  ).verify(clientAssertion)
194
230
  } catch (e) {
195
- return res
196
- .status(400)
197
- .send(`Unable to verify client_assertion: ${e.message}`)
231
+ return res.status(400).send({
232
+ error: 'invalid_client',
233
+ error_description: `Unable to verify client_assertion: ${e.message}`,
234
+ })
198
235
  }
199
236
 
200
237
  let clientAssertionClaims
201
238
  try {
202
239
  clientAssertionClaims = JSON.parse(clientAssertionVerified.payload)
203
240
  } catch (e) {
204
- return res
205
- .status(400)
206
- .send(`Unable to parse client_assertion: ${e.message}`)
241
+ return res.status(400).send({
242
+ error: 'invalid_client',
243
+ error_description: `Unable to parse client_assertion: ${e.message}`,
244
+ })
207
245
  }
208
246
 
209
247
  if (idp === 'singPass') {
210
248
  if (clientAssertionClaims['sub'] !== client_id) {
211
- return res
212
- .status(400)
213
- .send('Incorrect sub in client_assertion claims')
249
+ return res.status(400).send({
250
+ error: 'invalid_client',
251
+ error_description: 'Incorrect sub in client_assertion claims',
252
+ })
214
253
  }
215
254
  } else {
216
255
  // Since client_id is not given for corpPass, sub claim is required in
217
256
  // order to get aud for id_token.
218
257
  if (!clientAssertionClaims['sub']) {
219
- return res
220
- .status(400)
221
- .send('Missing sub in client_assertion claims')
258
+ return res.status(400).send({
259
+ error: 'invalid_client',
260
+ error_description: 'Missing sub in client_assertion claims',
261
+ })
222
262
  }
223
263
  }
224
264
 
@@ -228,9 +268,10 @@ function config(app, { showLoginPage }) {
228
268
  )}/${idp.toLowerCase()}/v2`
229
269
 
230
270
  if (clientAssertionClaims['aud'] !== iss) {
231
- return res
232
- .status(400)
233
- .send('Incorrect aud in client_assertion claims')
271
+ return res.status(400).send({
272
+ error: 'invalid_client',
273
+ error_description: 'Incorrect aud in client_assertion claims',
274
+ })
234
275
  }
235
276
 
236
277
  // Step 1: Obtain profile for which the auth code requested data for
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opengovsg/mockpass",
3
- "version": "4.0.6",
3
+ "version": "4.0.8",
4
4
  "description": "A mock SingPass/CorpPass server for dev purposes",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -41,6 +41,7 @@
41
41
  "dotenv": "^16.0.0",
42
42
  "expiry-map": "^2.0.0",
43
43
  "express": "^4.16.3",
44
+ "jose": "^4.14.4",
44
45
  "jsonwebtoken": "^9.0.0",
45
46
  "lodash": "^4.17.11",
46
47
  "morgan": "^1.9.1",
@@ -60,7 +61,7 @@
60
61
  "eslint-plugin-prettier": "^4.0.0",
61
62
  "husky": "^8.0.1",
62
63
  "lint-staged": "^13.0.3",
63
- "nodemon": "^2.0.4",
64
+ "nodemon": "^3.0.1",
64
65
  "pinst": "^3.0.0",
65
66
  "prettier": "^2.0.5"
66
67
  },