Skip to content

Commit

Permalink
feat(saml20): adds Saml20.createUnsignedAssertion()
Browse files Browse the repository at this point in the history
  • Loading branch information
luuuis committed Sep 11, 2020
1 parent cb3bfcd commit de0e766
Show file tree
Hide file tree
Showing 4 changed files with 729 additions and 591 deletions.
244 changes: 183 additions & 61 deletions lib/saml20.js
Original file line number Diff line number Diff line change
Expand Up @@ -62,39 +62,190 @@ 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');

if (!options.cert)
throw new Error('Expect a public key cert in pem format');

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;

// 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 : '' ;
function extractSaml20Options(opts) {
return {
uid: opts.uid,
issuer: opts.issuer,
lifetimeInSeconds: opts.lifetimeInSeconds,
audiences: opts.audiences,
recipient: opts.recipient,
inResponseTo: opts.inResponseTo,
attributes: opts.attributes,
includeAttributeNameFormat: (typeof opts.includeAttributeNameFormat !== 'undefined') ? opts.includeAttributeNameFormat : true,
typedAttributes: (typeof opts.typedAttributes !== 'undefined') ? opts.typedAttributes : true,
sessionIndex: opts.sessionIndex,
nameIdentifier: opts.nameIdentifier,
nameIdentifierFormat: opts.nameIdentifierFormat,
authnContextClassRef: opts.authnContextClassRef
};
}

var cert = utils.pemToCert(options.cert);
var SignXml = Object.freeze({
fromSignXmlOptions: function (options) {
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');

var key = options.key;
var pem = options.cert;
var signatureAlgorithm = options.signatureAlgorithm || 'rsa-sha256';
var digestAlgorithm = options.digestAlgorithm || 'sha256';
var signatureNamespacePrefix = (function (prefix) {
// 0.10.1 added prefix, but we want to name it signatureNamespacePrefix - This is just to keep supporting prefix
return typeof prefix === 'string' ? prefix : '';
} )(options.signatureNamespacePrefix || options.prefix);
var xpathToNodeBeforeSignature = options.xpathToNodeBeforeSignature || "//*[local-name(.)='Issuer']";

return function signXmlAssertion(token) {
var cert = utils.pemToCert(pem);

var sig = new SignedXml(null, { signatureAlgorithm: algorithms.signature[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[digestAlgorithm]);

sig.signingKey = key;

sig.keyInfoProvider = {
getKeyInfo: function (key, prefix) {
prefix = prefix ? prefix + ':' : prefix;
return "<" + prefix + "X509Data><" + prefix + "X509Certificate>" + cert + "</" + prefix + "X509Certificate></" + prefix + "X509Data>";
}
};

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]);
sig.computeSignature(token, {
location: { reference: xpathToNodeBeforeSignature, action: 'after' },
prefix: signatureNamespacePrefix
});
return sig.getSignedXml();
};
},

sig.signingKey = options.key;

sig.keyInfoProvider = {
getKeyInfo: function (key, prefix) {
prefix = prefix ? prefix + ':' : prefix;
return "<" + prefix + "X509Data><" + prefix + "X509Certificate>" + cert + "</" + prefix + "X509Certificate></" + prefix + "X509Data>";
unsigned: function (xml) {
return xml;
}
});

var EncryptXml = Object.freeze({
fromEncryptXmlOptions: function (options) {
if (!options.encryptionCert) {
return function(xml, callback) {
if (callback) {
return setImmediate(callback, null, xml);
} else {
return xml;
}
}
} else {
return function(xml, 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(xml, 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));
});
}
}
};
}
});

/**
* Creates a signed SAML 2.0 assertion from the given options.
*
* @param options
*
* // SAML
* @param [options.uid] {string}
* @param [options.issuer] {string}
* @param [options.lifetimeInSeconds] {number}
* @param [options.audiences] {string|string[]}
* @param [options.recipient] {string}
* @param [options.inResponseTo] {string}
* @param [options.attributes]
* @param [options.includeAttributeNameFormat] {boolean}
* @param [options.typedAttributes] {boolean}
* @param [options.sessionIndex] {string}
* @param [options.nameIdentifier] {string}
* @param [options.nameIdentifierFormat] {string}
* @param [options.authnContextClassRef] {string}
*
* // XML Dsig
* @param options.key {Buffer}
* @param options.cert {Buffer}
* @param [options.signatureAlgorithm] {string}
* @param [options.digestAlgorithm] {string}
* @param [options.signatureNamespacePrefix] {string}
* @param [options.xpathToNodeBeforeSignature] {string}
*
* // XML encryption
* @param [options.encryptionCert] {Buffer}
* @param [options.encryptionPublicKey] {Buffer}
* @param [options.encryptionAlgorithm] {string}
* @param [options.keyEncryptionAlgorighm] {string}
*
* @param {Function} [callback] required if encrypting
* @return {*}
*/
exports.create = function createSignedAssertion(options, callback) {
return createAssertion(extractSaml20Options(options), {
signXml: SignXml.fromSignXmlOptions(options),
encryptXml: EncryptXml.fromEncryptXmlOptions(options)
}, callback);
};

/**
* Creates an **unsigned** SAML 2.0 assertion from the given options.
*
* @param options
*
* // SAML
* @param [options.uid] {string}
* @param [options.issuer] {string}
* @param [options.lifetimeInSeconds] {number}
* @param [options.audiences] {string|string[]}
* @param [options.recipient] {string}
* @param [options.inResponseTo] {string}
* @param [options.attributes]
* @param [options.includeAttributeNameFormat] {boolean}
* @param [options.typedAttributes] {boolean}
* @param [options.sessionIndex] {string}
* @param [options.nameIdentifier] {string}
* @param [options.nameIdentifierFormat] {string}
* @param [options.authnContextClassRef] {string}
*
* // XML encryption
* @param [options.encryptionCert] {Buffer}
* @param [options.encryptionPublicKey] {Buffer}
* @param [options.encryptionAlgorithm] {string}
* @param [options.keyEncryptionAlgorighm] {string}
*
* @param {Function} [callback] required if encrypting
* @return {*}
*/
exports.createUnsignedAssertion = function createUnsignedAssertion(options, callback) {
return createAssertion(extractSaml20Options(options), {
signXml: SignXml.unsigned,
encryptXml: EncryptXml.fromEncryptXmlOptions(options)
}, callback);
};

/**
* @param options SAML options
* @param strategies
* @param strategies.signXml {Function} strategy to sign the assertion
* @param strategies.encryptXml {Function} strategy to encrypt the assertion
* @param callback
* @return {*}
*/
function createAssertion(options, strategies, callback) {
var doc = newSaml20Document();

doc.documentElement.setAttribute('ID', '_' + (options.uid || utils.uid(32)));
Expand All @@ -112,7 +263,7 @@ exports.create = function(options, callback) {
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) {
Expand Down Expand Up @@ -195,39 +346,10 @@ exports.create = function(options, callback) {
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();
signed = strategies.signXml(token);
} catch(err){
return utils.reportError(err, callback);
}

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

return strategies.encryptXml(signed, callback);
}
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
"license": "MIT",
"dependencies": {
"async": "~0.2.9",
"chai": "^4.2.0",
"moment": "2.19.3",
"valid-url": "~1.0.9",
"xml-crypto": "~1.0.1",
Expand Down
Loading

0 comments on commit de0e766

Please sign in to comment.