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 1/3] 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 2/3] =?UTF-8?q?feat:=20add=20SAML=20signed=20response=20wi?=
=?UTF-8?q?th=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 3/3] 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];
};