diff --git a/.gitignore b/.gitignore index 28f1ba75..f854f3fb 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ node_modules -.DS_Store \ No newline at end of file +.DS_Store +package-lock.json diff --git a/lib/saml20.js b/lib/saml20.js index 21c1f278..acd501f4 100644 --- a/lib/saml20.js +++ b/lib/saml20.js @@ -74,7 +74,13 @@ exports.create = function(options, callback) { var cert = utils.pemToCert(options.cert); - var sig = new SignedXml(null, { signatureAlgorithm: algorithms.signature[options.signatureAlgorithm], idAttribute: 'ID' }); + var signatureAlgorithm = algorithms.signature[options.signatureAlgorithm]; + if (typeof (options.signatureFunction) === 'function') { + signatureAlgorithm = (new options.signatureFunction()).getAlgorithmName(); + SignedXml.SignatureAlgorithms[signatureAlgorithm] = options.signatureFunction; + } + var sig = new SignedXml(null, { signatureAlgorithm: signatureAlgorithm, idAttribute: 'ID'}); + sig.addReference("//*[local-name(.)='Assertion']", ["http://www.w3.org/2000/09/xmldsig#enveloped-signature", "http://www.w3.org/2001/10/xml-exc-c14n#"], algorithms.digest[options.digestAlgorithm]); @@ -200,32 +206,39 @@ exports.create = function(options, callback) { }, prefix: options.signatureNamespacePrefix }; - - sig.computeSignature(token, opts); - signed = sig.getSignedXml(); - } catch(err){ - return utils.reportError(err, callback); - } - if (!options.encryptionCert) { - if (callback) - return callback(null, signed); - else - return signed; - } + if (callback) { + sig.computeSignature(token, opts, function(err, sigXML){ + if (err || !sigXML) return callback(err) + signed = sigXML.getSignedXml(); + if (!options.encryptionCert) { + return callback(null, signed); + } - var encryptOptions = { - rsa_pub: options.encryptionPublicKey, - pem: options.encryptionCert, - encryptionAlgorithm: options.encryptionAlgorithm || 'http://www.w3.org/2001/04/xmlenc#aes256-cbc', - keyEncryptionAlgorighm: options.keyEncryptionAlgorighm || 'http://www.w3.org/2001/04/xmlenc#rsa-oaep-mgf1p' - }; + var encryptOptions = { + rsa_pub: options.encryptionPublicKey, + pem: options.encryptionCert, + encryptionAlgorithm: options.encryptionAlgorithm || 'http://www.w3.org/2001/04/xmlenc#aes256-cbc', + keyEncryptionAlgorighm: options.keyEncryptionAlgorighm || 'http://www.w3.org/2001/04/xmlenc#rsa-oaep-mgf1p' + }; + + xmlenc.encrypt(signed, encryptOptions, function(err, encrypted) { + if (err) return callback(err); + encrypted = '' + encrypted + ''; + callback(null, utils.removeWhitespace(encrypted)); + }); + }); + } else { + sig.computeSignature(token, opts); + signed = sig.getSignedXml(); - xmlenc.encrypt(signed, encryptOptions, function(err, encrypted) { - if (err) return callback(err); - encrypted = '' + encrypted + ''; - callback(null, utils.removeWhitespace(encrypted)); - }); + if (!options.encryptionCert) { + return signed; + } + } + } catch(err){ + return utils.reportError(err, callback); + } }; diff --git a/package.json b/package.json index 83c47b14..cab72b9d 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ "async": "~0.2.9", "moment": "2.19.3", "valid-url": "~1.0.9", - "xml-crypto": "~1.0.1", + "xml-crypto": "~1.5.3", "xml-encryption": "0.11.2", "xml-name-validator": "~2.0.1", "xmldom": "=0.1.15", diff --git a/test/saml20.tests.js b/test/saml20.tests.js index e351cfa3..29684430 100644 --- a/test/saml20.tests.js +++ b/test/saml20.tests.js @@ -9,6 +9,74 @@ var assert = require('assert'), describe('saml 2.0', function () { + it('testing all using async and custom signing algo', function () { + var options = { + cert: fs.readFileSync(__dirname + '/test-auth0.pem'), + key: fs.readFileSync(__dirname + '/test-auth0.key'), + issuer: 'urn:issuer', + lifetimeInSeconds: 600, + audiences: 'urn:myapp', + attributes: { + 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress': 'foo@bar.com', + 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name': 'Foo Bar' + }, + nameIdentifier: 'foo', + nameIdentifierFormat: 'urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified', + signingFunction: function RSASHA256() { + this.getSignature = function(signedInfo, signingKey, callback) { + var signer = crypto.createSign("RSA-SHA256") + signer.update(signedInfo) + var res = signer.sign(signingKey, 'base64') + //or do async signing from key server or HSM + callback(null, res) + } + + this.verifySignature = function(str, key, signatureValue, callback) { + var verifier = crypto.createVerify("RSA-SHA256") + verifier.update(str) + var res = verifier.verify(key, signatureValue, 'base64') + //verify async as well + callback(null, res) + } + + this.getAlgorithmName = function() { + return "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256" + } + } + }; + + saml.create(options, function(err, signedAssertion){ + var isValid = utils.isValidSignature(signedAssertion, options.cert); + assert.equal(true, isValid); + + var nameIdentifier = utils.getNameID(signedAssertion); + assert.equal('foo', nameIdentifier.textContent); + assert.equal('urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified', nameIdentifier.getAttribute('Format')); + + var attributes = utils.getAttributes(signedAssertion); + assert.equal(2, attributes.length); + assert.equal('http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress', attributes[0].getAttribute('Name')); + assert.equal('foo@bar.com', attributes[0].textContent); + assert.equal('http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name', attributes[1].getAttribute('Name')); + assert.equal('Foo Bar', attributes[1].textContent); + + assert.equal('urn:issuer', utils.getSaml2Issuer(signedAssertion).textContent); + + var conditions = utils.getConditions(signedAssertion); + assert.equal(1, conditions.length); + var notBefore = conditions[0].getAttribute('NotBefore'); + var notOnOrAfter = conditions[0].getAttribute('NotOnOrAfter'); + should.ok(notBefore); + should.ok(notOnOrAfter); + + var lifetime = Math.round((moment(notOnOrAfter).utc() - moment(notBefore).utc()) / 1000); + assert.equal(600, lifetime); + + var authnContextClassRef = utils.getAuthnContextClassRef(signedAssertion); + assert.equal('urn:oasis:names:tc:SAML:2.0:ac:classes:unspecified', authnContextClassRef.textContent); + }); + }); + it('whole thing with default authnContextClassRef', function () { var options = { cert: fs.readFileSync(__dirname + '/test-auth0.pem'),