@opengovsg/mockpass 2.9.5 → 3.0.1
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 +6 -15
- package/index.js +1 -30
- package/lib/assertions.js +0 -97
- package/lib/crypto/myinfo-signature.js +1 -82
- package/lib/express/index.js +0 -1
- package/lib/express/myinfo/consent.js +3 -15
- package/lib/express/myinfo/controllers.js +4 -10
- package/lib/express/myinfo/index.js +1 -2
- package/package.json +1 -4
- package/lib/crypto/index.js +0 -61
- package/lib/express/saml.js +0 -195
- package/lib/saml-artifact.js +0 -39
- package/static/myinfo/v2.json +0 -6154
- package/static/saml/corppass.xml +0 -21
- package/static/saml/unsigned-assertion.xml +0 -24
- package/static/saml/unsigned-response.xml +0 -19
package/lib/express/saml.js
DELETED
|
@@ -1,195 +0,0 @@
|
|
|
1
|
-
const express = require('express')
|
|
2
|
-
const fs = require('fs')
|
|
3
|
-
const { render } = require('mustache')
|
|
4
|
-
const path = require('path')
|
|
5
|
-
const { DOMParser } = require('@xmldom/xmldom')
|
|
6
|
-
const xpath = require('xpath')
|
|
7
|
-
const moment = require('moment')
|
|
8
|
-
|
|
9
|
-
const assertions = require('../assertions')
|
|
10
|
-
const crypto = require('../crypto')
|
|
11
|
-
const {
|
|
12
|
-
generateSamlArtifact,
|
|
13
|
-
lookUpBySamlArtifact,
|
|
14
|
-
} = require('../saml-artifact')
|
|
15
|
-
|
|
16
|
-
const domParser = new DOMParser()
|
|
17
|
-
const dom = (xmlString) => domParser.parseFromString(xmlString)
|
|
18
|
-
|
|
19
|
-
const TEMPLATE = fs.readFileSync(
|
|
20
|
-
path.resolve(__dirname, '../../static/saml/unsigned-response.xml'),
|
|
21
|
-
'utf8',
|
|
22
|
-
)
|
|
23
|
-
const LOGIN_TEMPLATE = fs.readFileSync(
|
|
24
|
-
path.resolve(__dirname, '../../static/html/login-page.html'),
|
|
25
|
-
'utf8',
|
|
26
|
-
)
|
|
27
|
-
|
|
28
|
-
const MYINFO_ASSERT_ENDPOINT = '/consent/myinfo-com'
|
|
29
|
-
|
|
30
|
-
const buildAssertURL = (assertEndpoint, samlArt, relayState) => {
|
|
31
|
-
let assertURL = `${assertEndpoint}?SAMLart=${encodeURIComponent(samlArt)}`
|
|
32
|
-
if (relayState !== undefined) {
|
|
33
|
-
assertURL += `&RelayState=${encodeURIComponent(relayState)}`
|
|
34
|
-
}
|
|
35
|
-
return assertURL
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
const idGenerator = {
|
|
39
|
-
singPass: ({ nric }) =>
|
|
40
|
-
assertions.myinfo.v2.personas[nric] ? `${nric} [MyInfo]` : nric,
|
|
41
|
-
corpPass: ({ nric, uen }) => `${nric} / UEN: ${uen}`,
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
const customProfileFromHeaders = {
|
|
45
|
-
singPass: (req) => {
|
|
46
|
-
const customNricHeader = req.header('X-Custom-NRIC')
|
|
47
|
-
if (!customNricHeader) {
|
|
48
|
-
return false
|
|
49
|
-
}
|
|
50
|
-
return { nric: customNricHeader }
|
|
51
|
-
},
|
|
52
|
-
corpPass: (req) => {
|
|
53
|
-
const customNricHeader = req.header('X-Custom-NRIC')
|
|
54
|
-
const customUenHeader = req.header('X-Custom-UEN')
|
|
55
|
-
if (!customNricHeader || !customUenHeader) {
|
|
56
|
-
return false
|
|
57
|
-
}
|
|
58
|
-
return { nric: customNricHeader, uen: customUenHeader }
|
|
59
|
-
},
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
function config(
|
|
63
|
-
app,
|
|
64
|
-
{ showLoginPage, serviceProvider, idpConfig, cryptoConfig },
|
|
65
|
-
) {
|
|
66
|
-
const { verifySignature, sign, promiseToEncryptAssertion } =
|
|
67
|
-
crypto(serviceProvider)
|
|
68
|
-
|
|
69
|
-
for (const idp of ['singPass', 'corpPass']) {
|
|
70
|
-
const partnerId = idpConfig[idp].id
|
|
71
|
-
const partnerAssertEndpoint = idpConfig[idp].assertEndpoint
|
|
72
|
-
|
|
73
|
-
const profiles = assertions.saml[idp]
|
|
74
|
-
const defaultProfile =
|
|
75
|
-
profiles.find((p) => p.nric === process.env.MOCKPASS_NRIC) || profiles[0]
|
|
76
|
-
|
|
77
|
-
app.get(`/${idp.toLowerCase()}/logininitial`, (req, res) => {
|
|
78
|
-
const assertEndpoint =
|
|
79
|
-
req.query.esrvcID === 'MYINFO-CONSENTPLATFORM' && idp === 'singPass'
|
|
80
|
-
? MYINFO_ASSERT_ENDPOINT
|
|
81
|
-
: partnerAssertEndpoint || req.query.PartnerId
|
|
82
|
-
const relayState = req.query.Target
|
|
83
|
-
if (showLoginPage(req)) {
|
|
84
|
-
const values = profiles.map((profile) => {
|
|
85
|
-
const samlArt = generateSamlArtifact(partnerId, profile)
|
|
86
|
-
const assertURL = buildAssertURL(assertEndpoint, samlArt, relayState)
|
|
87
|
-
const id = idGenerator[idp](profile)
|
|
88
|
-
return { id, assertURL }
|
|
89
|
-
})
|
|
90
|
-
const response = render(LOGIN_TEMPLATE, {
|
|
91
|
-
values,
|
|
92
|
-
customProfileConfig: {
|
|
93
|
-
endpoint: `/${idp.toLowerCase()}/logininitial/custom-profile`,
|
|
94
|
-
showUuid: false,
|
|
95
|
-
showUen: idp === 'corpPass',
|
|
96
|
-
assertEndpoint,
|
|
97
|
-
relayState,
|
|
98
|
-
},
|
|
99
|
-
})
|
|
100
|
-
res.send(response)
|
|
101
|
-
} else {
|
|
102
|
-
const profile = customProfileFromHeaders[idp](req) || defaultProfile
|
|
103
|
-
const samlArt = generateSamlArtifact(partnerId, profile)
|
|
104
|
-
const assertURL = buildAssertURL(assertEndpoint, samlArt, relayState)
|
|
105
|
-
console.warn(
|
|
106
|
-
`Redirecting login from ${req.query.PartnerId} to ${assertURL}`,
|
|
107
|
-
)
|
|
108
|
-
res.redirect(assertURL)
|
|
109
|
-
}
|
|
110
|
-
})
|
|
111
|
-
|
|
112
|
-
app.get(`/${idp.toLowerCase()}/logininitial/custom-profile`, (req, res) => {
|
|
113
|
-
const { nric, uen, assertEndpoint, relayState } = req.query
|
|
114
|
-
|
|
115
|
-
const profile = { nric }
|
|
116
|
-
if (idp === 'corpPass') {
|
|
117
|
-
profile.uen = uen
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
const samlArt = generateSamlArtifact(partnerId, profile)
|
|
121
|
-
const assertURL = buildAssertURL(assertEndpoint, samlArt, relayState)
|
|
122
|
-
res.redirect(assertURL)
|
|
123
|
-
})
|
|
124
|
-
|
|
125
|
-
app.post(
|
|
126
|
-
`/${idp.toLowerCase()}/soap`,
|
|
127
|
-
express.text({ type: 'text/xml' }),
|
|
128
|
-
(req, res) => {
|
|
129
|
-
// Extract the body of the SOAP request
|
|
130
|
-
const { body } = req
|
|
131
|
-
const xml = dom(body)
|
|
132
|
-
|
|
133
|
-
if (
|
|
134
|
-
cryptoConfig.resolveArtifactRequestSigned &&
|
|
135
|
-
!verifySignature(xml)
|
|
136
|
-
) {
|
|
137
|
-
res.status(400).send('Request has bad signature')
|
|
138
|
-
} else {
|
|
139
|
-
// Grab the SAML artifact
|
|
140
|
-
// TODO: verify the SAML artifact is something we sent
|
|
141
|
-
// TODO: do something about the partner entity id
|
|
142
|
-
const samlArtifact = xpath.select(
|
|
143
|
-
"string(//*[local-name(.)='Artifact'])",
|
|
144
|
-
xml,
|
|
145
|
-
)
|
|
146
|
-
console.warn(`Received SAML Artifact ${samlArtifact}`)
|
|
147
|
-
// Handle encoded base64 Artifact
|
|
148
|
-
// Take the template and plug in the typical SingPass/CorpPass response
|
|
149
|
-
// Sign and encrypt the assertion
|
|
150
|
-
const profile = lookUpBySamlArtifact(samlArtifact)
|
|
151
|
-
|
|
152
|
-
const samlArtifactResolveId = xpath.select(
|
|
153
|
-
"string(//*[local-name(.)='ArtifactResolve']/@ID)",
|
|
154
|
-
xml,
|
|
155
|
-
)
|
|
156
|
-
|
|
157
|
-
let result = assertions.saml.create[idp](
|
|
158
|
-
profile,
|
|
159
|
-
partnerId,
|
|
160
|
-
partnerAssertEndpoint,
|
|
161
|
-
samlArtifactResolveId,
|
|
162
|
-
)
|
|
163
|
-
|
|
164
|
-
if (cryptoConfig.signAssertion) {
|
|
165
|
-
result = sign(result, "//*[local-name(.)='Assertion']")
|
|
166
|
-
}
|
|
167
|
-
const assertionPromise = cryptoConfig.encryptAssertion
|
|
168
|
-
? promiseToEncryptAssertion(result)
|
|
169
|
-
: Promise.resolve(result)
|
|
170
|
-
|
|
171
|
-
assertionPromise.then((assertion) => {
|
|
172
|
-
let response = render(TEMPLATE, {
|
|
173
|
-
assertion,
|
|
174
|
-
issueInstant: moment.utc().format(),
|
|
175
|
-
issuer: partnerId,
|
|
176
|
-
destination: partnerAssertEndpoint,
|
|
177
|
-
inResponseTo: samlArtifactResolveId,
|
|
178
|
-
})
|
|
179
|
-
if (cryptoConfig.signResponse) {
|
|
180
|
-
response = sign(
|
|
181
|
-
sign(response, "//*[local-name(.)='Response']"),
|
|
182
|
-
"//*[local-name(.)='ArtifactResponse']",
|
|
183
|
-
)
|
|
184
|
-
}
|
|
185
|
-
res.send(response)
|
|
186
|
-
})
|
|
187
|
-
}
|
|
188
|
-
},
|
|
189
|
-
)
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
return app
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
module.exports = config
|
package/lib/saml-artifact.js
DELETED
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
const ExpiryMap = require('expiry-map')
|
|
2
|
-
const crypto = require('crypto')
|
|
3
|
-
|
|
4
|
-
const SAML_ARTIFACT_TIMEOUT = 5 * 60 * 1000
|
|
5
|
-
const profileStore = new ExpiryMap(SAML_ARTIFACT_TIMEOUT)
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Construct a SingPass/CorpPass SAML artifact, a base64
|
|
9
|
-
* encoding of a byte sequence consisting of the following:
|
|
10
|
-
* - a two-byte type code, always 0x0004, per SAML 2.0;
|
|
11
|
-
* - a two-byte endpoint index, currently always 0x0000;
|
|
12
|
-
* - a 20-byte sha1 hash of the partner id, and;
|
|
13
|
-
* - a 20-byte random sequence that is effectively the message id
|
|
14
|
-
* @param {string} partnerId - the partner id
|
|
15
|
-
* @param {string} profile - the profile (identity) to store
|
|
16
|
-
* @return {string} the SAML artifact, a base64 string
|
|
17
|
-
* containing the type code, the endpoint index,
|
|
18
|
-
* the hash of the partner id, followed by 20 random bytes;
|
|
19
|
-
* this can be used to look up the stored profile (identity)
|
|
20
|
-
*/
|
|
21
|
-
function generateSamlArtifact(partnerId, profile) {
|
|
22
|
-
const hashedPartnerId = crypto
|
|
23
|
-
.createHash('sha1')
|
|
24
|
-
.update(partnerId, 'utf8')
|
|
25
|
-
.digest('hex')
|
|
26
|
-
const randomBytes = crypto.randomBytes(20).toString('hex')
|
|
27
|
-
const samlArtifact = Buffer.from(
|
|
28
|
-
`00040000${hashedPartnerId}${randomBytes}`,
|
|
29
|
-
'hex',
|
|
30
|
-
).toString('base64')
|
|
31
|
-
profileStore.set(samlArtifact, profile)
|
|
32
|
-
return samlArtifact
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function lookUpBySamlArtifact(samlArtifact) {
|
|
36
|
-
return profileStore.get(samlArtifact)
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
module.exports = { generateSamlArtifact, lookUpBySamlArtifact }
|