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 + "" + prefix + "X509Certificate>" + prefix + "X509Data>";
}
};
-
+
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 + "" + prefix + "X509Certificate>" + prefix + "X509Data>";
}
};
-
+
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