From 1916fbec849de23150adb76a83660677944841d5 Mon Sep 17 00:00:00 2001 From: Jon Lindsey <49694086+jonlindsey@users.noreply.github.com> Date: Thu, 7 May 2020 21:27:31 -0500 Subject: [PATCH 01/11] WIP: Library: Enhance Saml package (saml20) - Adds 2 new return products for SAML 2.0 messaging. A full Signed SAML response with assertion ( encrypted assertion option). --- lib/saml20.js | 231 ++++++++++++++++++++++++------------ lib/saml20Response.template | 6 + test/saml20.tests.js | 170 +++++++++++++++++++++++++- test/utils.js | 8 +- 4 files changed, 334 insertions(+), 81 deletions(-) create mode 100644 lib/saml20Response.template diff --git a/lib/saml20.js b/lib/saml20.js index 21c1f278..00d5301d 100644 --- a/lib/saml20.js +++ b/lib/saml20.js @@ -1,11 +1,11 @@ var utils = require('./utils'), - Parser = require('xmldom').DOMParser, - SignedXml = require('xml-crypto').SignedXml, - xmlenc = require('xml-encryption'), - moment = require('moment'), - xmlNameValidator = require('xml-name-validator'), - is_uri = require('valid-url').is_uri; + Parser = require('xmldom').DOMParser, + SignedXml = require('xml-crypto').SignedXml, + xmlenc = require('xml-encryption'), + moment = require('moment'), + xmlNameValidator = require('xml-name-validator'), + is_uri = require('valid-url').is_uri; var fs = require('fs'); var path = require('path'); @@ -16,7 +16,7 @@ var NAMESPACE = 'urn:oasis:names:tc:SAML:2.0:assertion'; var algorithms = { signature: { 'rsa-sha256': 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha256', - 'rsa-sha1': 'http://www.w3.org/2000/09/xmldsig#rsa-sha1' + 'rsa-sha1': 'http://www.w3.org/2000/09/xmldsig#rsa-sha1' }, digest: { 'sha256': 'http://www.w3.org/2001/04/xmlenc#sha256', @@ -24,8 +24,8 @@ var algorithms = { } }; -function getAttributeType(value){ - switch(typeof value) { +function getAttributeType(value) { + switch (typeof value) { case "string": return 'xs:string'; case "boolean": @@ -38,16 +38,16 @@ function getAttributeType(value){ } } -function getNameFormat(name){ - if (is_uri(name)){ +function getNameFormat(name) { + if (is_uri(name)) { return 'urn:oasis:names:tc:SAML:2.0:attrname-format:uri'; } // Check that the name is a valid xs:Name -> https://www.w3.org/TR/xmlschema-2/#Name - // xmlNameValidate.name takes a string and will return an object of the form { success, error }, - // where success is a boolean + // xmlNameValidate.name takes a string and will return an object of the form { success, error }, + // where success is a boolean // if it is false, then error is a string containing some hint as to where the match went wrong. - if (xmlNameValidator.name(name).success){ + if (xmlNameValidator.name(name).success) { return 'urn:oasis:names:tc:SAML:2.0:attrname-format:basic'; } @@ -55,32 +55,59 @@ function getNameFormat(name){ return 'urn:oasis:names:tc:SAML:2.0:attrname-format:unspecified'; } -exports.create = function(options, callback) { - if (!options.key) - throw new Error('Expect a private key in pem format'); +/** +* Gets the complere SAML Response merged with assertion (encrypted optional) and uses the +* saml20 argument options to set parts of the response utilizing the saml20Response.template file. +* @param assertion - the SAML assertion to add to the SAML response. +* @param options - The saml20 class options argument. +*/ +function getSamlResponseXml(assertion, options) { + var issueTime = new Date().toISOString(); - if (!options.cert) - throw new Error('Expect a public key cert in pem format'); + var assertionXml = new Parser().parseFromString(assertion); + var saml20Response = fs.readFileSync(path.join(__dirname, 'saml20Response.template')).toString(); - options.signatureAlgorithm = options.signatureAlgorithm || 'rsa-sha256'; - options.digestAlgorithm = options.digestAlgorithm || 'sha256'; + var doc = new Parser().parseFromString(saml20Response.toString()); - options.includeAttributeNameFormat = (typeof options.includeAttributeNameFormat !== 'undefined') ? options.includeAttributeNameFormat : true; - options.typedAttributes = (typeof options.typedAttributes !== 'undefined') ? options.typedAttributes : true; + doc.documentElement.setAttribute('ID', '_' + (options.uid || utils.uid(32))); + doc.documentElement.setAttribute('IssueInstant', moment.utc().format('YYYY-MM-DDTHH:mm:ss.SSS[Z]')); + doc.documentElement.setAttribute('Destination', options.destination); + if (options.issuer) { + var issuer = doc.documentElement.getElementsByTagName('saml:Issuer'); + issuer[0].textContent = options.issuer; + } + doc.lastChild.appendChild(assertionXml.documentElement); + return doc.toString(); +} + +/** +* Signs the SAML XML at the Assertion level (default) or the Response Level (optional) using private key and cert. +* @param xmlToSign - The XML in string form containing the XML assertion or response. +* @param options - The saml20 class options argument. +*/ +function signXml(xmlToSign, options) { // 0.10.1 added prefix, but we want to name it signatureNamespacePrefix - This is just to keep supporting prefix options.signatureNamespacePrefix = options.signatureNamespacePrefix || options.prefix; - options.signatureNamespacePrefix = typeof options.signatureNamespacePrefix === 'string' ? options.signatureNamespacePrefix : '' ; + options.signatureNamespacePrefix = typeof options.signatureNamespacePrefix === 'string' ? options.signatureNamespacePrefix : ''; var cert = utils.pemToCert(options.cert); - var sig = new SignedXml(null, { signatureAlgorithm: algorithms.signature[options.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]); + var signingLocation = options.createSignedSamlResponse ? 'Response' : 'Assertion'; + sig.addReference("//*[local-name(.)='" + signingLocation + "']", + ["http://www.w3.org/2000/09/xmldsig#enveloped-signature", "http://www.w3.org/2001/10/xml-exc-c14n#"], + algorithms.digest[options.digestAlgorithm]); sig.signingKey = options.key; - + + var opts = { + location: { + reference: options.xpathToNodeBeforeSignature || "//*[local-name(.)='Issuer']", + action: 'after' + }, + prefix: options.signatureNamespacePrefix + }; + sig.keyInfoProvider = { getKeyInfo: function (key, prefix) { prefix = prefix ? prefix + ':' : prefix; @@ -88,10 +115,53 @@ exports.create = function(options, callback) { } }; + sig.computeSignature(xmlToSign, opts); + + return sig.getSignedXml(); +} + +/** +* Encrypts s SAML assertion and formats with EncryptedAssertion wrapper using with provided cert. +* @param assertionToEncrypt - The SAML assertion to encrypt. +* @param options - The saml20 class options argument. +* @param callback - The callback function for ASYNC processing completion. +*/ +function encryptAssertionXml(assertionToEncrypt, options, callback) { + 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(assertionToEncrypt, encryptOptions, function (err, encrypted) { + if (err) return callback(err); + var assertion = '' + encrypted + ''; + return callback(null, assertion); + }) +} + +exports.create = function (options, callback) { + if (!options.key) + throw new Error('Expect a private key in pem format'); + + if (!options.cert) + throw new Error('Expect a public key cert in pem format'); + + if (options.createSignedSamlResponse && + (!options.destination || options.destination.length < 1)) + throw new Error('Expect a SAML Response destination for message to be valid.') + + options.signatureAlgorithm = options.signatureAlgorithm || 'rsa-sha256'; + options.digestAlgorithm = options.digestAlgorithm || 'sha256'; + + options.includeAttributeNameFormat = (typeof options.includeAttributeNameFormat !== 'undefined') ? options.includeAttributeNameFormat : true; + options.typedAttributes = (typeof options.typedAttributes !== 'undefined') ? options.typedAttributes : true; + var doc; try { doc = new Parser().parseFromString(saml20.toString()); - } catch(err){ + } catch (err) { return utils.reportError(err, callback); } @@ -109,10 +179,10 @@ exports.create = function(options, callback) { if (options.lifetimeInSeconds) { conditions[0].setAttribute('NotBefore', now.format('YYYY-MM-DDTHH:mm:ss.SSS[Z]')); conditions[0].setAttribute('NotOnOrAfter', now.clone().add(options.lifetimeInSeconds, 'seconds').format('YYYY-MM-DDTHH:mm:ss.SSS[Z]')); - - confirmationData[0].setAttribute('NotOnOrAfter', now.clone().add(options.lifetimeInSeconds, 'seconds').format('YYYY-MM-DDTHH:mm:ss.SSS[Z]')); + + confirmationData[0].setAttribute('NotOnOrAfter', now.clone().add(options.lifetimeInSeconds, 'seconds').format('YYYY-MM-DDTHH:mm:ss.SSS[Z]')); } - + if (options.audiences) { var audienceRestriction = doc.createElementNS(NAMESPACE, 'saml:AudienceRestriction'); var audiences = options.audiences instanceof Array ? options.audiences : [options.audiences]; @@ -122,7 +192,7 @@ exports.create = function(options, callback) { audienceRestriction.appendChild(element); }); - conditions[0].appendChild(audienceRestriction); + conditions[0].appendChild(audienceRestriction); } if (options.recipient) @@ -136,16 +206,16 @@ exports.create = function(options, callback) { statement.setAttribute('xmlns:xs', 'http://www.w3.org/2001/XMLSchema'); statement.setAttribute('xmlns:xsi', 'http://www.w3.org/2001/XMLSchema-instance'); doc.documentElement.appendChild(statement); - Object.keys(options.attributes).forEach(function(prop) { - if(typeof options.attributes[prop] === 'undefined') return; + Object.keys(options.attributes).forEach(function (prop) { + if (typeof options.attributes[prop] === 'undefined') return; // // Foo Bar // var attributeElement = doc.createElementNS(NAMESPACE, 'saml:Attribute'); attributeElement.setAttribute('Name', prop); - if (options.includeAttributeNameFormat){ - attributeElement.setAttribute('NameFormat', getNameFormat(prop)); + if (options.includeAttributeNameFormat) { + attributeElement.setAttribute('NameFormat', getNameFormat(prop)); } var values = options.attributes[prop] instanceof Array ? options.attributes[prop] : [options.attributes[prop]]; @@ -160,7 +230,7 @@ exports.create = function(options, callback) { } }); - if (values && values.filter(function(i){ return typeof i !== 'undefined'; }).length > 0) { + if (values && values.filter(function (i) { return typeof i !== 'undefined'; }).length > 0) { // saml:Attribute must have at least one saml:AttributeValue statement.appendChild(attributeElement); } @@ -176,7 +246,7 @@ exports.create = function(options, callback) { } var nameID = doc.documentElement.getElementsByTagNameNS(NAMESPACE, 'NameID')[0]; - + if (options.nameIdentifier) { nameID.textContent = options.nameIdentifier; } @@ -184,48 +254,55 @@ exports.create = function(options, callback) { if (options.nameIdentifierFormat) { nameID.setAttribute('Format', options.nameIdentifierFormat); } - - if( options.authnContextClassRef ) { + + if (options.authnContextClassRef) { var authnCtxClassRef = doc.getElementsByTagName('saml:AuthnContextClassRef')[0]; authnCtxClassRef.textContent = options.authnContextClassRef; } - var token = utils.removeWhitespace(doc.toString()); - var signed; - try { - var opts = { - location: { - reference: options.xpathToNodeBeforeSignature || "//*[local-name(.)='Issuer']", - action: 'after' - }, - prefix: options.signatureNamespacePrefix - }; - - sig.computeSignature(token, opts); - signed = sig.getSignedXml(); - } catch(err){ - return utils.reportError(err, callback); + var assertion = utils.removeWhitespace(doc.toString()); + + // NEW: Option: build a complete signed SAML response with embedded (option encrypted) assertion + if (options.createSignedSamlResponse) { + try { + // IF SAML response assertion is set to be encrypted + if (options.encryptionCert) { + encryptAssertionXml(assertion, options, function (err, encryptedAssertion) { + if (err) return callback(err); + var signedResponse = signSamlResponse(encryptedAssertion); + return callback(null, signedResponse); + }); + } else { + // Do not encrypt assertion and send back + var signedPlainResponse = signSamlResponse(assertion); + return (callback) ? callback(null, signedPlainResponse) : signedPlainResponse; + } + } catch (err) { + return (callback) ? callback(err) : err; + } + } else { + try { + // Sign the assertion always for both options + var signedAssertion = signXml(utils.removeWhitespace(assertion), options); + if (options.encryptionCert) { + // If assertion is set to be encrypted + encryptAssertionXml(signedAssertion, options, function (err, encryptedAssertion) { + if (err) return callback(err); + return callback(null, encryptedAssertion) + }); + } else { + // If assertion encryption not set just send back + return (callback) ? callback(null, signedAssertion) : signedAssertion; + } + } catch (err) { + return (callback) ? callback(err) : err; + } } - if (!options.encryptionCert) { - if (callback) - return callback(null, signed); - else - return signed; + // Generates response with inserted assertion (or encrypted assertion) and signs + function signSamlResponse(assertion) { + var samlResponse = getSamlResponseXml(assertion, options); + return signXml(utils.removeWhitespace(samlResponse), options); } - - 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)); - }); -}; - +}; diff --git a/lib/saml20Response.template b/lib/saml20Response.template new file mode 100644 index 00000000..837b62a7 --- /dev/null +++ b/lib/saml20Response.template @@ -0,0 +1,6 @@ + + + + + + diff --git a/test/saml20.tests.js b/test/saml20.tests.js index e351cfa3..bb694803 100644 --- a/test/saml20.tests.js +++ b/test/saml20.tests.js @@ -484,6 +484,100 @@ describe('saml 2.0', function () { assert.equal(attributeStatement.length, 0); }); + describe('saml 2.0 full SAML response', function () { + + it('should create a saml 2.0 signed response including plain assertion', function (done) { + var options = { + cert: fs.readFileSync(__dirname + '/test-auth0.pem'), + key: fs.readFileSync(__dirname + '/test-auth0.key'), + xpathToNodeBeforeSignature: "//*[local-name(.)='Issuer']", + createSignedSamlResponse: true, + destination: 'https:/foo.com' + }; + + var samlResponse = saml.create(options); + + var isValid = utils.isValidSignature(samlResponse, options.cert); + assert.equal(true, isValid); + + done(); + }); + + it('...with attributes', function (done) { + var options = { + cert: fs.readFileSync(__dirname + '/test-auth0.pem'), + key: fs.readFileSync(__dirname + '/test-auth0.key'), + xpathToNodeBeforeSignature: "//*[local-name(.)='Issuer']", + createSignedSamlResponse: true, + destination: 'https:/foo.com', + 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', + 'http://example.org/claims/testaccent': 'fóo', // should supports accents + 'http://undefinedattribute/ws/com.com': undefined + } + }; + + var samlResponse = saml.create(options); + + var isValid = utils.isValidSignature(samlResponse, options.cert); + assert.equal(true, isValid); + + var attributes = utils.getAttributes(samlResponse); + assert.equal(3, 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('http://example.org/claims/testaccent', attributes[2].getAttribute('Name')); + assert.equal('fóo', attributes[2].textContent); + + done(); + }); + + it('should insure SAML response attribute [ID] matches signature reference attribute [URI]', function (done) { + var options = { + cert: fs.readFileSync(__dirname + '/test-auth0.pem'), + key: fs.readFileSync(__dirname + '/test-auth0.key'), + xpathToNodeBeforeSignature: "//*[local-name(.)='Issuer']", + createSignedSamlResponse: true, + destination: 'https:/foo.com' + }; + + var samlResponse = saml.create(options); + + var isValid = utils.isValidSignature(samlResponse, options.cert); + assert.equal(true, isValid); + + var responseData = utils.getResponseData(samlResponse); + var responseId = responseData.getAttribute('ID'); + var referenceUri = (responseData.getElementsByTagName('Reference')[0].getAttribute('URI')); + assert.equal(referenceUri, '#' + responseId); + + done(); + }); + + it('should require a [Destination] attribute on SAML Response element', function (done) { + var options = { + cert: fs.readFileSync(__dirname + '/test-auth0.pem'), + key: fs.readFileSync(__dirname + '/test-auth0.key'), + xpathToNodeBeforeSignature: "//*[local-name(.)='Issuer']", + createSignedSamlResponse: true, + destination: '' + }; + + try{ + var samlResponse = saml.create(options); + }catch(err){ + assert(err.message.includes('Expect a SAML Response destination for message to be valid.')); + done(); + } + + throw "Error did not throw as expected!"; + done(); + }); + }); + describe('encryption', function () { it('should create a saml 2.0 signed and encrypted assertion', function (done) { @@ -508,7 +602,7 @@ describe('saml 2.0', function () { }); }); - it('should set attributes', function (done) { + it('...with assertion attributes', function (done) { var options = { cert: fs.readFileSync(__dirname + '/test-auth0.pem'), key: fs.readFileSync(__dirname + '/test-auth0.key'), @@ -546,7 +640,77 @@ describe('saml 2.0', function () { }); }); }); - - }); + describe('encryption full SAML response', function () { + + it('should create a saml 2.0 signed response including encrypted assertion', function (done) { + var options = { + cert: fs.readFileSync(__dirname + '/test-auth0.pem'), + key: fs.readFileSync(__dirname + '/test-auth0.key'), + encryptionPublicKey: fs.readFileSync(__dirname + '/test-auth0_rsa.pub'), + encryptionCert: fs.readFileSync(__dirname + '/test-auth0.pem'), + xpathToNodeBeforeSignature: "//*[local-name(.)='Issuer']", + createSignedSamlResponse: true, + destination: 'https:/foo.com' + }; + + saml.create(options, function(err, encrypted) { + if (err) return done(err); + + var isValid = utils.isValidSignature(encrypted, options.cert); + assert.equal(true, isValid); + + var encryptedData = utils.getEncryptedData(encrypted); + + xmlenc.decrypt(encryptedData.toString(), { key: fs.readFileSync(__dirname + '/test-auth0.key')}, function(err, decrypted) { + if (err) return done(err); + + done(); + }); + }); + }); + + it('...with assertion attributes', function (done) { + var options = { + cert: fs.readFileSync(__dirname + '/test-auth0.pem'), + key: fs.readFileSync(__dirname + '/test-auth0.key'), + encryptionPublicKey: fs.readFileSync(__dirname + '/test-auth0_rsa.pub'), + encryptionCert: fs.readFileSync(__dirname + '/test-auth0.pem'), + xpathToNodeBeforeSignature: "//*[local-name(.)='Issuer']", + createSignedSamlResponse: true, + destination: 'https:/foo.com', + 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', + 'http://example.org/claims/testaccent': 'fóo', // should supports accents + 'http://undefinedattribute/ws/com.com': undefined + } + }; + + saml.create(options, function(err, encrypted) { + if (err) return done(err); + + var isValid = utils.isValidSignature(encrypted, options.cert); + assert.equal(true, isValid); + + var encryptedData = utils.getEncryptedData(encrypted); + + xmlenc.decrypt(encryptedData.toString(), { key: fs.readFileSync(__dirname + '/test-auth0.key')}, function(err, decrypted) { + if (err) return done(err); + + var attributes = utils.getAttributes(decrypted); + assert.equal(3, 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('http://example.org/claims/testaccent', attributes[2].getAttribute('Name')); + assert.equal('fóo', attributes[2].textContent); + + done(); + }); + }); + }); + }); + }); }); diff --git a/test/utils.js b/test/utils.js index 4d7e0426..463a8a3f 100644 --- a/test/utils.js +++ b/test/utils.js @@ -94,5 +94,11 @@ exports.getSubjectConfirmation = function(assertion) { exports.getEncryptedData = function(encryptedAssertion) { var doc = new xmldom.DOMParser().parseFromString(encryptedAssertion); return doc.documentElement - .getElementsByTagName('xenc:EncryptedData')[0]; + .getElementsByTagName('xenc:EncryptedData')[0]; }; + +exports.getResponseData = function(assertion) { + var doc = new xmldom.DOMParser().parseFromString(assertion); + return doc.getElementsByTagName('samlp:Response')[0]; +}; + From e032128b04881516cd8fddd486bc093e256f2a30 Mon Sep 17 00:00:00 2001 From: Jon Lindsey <49694086+jonlindsey@users.noreply.github.com> Date: Fri, 8 May 2020 12:54:53 -0500 Subject: [PATCH 02/11] =?UTF-8?q?feat:=20add=20SAML=20signed=20response=20?= =?UTF-8?q?with=20assertion=20and=20encrypted=C2=A0option.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/saml20.js | 231 ++++++++++++------------------------ lib/saml20Response.template | 4 +- test/saml20.tests.js | 2 +- test/utils.js | 5 +- 4 files changed, 82 insertions(+), 160 deletions(-) diff --git a/lib/saml20.js b/lib/saml20.js index 00d5301d..d03c1351 100644 --- a/lib/saml20.js +++ b/lib/saml20.js @@ -1,11 +1,11 @@ var utils = require('./utils'), - Parser = require('xmldom').DOMParser, - SignedXml = require('xml-crypto').SignedXml, - xmlenc = require('xml-encryption'), - moment = require('moment'), - xmlNameValidator = require('xml-name-validator'), - is_uri = require('valid-url').is_uri; + Parser = require('xmldom').DOMParser, + SignedXml = require('xml-crypto').SignedXml, + xmlenc = require('xml-encryption'), + moment = require('moment'), + xmlNameValidator = require('xml-name-validator'), + is_uri = require('valid-url').is_uri; var fs = require('fs'); var path = require('path'); @@ -16,7 +16,7 @@ var NAMESPACE = 'urn:oasis:names:tc:SAML:2.0:assertion'; var algorithms = { signature: { 'rsa-sha256': 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha256', - 'rsa-sha1': 'http://www.w3.org/2000/09/xmldsig#rsa-sha1' + 'rsa-sha1': 'http://www.w3.org/2000/09/xmldsig#rsa-sha1' }, digest: { 'sha256': 'http://www.w3.org/2001/04/xmlenc#sha256', @@ -24,8 +24,8 @@ var algorithms = { } }; -function getAttributeType(value) { - switch (typeof value) { +function getAttributeType(value){ + switch(typeof value) { case "string": return 'xs:string'; case "boolean": @@ -38,16 +38,16 @@ function getAttributeType(value) { } } -function getNameFormat(name) { - if (is_uri(name)) { +function getNameFormat(name){ + if (is_uri(name)){ return 'urn:oasis:names:tc:SAML:2.0:attrname-format:uri'; } // Check that the name is a valid xs:Name -> https://www.w3.org/TR/xmlschema-2/#Name - // xmlNameValidate.name takes a string and will return an object of the form { success, error }, - // where success is a boolean + // xmlNameValidate.name takes a string and will return an object of the form { success, error }, + // where success is a boolean // if it is false, then error is a string containing some hint as to where the match went wrong. - if (xmlNameValidator.name(name).success) { + if (xmlNameValidator.name(name).success){ return 'urn:oasis:names:tc:SAML:2.0:attrname-format:basic'; } @@ -55,59 +55,32 @@ function getNameFormat(name) { return 'urn:oasis:names:tc:SAML:2.0:attrname-format:unspecified'; } -/** -* Gets the complere SAML Response merged with assertion (encrypted optional) and uses the -* saml20 argument options to set parts of the response utilizing the saml20Response.template file. -* @param assertion - the SAML assertion to add to the SAML response. -* @param options - The saml20 class options argument. -*/ -function getSamlResponseXml(assertion, options) { - var issueTime = new Date().toISOString(); - - var assertionXml = new Parser().parseFromString(assertion); - var saml20Response = fs.readFileSync(path.join(__dirname, 'saml20Response.template')).toString(); +exports.create = function(options, callback) { + if (!options.key) + throw new Error('Expect a private key in pem format'); - var doc = new Parser().parseFromString(saml20Response.toString()); + if (!options.cert) + throw new Error('Expect a public key cert in pem format'); - doc.documentElement.setAttribute('ID', '_' + (options.uid || utils.uid(32))); - doc.documentElement.setAttribute('IssueInstant', moment.utc().format('YYYY-MM-DDTHH:mm:ss.SSS[Z]')); - doc.documentElement.setAttribute('Destination', options.destination); - if (options.issuer) { - var issuer = doc.documentElement.getElementsByTagName('saml:Issuer'); - issuer[0].textContent = options.issuer; - } - doc.lastChild.appendChild(assertionXml.documentElement); + options.signatureAlgorithm = options.signatureAlgorithm || 'rsa-sha256'; + options.digestAlgorithm = options.digestAlgorithm || 'sha256'; - return doc.toString(); -} + options.includeAttributeNameFormat = (typeof options.includeAttributeNameFormat !== 'undefined') ? options.includeAttributeNameFormat : true; + options.typedAttributes = (typeof options.typedAttributes !== 'undefined') ? options.typedAttributes : true; -/** -* Signs the SAML XML at the Assertion level (default) or the Response Level (optional) using private key and cert. -* @param xmlToSign - The XML in string form containing the XML assertion or response. -* @param options - The saml20 class options argument. -*/ -function signXml(xmlToSign, options) { // 0.10.1 added prefix, but we want to name it signatureNamespacePrefix - This is just to keep supporting prefix options.signatureNamespacePrefix = options.signatureNamespacePrefix || options.prefix; - options.signatureNamespacePrefix = typeof options.signatureNamespacePrefix === 'string' ? options.signatureNamespacePrefix : ''; + options.signatureNamespacePrefix = typeof options.signatureNamespacePrefix === 'string' ? options.signatureNamespacePrefix : '' ; var cert = utils.pemToCert(options.cert); + var sig = new SignedXml(null, { signatureAlgorithm: algorithms.signature[options.signatureAlgorithm], idAttribute: 'ID' }); - var signingLocation = options.createSignedSamlResponse ? 'Response' : 'Assertion'; - sig.addReference("//*[local-name(.)='" + signingLocation + "']", - ["http://www.w3.org/2000/09/xmldsig#enveloped-signature", "http://www.w3.org/2001/10/xml-exc-c14n#"], - algorithms.digest[options.digestAlgorithm]); + 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]); sig.signingKey = options.key; - - var opts = { - location: { - reference: options.xpathToNodeBeforeSignature || "//*[local-name(.)='Issuer']", - action: 'after' - }, - prefix: options.signatureNamespacePrefix - }; - + sig.keyInfoProvider = { getKeyInfo: function (key, prefix) { prefix = prefix ? prefix + ':' : prefix; @@ -115,53 +88,10 @@ function signXml(xmlToSign, options) { } }; - sig.computeSignature(xmlToSign, opts); - - return sig.getSignedXml(); -} - -/** -* Encrypts s SAML assertion and formats with EncryptedAssertion wrapper using with provided cert. -* @param assertionToEncrypt - The SAML assertion to encrypt. -* @param options - The saml20 class options argument. -* @param callback - The callback function for ASYNC processing completion. -*/ -function encryptAssertionXml(assertionToEncrypt, options, callback) { - 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(assertionToEncrypt, encryptOptions, function (err, encrypted) { - if (err) return callback(err); - var assertion = '' + encrypted + ''; - return callback(null, assertion); - }) -} - -exports.create = function (options, callback) { - if (!options.key) - throw new Error('Expect a private key in pem format'); - - if (!options.cert) - throw new Error('Expect a public key cert in pem format'); - - if (options.createSignedSamlResponse && - (!options.destination || options.destination.length < 1)) - throw new Error('Expect a SAML Response destination for message to be valid.') - - options.signatureAlgorithm = options.signatureAlgorithm || 'rsa-sha256'; - options.digestAlgorithm = options.digestAlgorithm || 'sha256'; - - options.includeAttributeNameFormat = (typeof options.includeAttributeNameFormat !== 'undefined') ? options.includeAttributeNameFormat : true; - options.typedAttributes = (typeof options.typedAttributes !== 'undefined') ? options.typedAttributes : true; - var doc; try { doc = new Parser().parseFromString(saml20.toString()); - } catch (err) { + } catch(err){ return utils.reportError(err, callback); } @@ -179,10 +109,10 @@ exports.create = function (options, callback) { if (options.lifetimeInSeconds) { conditions[0].setAttribute('NotBefore', now.format('YYYY-MM-DDTHH:mm:ss.SSS[Z]')); conditions[0].setAttribute('NotOnOrAfter', now.clone().add(options.lifetimeInSeconds, 'seconds').format('YYYY-MM-DDTHH:mm:ss.SSS[Z]')); - - confirmationData[0].setAttribute('NotOnOrAfter', now.clone().add(options.lifetimeInSeconds, 'seconds').format('YYYY-MM-DDTHH:mm:ss.SSS[Z]')); + + confirmationData[0].setAttribute('NotOnOrAfter', now.clone().add(options.lifetimeInSeconds, 'seconds').format('YYYY-MM-DDTHH:mm:ss.SSS[Z]')); } - + if (options.audiences) { var audienceRestriction = doc.createElementNS(NAMESPACE, 'saml:AudienceRestriction'); var audiences = options.audiences instanceof Array ? options.audiences : [options.audiences]; @@ -192,7 +122,7 @@ exports.create = function (options, callback) { audienceRestriction.appendChild(element); }); - conditions[0].appendChild(audienceRestriction); + conditions[0].appendChild(audienceRestriction); } if (options.recipient) @@ -206,16 +136,16 @@ exports.create = function (options, callback) { statement.setAttribute('xmlns:xs', 'http://www.w3.org/2001/XMLSchema'); statement.setAttribute('xmlns:xsi', 'http://www.w3.org/2001/XMLSchema-instance'); doc.documentElement.appendChild(statement); - Object.keys(options.attributes).forEach(function (prop) { - if (typeof options.attributes[prop] === 'undefined') return; + Object.keys(options.attributes).forEach(function(prop) { + if(typeof options.attributes[prop] === 'undefined') return; // // Foo Bar // var attributeElement = doc.createElementNS(NAMESPACE, 'saml:Attribute'); attributeElement.setAttribute('Name', prop); - if (options.includeAttributeNameFormat) { - attributeElement.setAttribute('NameFormat', getNameFormat(prop)); + if (options.includeAttributeNameFormat){ + attributeElement.setAttribute('NameFormat', getNameFormat(prop)); } var values = options.attributes[prop] instanceof Array ? options.attributes[prop] : [options.attributes[prop]]; @@ -230,7 +160,7 @@ exports.create = function (options, callback) { } }); - if (values && values.filter(function (i) { return typeof i !== 'undefined'; }).length > 0) { + if (values && values.filter(function(i){ return typeof i !== 'undefined'; }).length > 0) { // saml:Attribute must have at least one saml:AttributeValue statement.appendChild(attributeElement); } @@ -246,7 +176,7 @@ exports.create = function (options, callback) { } var nameID = doc.documentElement.getElementsByTagNameNS(NAMESPACE, 'NameID')[0]; - + if (options.nameIdentifier) { nameID.textContent = options.nameIdentifier; } @@ -254,55 +184,48 @@ exports.create = function (options, callback) { if (options.nameIdentifierFormat) { nameID.setAttribute('Format', options.nameIdentifierFormat); } - - if (options.authnContextClassRef) { + + if( options.authnContextClassRef ) { var authnCtxClassRef = doc.getElementsByTagName('saml:AuthnContextClassRef')[0]; authnCtxClassRef.textContent = options.authnContextClassRef; } - var assertion = utils.removeWhitespace(doc.toString()); - - // NEW: Option: build a complete signed SAML response with embedded (option encrypted) assertion - if (options.createSignedSamlResponse) { - try { - // IF SAML response assertion is set to be encrypted - if (options.encryptionCert) { - encryptAssertionXml(assertion, options, function (err, encryptedAssertion) { - if (err) return callback(err); - var signedResponse = signSamlResponse(encryptedAssertion); - return callback(null, signedResponse); - }); - } else { - // Do not encrypt assertion and send back - var signedPlainResponse = signSamlResponse(assertion); - return (callback) ? callback(null, signedPlainResponse) : signedPlainResponse; - } - } catch (err) { - return (callback) ? callback(err) : err; - } - } else { - try { - // Sign the assertion always for both options - var signedAssertion = signXml(utils.removeWhitespace(assertion), options); - if (options.encryptionCert) { - // If assertion is set to be encrypted - encryptAssertionXml(signedAssertion, options, function (err, encryptedAssertion) { - if (err) return callback(err); - return callback(null, encryptedAssertion) - }); - } else { - // If assertion encryption not set just send back - return (callback) ? callback(null, signedAssertion) : signedAssertion; - } - } catch (err) { - return (callback) ? callback(err) : err; - } + var token = utils.removeWhitespace(doc.toString()); + var signed; + try { + var opts = { + location: { + reference: options.xpathToNodeBeforeSignature || "//*[local-name(.)='Issuer']", + action: 'after' + }, + prefix: options.signatureNamespacePrefix + }; + + sig.computeSignature(token, opts); + signed = sig.getSignedXml(); + } catch(err){ + return utils.reportError(err, callback); } - // Generates response with inserted assertion (or encrypted assertion) and signs - function signSamlResponse(assertion) { - var samlResponse = getSamlResponseXml(assertion, options); - return signXml(utils.removeWhitespace(samlResponse), options); + if (!options.encryptionCert) { + if (callback) + return callback(null, signed); + else + return 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' + }; + + xmlenc.encrypt(signed, encryptOptions, function(err, encrypted) { + if (err) return callback(err); + + encrypted = '' + encrypted + ''; + callback(null, utils.removeWhitespace(encrypted)); + }); +}; + diff --git a/lib/saml20Response.template b/lib/saml20Response.template index 837b62a7..49f2db2f 100644 --- a/lib/saml20Response.template +++ b/lib/saml20Response.template @@ -1,6 +1,6 @@ - + - + \ No newline at end of file diff --git a/test/saml20.tests.js b/test/saml20.tests.js index bb694803..0befa001 100644 --- a/test/saml20.tests.js +++ b/test/saml20.tests.js @@ -484,7 +484,7 @@ describe('saml 2.0', function () { assert.equal(attributeStatement.length, 0); }); - describe('saml 2.0 full SAML response', function () { + describe('saml 2.0 test full SAML response', function () { it('should create a saml 2.0 signed response including plain assertion', function (done) { var options = { diff --git a/test/utils.js b/test/utils.js index 463a8a3f..8780e049 100644 --- a/test/utils.js +++ b/test/utils.js @@ -97,8 +97,7 @@ exports.getEncryptedData = function(encryptedAssertion) { .getElementsByTagName('xenc:EncryptedData')[0]; }; -exports.getResponseData = function(assertion) { - var doc = new xmldom.DOMParser().parseFromString(assertion); +exports.getResponseData = function(samlResponse) { + var doc = new xmldom.DOMParser().parseFromString(samlResponse); return doc.getElementsByTagName('samlp:Response')[0]; }; - From bd6667a284b886d173ac33c28a6bcaab98718491 Mon Sep 17 00:00:00 2001 From: Jon Lindsey <49694086+jonlindsey@users.noreply.github.com> Date: Fri, 8 May 2020 14:19:24 -0500 Subject: [PATCH 03/11] feat: add SAML signed response with assertion - formatting redos. --- lib/saml20.js | 178 +++++++++++++++++++++++++----------- lib/saml20Response.template | 4 +- test/saml20.tests.js | 4 +- test/utils.js | 4 +- 4 files changed, 133 insertions(+), 57 deletions(-) diff --git a/lib/saml20.js b/lib/saml20.js index d03c1351..2ada2a7d 100644 --- a/lib/saml20.js +++ b/lib/saml20.js @@ -55,32 +55,59 @@ function getNameFormat(name){ return 'urn:oasis:names:tc:SAML:2.0:attrname-format:unspecified'; } -exports.create = function(options, callback) { - if (!options.key) - throw new Error('Expect a private key in pem format'); +/** +* Gets the complere SAML Response merged with assertion (encrypted optional) and uses the +* saml20 argument options to set parts of the response utilizing the saml20Response.template file. +* @param assertion - the SAML assertion to add to the SAML response. +* @param options - The saml20 class options argument. +*/ +function getSamlResponseXml(assertion, options) { + var issueTime = new Date().toISOString(); - if (!options.cert) - throw new Error('Expect a public key cert in pem format'); + var assertionXml = new Parser().parseFromString(assertion); + var saml20Response = fs.readFileSync(path.join(__dirname, 'saml20Response.template')).toString(); - options.signatureAlgorithm = options.signatureAlgorithm || 'rsa-sha256'; - options.digestAlgorithm = options.digestAlgorithm || 'sha256'; + var doc = new Parser().parseFromString(saml20Response.toString()); - options.includeAttributeNameFormat = (typeof options.includeAttributeNameFormat !== 'undefined') ? options.includeAttributeNameFormat : true; - options.typedAttributes = (typeof options.typedAttributes !== 'undefined') ? options.typedAttributes : true; + doc.documentElement.setAttribute('ID', '_' + (options.uid || utils.uid(32))); + doc.documentElement.setAttribute('IssueInstant', moment.utc().format('YYYY-MM-DDTHH:mm:ss.SSS[Z]')); + doc.documentElement.setAttribute('Destination', options.destination); + if (options.issuer) { + var issuer = doc.documentElement.getElementsByTagName('saml:Issuer'); + issuer[0].textContent = options.issuer; + } + doc.lastChild.appendChild(assertionXml.documentElement); + + return doc.toString(); +} +/** +* Signs the SAML XML at the Assertion level (default) or the Response Level (optional) using private key and cert. +* @param xmlToSign - The XML in string form containing the XML assertion or response. +* @param options - The saml20 class options argument. +*/ +function signXml(xmlToSign, options) { // 0.10.1 added prefix, but we want to name it signatureNamespacePrefix - This is just to keep supporting prefix options.signatureNamespacePrefix = options.signatureNamespacePrefix || options.prefix; options.signatureNamespacePrefix = typeof options.signatureNamespacePrefix === 'string' ? options.signatureNamespacePrefix : '' ; var cert = utils.pemToCert(options.cert); - var sig = new SignedXml(null, { signatureAlgorithm: algorithms.signature[options.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]); + var signingLocation = options.createSignedSamlResponse ? 'Response' : 'Assertion'; + sig.addReference("//*[local-name(.)='" + signingLocation + "']", + ["http://www.w3.org/2000/09/xmldsig#enveloped-signature", "http://www.w3.org/2001/10/xml-exc-c14n#"], + algorithms.digest[options.digestAlgorithm]); sig.signingKey = options.key; - + + var opts = { + location: { + reference: options.xpathToNodeBeforeSignature || "//*[local-name(.)='Issuer']", + action: 'after' + }, + prefix: options.signatureNamespacePrefix + }; + sig.keyInfoProvider = { getKeyInfo: function (key, prefix) { prefix = prefix ? prefix + ':' : prefix; @@ -88,6 +115,49 @@ exports.create = function(options, callback) { } }; + sig.computeSignature(xmlToSign, opts); + + return sig.getSignedXml(); +} + +/** +* Encrypts s SAML assertion and formats with EncryptedAssertion wrapper using provided cert. +* @param assertionToEncrypt - The SAML assertion to encrypt. +* @param options - The saml20 class options argument. +* @param callback - The callback function for ASYNC processing completion. +*/ +function encryptAssertionXml(assertionToEncrypt, options, callback) { + 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(assertionToEncrypt, encryptOptions, function (err, encrypted) { + if (err) return callback(err); + var assertion = '' + encrypted + ''; + return callback(null, assertion); + }) +} + +exports.create = function (options, callback) { + if (!options.key) + throw new Error('Expect a private key in pem format'); + + if (!options.cert) + throw new Error('Expect a public key cert in pem format'); + + if (options.createSignedSamlResponse && + (!options.destination || options.destination.length < 1)) + throw new Error('Expect a SAML Response destination for message to be valid.') + + options.signatureAlgorithm = options.signatureAlgorithm || 'rsa-sha256'; + options.digestAlgorithm = options.digestAlgorithm || 'sha256'; + + options.includeAttributeNameFormat = (typeof options.includeAttributeNameFormat !== 'undefined') ? options.includeAttributeNameFormat : true; + options.typedAttributes = (typeof options.typedAttributes !== 'undefined') ? options.typedAttributes : true; + var doc; try { doc = new Parser().parseFromString(saml20.toString()); @@ -184,48 +254,54 @@ exports.create = function(options, callback) { if (options.nameIdentifierFormat) { nameID.setAttribute('Format', options.nameIdentifierFormat); } - + if( options.authnContextClassRef ) { var authnCtxClassRef = doc.getElementsByTagName('saml:AuthnContextClassRef')[0]; authnCtxClassRef.textContent = options.authnContextClassRef; } - var token = utils.removeWhitespace(doc.toString()); - var signed; - try { - var opts = { - location: { - reference: options.xpathToNodeBeforeSignature || "//*[local-name(.)='Issuer']", - action: 'after' - }, - prefix: options.signatureNamespacePrefix - }; - - sig.computeSignature(token, opts); - signed = sig.getSignedXml(); - } catch(err){ - return utils.reportError(err, callback); + var assertion = utils.removeWhitespace(doc.toString()); + + // NEW: Option: build a complete signed SAML response with embedded (option encrypted) assertion + if (options.createSignedSamlResponse) { + try { + // IF SAML response assertion is set to be encrypted + if (options.encryptionCert) { + encryptAssertionXml(assertion, options, function (err, encryptedAssertion) { + if (err) return callback(err); + var signedResponse = signSamlResponse(encryptedAssertion); + return callback(null, signedResponse); + }); + } else { + // Do not encrypt assertion and send back + var signedPlainResponse = signSamlResponse(assertion); + return (callback) ? callback(null, signedPlainResponse) : signedPlainResponse; + } + } catch (err) { + return (callback) ? callback(err) : err; + } + } else { + try { + // Sign the assertion always for both options + var signedAssertion = signXml(utils.removeWhitespace(assertion), options); + if (options.encryptionCert) { + // If assertion is set to be encrypted + encryptAssertionXml(signedAssertion, options, function (err, encryptedAssertion) { + if (err) return callback(err); + return callback(null, encryptedAssertion) + }); + } else { + // If assertion encryption not set just send back + return (callback) ? callback(null, signedAssertion) : signedAssertion; + } + } catch (err) { + return (callback) ? callback(err) : err; + } } - if (!options.encryptionCert) { - if (callback) - return callback(null, signed); - else - return signed; + // Generates response with inserted assertion (or encrypted assertion) and signs + function signSamlResponse(assertion) { + var samlResponse = getSamlResponseXml(assertion, options); + return signXml(utils.removeWhitespace(samlResponse), options); } - - 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)); - }); -}; - +}; diff --git a/lib/saml20Response.template b/lib/saml20Response.template index 49f2db2f..837b62a7 100644 --- a/lib/saml20Response.template +++ b/lib/saml20Response.template @@ -1,6 +1,6 @@ - + - \ No newline at end of file + diff --git a/test/saml20.tests.js b/test/saml20.tests.js index 0befa001..c90244a3 100644 --- a/test/saml20.tests.js +++ b/test/saml20.tests.js @@ -484,7 +484,7 @@ describe('saml 2.0', function () { assert.equal(attributeStatement.length, 0); }); - describe('saml 2.0 test full SAML response', function () { + describe('saml 2.0 full SAML response', function () { it('should create a saml 2.0 signed response including plain assertion', function (done) { var options = { @@ -713,4 +713,4 @@ describe('saml 2.0', function () { }); }); }); -}); +}); \ No newline at end of file diff --git a/test/utils.js b/test/utils.js index 8780e049..009cc067 100644 --- a/test/utils.js +++ b/test/utils.js @@ -97,7 +97,7 @@ exports.getEncryptedData = function(encryptedAssertion) { .getElementsByTagName('xenc:EncryptedData')[0]; }; -exports.getResponseData = function(samlResponse) { - var doc = new xmldom.DOMParser().parseFromString(samlResponse); +exports.getResponseData = function(assertion) { + var doc = new xmldom.DOMParser().parseFromString(assertion); return doc.getElementsByTagName('samlp:Response')[0]; }; From 428a8f6a345b007911f24d06da910a95a7962178 Mon Sep 17 00:00:00 2001 From: Jon Lindsey <49694086+jonlindsey@users.noreply.github.com> Date: Fri, 8 May 2020 18:52:13 -0500 Subject: [PATCH 04/11] 0.15.0 --- package-lock.json | 379 ++++++++++++++++++++++++++++++++++++++++++++++ package.json | 2 +- 2 files changed, 380 insertions(+), 1 deletion(-) create mode 100644 package-lock.json diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 00000000..c14fc296 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,379 @@ +{ + "name": "saml", + "version": "0.15.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "async": { + "version": "0.2.10", + "resolved": "https://registry.npmjs.org/async/-/async-0.2.10.tgz", + "integrity": "sha1-trvgsGdLnXGXCMo43owjfLUmw9E=" + }, + "balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", + "dev": true + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "browser-stdout": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.0.tgz", + "integrity": "sha1-81HTKWnTL6XXpVZxVCY9korjvR8=", + "dev": true + }, + "commander": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.9.0.tgz", + "integrity": "sha1-nJkJQXbhIkDLItbFFGCYQA/g99Q=", + "dev": true, + "requires": { + "graceful-readlink": ">= 1.0.0" + } + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true + }, + "debug": { + "version": "2.6.8", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.8.tgz", + "integrity": "sha1-5zFTHKLt4n0YgiJCfaF4IdaP9Pw=", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "diff": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-3.2.0.tgz", + "integrity": "sha1-yc45Okt8vQsFinJck98pkCeGj/k=", + "dev": true + }, + "ejs": { + "version": "2.7.4", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-2.7.4.tgz", + "integrity": "sha512-7vmuyh5+kuUyJKePhQfRQBhXV5Ce+RnaeeQArKu1EAMpL3WbgMt5WG6uQZpEVvYSSsxMXRKOewtDk9RaTKXRlA==" + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "dev": true + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "dev": true + }, + "glob": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.1.tgz", + "integrity": "sha1-gFIR3wT6rxxjo2ADBs31reULLsg=", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.2", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "graceful-readlink": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/graceful-readlink/-/graceful-readlink-1.0.1.tgz", + "integrity": "sha1-TK+tdrxi8C+gObL5Tpo906ORpyU=", + "dev": true + }, + "growl": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/growl/-/growl-1.9.2.tgz", + "integrity": "sha1-Dqd0NxXbjY3ixe3hd14bRayFwC8=", + "dev": true + }, + "has-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz", + "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=", + "dev": true + }, + "he": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/he/-/he-1.1.1.tgz", + "integrity": "sha1-k0EP0hsAlzUVH4howvJx80J+I/0=", + "dev": true + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dev": true, + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "json3": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/json3/-/json3-3.3.2.tgz", + "integrity": "sha1-PAQ0dD35Pi9cQq7nsZvLSDV19OE=", + "dev": true + }, + "lodash": { + "version": "4.17.15", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", + "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==" + }, + "lodash._baseassign": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/lodash._baseassign/-/lodash._baseassign-3.2.0.tgz", + "integrity": "sha1-jDigmVAPIVrQnlnxci/QxSv+Ck4=", + "dev": true, + "requires": { + "lodash._basecopy": "^3.0.0", + "lodash.keys": "^3.0.0" + } + }, + "lodash._basecopy": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/lodash._basecopy/-/lodash._basecopy-3.0.1.tgz", + "integrity": "sha1-jaDmqHbPNEwK2KVIghEd08XHyjY=", + "dev": true + }, + "lodash._basecreate": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash._basecreate/-/lodash._basecreate-3.0.3.tgz", + "integrity": "sha1-G8ZhYU2qf8MRt9A78WgGoCE8+CE=", + "dev": true + }, + "lodash._getnative": { + "version": "3.9.1", + "resolved": "https://registry.npmjs.org/lodash._getnative/-/lodash._getnative-3.9.1.tgz", + "integrity": "sha1-VwvH3t5G1hzc3mh9ZdPuy6o6r/U=", + "dev": true + }, + "lodash._isiterateecall": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/lodash._isiterateecall/-/lodash._isiterateecall-3.0.9.tgz", + "integrity": "sha1-UgOte6Ql+uhCRg5pbbnPPmqsBXw=", + "dev": true + }, + "lodash.create": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/lodash.create/-/lodash.create-3.1.1.tgz", + "integrity": "sha1-1/KEnw29p+BGgruM1yqwIkYd6+c=", + "dev": true, + "requires": { + "lodash._baseassign": "^3.0.0", + "lodash._basecreate": "^3.0.0", + "lodash._isiterateecall": "^3.0.0" + } + }, + "lodash.isarguments": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", + "integrity": "sha1-L1c9hcaiQon/AGY7SRwdM4/zRYo=", + "dev": true + }, + "lodash.isarray": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/lodash.isarray/-/lodash.isarray-3.0.4.tgz", + "integrity": "sha1-eeTriMNqgSKvhvhEqpvNhRtfu1U=", + "dev": true + }, + "lodash.keys": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/lodash.keys/-/lodash.keys-3.1.2.tgz", + "integrity": "sha1-TbwEcrFWvlCgsoaFXRvQsMZWCYo=", + "dev": true, + "requires": { + "lodash._getnative": "^3.0.0", + "lodash.isarguments": "^3.0.0", + "lodash.isarray": "^3.0.0" + } + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", + "dev": true + }, + "mkdirp": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "dev": true, + "requires": { + "minimist": "0.0.8" + } + }, + "mocha": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-3.5.3.tgz", + "integrity": "sha512-/6na001MJWEtYxHOV1WLfsmR4YIynkUEhBwzsb+fk2qmQ3iqsi258l/Q2MWHJMImAcNpZ8DEdYAK72NHoIQ9Eg==", + "dev": true, + "requires": { + "browser-stdout": "1.3.0", + "commander": "2.9.0", + "debug": "2.6.8", + "diff": "3.2.0", + "escape-string-regexp": "1.0.5", + "glob": "7.1.1", + "growl": "1.9.2", + "he": "1.1.1", + "json3": "3.3.2", + "lodash.create": "3.1.1", + "mkdirp": "0.5.1", + "supports-color": "3.1.2" + } + }, + "moment": { + "version": "2.19.3", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.19.3.tgz", + "integrity": "sha1-vbmdJw1tf9p4zA+6zoVeJ/59pp8=" + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + }, + "node-forge": { + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.7.6.tgz", + "integrity": "sha512-sol30LUpz1jQFBjOKwbjxijiE3b6pjd74YwfD0fJOKPjF+fONKb2Yg8rYgS6+bK6VDl+/wfr4IYpC7jDzLUIfw==" + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dev": true, + "requires": { + "wrappy": "1" + } + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "dev": true + }, + "should": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/should/-/should-1.2.2.tgz", + "integrity": "sha1-DwP3dQZtnqJjJpDJF7EoJPzB1YI=", + "dev": true + }, + "supports-color": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.1.2.tgz", + "integrity": "sha1-cqJiiU2dQIuVbKBf83su2KbiotU=", + "dev": true, + "requires": { + "has-flag": "^1.0.0" + } + }, + "valid-url": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/valid-url/-/valid-url-1.0.9.tgz", + "integrity": "sha1-HBRHm0DxOXp1eC8RXkCGRHQzogA=" + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "dev": true + }, + "xml-crypto": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/xml-crypto/-/xml-crypto-1.0.2.tgz", + "integrity": "sha512-bDQkgu1yuwl+QoJbi4GBP9MWxpmYkXc8a9iSHbZ7lKqcxzGlDqMRugcl7qK7TsMI0ydU66GG8/eLNvRUk5T2fw==", + "requires": { + "xmldom": "0.1.27", + "xpath.js": ">=0.0.3" + }, + "dependencies": { + "xmldom": { + "version": "0.1.27", + "resolved": "https://registry.npmjs.org/xmldom/-/xmldom-0.1.27.tgz", + "integrity": "sha1-1QH5ezvbQDr4757MIFcxh6rawOk=" + } + } + }, + "xml-encryption": { + "version": "0.11.2", + "resolved": "https://registry.npmjs.org/xml-encryption/-/xml-encryption-0.11.2.tgz", + "integrity": "sha512-jVvES7i5ovdO7N+NjgncA326xYKjhqeAnnvIgRnY7ROLCfFqEDLwP0Sxp/30SHG0AXQV1048T5yinOFyvwGFzg==", + "requires": { + "async": "^2.1.5", + "ejs": "^2.5.6", + "node-forge": "^0.7.0", + "xmldom": "~0.1.15", + "xpath": "0.0.27" + }, + "dependencies": { + "async": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/async/-/async-2.6.3.tgz", + "integrity": "sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==", + "requires": { + "lodash": "^4.17.14" + } + }, + "xpath": { + "version": "0.0.27", + "resolved": "https://registry.npmjs.org/xpath/-/xpath-0.0.27.tgz", + "integrity": "sha512-fg03WRxtkCV6ohClePNAECYsmpKKTv5L8y/X3Dn1hQrec3POx2jHZ/0P2qQ6HvsrU1BmeqXcof3NGGueG6LxwQ==" + } + } + }, + "xml-name-validator": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-2.0.1.tgz", + "integrity": "sha1-TYuPHszTQZqjYgYb7O9RXh5VljU=" + }, + "xmldom": { + "version": "0.1.15", + "resolved": "https://registry.npmjs.org/xmldom/-/xmldom-0.1.15.tgz", + "integrity": "sha1-swSAYvG91S7cQhQkRZ8G3O6y+U0=" + }, + "xpath": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/xpath/-/xpath-0.0.5.tgz", + "integrity": "sha1-RUA29u8PPfWvXUukoRn7dWdLPmw=" + }, + "xpath.js": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/xpath.js/-/xpath.js-1.1.0.tgz", + "integrity": "sha512-jg+qkfS4K8E7965sqaUl8mRngXiKb3WZGfONgE18pr03FUQiuSV6G+Ej4tS55B+rIQSFEIw3phdVAQ4pPqNWfQ==" + } + } +} diff --git a/package.json b/package.json index 83c47b14..4fdd11db 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "saml", - "version": "0.14.0", + "version": "0.15.0", "devDependencies": { "mocha": "3.5.3", "should": "~1.2.1" From 8ba8279f29f178d4504d039249c1690ca6ea9d38 Mon Sep 17 00:00:00 2001 From: Jon Lindsey <49694086+jonlindsey@users.noreply.github.com> Date: Sat, 9 May 2020 09:09:24 -0500 Subject: [PATCH 05/11] [fix] fixes callback already called when async errors occur --- lib/saml20.js | 105 ++++++++++++++++++++++++++------------------------ 1 file changed, 54 insertions(+), 51 deletions(-) diff --git a/lib/saml20.js b/lib/saml20.js index 2ada2a7d..fd1c2e02 100644 --- a/lib/saml20.js +++ b/lib/saml20.js @@ -56,14 +56,20 @@ function getNameFormat(name){ } /** -* Gets the complere SAML Response merged with assertion (encrypted optional) and uses the -* saml20 argument options to set parts of the response utilizing the saml20Response.template file. +* Gets the complete SAML20 response embedding the assertion (encrypted optional) and +* options argument to set attributes for response utilizing the saml20Response.template file. * @param assertion - the SAML assertion to add to the SAML response. * @param options - The saml20 class options argument. +* @returns string - SAML20 Full Response with embedded assertion XML. +* @throws assertion argument null or empty error. */ function getSamlResponseXml(assertion, options) { + var issueTime = new Date().toISOString(); + if (!assertion || assertion.length < 1) + throw new ReferenceError('Assertion XML cannot be empty for parsing while creating SAML20 Response.') + var assertionXml = new Parser().parseFromString(assertion); var saml20Response = fs.readFileSync(path.join(__dirname, 'saml20Response.template')).toString(); @@ -77,7 +83,7 @@ function getSamlResponseXml(assertion, options) { issuer[0].textContent = options.issuer; } doc.lastChild.appendChild(assertionXml.documentElement); - + return doc.toString(); } @@ -85,8 +91,14 @@ function getSamlResponseXml(assertion, options) { * Signs the SAML XML at the Assertion level (default) or the Response Level (optional) using private key and cert. * @param xmlToSign - The XML in string form containing the XML assertion or response. * @param options - The saml20 class options argument. +* @returns string - Signed SAML assertion or response depending on option. +* @throws ReferenceError if xml argument sent for signing is null or empty. */ function signXml(xmlToSign, options) { + + if (!xmlToSign || xmlToSign.length < 1) + throw new ReferenceError('XML to sign cannot be null or empty.') + // 0.10.1 added prefix, but we want to name it signatureNamespacePrefix - This is just to keep supporting prefix options.signatureNamespacePrefix = options.signatureNamespacePrefix || options.prefix; options.signatureNamespacePrefix = typeof options.signatureNamespacePrefix === 'string' ? options.signatureNamespacePrefix : '' ; @@ -114,7 +126,7 @@ function signXml(xmlToSign, options) { return "<" + prefix + "X509Data><" + prefix + "X509Certificate>" + cert + ""; } }; - + sig.computeSignature(xmlToSign, opts); return sig.getSignedXml(); @@ -124,22 +136,24 @@ function signXml(xmlToSign, options) { * Encrypts s SAML assertion and formats with EncryptedAssertion wrapper using provided cert. * @param assertionToEncrypt - The SAML assertion to encrypt. * @param options - The saml20 class options argument. -* @param callback - The callback function for ASYNC processing completion. +* @returns Promise (resolve, reject) for the embedded ASYNC encrypt function callback wrapper. */ -function encryptAssertionXml(assertionToEncrypt, options, callback) { - 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(assertionToEncrypt, encryptOptions, function (err, encrypted) { - if (err) return callback(err); - var assertion = '' + encrypted + ''; - return callback(null, assertion); - }) -} +var encryptAssertionXml = (assertionToEncrypt, options) => + new Promise((resolve, reject) => { + 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(assertionToEncrypt, encryptOptions, function (err, encryptedAssertion) { + if (err) reject(err); + encryptedAssertion = `${encryptedAssertion}`; + + resolve(encryptedAssertion); + }); +}); exports.create = function (options, callback) { if (!options.key) @@ -262,44 +276,33 @@ exports.create = function (options, callback) { var assertion = utils.removeWhitespace(doc.toString()); - // NEW: Option: build a complete signed SAML response with embedded (option encrypted) assertion + // NEW: construct a SAML20 response signed at the response with embedded (encrypted option) assertion if (options.createSignedSamlResponse) { - try { - // IF SAML response assertion is set to be encrypted - if (options.encryptionCert) { - encryptAssertionXml(assertion, options, function (err, encryptedAssertion) { - if (err) return callback(err); - var signedResponse = signSamlResponse(encryptedAssertion); - return callback(null, signedResponse); - }); - } else { - // Do not encrypt assertion and send back - var signedPlainResponse = signSamlResponse(assertion); - return (callback) ? callback(null, signedPlainResponse) : signedPlainResponse; - } - } catch (err) { - return (callback) ? callback(err) : err; + + if (options.encryptionCert) { + encryptAssertionXml(assertion, options) + .then(encryptedAssertion => (callback(null, signSamlResponse(encryptedAssertion)))) + .catch((err) => callback(err)); + } else { + // Send saml response back signed if not set for encryption + var signedPlainResponse = signSamlResponse(assertion); + return (callback) ? callback(null, signedPlainResponse) : signedPlainResponse; } } else { - try { - // Sign the assertion always for both options - var signedAssertion = signXml(utils.removeWhitespace(assertion), options); - if (options.encryptionCert) { - // If assertion is set to be encrypted - encryptAssertionXml(signedAssertion, options, function (err, encryptedAssertion) { - if (err) return callback(err); - return callback(null, encryptedAssertion) - }); - } else { - // If assertion encryption not set just send back - return (callback) ? callback(null, signedAssertion) : signedAssertion; - } - } catch (err) { - return (callback) ? callback(err) : err; + // Sign the assertion always for both options + var signedAssertion = signXml(utils.removeWhitespace(assertion), options); + + if (options.encryptionCert) { + encryptAssertionXml(signedAssertion, options) + .then(encryptedAssertion => callback(null, encryptedAssertion)) + .catch((err) => callback(err)); + } else { + // Send back signed if not set for encryption + return (callback) ? callback(null, signedAssertion) : signedAssertion; } } - // Generates response with inserted assertion (or encrypted assertion) and signs + // Sign response with inserted assertion (or encrypted assertion) function signSamlResponse(assertion) { var samlResponse = getSamlResponseXml(assertion, options); return signXml(utils.removeWhitespace(samlResponse), options); From 82e6fc1390738729732a41aaff6fe6c3b87bc517 Mon Sep 17 00:00:00 2001 From: Brandon Ros Date: Mon, 11 May 2020 13:02:38 -0400 Subject: [PATCH 06/11] banno scope --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 4fdd11db..9f84b612 100644 --- a/package.json +++ b/package.json @@ -1,12 +1,12 @@ { - "name": "saml", + "name": "@banno/saml", "version": "0.15.0", "devDependencies": { "mocha": "3.5.3", "should": "~1.2.1" }, "main": "./lib", - "repository": "https://github.com/auth0/node-saml", + "repository": "https://github.com/banno/node-saml", "keywords": [ "saml", "authentication" From 64f673040f48b2790dad7dc393b1c58020e050e8 Mon Sep 17 00:00:00 2001 From: Brandon Ros Date: Mon, 11 May 2020 13:12:40 -0400 Subject: [PATCH 07/11] 0.16.0 --- package-lock.json | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index c14fc296..83bc039f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "saml", - "version": "0.15.0", + "version": "0.16.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 9f84b612..6e5fbf04 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@banno/saml", - "version": "0.15.0", + "version": "0.16.0", "devDependencies": { "mocha": "3.5.3", "should": "~1.2.1" From d36924c4330adf2d6e9af62d084403f93c4c5a61 Mon Sep 17 00:00:00 2001 From: Jon Lindsey <49694086+jonlindsey@users.noreply.github.com> Date: Wed, 24 Jun 2020 16:33:12 -0500 Subject: [PATCH 08/11] feat: SAML 20 signed response with signed encrypted option for assertion. --- lib/saml20.js | 62 ++++++++++++++++++++----------- test/saml20.tests.js | 87 +++++++++++++++++++++++++++++++++++++++++++- test/utils.js | 18 +++++++++ 3 files changed, 144 insertions(+), 23 deletions(-) diff --git a/lib/saml20.js b/lib/saml20.js index fd1c2e02..48be421e 100644 --- a/lib/saml20.js +++ b/lib/saml20.js @@ -10,6 +10,7 @@ var utils = require('./utils'), var fs = require('fs'); var path = require('path'); var saml20 = fs.readFileSync(path.join(__dirname, 'saml20.template')).toString(); +var documentSigningLocation = 'Assertion'; var NAMESPACE = 'urn:oasis:names:tc:SAML:2.0:assertion'; @@ -24,6 +25,11 @@ var algorithms = { } }; +var ResponseSigningLevel = { + ResponseOnly: 'ResponseOnly', + AssertionAndResponse: 'AssertionAndResponse' +} + function getAttributeType(value){ switch(typeof value) { case "string": @@ -44,8 +50,8 @@ function getNameFormat(name){ } // Check that the name is a valid xs:Name -> https://www.w3.org/TR/xmlschema-2/#Name - // xmlNameValidate.name takes a string and will return an object of the form { success, error }, - // where success is a boolean + // xmlNameValidate.name takes a string and will return an object of the form { success, error }, + // where success is a boolean // if it is false, then error is a string containing some hint as to where the match went wrong. if (xmlNameValidator.name(name).success){ return 'urn:oasis:names:tc:SAML:2.0:attrname-format:basic'; @@ -56,7 +62,7 @@ function getNameFormat(name){ } /** -* Gets the complete SAML20 response embedding the assertion (encrypted optional) and +* Gets the complete SAML20 response embedding the assertion (encrypted optional) and * options argument to set attributes for response utilizing the saml20Response.template file. * @param assertion - the SAML assertion to add to the SAML response. * @param options - The saml20 class options argument. @@ -75,7 +81,8 @@ function getSamlResponseXml(assertion, options) { var doc = new Parser().parseFromString(saml20Response.toString()); - doc.documentElement.setAttribute('ID', '_' + (options.uid || utils.uid(32))); + // doc.documentElement.setAttribute('ID', '_' + (options.uid || utils.uid(32))); + doc.documentElement.setAttribute('ID', '_' + (options.responseUid)); doc.documentElement.setAttribute('IssueInstant', moment.utc().format('YYYY-MM-DDTHH:mm:ss.SSS[Z]')); doc.documentElement.setAttribute('Destination', options.destination); if (options.issuer) { @@ -94,18 +101,19 @@ function getSamlResponseXml(assertion, options) { * @returns string - Signed SAML assertion or response depending on option. * @throws ReferenceError if xml argument sent for signing is null or empty. */ -function signXml(xmlToSign, options) { +function signXml(xmlToSign, options, documentSigningLocation) { if (!xmlToSign || xmlToSign.length < 1) throw new ReferenceError('XML to sign cannot be null or empty.') // 0.10.1 added prefix, but we want to name it signatureNamespacePrefix - This is just to keep supporting prefix options.signatureNamespacePrefix = options.signatureNamespacePrefix || options.prefix; - options.signatureNamespacePrefix = typeof options.signatureNamespacePrefix === 'string' ? options.signatureNamespacePrefix : '' ; + options.signatureNamespacePrefix = typeof options.signatureNamespacePrefix === 'string' ? options.signatureNamespacePrefix : '' ; var cert = utils.pemToCert(options.cert); var sig = new SignedXml(null, { signatureAlgorithm: algorithms.signature[options.signatureAlgorithm], idAttribute: 'ID' }); - var signingLocation = options.createSignedSamlResponse ? 'Response' : 'Assertion'; + var signingLocation = documentSigningLocation || 'Assertion'; + sig.addReference("//*[local-name(.)='" + signingLocation + "']", ["http://www.w3.org/2000/09/xmldsig#enveloped-signature", "http://www.w3.org/2001/10/xml-exc-c14n#"], algorithms.digest[options.digestAlgorithm]); @@ -126,7 +134,7 @@ function signXml(xmlToSign, options) { return "<" + prefix + "X509Data><" + prefix + "X509Certificate>" + cert + ""; } }; - + sig.computeSignature(xmlToSign, opts); return sig.getSignedXml(); @@ -162,9 +170,15 @@ exports.create = function (options, callback) { if (!options.cert) throw new Error('Expect a public key cert in pem format'); - if (options.createSignedSamlResponse && - (!options.destination || options.destination.length < 1)) - throw new Error('Expect a SAML Response destination for message to be valid.') + if (options.createSignedSamlResponse) { + + if (!options.destination || options.destination.length < 1) { + throw new Error('Expect a SAML Response destination for message to be valid.'); + } + + options.responseSigningLevel = options.responseSigningLevel || ResponseSigningLevel.ResponseOnly; + options.responseUid = options.responseUid || utils.uid(32); + } options.signatureAlgorithm = options.signatureAlgorithm || 'rsa-sha256'; options.digestAlgorithm = options.digestAlgorithm || 'sha256'; @@ -193,10 +207,10 @@ exports.create = function (options, callback) { if (options.lifetimeInSeconds) { conditions[0].setAttribute('NotBefore', now.format('YYYY-MM-DDTHH:mm:ss.SSS[Z]')); conditions[0].setAttribute('NotOnOrAfter', now.clone().add(options.lifetimeInSeconds, 'seconds').format('YYYY-MM-DDTHH:mm:ss.SSS[Z]')); - - confirmationData[0].setAttribute('NotOnOrAfter', now.clone().add(options.lifetimeInSeconds, 'seconds').format('YYYY-MM-DDTHH:mm:ss.SSS[Z]')); + + confirmationData[0].setAttribute('NotOnOrAfter', now.clone().add(options.lifetimeInSeconds, 'seconds').format('YYYY-MM-DDTHH:mm:ss.SSS[Z]')); } - + if (options.audiences) { var audienceRestriction = doc.createElementNS(NAMESPACE, 'saml:AudienceRestriction'); var audiences = options.audiences instanceof Array ? options.audiences : [options.audiences]; @@ -206,7 +220,7 @@ exports.create = function (options, callback) { audienceRestriction.appendChild(element); }); - conditions[0].appendChild(audienceRestriction); + conditions[0].appendChild(audienceRestriction); } if (options.recipient) @@ -229,7 +243,7 @@ exports.create = function (options, callback) { attributeElement.setAttribute('Name', prop); if (options.includeAttributeNameFormat){ - attributeElement.setAttribute('NameFormat', getNameFormat(prop)); + attributeElement.setAttribute('NameFormat', getNameFormat(prop)); } var values = options.attributes[prop] instanceof Array ? options.attributes[prop] : [options.attributes[prop]]; @@ -260,7 +274,7 @@ exports.create = function (options, callback) { } var nameID = doc.documentElement.getElementsByTagNameNS(NAMESPACE, 'NameID')[0]; - + if (options.nameIdentifier) { nameID.textContent = options.nameIdentifier; } @@ -276,21 +290,24 @@ exports.create = function (options, callback) { var assertion = utils.removeWhitespace(doc.toString()); - // NEW: construct a SAML20 response signed at the response with embedded (encrypted option) assertion + // Enhanced path - construct a SAML20 response signed at the response + // with embedded (encrypted option) (signed option) assertion if (options.createSignedSamlResponse) { + if (options.responseSigningLevel === ResponseSigningLevel.AssertionAndResponse) { + assertion = signXml(utils.removeWhitespace(assertion), options, 'Assertion'); + } if (options.encryptionCert) { encryptAssertionXml(assertion, options) .then(encryptedAssertion => (callback(null, signSamlResponse(encryptedAssertion)))) .catch((err) => callback(err)); } else { - // Send saml response back signed if not set for encryption var signedPlainResponse = signSamlResponse(assertion); return (callback) ? callback(null, signedPlainResponse) : signedPlainResponse; } + // Original path - create a simple signed (encrypted option) assertion. } else { - // Sign the assertion always for both options - var signedAssertion = signXml(utils.removeWhitespace(assertion), options); + var signedAssertion = signXml(utils.removeWhitespace(assertion), options, 'Assertion'); if (options.encryptionCert) { encryptAssertionXml(signedAssertion, options) @@ -305,6 +322,7 @@ exports.create = function (options, callback) { // Sign response with inserted assertion (or encrypted assertion) function signSamlResponse(assertion) { var samlResponse = getSamlResponseXml(assertion, options); - return signXml(utils.removeWhitespace(samlResponse), options); + return signXml(utils.removeWhitespace(samlResponse), options, 'Response'); } + }; diff --git a/test/saml20.tests.js b/test/saml20.tests.js index c90244a3..a727d075 100644 --- a/test/saml20.tests.js +++ b/test/saml20.tests.js @@ -641,7 +641,7 @@ describe('saml 2.0', function () { }); }); - describe('encryption full SAML response', function () { + describe('full signed SAML 2.0 response with encrypted assertion', function () { it('should create a saml 2.0 signed response including encrypted assertion', function (done) { var options = { @@ -712,5 +712,90 @@ describe('saml 2.0', function () { }); }); }); + + describe('full signed SAML 2.0 response with encryped signed assertion', function () { + + it('should create a saml 2.0 signed response including encrypted signed assertion', function (done) { + var options = { + cert: fs.readFileSync(__dirname + '/test-auth0.pem'), + key: fs.readFileSync(__dirname + '/test-auth0.key'), + encryptionPublicKey: fs.readFileSync(__dirname + '/test-auth0_rsa.pub'), + encryptionCert: fs.readFileSync(__dirname + '/test-auth0.pem'), + xpathToNodeBeforeSignature: "//*[local-name(.)='Issuer']", + createSignedSamlResponse: true, + responseSigningLevel: 'AssertionAndResponse', + destination: 'https:/foo.com' + }; + var isValid = false; + saml.create(options, function(err, responseData) { + if (err) return done(err); + + // Response Signature + isValid = utils.isValidResponseSignature(responseData, options.cert); + assert.equal(true, isValid); + + // Assertion Signature + var isValid = utils.isValidSignature(responseData, options.cert); + assert.equal(true, isValid); + + var encryptedData = utils.getEncryptedData(responseData); + + xmlenc.decrypt(encryptedData.toString(), { key: fs.readFileSync(__dirname + '/test-auth0.key')}, function(err, decrypted) { + if (err) return done(err); + }); + + done(); + }); + }); + + it('...with assertion attributes', function (done) { + var options = { + cert: fs.readFileSync(__dirname + '/test-auth0.pem'), + key: fs.readFileSync(__dirname + '/test-auth0.key'), + encryptionPublicKey: fs.readFileSync(__dirname + '/test-auth0_rsa.pub'), + encryptionCert: fs.readFileSync(__dirname + '/test-auth0.pem'), + xpathToNodeBeforeSignature: "//*[local-name(.)='Issuer']", + createSignedSamlResponse: true, + responseSigningLevel: 'AssertionAndResponse', + destination: 'https:/foo.com', + 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', + 'http://example.org/claims/testaccent': 'fóo', // should supports accents + 'http://undefinedattribute/ws/com.com': undefined + } + }; + + saml.create(options, function(err, responseData) { + if (err) return done(err); + + // Response Signature + isValid = utils.isValidResponseSignature(responseData, options.cert); + assert.equal(true, isValid); + + // Assertion Signature + var isValid = utils.isValidSignature(responseData, options.cert); + assert.equal(true, isValid); + + var encryptedData = utils.getEncryptedData(responseData); + + xmlenc.decrypt(encryptedData.toString(), { key: fs.readFileSync(__dirname + '/test-auth0.key')}, function(err, decrypted) { + if (err) return done(err); + + var attributes = utils.getAttributes(decrypted); + assert.equal(3, 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('http://example.org/claims/testaccent', attributes[2].getAttribute('Name')); + assert.equal('fóo', attributes[2].textContent); + + done(); + }); + }); + }); + }); + }); }); \ No newline at end of file diff --git a/test/utils.js b/test/utils.js index 009cc067..15dfb0ce 100644 --- a/test/utils.js +++ b/test/utils.js @@ -18,6 +18,24 @@ exports.isValidSignature = function(assertion, cert) { return sig.checkSignature(assertion); }; +exports.isValidResponseSignature = function(response, cert) { + var doc = new xmldom.DOMParser().parseFromString(response); + var signature = doc.documentElement + .getElementsByTagName('Signature')[0] + + var sig = new xmlCrypto.SignedXml(null, { idAttribute: 'ResponseID' }); + sig.keyInfoProvider = { + getKeyInfo: function (key) { + return ""; + }, + getKey: function (keyInfo) { + return cert; + } + }; + sig.loadSignature(signature.toString()); + return sig.checkSignature(response); +}; + exports.getIssuer = function(assertion) { var doc = new xmldom.DOMParser().parseFromString(assertion); return doc.documentElement.getAttribute('Issuer'); From 0336cebe3d68c7ee6c9615bfeac50626312a4f75 Mon Sep 17 00:00:00 2001 From: Jon Lindsey <49694086+jonlindsey@users.noreply.github.com> Date: Wed, 24 Jun 2020 16:50:21 -0500 Subject: [PATCH 09/11] feat: SAML 20 signed response with signed encrypted option for assertion-remove comment. --- lib/saml20.js | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/saml20.js b/lib/saml20.js index 48be421e..e59c252a 100644 --- a/lib/saml20.js +++ b/lib/saml20.js @@ -81,7 +81,6 @@ function getSamlResponseXml(assertion, options) { var doc = new Parser().parseFromString(saml20Response.toString()); - // doc.documentElement.setAttribute('ID', '_' + (options.uid || utils.uid(32))); doc.documentElement.setAttribute('ID', '_' + (options.responseUid)); doc.documentElement.setAttribute('IssueInstant', moment.utc().format('YYYY-MM-DDTHH:mm:ss.SSS[Z]')); doc.documentElement.setAttribute('Destination', options.destination); From fa62208d8967df5333b11058018d1aa26b9c2df9 Mon Sep 17 00:00:00 2001 From: Brandon Ros Date: Wed, 24 Jun 2020 18:32:30 -0400 Subject: [PATCH 10/11] 0.17.0 --- package-lock.json | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 83bc039f..b6aba431 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "saml", - "version": "0.16.0", + "version": "0.17.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 6e5fbf04..6e4b5582 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@banno/saml", - "version": "0.16.0", + "version": "0.17.0", "devDependencies": { "mocha": "3.5.3", "should": "~1.2.1" From 6a70c637a29b0ef1af28072f7045860fa08185f0 Mon Sep 17 00:00:00 2001 From: Imola Heffley Date: Thu, 5 May 2022 13:41:32 -0500 Subject: [PATCH 11/11] Create index.d.ts --- index.d.ts | 57 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 index.d.ts diff --git a/index.d.ts b/index.d.ts new file mode 100644 index 00000000..4e0c25b4 --- /dev/null +++ b/index.d.ts @@ -0,0 +1,57 @@ +/** + * Interface for lib/saml (Banno/node-saml). It differs from the original + * designed for auth0/node-saml to include the enhancements for supporting + * a full SAML 2.0 Response in all signing modes. + */ + declare module '@banno/saml' { + export interface SamlAttributes { + [key: string]: string; + } + + export interface KeyInfoProvider { + getKeyInfo(key: string, prefix: string): string; + } + + export interface SamlOpts { + authnContextClassRef?: string; + attributes?: SamlAttributes; + audiences?: string | string[]; + cert: Buffer; + digestAlgorithm?: string; + encryptionAlgorithm?: string; + encryptionCert?: Buffer; + encryptionPublicKey?: Buffer; + holderOfKeyProofSecret?: string; + includeAttributeNameFormat?: boolean; + inResponseTo?: string; + issuer?: string; + key: Buffer; + keyEncryptionAlgorighm?: string; // sic https://github.com/auth0/node-xml-encryption/issues/17 + keyInfoProvider?: KeyInfoProvider; + lifetimeInSeconds?: number; + nameIdentifier?: string; + nameIdentifierFormat?: string; + prefix?: string; + recipient?: string; + sessionIndex?: string; + signatureAlgorithm?: string; + signatureNamespacePrefix?: string; + subjectConfirmationMethod?: string; + typedAttributes?: boolean; + uid?: string; + responseUid?: string; + xpathToNodeBeforeSignature?: string; + createSignedSamlResponse?: boolean; + responseSigningLevel?: string; + destination?: string; + } + + export namespace Saml11 { + function create(opts: SamlOpts, cb?: (err: Error | null, result: any[], proofSecret: Buffer) => void): any; + } + + export namespace Saml20 { + function create(opts: SamlOpts, cb?: (err: Error | null, signed: string) => void): any; + } + } + \ No newline at end of file