@opengovsg/mockpass 4.6.5 → 4.6.7

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.
@@ -23,7 +23,12 @@ class FapiService {
23
23
  ).pushed_authorization_request_endpoint
24
24
  const [, dpopJkt] = await Promise.all([
25
25
  verifyClientAssertion(req),
26
- verifyDpop(req.headers['dpop'], null, parEndpoint),
26
+ verifyDpop(
27
+ req.headers['dpop'],
28
+ // clients can pass either the DPoP header, or POST body dpop_jkt
29
+ req.body['dpop_jkt'] ?? null,
30
+ parEndpoint,
31
+ ),
27
32
  ])
28
33
 
29
34
  const request_uri = `urn:ietf:params:oauth:request_uri:${crypto
@@ -123,7 +128,11 @@ class FapiService {
123
128
  const id_token = {
124
129
  sub: profile.uuid,
125
130
  sub_attributes: {
131
+ // coi, type, name are all needed by Corppass
132
+ account_type: 'standard',
126
133
  identity_number: profile.nric,
134
+ identity_coi: 'SG',
135
+ name: `USER ${profile.nric}`,
127
136
  },
128
137
  aud: authRequest.client_id,
129
138
  acr: 'urn:singpass:authentication:loa:1', //Mockpass only
@@ -244,6 +253,8 @@ async function verifyDpop(dpop, dpop_jkt, expectedEndpoint) {
244
253
  throw new Error('Invalid DPoP jkt')
245
254
  return jwkThumbprint
246
255
  }
256
+ // if dpop_jkt was passed in, then we store it for later reference
257
+ return dpop_jkt
247
258
  }
248
259
  async function verifyClientAssertion(req) {
249
260
  const { client_assertion, client_assertion_type } = req.body
@@ -267,21 +278,40 @@ async function verifyClientAssertion(req) {
267
278
 
268
279
  const [headerB64] = client_assertion.split('.')
269
280
  const header = JSON.parse(Buffer.from(headerB64, 'base64').toString())
270
- if (!header.kid) throw new Error('No kid found in client assertion header')
271
281
 
272
- const keyMap = Object.fromEntries(jwks.keys.map((k) => [k.kid, k]))
273
- const keyToTry = await jose.importJWK(keyMap[header.kid], 'ES256')
274
- if (!keyToTry) throw new Error('No matching key found for kid')
282
+ const verifyOpts = {
283
+ algorithms: ['ES256'],
284
+ clockTolerance: FapiUtils.dpopConfiguration.DPOP_CLOCK_SKEW,
285
+ requiredClaims: ['iss', 'sub', 'aud', 'exp', 'iat', 'jti'],
286
+ }
275
287
 
276
- const { payload } = await jose
277
- .jwtVerify(client_assertion, keyToTry, {
278
- algorithms: ['ES256'],
279
- clockTolerance: FapiUtils.dpopConfiguration.DPOP_CLOCK_SKEW,
280
- requiredClaims: ['iss', 'sub', 'aud', 'exp', 'iat', 'jti'],
281
- })
282
- .catch(() => {
283
- throw new Error('Invalid client assertion signature')
284
- })
288
+ let payload
289
+ if (header.kid) {
290
+ // if a kid is provided in the header, we assert a matching jwk
291
+ const matchingJwk = jwks.keys.find((k) => k.kid === header.kid)
292
+ if (!matchingJwk) throw new Error('No matching key found for kid')
293
+ const key = await jose.importJWK(matchingJwk, 'ES256')
294
+ const result = await jose
295
+ .jwtVerify(client_assertion, key, verifyOpts)
296
+ .catch(() => {
297
+ throw new Error('Invalid client assertion signature')
298
+ })
299
+ payload = result.payload
300
+ } else {
301
+ // if no kid was provided, then we look for a usable key. this is consistent with
302
+ // SP auth implementation/behaviour.
303
+ for (const jwk of jwks.keys) {
304
+ try {
305
+ const key = await jose.importJWK(jwk, 'ES256')
306
+ const result = await jose.jwtVerify(client_assertion, key, verifyOpts)
307
+ payload = result.payload
308
+ break
309
+ } catch {
310
+ /* empty */
311
+ }
312
+ }
313
+ if (!payload) throw new Error('Invalid client assertion signature')
314
+ }
285
315
 
286
316
  if (payload.aud !== FapiUtils.getFapiOpenIdConfiguration(req).issuer)
287
317
  throw new Error('Invalid client assertion aud')
@@ -400,7 +430,8 @@ function verifyAuthenticationContext(
400
430
  authenticationContextType,
401
431
  authenticationContextMessage,
402
432
  ) {
403
- //Not enforced if not provided. This parameter seems to fail when provided for some login apps
433
+ // for non-login apps, this is rejected if present, and since mockpass
434
+ // won't have context of the app type, we just make it optional
404
435
  if (!authenticationContextType) return
405
436
  if (
406
437
  !FapiUtils.AllowedAuthenticationContextTypes.includes(
@@ -408,11 +439,9 @@ function verifyAuthenticationContext(
408
439
  )
409
440
  )
410
441
  throw new Error('Invalid authentication context type')
411
- assert(
412
- authenticationContextMessage,
413
- 'Authentication context message is required',
414
- )
415
- if (authenticationContextMessage.length > 100)
442
+
443
+ // this is optional by SP FAPI spec, and should only be validated when provided.
444
+ if (authenticationContextMessage && authenticationContextMessage.length > 100)
416
445
  throw new Error(
417
446
  'Authentication context message must be less than 100 characters',
418
447
  )
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opengovsg/mockpass",
3
- "version": "4.6.5",
3
+ "version": "4.6.7",
4
4
  "description": "A mock SingPass/CorpPass server for dev purposes",
5
5
  "main": "app.js",
6
6
  "bin": {