From b3dd67db85d8a1e9a1868bed1765638349111c4e Mon Sep 17 00:00:00 2001 From: LoneRifle Date: Sat, 20 Oct 2018 11:54:34 +0800 Subject: [PATCH 1/3] Lay groundwork for identity selection * Create identities object, holding identities for SingPass/CorpPass * Allow assertions to be rendered using either default or specified identities --- lib/assertions.js | 33 ++++++++++++++++++++++++++++----- lib/express.js | 3 ++- 2 files changed, 30 insertions(+), 6 deletions(-) diff --git a/lib/assertions.js b/lib/assertions.js index e60d7d8..425132d 100644 --- a/lib/assertions.js +++ b/lib/assertions.js @@ -8,12 +8,35 @@ const readFrom = p => fs.readFileSync(path.resolve(__dirname, p), 'utf8') const TEMPLATE = readFrom('../static/saml/unsigned-assertion.xml') const corpPassTemplate = readFrom('../static/saml/corppass.xml') -const NRIC = process.env.MOCKPASS_NRIC || 'S8979373D' -const UEN = process.env.MOCKPASS_UEN || '123456789A' +const identities = { + singPass: ['S8979373D'], + corpPass: [ + { NRIC: 'S8979373D', UEN: '123456789A' }, + ], +} + +const makeCorpPass = ({ NRIC, UEN }) => render( + TEMPLATE, + { + name: UEN, + value: base64.encode(render(corpPassTemplate, { NRIC, UEN })), + } +) + +const makeSingPass = NRIC => render(TEMPLATE, { name: 'UserName', value: NRIC }) -const CORPPASS = base64.encode(render(corpPassTemplate, { NRIC, UEN })) +const NRIC = process.env.MOCKPASS_NRIC || identities.singPass[0] +const CORPPASS_NRIC = process.env.MOCKPASS_NRIC || identities.corpPass[0].NRIC +const UEN = process.env.MOCKPASS_UEN || identities.corpPass[0].UEN module.exports = { - singPass: render(TEMPLATE, { name: 'UserName', value: NRIC }), - corpPass: render(TEMPLATE, { name: UEN, value: CORPPASS }), + singPass: { + default: makeSingPass(NRIC), + create: makeSingPass, + }, + corpPass: { + default: makeCorpPass({ NRIC: CORPPASS_NRIC, UEN }), + create: makeCorpPass, + }, + identities, } diff --git a/lib/express.js b/lib/express.js index a5941c9..c40c13b 100644 --- a/lib/express.js +++ b/lib/express.js @@ -54,7 +54,8 @@ function config (app, { showLoginPage, serviceProvider, idpConfig }) { console.warn(`Received SAML Artifact ${samlArtifact}`) // Take the template and plug in the typical SingPass/CorpPass response // Sign and encrypt the assertion - const signedAssertion = sign(assertions[idp], "//*[local-name(.)='Assertion']") + const assertion = assertions[idp].default + const signedAssertion = sign(assertion, "//*[local-name(.)='Assertion']") promiseToEncryptAssertion(signedAssertion) .then(assertion => { const response = render(TEMPLATE, { assertion }) From 4e423d8b30e0ebd7ba0b73eb1e63912ca47a15f6 Mon Sep 17 00:00:00 2001 From: LoneRifle Date: Sat, 20 Oct 2018 14:07:28 +0800 Subject: [PATCH 2/3] Implement multiple logins * Inject the index of the mockpass identities into the saml artifact, appending it as a single byte at the end * Read this byte when receiving the artifact from the service provider; if this index corresponds to a mockpass identity, return that, else return the default one --- lib/assertions.js | 2 +- lib/express.js | 29 +++++++++++++++++++++-------- lib/saml-artifact.js | 10 +++++++--- 3 files changed, 29 insertions(+), 12 deletions(-) diff --git a/lib/assertions.js b/lib/assertions.js index 425132d..cfe5624 100644 --- a/lib/assertions.js +++ b/lib/assertions.js @@ -9,7 +9,7 @@ const TEMPLATE = readFrom('../static/saml/unsigned-assertion.xml') const corpPassTemplate = readFrom('../static/saml/corppass.xml') const identities = { - singPass: ['S8979373D'], + singPass: ['S8979373D', 'S1234567A'], corpPass: [ { NRIC: 'S8979373D', UEN: '123456789A' }, ], diff --git a/lib/express.js b/lib/express.js index c40c13b..dc32a1c 100644 --- a/lib/express.js +++ b/lib/express.js @@ -23,15 +23,24 @@ function config (app, { showLoginPage, serviceProvider, idpConfig }) { for (const idp of ['singPass', 'corpPass']) { app.get(`/${idp.toLowerCase()}/logininitial`, (req, res) => { const relayState = encodeURIComponent(req.query.Target) - const samlArt = samlArtifact(idpConfig[idp].id) - const assertURL = - `${idpConfig[idp].assertEndpoint}?SAMLart=${samlArt}&RelayState=${relayState}` - console.warn(`Redirecting login from ${req.query.PartnerId} to ${assertURL}`) if (showLoginPage) { - res.send(` - Click to login here - `) + const identities = assertions.identities[idp] + const body = identities + .map((value, index) => { + const samlArt = samlArtifact(idpConfig[idp].id, index) + const assertURL = + `${idpConfig[idp].assertEndpoint}?SAMLart=${samlArt}&RelayState=${relayState}` + return ` +

Click to login as ${value}

+ ` + }) + .join('\n') + res.send(`${body}`) } else { + const samlArt = samlArtifact(idpConfig[idp].id) + const assertURL = + `${idpConfig[idp].assertEndpoint}?SAMLart=${samlArt}&RelayState=${relayState}` + console.warn(`Redirecting login from ${req.query.PartnerId} to ${assertURL}`) res.redirect(assertURL) } }) @@ -54,7 +63,11 @@ function config (app, { showLoginPage, serviceProvider, idpConfig }) { console.warn(`Received SAML Artifact ${samlArtifact}`) // Take the template and plug in the typical SingPass/CorpPass response // Sign and encrypt the assertion - const assertion = assertions[idp].default + const samlArtifactBuffer = Buffer.from(samlArtifact, 'base64') + const index = samlArtifactBuffer.readInt8(samlArtifactBuffer.length - 1) + const assertion = assertions.identities[idp][index] + ? assertions[idp].create(assertions.identities[idp][index]) + : assertions[idp].default const signedAssertion = sign(assertion, "//*[local-name(.)='Assertion']") promiseToEncryptAssertion(signedAssertion) .then(assertion => { diff --git a/lib/saml-artifact.js b/lib/saml-artifact.js index 71ed7e8..b9018d2 100644 --- a/lib/saml-artifact.js +++ b/lib/saml-artifact.js @@ -8,16 +8,20 @@ const crypto = require('crypto') * - a 20-byte sha1 hash of the partner id, and; * - a 20-byte random sequence that is effectively the message id * @param {string} partnerId - the partner id + * @param {number} [index] - represents the nth identity to use. Defaults to -1 * @return {string} the SAML artifact, a base64 string * containing the type code, the endpoint index, * the hash of the partner id, followed by 20 random bytes */ -function samlArtifact (partnerId) { +function samlArtifact (partnerId, index) { let hashedPartnerId = crypto.createHash('sha1') .update(partnerId, 'utf8') .digest('hex') - const randomBytes = crypto.randomBytes(20).toString('hex') - return Buffer.from(`00040000${hashedPartnerId}${randomBytes}`, 'hex') + const randomBytes = crypto.randomBytes(19).toString('hex') + const indexBuffer = Buffer.alloc(1) + indexBuffer.writeInt8((index || index === 0) ? index : -1) + const indexString = indexBuffer.toString('hex') + return Buffer.from(`00040000${hashedPartnerId}${randomBytes}${indexString}`, 'hex') .toString('base64') } From 7a6c4072d7e4742ad16c300eee7bb33639464bd9 Mon Sep 17 00:00:00 2001 From: LoneRifle Date: Sat, 20 Oct 2018 14:17:08 +0800 Subject: [PATCH 3/3] Add well-known SingPass identities --- lib/assertions.js | 38 +++++++++++++++++++++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/lib/assertions.js b/lib/assertions.js index cfe5624..bca3816 100644 --- a/lib/assertions.js +++ b/lib/assertions.js @@ -9,7 +9,43 @@ const TEMPLATE = readFrom('../static/saml/unsigned-assertion.xml') const corpPassTemplate = readFrom('../static/saml/corppass.xml') const identities = { - singPass: ['S8979373D', 'S1234567A'], + singPass: [ + 'S8979373D', + 'S8116474F', + 'S8723211E', + 'S5062854Z', + 'T0066846F', + 'F9477325W', + 'S3000024B', + 'S6005040F', + 'S6005041D', + 'S6005042B', + 'S6005043J', + 'S6005044I', + 'S6005045G', + 'S6005046E', + 'S6005047C', + 'S6005064C', + 'S6005065A', + 'S6005066Z', + 'S6005037F', + 'S6005038D', + 'S6005039B', + 'G1612357P', + 'G1612358M', + 'F1612359P', + 'F1612360U', + 'F1612361R', + 'F1612362P', + 'F1612363M', + 'F1612364K', + 'F1612365W', + 'F1612366T', + 'F1612367Q', + 'F1612358R', + 'F1612354N', + 'F1612357U', + ], corpPass: [ { NRIC: 'S8979373D', UEN: '123456789A' }, ],