Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
105 changes: 54 additions & 51 deletions lib/saml20.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand All @@ -77,16 +83,22 @@ function getSamlResponseXml(assertion, options) {
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.
* @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 : '' ;
Expand Down Expand Up @@ -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();
Expand All @@ -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 = '<saml:EncryptedAssertion xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">' + encrypted + '</saml:EncryptedAssertion>';
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 = `<saml:EncryptedAssertion xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">${encryptedAssertion}</saml:EncryptedAssertion>`;

resolve(encryptedAssertion);
});
});

exports.create = function (options, callback) {
if (!options.key)
Expand Down Expand Up @@ -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);
Expand Down
Loading