@opengovsg/mockpass 4.3.5 → 4.4.0
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/README.md +18 -1
- package/app.js +2 -0
- package/lib/express/index.js +1 -0
- package/lib/express/signv3.js +98 -0
- package/package.json +1 -1
- package/public/mockpass/resources/dummy-signed.pdf +0 -0
- package/static/certs/sign-v3-secret.json +10 -0
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# MockPass
|
|
2
2
|
|
|
3
|
-
A mock Singpass/Corppass/Myinfo v3/sgID v2 server for dev purposes
|
|
3
|
+
A mock Singpass/Corppass/Myinfo v3/sgID v2/Sign v3 server for dev purposes
|
|
4
4
|
|
|
5
5
|
## Quick Start (hosted remotely on Gitpod)
|
|
6
6
|
|
|
@@ -164,6 +164,23 @@ Self-signed or untrusted certificates are supported.
|
|
|
164
164
|
Alternatively, provide your application with the certificate and private key
|
|
165
165
|
from `static/certs/(server.crt|key.pem)`.
|
|
166
166
|
|
|
167
|
+
### Sign v3
|
|
168
|
+
Check out Sign V3 documentation [here](https://docs.sign.singpass.gov.sg/for-relying-parties/api-documentation/document-signing-v3).
|
|
169
|
+
|
|
170
|
+
Sign v3 URLs
|
|
171
|
+
- http://localhost:5156/sign-v3/sign-requests - Create sign request
|
|
172
|
+
- http://localhost:5156/sign-v3/sign-requests/:request_id/signed-doc - Get signed document
|
|
173
|
+
- http://localhost:5156/sign-v3/jwks - JWKS URL
|
|
174
|
+
|
|
175
|
+
- http://localhost:5156/sign-v3/sign - The stubbed signing portal path
|
|
176
|
+
|
|
177
|
+
Configure MockPass with your application client details:
|
|
178
|
+
- Client ID: SIGNV3_CLIENT_ID, default mockpass-sign-v3-client
|
|
179
|
+
- JWKS URL: SIGNV3_CLIENT_JWKS_URL, default http://localhost:4000/jwks
|
|
180
|
+
- Redirect URL: SIGNV3_CLIENT_REDIRECT_URL, default http://localhost:4000/redirect
|
|
181
|
+
- Webhook URL: SIGNV3_CLIENT_WEBHOOK_URL, default http://localhost:4000/webhook
|
|
182
|
+
- Server host URL: MOCKPASS_SERVER_HOST: default: http://localhost:5156
|
|
183
|
+
|
|
167
184
|
### Run MockPass
|
|
168
185
|
|
|
169
186
|
Common configuration:
|
package/app.js
CHANGED
|
@@ -10,6 +10,7 @@ const {
|
|
|
10
10
|
configOIDCv2,
|
|
11
11
|
configMyInfo,
|
|
12
12
|
configSGID,
|
|
13
|
+
configSignV3,
|
|
13
14
|
} = require('./lib/express')
|
|
14
15
|
|
|
15
16
|
const serviceProvider = {
|
|
@@ -52,6 +53,7 @@ app.use(morgan('combined'))
|
|
|
52
53
|
configOIDC(app, options)
|
|
53
54
|
configOIDCv2(app, options)
|
|
54
55
|
configSGID(app, options)
|
|
56
|
+
configSignV3(app, options)
|
|
55
57
|
|
|
56
58
|
configMyInfo.consent(app, options)
|
|
57
59
|
configMyInfo.v3(app, options)
|
package/lib/express/index.js
CHANGED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
const jose = require('jose')
|
|
2
|
+
const assert = require('node:assert')
|
|
3
|
+
const fs = require('fs')
|
|
4
|
+
const path = require('path')
|
|
5
|
+
const { SignJWT, importJWK } = require('jose')
|
|
6
|
+
|
|
7
|
+
const PATH_PREFIX = '/sign-v3'
|
|
8
|
+
const CLIENT_JWKS_URL =
|
|
9
|
+
process.env.SIGNV3_CLIENT_JWKS_URL || 'http://localhost:4000/jwks'
|
|
10
|
+
const CLIENT_ID = process.env.SIGNV3_CLIENT_ID || 'mockpass-sign-v3-client'
|
|
11
|
+
const MOCKPASS_SERVER_HOST =
|
|
12
|
+
process.env.MOCKPASS_SERVER_HOST || 'http://localhost:5156'
|
|
13
|
+
const CLIENT_REDIRECT_URI =
|
|
14
|
+
process.env.SIGNV3_CLIENT_REDIRECT_URI || 'http://localhost:4000/redirect'
|
|
15
|
+
const CLIENT_WEBHOOK_URL =
|
|
16
|
+
process.env.SIGNV3_CLIENT_WEBHOOK_URL || 'http://localhost:4000/webhook'
|
|
17
|
+
const SIGNED_DOC_URL = `${MOCKPASS_SERVER_HOST}/mockpass/resources/dummy-signed.pdf`
|
|
18
|
+
const MOCKPASS_SIGNV3_PRIVATE_JWK = JSON.parse(
|
|
19
|
+
fs.readFileSync(
|
|
20
|
+
path.resolve(__dirname, '../../static/certs/sign-v3-secret.json'),
|
|
21
|
+
),
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
function config(app) {
|
|
25
|
+
app.post(`${PATH_PREFIX}/sign-requests`, async (req, res) => {
|
|
26
|
+
try {
|
|
27
|
+
assert(req.headers['content-type'] === 'application/octet-stream')
|
|
28
|
+
assert(req.headers.authorization)
|
|
29
|
+
const { payload } = await jose.jwtVerify(
|
|
30
|
+
req.headers.authorization,
|
|
31
|
+
jose.createRemoteJWKSet(new URL(CLIENT_JWKS_URL)),
|
|
32
|
+
{ requiredClaims: ['client_id', 'x', 'y', 'page', 'doc_name'] },
|
|
33
|
+
)
|
|
34
|
+
assert(payload.client_id === CLIENT_ID)
|
|
35
|
+
} catch (err) {
|
|
36
|
+
console.error(err)
|
|
37
|
+
return res
|
|
38
|
+
.status(400)
|
|
39
|
+
.json({ error: 'UNAUTHORIZED', error_description: 'Unauthorized.' })
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const request_id = `signv3-${crypto.randomUUID()}`
|
|
43
|
+
console.info(`Creating sign request: ${request_id}`)
|
|
44
|
+
return res.status(200).json({
|
|
45
|
+
signing_url: `${MOCKPASS_SERVER_HOST}/sign-v3/sign?request_id=${request_id}`,
|
|
46
|
+
request_id,
|
|
47
|
+
exchange_code: crypto.randomUUID(),
|
|
48
|
+
})
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
app.get(`${PATH_PREFIX}/sign`, async (req, res) => {
|
|
52
|
+
const { request_id } = req.query
|
|
53
|
+
await sendSignedDocWebhook(request_id)
|
|
54
|
+
|
|
55
|
+
const redirect_uri = new URL(CLIENT_REDIRECT_URI)
|
|
56
|
+
redirect_uri.searchParams.set('request_id', request_id)
|
|
57
|
+
return res.redirect(redirect_uri)
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
app.get(`${PATH_PREFIX}/sign-requests/:request_id/signed_doc`, (req, res) => {
|
|
61
|
+
console.info(`Retrieving signed doc of: ${req.params.request_id}`)
|
|
62
|
+
return res.status(200).json({ signed_doc_url: SIGNED_DOC_URL })
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
app.get(`${PATH_PREFIX}/jwks`, async (req, res) => {
|
|
66
|
+
// eslint-disable-next-line no-unused-vars
|
|
67
|
+
const { d, ...publicJwk } = { ...MOCKPASS_SIGNV3_PRIVATE_JWK }
|
|
68
|
+
return res.status(200).json({ keys: [publicJwk] })
|
|
69
|
+
})
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const sendSignedDocWebhook = async (request_id) => {
|
|
73
|
+
await fetch(CLIENT_WEBHOOK_URL, {
|
|
74
|
+
method: 'POST',
|
|
75
|
+
headers: { 'Content-Type': 'application/json' },
|
|
76
|
+
body: JSON.stringify({
|
|
77
|
+
token: await new SignJWT({
|
|
78
|
+
signed_doc_url: SIGNED_DOC_URL,
|
|
79
|
+
request_type: 'signed_doc_url',
|
|
80
|
+
request_id,
|
|
81
|
+
})
|
|
82
|
+
.setIssuedAt()
|
|
83
|
+
.setProtectedHeader({
|
|
84
|
+
alg: 'ES256',
|
|
85
|
+
kid: MOCKPASS_SIGNV3_PRIVATE_JWK.kid,
|
|
86
|
+
})
|
|
87
|
+
.setExpirationTime('120s')
|
|
88
|
+
.sign(await importJWK(MOCKPASS_SIGNV3_PRIVATE_JWK)),
|
|
89
|
+
}),
|
|
90
|
+
}).then((response) => {
|
|
91
|
+
if (!response.ok) {
|
|
92
|
+
console.error('signed doc webhook failed', response)
|
|
93
|
+
}
|
|
94
|
+
console.info(`signed doc webhook success ${CLIENT_WEBHOOK_URL}`)
|
|
95
|
+
})
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
module.exports = config
|
package/package.json
CHANGED
|
Binary file
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"kty": "EC",
|
|
3
|
+
"d": "AxQ5ckZCNkTpCNG_7anl7mZ2ccesghaJS7MamwIHkHg",
|
|
4
|
+
"use": "sig",
|
|
5
|
+
"crv": "P-256",
|
|
6
|
+
"kid": "_dCTwISv-TbO7mJ6vuAXnm_ucqdYlaInsLozbT1MQQc",
|
|
7
|
+
"x": "OpMaeVXELLmjNVTw9OIfNQHyIi14Rzo18sE4dvJ4FBE",
|
|
8
|
+
"y": "td5SW3US0az5N3ssQdFzNbDCRj0fKF6O1ov9tETSQ4Q",
|
|
9
|
+
"alg": "ES256"
|
|
10
|
+
}
|