@opengovsg/mockpass 4.6.5 → 4.6.6
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/fapi/fapi.service.js +49 -20
- package/package.json +1 -1
|
@@ -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(
|
|
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
|
|
273
|
-
|
|
274
|
-
|
|
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
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
412
|
-
|
|
413
|
-
|
|
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
|
)
|