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
178 changes: 127 additions & 51 deletions lib/saml20.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,39 +55,109 @@ 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;
return "<" + prefix + "X509Data><" + prefix + "X509Certificate>" + cert + "</" + prefix + "X509Certificate></" + prefix + "X509Data>";
}
};

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 = '<saml:EncryptedAssertion xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">' + encrypted + '</saml:EncryptedAssertion>';
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());
Expand Down Expand Up @@ -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 = '<saml:EncryptedAssertion xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">' + encrypted + '</saml:EncryptedAssertion>';
callback(null, utils.removeWhitespace(encrypted));
});
};

};
6 changes: 6 additions & 0 deletions lib/saml20Response.template
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<samlp:Response xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" Version="2.0" IssueInstant="" ID="" Destination="">
<saml:Issuer Format="urn:oasis:names:tc:SAML:2.0:nameid-format:entity"></saml:Issuer>
<samlp:Status>
<samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success"/>
</samlp:Status>
</samlp:Response>
Loading