@opengovsg/mockpass 2.7.9
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/.eslintrc.json +13 -0
- package/.gitattributes +2 -0
- package/.github/dependabot.yml +14 -0
- package/.github/mergify.yml +12 -0
- package/.github/workflows/ci.yml +27 -0
- package/.github/workflows/npmpublish.yml +22 -0
- package/.gitpod.yml +5 -0
- package/.husky/pre-commit +4 -0
- package/.husky/pre-push +4 -0
- package/.prettierrc.js +5 -0
- package/Dockerfile +11 -0
- package/LICENSE +21 -0
- package/README.md +99 -0
- package/commitlint.config.js +7 -0
- package/index.js +87 -0
- package/lib/assertions.js +319 -0
- package/lib/crypto/index.js +61 -0
- package/lib/crypto/myinfo-signature.js +153 -0
- package/lib/express/index.js +6 -0
- package/lib/express/myinfo/consent.js +160 -0
- package/lib/express/myinfo/controllers.js +179 -0
- package/lib/express/myinfo/index.js +10 -0
- package/lib/express/oidc.js +131 -0
- package/lib/express/saml.js +171 -0
- package/lib/express/sgid.js +168 -0
- package/lib/saml-artifact.js +32 -0
- package/package.json +81 -0
- package/public/mockpass/resources/css/animate.css +43 -0
- package/public/mockpass/resources/css/common.css +121 -0
- package/public/mockpass/resources/css/reset.css +95 -0
- package/public/mockpass/resources/css/style-baseline-small-media.css +567 -0
- package/public/mockpass/resources/css/style-baseline.css +1006 -0
- package/public/mockpass/resources/css/style-common-small-media.css +156 -0
- package/public/mockpass/resources/css/style-common.css +510 -0
- package/public/mockpass/resources/css/style-homepage-small-media.css +588 -0
- package/public/mockpass/resources/css/style-homepage.css +674 -0
- package/public/mockpass/resources/css/style-main.css +9 -0
- package/public/mockpass/resources/img/ajax-loader.gif +0 -0
- package/public/mockpass/resources/img/ask_cheryl_tab.png +0 -0
- package/public/mockpass/resources/img/background/large-device/sp_bg.jpg +0 -0
- package/public/mockpass/resources/img/background/medium-device/ipad-bg.jpg +0 -0
- package/public/mockpass/resources/img/background/medium-device/ipad-landscape-sp-bg.jpg +0 -0
- package/public/mockpass/resources/img/background/small-device/mobile-sp-bg.jpg +0 -0
- package/public/mockpass/resources/img/carousel/large-device/how-to-setup-2fa-icon.png +0 -0
- package/public/mockpass/resources/img/carousel/large-device/register-icon.png +0 -0
- package/public/mockpass/resources/img/carousel/large-device/reset-password-icon.png +0 -0
- package/public/mockpass/resources/img/carousel/large-device/setup-2fa-icon.png +0 -0
- package/public/mockpass/resources/img/carousel/large-device/update-acct-icon.png +0 -0
- package/public/mockpass/resources/img/carousel/medium-device/ipad-register-icon.png +0 -0
- package/public/mockpass/resources/img/carousel/medium-device/ipad-reset-password-icon.png +0 -0
- package/public/mockpass/resources/img/carousel/medium-device/ipad-setup-2fa-icon.png +0 -0
- package/public/mockpass/resources/img/carousel/medium-device/ipad-update-acct-icon.png +0 -0
- package/public/mockpass/resources/img/carousel/small-device/mobile-register.png +0 -0
- package/public/mockpass/resources/img/carousel/small-device/mobile-reset-password-icon.png +0 -0
- package/public/mockpass/resources/img/carousel/small-device/mobile-update-acct-icon.png +0 -0
- package/public/mockpass/resources/img/close.png +0 -0
- package/public/mockpass/resources/img/id-pw-icon.png +0 -0
- package/public/mockpass/resources/img/logo/mockpass-logo.png +0 -0
- package/public/mockpass/resources/img/logo/mockpass-placeholder-logo.png +0 -0
- package/public/mockpass/resources/img/logo/mockpass_watermark.png +0 -0
- package/public/mockpass/resources/img/qr-icon.png +0 -0
- package/public/mockpass/resources/img/qr-shadow.png +0 -0
- package/public/mockpass/resources/img/refresh.jpg +0 -0
- package/public/mockpass/resources/img/sidebar-icons.png +0 -0
- package/public/mockpass/resources/img/sp-qr-unavailable.png +0 -0
- package/public/mockpass/resources/img/utility-icon-black.png +0 -0
- package/public/mockpass/resources/js/bootstrap.min.js +7 -0
- package/public/mockpass/resources/js/jquery-3.5.1.js +10872 -0
- package/public/mockpass/resources/js/login-common.js +849 -0
- package/public/mockpass/resources/plugins/bootstrap-3.3.6/css/bootstrap.min.css +6 -0
- package/public/mockpass/resources/plugins/bootstrap-3.3.6/fonts/glyphicons-halflings-regular.woff2 +0 -0
- package/static/certs/csr.pem +17 -0
- package/static/certs/key.pem +28 -0
- package/static/certs/key.pub +9 -0
- package/static/certs/server.crt +21 -0
- package/static/certs/spcp-csr.pem +17 -0
- package/static/certs/spcp-key.pem +28 -0
- package/static/certs/spcp.crt +20 -0
- package/static/html/consent.html +40 -0
- package/static/html/login-page.html +271 -0
- package/static/myinfo/v2.json +6154 -0
- package/static/myinfo/v3.json +29386 -0
- package/static/saml/corppass.xml +21 -0
- package/static/saml/unsigned-assertion.xml +24 -0
- package/static/saml/unsigned-response.xml +19 -0
|
@@ -0,0 +1,171 @@
|
|
|
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 { samlArtifact, hashPartnerId } = require('../saml-artifact')
|
|
12
|
+
|
|
13
|
+
const domParser = new DOMParser()
|
|
14
|
+
const dom = (xmlString) => domParser.parseFromString(xmlString)
|
|
15
|
+
|
|
16
|
+
const TEMPLATE = fs.readFileSync(
|
|
17
|
+
path.resolve(__dirname, '../../static/saml/unsigned-response.xml'),
|
|
18
|
+
'utf8',
|
|
19
|
+
)
|
|
20
|
+
const LOGIN_TEMPLATE = fs.readFileSync(
|
|
21
|
+
path.resolve(__dirname, '../../static/html/login-page.html'),
|
|
22
|
+
'utf8',
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
const MYINFO_ASSERT_ENDPOINT = '/consent/myinfo-com'
|
|
26
|
+
|
|
27
|
+
const idGenerator = {
|
|
28
|
+
singPass: (rawId) =>
|
|
29
|
+
assertions.myinfo.v2.personas[rawId] ? `${rawId} [MyInfo]` : rawId,
|
|
30
|
+
corpPass: (rawId) => `${rawId.nric} / UEN: ${rawId.uen}`,
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function config(
|
|
34
|
+
app,
|
|
35
|
+
{ showLoginPage, serviceProvider, idpConfig, cryptoConfig },
|
|
36
|
+
) {
|
|
37
|
+
const { verifySignature, sign, promiseToEncryptAssertion } =
|
|
38
|
+
crypto(serviceProvider)
|
|
39
|
+
|
|
40
|
+
for (const idp of ['singPass', 'corpPass']) {
|
|
41
|
+
app.get(`/${idp.toLowerCase()}/logininitial`, (req, res) => {
|
|
42
|
+
const assertEndpoint =
|
|
43
|
+
req.query.esrvcID === 'MYINFO-CONSENTPLATFORM' && idp === 'singPass'
|
|
44
|
+
? MYINFO_ASSERT_ENDPOINT
|
|
45
|
+
: idpConfig[idp].assertEndpoint || req.query.PartnerId
|
|
46
|
+
const relayState = req.query.Target
|
|
47
|
+
const partnerId = idpConfig[idp].id
|
|
48
|
+
if (showLoginPage) {
|
|
49
|
+
const saml = assertions.saml[idp]
|
|
50
|
+
const values = saml.map((rawId, index) => {
|
|
51
|
+
const samlArt = encodeURIComponent(samlArtifact(partnerId, index))
|
|
52
|
+
let assertURL = `${assertEndpoint}?SAMLart=${samlArt}`
|
|
53
|
+
if (relayState !== undefined) {
|
|
54
|
+
assertURL += `&RelayState=${encodeURIComponent(relayState)}`
|
|
55
|
+
}
|
|
56
|
+
const id = idGenerator[idp](rawId)
|
|
57
|
+
return { id, assertURL }
|
|
58
|
+
})
|
|
59
|
+
const hashedPartnerId = hashPartnerId(partnerId)
|
|
60
|
+
const response = render(LOGIN_TEMPLATE, {
|
|
61
|
+
values,
|
|
62
|
+
assertEndpoint,
|
|
63
|
+
relayState,
|
|
64
|
+
hashedPartnerId,
|
|
65
|
+
})
|
|
66
|
+
res.send(response)
|
|
67
|
+
} else {
|
|
68
|
+
const samlArt = encodeURIComponent(samlArtifact(partnerId))
|
|
69
|
+
let assertURL = `${assertEndpoint}?SAMLart=${samlArt}`
|
|
70
|
+
if (relayState !== undefined) {
|
|
71
|
+
assertURL += `&RelayState=${encodeURIComponent(relayState)}`
|
|
72
|
+
}
|
|
73
|
+
console.warn(
|
|
74
|
+
`Redirecting login from ${req.query.PartnerId} to ${assertURL}`,
|
|
75
|
+
)
|
|
76
|
+
res.redirect(assertURL)
|
|
77
|
+
}
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
app.post(
|
|
81
|
+
`/${idp.toLowerCase()}/soap`,
|
|
82
|
+
express.text({ type: 'text/xml' }),
|
|
83
|
+
(req, res) => {
|
|
84
|
+
// Extract the body of the SOAP request
|
|
85
|
+
const { body } = req
|
|
86
|
+
const xml = dom(body)
|
|
87
|
+
|
|
88
|
+
if (
|
|
89
|
+
cryptoConfig.resolveArtifactRequestSigned &&
|
|
90
|
+
!verifySignature(xml)
|
|
91
|
+
) {
|
|
92
|
+
res.status(400).send('Request has bad signature')
|
|
93
|
+
} else {
|
|
94
|
+
// Grab the SAML artifact
|
|
95
|
+
// TODO: verify the SAML artifact is something we sent
|
|
96
|
+
// TODO: do something about the partner entity id
|
|
97
|
+
const samlArtifact = xpath.select(
|
|
98
|
+
"string(//*[local-name(.)='Artifact'])",
|
|
99
|
+
xml,
|
|
100
|
+
)
|
|
101
|
+
console.warn(`Received SAML Artifact ${samlArtifact}`)
|
|
102
|
+
// Handle encoded base64 Artifact
|
|
103
|
+
// Take the template and plug in the typical SingPass/CorpPass response
|
|
104
|
+
// Sign and encrypt the assertion
|
|
105
|
+
const samlArtifactBuffer = Buffer.from(samlArtifact, 'base64')
|
|
106
|
+
const samlArtifactMessage = samlArtifactBuffer.toString('utf8', 24)
|
|
107
|
+
|
|
108
|
+
let nric
|
|
109
|
+
if (samlArtifactMessage.startsWith('customNric:')) {
|
|
110
|
+
nric = samlArtifactMessage.slice('customNric:'.length)
|
|
111
|
+
} else {
|
|
112
|
+
let index = samlArtifactBuffer.readInt8(
|
|
113
|
+
samlArtifactBuffer.length - 1,
|
|
114
|
+
)
|
|
115
|
+
// use env NRIC when SHOW_LOGIN_PAGE is false
|
|
116
|
+
if (index === -1) {
|
|
117
|
+
index =
|
|
118
|
+
idp === 'singPass'
|
|
119
|
+
? assertions.saml.singPass.indexOf(assertions.singPassNric)
|
|
120
|
+
: assertions.saml.corpPass.findIndex(
|
|
121
|
+
(c) => c.nric === assertions.corpPassNric,
|
|
122
|
+
)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
nric = assertions.saml[idp][index]
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const samlArtifactResolveId = xpath.select(
|
|
129
|
+
"string(//*[local-name(.)='ArtifactResolve']/@ID)",
|
|
130
|
+
xml,
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
let result = assertions.saml.create[idp](
|
|
134
|
+
nric,
|
|
135
|
+
idpConfig[idp].id,
|
|
136
|
+
idpConfig[idp].assertEndpoint,
|
|
137
|
+
samlArtifactResolveId,
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
if (cryptoConfig.signAssertion) {
|
|
141
|
+
result = sign(result, "//*[local-name(.)='Assertion']")
|
|
142
|
+
}
|
|
143
|
+
const assertionPromise = cryptoConfig.encryptAssertion
|
|
144
|
+
? promiseToEncryptAssertion(result)
|
|
145
|
+
: Promise.resolve(result)
|
|
146
|
+
|
|
147
|
+
assertionPromise.then((assertion) => {
|
|
148
|
+
let response = render(TEMPLATE, {
|
|
149
|
+
assertion,
|
|
150
|
+
issueInstant: moment.utc().format(),
|
|
151
|
+
issuer: idpConfig[idp].id,
|
|
152
|
+
destination: idpConfig[idp].assertEndpoint,
|
|
153
|
+
inResponseTo: samlArtifactResolveId,
|
|
154
|
+
})
|
|
155
|
+
if (cryptoConfig.signResponse) {
|
|
156
|
+
response = sign(
|
|
157
|
+
sign(response, "//*[local-name(.)='Response']"),
|
|
158
|
+
"//*[local-name(.)='ArtifactResponse']",
|
|
159
|
+
)
|
|
160
|
+
}
|
|
161
|
+
res.send(response)
|
|
162
|
+
})
|
|
163
|
+
}
|
|
164
|
+
},
|
|
165
|
+
)
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return app
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
module.exports = config
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
const express = require('express')
|
|
2
|
+
const fs = require('fs')
|
|
3
|
+
const { render } = require('mustache')
|
|
4
|
+
const jose = require('node-jose')
|
|
5
|
+
const path = require('path')
|
|
6
|
+
const ExpiryMap = require('expiry-map')
|
|
7
|
+
|
|
8
|
+
const assertions = require('../assertions')
|
|
9
|
+
const { samlArtifact } = require('../saml-artifact')
|
|
10
|
+
|
|
11
|
+
const LOGIN_TEMPLATE = fs.readFileSync(
|
|
12
|
+
path.resolve(__dirname, '../../static/html/login-page.html'),
|
|
13
|
+
'utf8',
|
|
14
|
+
)
|
|
15
|
+
const NONCE_TIMEOUT = 5 * 60 * 1000
|
|
16
|
+
const nonceStore = new ExpiryMap(NONCE_TIMEOUT)
|
|
17
|
+
|
|
18
|
+
const PATH_PREFIX = '/sgid/v1/oauth'
|
|
19
|
+
|
|
20
|
+
const signingPem = fs.readFileSync(
|
|
21
|
+
path.resolve(__dirname, '../../static/certs/spcp-key.pem'),
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
const idGenerator = {
|
|
25
|
+
singPass: (rawId) =>
|
|
26
|
+
assertions.myinfo.v3.personas[rawId] ? `${rawId} [MyInfo]` : rawId,
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function config(app, { showLoginPage, idpConfig, serviceProvider }) {
|
|
30
|
+
app.get(`${PATH_PREFIX}/authorize`, (req, res) => {
|
|
31
|
+
const redirectURI = req.query.redirect_uri
|
|
32
|
+
const state = encodeURIComponent(req.query.state)
|
|
33
|
+
if (showLoginPage) {
|
|
34
|
+
const oidc = assertions.oidc.singPass
|
|
35
|
+
const values = oidc
|
|
36
|
+
.filter((rawId) => assertions.myinfo.v3.personas[rawId])
|
|
37
|
+
.map((rawId) => {
|
|
38
|
+
const index = oidc.indexOf(rawId)
|
|
39
|
+
const code = encodeURIComponent(
|
|
40
|
+
samlArtifact(idpConfig.singPass.id, index),
|
|
41
|
+
)
|
|
42
|
+
if (req.query.nonce) {
|
|
43
|
+
nonceStore.set(code, req.query.nonce)
|
|
44
|
+
}
|
|
45
|
+
const assertURL = `${redirectURI}?code=${code}&state=${state}`
|
|
46
|
+
const id = idGenerator.singPass(rawId)
|
|
47
|
+
return { id, assertURL }
|
|
48
|
+
})
|
|
49
|
+
const response = render(LOGIN_TEMPLATE, { values })
|
|
50
|
+
res.send(response)
|
|
51
|
+
} else {
|
|
52
|
+
const code = encodeURIComponent(samlArtifact(idpConfig.singPass.id))
|
|
53
|
+
if (req.query.nonce) {
|
|
54
|
+
nonceStore.set(code, req.query.nonce)
|
|
55
|
+
}
|
|
56
|
+
const assertURL = `${redirectURI}?code=${code}&state=${state}`
|
|
57
|
+
console.warn(
|
|
58
|
+
`Redirecting login from ${req.query.client_id} to ${assertURL}`,
|
|
59
|
+
)
|
|
60
|
+
res.redirect(assertURL)
|
|
61
|
+
}
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
app.post(
|
|
65
|
+
`${PATH_PREFIX}/token`,
|
|
66
|
+
express.json(),
|
|
67
|
+
express.urlencoded({ extended: true }),
|
|
68
|
+
async (req, res) => {
|
|
69
|
+
console.log(req.body)
|
|
70
|
+
const { client_id: aud, code: artifact } = req.body
|
|
71
|
+
let uuid
|
|
72
|
+
|
|
73
|
+
console.warn(
|
|
74
|
+
`Received artifact ${artifact} from ${aud} and ${req.body.redirect_uri}`,
|
|
75
|
+
)
|
|
76
|
+
try {
|
|
77
|
+
const artifactBuffer = Buffer.from(artifact, 'base64')
|
|
78
|
+
uuid = artifactBuffer.readInt8(artifactBuffer.length - 1)
|
|
79
|
+
const nonce = nonceStore.get(encodeURIComponent(artifact))
|
|
80
|
+
|
|
81
|
+
// use env NRIC when SHOW_LOGIN_PAGE is false
|
|
82
|
+
if (uuid === -1) {
|
|
83
|
+
uuid = assertions.oidc.singPass.indexOf(assertions.singPassNric)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const accessToken = `${uuid}`
|
|
87
|
+
const { idTokenClaims, refreshToken } =
|
|
88
|
+
await assertions.oidc.create.singPass(
|
|
89
|
+
uuid,
|
|
90
|
+
`${req.protocol}://${req.get('host')}`,
|
|
91
|
+
aud,
|
|
92
|
+
nonce,
|
|
93
|
+
accessToken,
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
const signingKey = await jose.JWK.asKey(signingPem, 'pem')
|
|
97
|
+
const idToken = await jose.JWS.createSign(
|
|
98
|
+
{ format: 'compact' },
|
|
99
|
+
signingKey,
|
|
100
|
+
)
|
|
101
|
+
.update(JSON.stringify(idTokenClaims))
|
|
102
|
+
.final()
|
|
103
|
+
|
|
104
|
+
res.json({
|
|
105
|
+
access_token: accessToken,
|
|
106
|
+
refresh_token: refreshToken,
|
|
107
|
+
expires_in: 24 * 60 * 60,
|
|
108
|
+
scope: 'openid',
|
|
109
|
+
token_type: 'bearer',
|
|
110
|
+
id_token: idToken,
|
|
111
|
+
})
|
|
112
|
+
} catch (error) {
|
|
113
|
+
console.error(error)
|
|
114
|
+
res.status(500).json({ message: error.message })
|
|
115
|
+
}
|
|
116
|
+
},
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
app.get(`${PATH_PREFIX}/userinfo`, async (req, res) => {
|
|
120
|
+
const uuid = (
|
|
121
|
+
req.headers.authorization || req.headers.Authorization
|
|
122
|
+
).replace('Bearer ', '')
|
|
123
|
+
const nric = assertions.oidc.singPass[uuid]
|
|
124
|
+
const persona = assertions.myinfo.v3.personas[nric]
|
|
125
|
+
const name = persona.name.value
|
|
126
|
+
const dateOfBirth = persona.dob.value
|
|
127
|
+
|
|
128
|
+
const payloadKey = await jose.JWK.createKey('oct', 256, {
|
|
129
|
+
alg: 'A256GCM',
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
const encryptedNric = await jose.JWE.createEncrypt(payloadKey)
|
|
133
|
+
.update(nric)
|
|
134
|
+
.final()
|
|
135
|
+
const encryptedName = await jose.JWE.createEncrypt(payloadKey)
|
|
136
|
+
.update(name)
|
|
137
|
+
.final()
|
|
138
|
+
const encryptedDateOfBirth = await jose.JWE.createEncrypt(payloadKey)
|
|
139
|
+
.update(dateOfBirth)
|
|
140
|
+
.final()
|
|
141
|
+
const data = {
|
|
142
|
+
'myinfo.nric_number': encryptedNric,
|
|
143
|
+
'myinfo.name': encryptedName,
|
|
144
|
+
'myinfo.date_of_birth': encryptedDateOfBirth,
|
|
145
|
+
}
|
|
146
|
+
const encryptionKey = await jose.JWK.asKey(serviceProvider.cert, 'pem')
|
|
147
|
+
|
|
148
|
+
const plaintextPayloadKey = JSON.stringify(payloadKey.toJSON(true))
|
|
149
|
+
console.log(plaintextPayloadKey)
|
|
150
|
+
const encryptedPayloadKey = await jose.JWE.createEncrypt(encryptionKey)
|
|
151
|
+
.update(plaintextPayloadKey)
|
|
152
|
+
.final()
|
|
153
|
+
res.json({
|
|
154
|
+
sub: `u=${uuid}`,
|
|
155
|
+
key: encryptedPayloadKey,
|
|
156
|
+
data,
|
|
157
|
+
})
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
app.get('/.well-known/jwks.json', async (_req, res) => {
|
|
161
|
+
const key = await jose.JWK.asKey(signingPem, 'pem')
|
|
162
|
+
const jwk = key.toJSON()
|
|
163
|
+
jwk.use = 'sig'
|
|
164
|
+
res.json({ keys: [jwk] })
|
|
165
|
+
})
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
module.exports = config
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
const crypto = require('crypto')
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Construct a SingPass/CorpPass SAML artifact, a base64
|
|
5
|
+
* encoding of a byte sequence consisting of the following:
|
|
6
|
+
* - a two-byte type code, always 0x0004, per SAML 2.0;
|
|
7
|
+
* - a two-byte endpoint index, currently always 0x0000;
|
|
8
|
+
* - a 20-byte sha1 hash of the partner id, and;
|
|
9
|
+
* - a 20-byte random sequence that is effectively the message id
|
|
10
|
+
* @param {string} partnerId - the partner id
|
|
11
|
+
* @param {number} [index] - represents the nth identity to use. Defaults to -1
|
|
12
|
+
* @return {string} the SAML artifact, a base64 string
|
|
13
|
+
* containing the type code, the endpoint index,
|
|
14
|
+
* the hash of the partner id, followed by 20 random bytes
|
|
15
|
+
*/
|
|
16
|
+
function samlArtifact(partnerId, index) {
|
|
17
|
+
const hashedPartnerId = hashPartnerId(partnerId)
|
|
18
|
+
const randomBytes = crypto.randomBytes(19).toString('hex')
|
|
19
|
+
const indexBuffer = Buffer.alloc(1)
|
|
20
|
+
indexBuffer.writeInt8(index || index === 0 ? index : -1)
|
|
21
|
+
const indexString = indexBuffer.toString('hex')
|
|
22
|
+
return Buffer.from(
|
|
23
|
+
`00040000${hashedPartnerId}${randomBytes}${indexString}`,
|
|
24
|
+
'hex',
|
|
25
|
+
).toString('base64')
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function hashPartnerId(partnerId) {
|
|
29
|
+
return crypto.createHash('sha1').update(partnerId, 'utf8').digest('hex')
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
module.exports = { samlArtifact, hashPartnerId }
|
package/package.json
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@opengovsg/mockpass",
|
|
3
|
+
"version": "2.7.9",
|
|
4
|
+
"description": "A mock SingPass/CorpPass server for dev purposes",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"mockpass": "index.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"test": "echo \"Error: no test specified\" && exit 1",
|
|
11
|
+
"start": "nodemon",
|
|
12
|
+
"cz": "git-cz",
|
|
13
|
+
"lint": "eslint lib",
|
|
14
|
+
"lint-fix": "eslint --fix lib",
|
|
15
|
+
"_postinstall": "husky install",
|
|
16
|
+
"prepublishOnly": "pinst --disable",
|
|
17
|
+
"postpublish": "pinst --enable"
|
|
18
|
+
},
|
|
19
|
+
"repository": {
|
|
20
|
+
"type": "git",
|
|
21
|
+
"url": "git+https://github.com/opengovsg/mockpass.git"
|
|
22
|
+
},
|
|
23
|
+
"keywords": [
|
|
24
|
+
"mock",
|
|
25
|
+
"test",
|
|
26
|
+
"singpass",
|
|
27
|
+
"corppass"
|
|
28
|
+
],
|
|
29
|
+
"author": "Government Technology Agency of Singapore (https://www.tech.gov.sg)",
|
|
30
|
+
"license": "MIT",
|
|
31
|
+
"bugs": {
|
|
32
|
+
"url": "https://github.com/opengovsg/mockpass/issues"
|
|
33
|
+
},
|
|
34
|
+
"homepage": "https://github.com/opengovsg/mockpass#readme",
|
|
35
|
+
"engines": {
|
|
36
|
+
"node": ">=8.0.0"
|
|
37
|
+
},
|
|
38
|
+
"dependencies": {
|
|
39
|
+
"@xmldom/xmldom": "^0.7.2",
|
|
40
|
+
"base-64": "^1.0.0",
|
|
41
|
+
"cookie-parser": "^1.4.3",
|
|
42
|
+
"dotenv": "^10.0.0",
|
|
43
|
+
"expiry-map": "^1.1.0",
|
|
44
|
+
"express": "^4.16.3",
|
|
45
|
+
"jsonwebtoken": "^8.4.0",
|
|
46
|
+
"lodash": "^4.17.11",
|
|
47
|
+
"moment": "^2.24.0",
|
|
48
|
+
"morgan": "^1.9.1",
|
|
49
|
+
"mustache": "^4.2.0",
|
|
50
|
+
"node-jose": "^2.0.0",
|
|
51
|
+
"uuid": "^8.0.0",
|
|
52
|
+
"xml-crypto": "^2.1.2",
|
|
53
|
+
"xml-encryption": "^1.2.4",
|
|
54
|
+
"xpath": "0.0.32"
|
|
55
|
+
},
|
|
56
|
+
"devDependencies": {
|
|
57
|
+
"@commitlint/cli": "^13.1.0",
|
|
58
|
+
"@commitlint/config-conventional": "^13.1.0",
|
|
59
|
+
"@commitlint/travis-cli": "^13.1.0",
|
|
60
|
+
"commitizen": "^4.2.4",
|
|
61
|
+
"cz-conventional-changelog": "^3.2.0",
|
|
62
|
+
"eslint": "^8.0.0",
|
|
63
|
+
"eslint-config-prettier": "^8.3.0",
|
|
64
|
+
"eslint-plugin-prettier": "^4.0.0",
|
|
65
|
+
"husky": "^7.0.0",
|
|
66
|
+
"lint-staged": "^11.0.0",
|
|
67
|
+
"nodemon": "^2.0.4",
|
|
68
|
+
"pinst": "^2.1.6",
|
|
69
|
+
"prettier": "^2.0.5"
|
|
70
|
+
},
|
|
71
|
+
"lint-staged": {
|
|
72
|
+
"**/*.(js|jsx|ts|tsx)": [
|
|
73
|
+
"eslint --fix"
|
|
74
|
+
]
|
|
75
|
+
},
|
|
76
|
+
"config": {
|
|
77
|
+
"commitizen": {
|
|
78
|
+
"path": "./node_modules/cz-conventional-changelog"
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
@charset "UTF-8";
|
|
2
|
+
|
|
3
|
+
/* login page, qr tab tooltip animation */
|
|
4
|
+
@keyframes tooltip-ani {
|
|
5
|
+
0% {
|
|
6
|
+
transform: translateX(0) rotateY(-16deg);
|
|
7
|
+
opacity: 1;
|
|
8
|
+
}
|
|
9
|
+
5% {
|
|
10
|
+
transform: translateX(0) rotateY(20deg);
|
|
11
|
+
opacity: 1;
|
|
12
|
+
}
|
|
13
|
+
10% {
|
|
14
|
+
transform: translateX(0) rotateY(-12deg);
|
|
15
|
+
opacity: 1;
|
|
16
|
+
}
|
|
17
|
+
15% {
|
|
18
|
+
transform: translateX(0) rotateY(6deg);
|
|
19
|
+
opacity: 1;
|
|
20
|
+
}
|
|
21
|
+
20% {
|
|
22
|
+
transform: translateX(0) rotateY(-4deg);
|
|
23
|
+
opacity: 1;
|
|
24
|
+
}
|
|
25
|
+
25% {
|
|
26
|
+
transform: translateX(0) rotateY(1deg);
|
|
27
|
+
opacity: 1;
|
|
28
|
+
}
|
|
29
|
+
30% {
|
|
30
|
+
transform: translateX(0) rotateY(0deg);
|
|
31
|
+
opacity: 1;
|
|
32
|
+
}
|
|
33
|
+
100% {
|
|
34
|
+
transform: translateX(0) rotateY(0deg);
|
|
35
|
+
opacity: 1;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
.ani-rotate {
|
|
39
|
+
animation-name: tooltip-ani;
|
|
40
|
+
animation-duration: 1.5s;
|
|
41
|
+
animation-iteration-count: 3;
|
|
42
|
+
animation-timing-function: cubic-bezier(1.000, 0.000, 0.000, 1.000);
|
|
43
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/*------------------------------------------------
|
|
2
|
+
LOADING SCREEN START
|
|
3
|
+
------------------------------------------------ */
|
|
4
|
+
.loading-screen-wrappper {
|
|
5
|
+
background-color: #000;
|
|
6
|
+
position: fixed;
|
|
7
|
+
height: 100%;
|
|
8
|
+
width: 100vw;
|
|
9
|
+
left: 0;
|
|
10
|
+
top: 0;
|
|
11
|
+
opacity: 0.7;
|
|
12
|
+
z-index: 9999;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
.loading-screen-container {
|
|
16
|
+
position: relative;
|
|
17
|
+
top: 50%;
|
|
18
|
+
transform: translateY(-50%);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
.loader-title {
|
|
22
|
+
width: 300px;
|
|
23
|
+
height: 50px;
|
|
24
|
+
color: #fff;
|
|
25
|
+
font-weight: bold;
|
|
26
|
+
font-size: 16px;
|
|
27
|
+
margin: auto;
|
|
28
|
+
text-align: center;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
.loader {
|
|
32
|
+
border: 16px solid #f3f3f3;
|
|
33
|
+
border-radius: 50%;
|
|
34
|
+
border-top: 16px solid #ff0000;
|
|
35
|
+
width: 50px;
|
|
36
|
+
height: 50px;
|
|
37
|
+
-webkit-animation: spin 2s linear infinite;
|
|
38
|
+
animation: spin 2s linear infinite;
|
|
39
|
+
margin: auto;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
@-webkit-keyframes spin {
|
|
43
|
+
0% { -webkit-transform: rotate(0deg); }
|
|
44
|
+
100% { -webkit-transform: rotate(360deg); }
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
@keyframes spin {
|
|
48
|
+
0% { transform: rotate(0deg); }
|
|
49
|
+
100% { transform: rotate(360deg); }
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/*------------------------------------------------
|
|
53
|
+
LOADING SCREEN END
|
|
54
|
+
------------------------------------------------ */
|
|
55
|
+
|
|
56
|
+
/*------------------------------------------------
|
|
57
|
+
PASSWORD COMPLEXITY START
|
|
58
|
+
------------------------------------------------ */
|
|
59
|
+
.password-complexity-checker-wrapper {
|
|
60
|
+
position: relative;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
.pwd-complexity-hidden {
|
|
64
|
+
display: none;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
.pwd-complexity-info {
|
|
68
|
+
border: 1px solid #a4a4a4;
|
|
69
|
+
background-color: #fff;
|
|
70
|
+
position: absolute;
|
|
71
|
+
z-index: 61;
|
|
72
|
+
font-size: 14px;
|
|
73
|
+
padding: 5px 10px;
|
|
74
|
+
width: 100%;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
.pc-form-success {
|
|
78
|
+
color: #2fa13e;
|
|
79
|
+
padding-right: 3px;
|
|
80
|
+
font-size: inherit;
|
|
81
|
+
vertical-align: top;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
.pc-form-error {
|
|
85
|
+
color: #cf2010;
|
|
86
|
+
padding-right: 3px;
|
|
87
|
+
font-size: inherit;
|
|
88
|
+
vertical-align: top;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
.pc-form-error .icon-exclamation, .pc-form-success .icon-exclamation {
|
|
92
|
+
display: inline-block;
|
|
93
|
+
width: 16px;
|
|
94
|
+
height: 16px;
|
|
95
|
+
margin-right: 10px;
|
|
96
|
+
position: relative;
|
|
97
|
+
top: 4px;
|
|
98
|
+
background: url("../img/pwd-complexity-icon-0a8ed77b6b99b6fd7cf2206943de1612.png");
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
.pc-form-success .icon-exclamation {
|
|
102
|
+
background-position: -16px 0;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
.pwd-complexity-info>p {
|
|
106
|
+
margin: 0 0 5px 0;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
.pwd-complexity-info>ul {
|
|
110
|
+
list-style: none;
|
|
111
|
+
margin: 0;
|
|
112
|
+
padding: 0 0 10px 0;
|
|
113
|
+
font-size: 12px;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
input.password-error {
|
|
117
|
+
border: 1px solid red;
|
|
118
|
+
}
|
|
119
|
+
/*------------------------------------------------
|
|
120
|
+
PASSWORD COMPLEXITY END
|
|
121
|
+
------------------------------------------------ */
|