Skip to content
This repository has been archived by the owner on Dec 2, 2022. It is now read-only.

Commit

Permalink
Merge pull request #10 from datagovsg/real-sign-tests
Browse files Browse the repository at this point in the history
Upgrade xml-crypto to 1.0.1, add tests for 100% coverage
  • Loading branch information
LoneRifle authored Sep 14, 2018
2 parents bec3506 + 452ba17 commit 14d53a6
Show file tree
Hide file tree
Showing 10 changed files with 375 additions and 132 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,9 @@ app.route('/login', (req, res) => {
// identity using out-of-band (OOB) authentication
app.route('/assert', (req, res) => {
const { SAMLArt: samlArt, RelayState: relayState } = req.query
client.getUserName(samlArt, relayState, (err, data) => {
client.getAttributes(samlArt, relayState, (err, data) => {
// If all is well and login occurs, the attributes are given
// In all cases, the relayState as provided in getUserName() is given
// In all cases, the relayState as provided in getAttributes() is given
const { attributes, relayState } = data
// For SingPass, a user name will be given
// Refer to unit tests to infer what CorpPass will give
Expand Down
14 changes: 10 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@opengovsg/spcp-auth-client",
"version": "1.2.2",
"version": "1.2.3",
"description": "Integrates SingPass and CorpPass into your node.js application",
"main": "SPCPAuthClient.class.js",
"scripts": {
Expand Down Expand Up @@ -28,7 +28,7 @@
"jsonwebtoken": "^8.3.0",
"lodash": "^4.17.10",
"request": "^2.87.0",
"xml-crypto": "^1.0.0",
"xml-crypto": "^1.0.1",
"xml-encryption": "^0.11.2",
"xml2json-light": "^1.0.6",
"xmldom": "^0.1.27",
Expand All @@ -45,6 +45,7 @@
"eslint-plugin-promise": "^3.8.0",
"eslint-plugin-standard": "^3.1.0",
"mocha": "^5.2.0",
"mustache": "^2.3.2",
"nyc": "^12.0.2",
"sinon": "^6.1.5",
"source-map-support": "^0.5.8"
Expand Down
218 changes: 218 additions & 0 deletions test/SPCPAuthClient.class.attrib.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,218 @@
const { expect } = require('chai')
const fs = require('fs')
const { render } = require('mustache')
const request = require('request')
const sinon = require('sinon')
const xmlCrypto = require('xml-crypto')
const xmlEnc = require('xml-encryption')

const { encrypt } = xmlEnc
const { SignedXml } = xmlCrypto
const SPCPAuthClient = require('../SPCPAuthClient.class')

describe('SPCPAuthClient - getAttributes', () => {
const authClient = new SPCPAuthClient({
partnerEntityId: 'partnerEntityId',
idpEndpoint: 'idpEndpoint',
idpLoginURL: 'idpLoginURL',
appKey: fs.readFileSync('./test/fixtures/certs/key.pem'),
appCert: 'appCert',
spcpCert: fs.readFileSync('./test/fixtures/certs/spcp.crt'),
esrvcID: 'esrvcID',
})

const input = { name: 'UserName', value: 'S1234567A' }
const assertion = render(
fs.readFileSync(
'./test/fixtures/saml/unsigned-assertion.xml', 'utf8'
),
input
)

const signatureTargets = {
response: {
location: {
reference: "//*[local-name(.)='Response']/*[local-name(.)='Issuer']",
action: 'after',
},
reference: "//*[local-name(.)='Response']",
},
artifactResponse: {
location: {
reference: "//*[local-name(.)='ArtifactResponse']/*[local-name(.)='Issuer']",
action: 'after',
},
reference: "//*[local-name(.)='ArtifactResponse']",
},
}

const prepareSignedXml = (payload, ...targets) => {
let result = payload
for (const { reference, location } of targets) {
const sig = new SignedXml()

const transforms = [
'http://www.w3.org/2000/09/xmldsig#enveloped-signature',
'http://www.w3.org/2001/10/xml-exc-c14n#',
]
const digestAlgorithm = 'http://www.w3.org/2001/04/xmlenc#sha256'
sig.addReference(reference, transforms, digestAlgorithm)
sig.signingKey = fs.readFileSync('./test/fixtures/certs/spcp-key.pem')
sig.signatureAlgorithm = 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha256'
sig.computeSignature(result, { prefix: 'ds', location })
result = sig.getSignedXml()
}
return result
}

let signedPackage

before(done => {
const options = {
rsa_pub: fs.readFileSync('./test/fixtures/certs/key.pub'),
pem: fs.readFileSync('./test/fixtures/certs/server.crt'),
encryptionAlgorithm: 'http://www.w3.org/2001/04/xmlenc#aes256-cbc',
keyEncryptionAlgorighm: 'http://www.w3.org/2001/04/xmlenc#rsa-1_5',
}
const callback = (err, data) => {
if (err) {
done(err)
} else {
const response = render(
fs.readFileSync('./test/fixtures/saml/unsigned-response.xml', 'utf8'),
{
assertion: `<saml:EncryptedAssertion>${data}</saml:EncryptedAssertion>`,
}
)
signedPackage = prepareSignedXml(
response,
signatureTargets.response,
signatureTargets.artifactResponse
)
done()
}
}
encrypt(assertion, options, callback)
})

afterEach(() => {
if (typeof request.post.restore === 'function') {
request.post.restore()
}
})

it('should error on no SAMLArt', done => {
authClient.getAttributes(undefined, 'relayState', (err, data) => {
expect(err).to.be.instanceOf(Error)
expect(data).to.eql({ relayState: 'relayState' })
done()
})
})

it('should error on no relayState', done => {
authClient.getAttributes('artifact', undefined, (err, data) => {
expect(err).to.be.instanceOf(Error)
expect(data).to.eql({ relayState: undefined })
done()
})
})

it('should error on artifact resolve fail', done => {
const signingError = new Error('sign')
sinon.stub(authClient, 'signXML').returns({
artifactResolve: undefined,
signingError,
})
try {
authClient.getAttributes('artifact', 'relayState', (err, data) => {
expect(err).to.be.instanceOf(Error)
expect(err.cause).to.equal(signingError)
expect(data).to.eql({ relayState: 'relayState' })
done()
})
} finally {
authClient.signXML.restore()
}
})

it('should error on POST fail', done => {
const resolveError = new Error('sign')
sinon.stub(request, 'post').callsFake((options, callback) => {
callback(resolveError, undefined, undefined)
})
authClient.getAttributes('artifact', 'relayState', (err, data) => {
expect(err).to.be.instanceOf(Error)
expect(err.cause).to.equal(resolveError)
expect(data).to.eql({ relayState: 'relayState' })
done()
})
})

it('should error on verify fail', done => {
sinon.stub(request, 'post').callsFake((options, callback) => {
callback(undefined, undefined, signedPackage)
})
const verificationError = new Error('verify')
sinon.stub(authClient, 'verifyXML').returns({
isVerified: null,
verificationError,
})
try {
authClient.getAttributes('artifact', 'relayState', (err, data) => {
expect(err).to.be.instanceOf(Error)
expect(err.cause).to.equal(verificationError)
expect(data).to.eql({ relayState: 'relayState' })
done()
})
} finally {
authClient.verifyXML.restore()
}
})

it('should error on decrypt fail', done => {
sinon.stub(request, 'post').callsFake((options, callback) => {
callback(undefined, undefined, signedPackage)
})
const decryptionError = new Error('decrypt')
sinon.stub(xmlEnc, 'decrypt').callsFake((data, options, callback) => {
expect(options.key).to.equal(authClient.appKey)
return callback(decryptionError)
})
try {
authClient.getAttributes('artifact', 'relayState', (err, data) => {
expect(err).to.be.instanceOf(Error)
expect(err.cause).to.equal(decryptionError)
expect(data).to.eql({ relayState: 'relayState' })
done()
})
} finally {
xmlEnc.decrypt.restore()
}
})

const expectedXML =
'<samlp:ArtifactResolve xmlns:ds="http://www.w3.org/2000/09/xmldsig#" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" ' +
'xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"' +
' Destination="' +
authClient.idpEndpoint +
'" ID="_0" Version="2.0">' +
'<saml:Issuer Format="urn:oasis:names:tc:SAML:2.0:nameid-format:entity">' +
authClient.partnerEntityId +
'</saml:Issuer>' +
'<samlp:Artifact>artifact</samlp:Artifact>' +
'</samlp:ArtifactResolve>'

it('should get attributes', done => {
sinon.stub(request, 'post').callsFake((options, callback) => {
expect(options.url).to.equal(authClient.idpEndpoint)
expect(options.body).to.equal(authClient.signXML(expectedXML).artifactResolve)
callback(undefined, undefined, signedPackage)
})
authClient.getAttributes('artifact', 'relayState', (err, data) => {
const attributes = { [input.name]: input.value }
const expected = { relayState: 'relayState', attributes }
expect(data, err).to.eql(expected)
done()
})
})
})
48 changes: 17 additions & 31 deletions test/SPCPAuthClient.class.extract.spec.js
Original file line number Diff line number Diff line change
@@ -1,32 +1,28 @@
const { expect } = require('chai')
const base64 = require('base-64')
const { expect } = require('chai')
const fs = require('fs')
const { render } = require('mustache')
const xpath = require('xpath')
const xmldom = require('xmldom')

const { extract: { SINGPASS: singPass, CORPPASS: corpPass } } = require('../SPCPAuthClient.class')

describe('SPCPAuthClient.extract - Attributes Extract Tests', () => {
it('should correctly return SingPass attributes', () => {
const expected = {
UserName: 'S1234567A',
MobileNumber: '91234567',
}
const attributeStatementString = `
<AttributeStatement>
<Attribute Name="UserName">
<AttributeValue>${expected.UserName}</AttributeValue>
</Attribute>
<Attribute Name="MobileNumber">
<AttributeValue>${expected.MobileNumber}</AttributeValue>
</Attribute>
</AttributeStatement>
`
const attributeElements = xpath.select(
const TEMPLATE = fs.readFileSync(
'./test/fixtures/saml/unsigned-assertion.xml', 'utf8'
)

const attributes = input => {
const assertion = render(TEMPLATE, input)
return xpath.select(
`//*[local-name(.)='Attribute']`,
new xmldom.DOMParser().parseFromString(attributeStatementString)
new xmldom.DOMParser().parseFromString(assertion)
)
}

expect(singPass(attributeElements)).to.eql(expected)
it('should correctly return SingPass attributes', () => {
const input = { name: 'UserName', value: 'S1234567A' }
expect(singPass(attributes(input))).to.eql({ UserName: 'S1234567A' })
})

it('should correctly return CorpPass attributes', () => {
Expand Down Expand Up @@ -56,17 +52,7 @@ describe('SPCPAuthClient.extract - Attributes Extract Tests', () => {
</Result_Set>
</AuthAccess>
`
const attributeStatementString = `
<AttributeStatement>
<Attribute Name="${entityId}">
<AttributeValue>${base64.encode(corpPassXMLString)}</AttributeValue>
</Attribute>
</AttributeStatement>
`
const attributeElements = xpath.select(
`//*[local-name(.)='Attribute']`,
new xmldom.DOMParser().parseFromString(attributeStatementString)
)
const input = { name: entityId, value: base64.encode(corpPassXMLString) }

const expected = {
UserInfo: {
Expand All @@ -91,6 +77,6 @@ describe('SPCPAuthClient.extract - Attributes Extract Tests', () => {
},
},
}
expect(corpPass(attributeElements)).to.eql(expected)
expect(corpPass(attributes(input))).to.eql(expected)
})
})
Loading

0 comments on commit 14d53a6

Please sign in to comment.