Skip to content

Instantly share code, notes, and snippets.

@danielkhan
Last active September 4, 2019 16:10
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save danielkhan/69f08fa633a12d4a4d4b to your computer and use it in GitHub Desktop.
Save danielkhan/69f08fa633a12d4a4d4b to your computer and use it in GitHub Desktop.
This is a hacked together version of passport-samls saml parser that supports shibboleth
var zlib = require('zlib');
var xml2js = require('xml2js');
var xmlCrypto = require('xml-crypto');
var crypto = require('crypto');
var xmldom = require('xmldom');
var querystring = require('querystring');
var moment = require('moment');
var xmlenc = require('xml-encryption');
var xpath = require('xpath');
var dom = require('xmldom').DOMParser;
var serializer = require('xmldom').XMLSerializer;
var util = require('util');
var SAML = function (options) {
this.options = this.initialize(options);
};
SAML.prototype.initialize = function (options) {
if (!options) {
options = {};
}
if (!options.protocol) {
options.protocol = 'https://';
}
if (!options.path) {
options.path = '/saml/consume';
}
if (!options.issuer) {
options.issuer = 'onelogin_saml';
}
if (options.identifierFormat === undefined) {
options.identifierFormat = "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress";
}
return options;
};
SAML.prototype.generateUniqueID = function () {
var chars = "abcdef0123456789";
var uniqueID = "";
for (var i = 0; i < 20; i++) {
uniqueID += chars.substr(Math.floor((Math.random() * 15)), 1);
}
return uniqueID;
};
SAML.prototype.generateInstant = function () {
var date = moment().format();
return date;
};
SAML.prototype.signRequest = function (xml) {
var signer = crypto.createSign('RSA-SHA1');
signer.update(xml);
return signer.sign(this.options.privateCert, 'base64');
};
SAML.prototype.generateAuthorizeRequest = function (req) {
var id = "_" + this.generateUniqueID();
var instant = this.generateInstant();
var callbackUrl;
// Post-auth destination
if (this.options.callbackUrl) {
callbackUrl = this.options.callbackUrl;
} else {
callbackUrl = this.options.protocol + req.headers.host + this.options.path;
}
var request =
"<samlp:AuthnRequest xmlns:samlp=\"urn:oasis:names:tc:SAML:2.0:protocol\" ID=\"" + id + "\" Version=\"2.0\" IssueInstant=\"" + instant +
"\" ProtocolBinding=\"urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST\" AssertionConsumerServiceURL=\"" + callbackUrl + "\" Destination=\"" +
this.options.entryPoint + "\">" +
"<saml:Issuer xmlns:saml=\"urn:oasis:names:tc:SAML:2.0:assertion\">" + this.options.issuer + "</saml:Issuer>\n";
if (this.options.identifierFormat) {
request += "<samlp:NameIDPolicy xmlns:samlp=\"urn:oasis:names:tc:SAML:2.0:protocol\" Format=\"" + this.options.identifierFormat +
"\" AllowCreate=\"true\"></samlp:NameIDPolicy>\n";
}
request +=
"<samlp:RequestedAuthnContext xmlns:samlp=\"urn:oasis:names:tc:SAML:2.0:protocol\" Comparison=\"exact\">" +
"<saml:AuthnContextClassRef xmlns:saml=\"urn:oasis:names:tc:SAML:2.0:assertion\">urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport</saml:AuthnContextClassRef></samlp:RequestedAuthnContext>\n" +
"</samlp:AuthnRequest>";
return request;
};
SAML.prototype.generateLogoutRequest = function (req) {
var id = "_" + this.generateUniqueID();
var instant = this.generateInstant();
//samlp:LogoutRequest xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"
// ID="_135ad2fd-b275-4428-b5d6-3ac3361c3a7f" Version="2.0" Destination="https://idphost/adfs/ls/"
//IssueInstant="2008-06-03T12:59:57Z"><saml:Issuer>myhost</saml:Issuer><NameID Format="urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress"
//NameQualifier="https://idphost/adfs/ls/">myemail@mydomain.com</NameID<samlp:SessionIndex>_0628125f-7f95-42cc-ad8e-fde86ae90bbe
//</samlp:SessionIndex></samlp:LogoutRequest>
var request = "<samlp:LogoutRequest xmlns:samlp=\"urn:oasis:names:tc:SAML:2.0:protocol\" " +
"xmlns:saml=\"urn:oasis:names:tc:SAML:2.0:assertion\" ID=\"" + id + "\" Version=\"2.0\" IssueInstant=\"" + instant +
"\" Destination=\"" + this.options.entryPoint + "\">" +
"<saml:Issuer xmlns:saml=\"urn:oasis:names:tc:SAML:2.0:assertion\">" + this.options.issuer + "</saml:Issuer>" +
"<saml:NameID Format=\"" + req.user.nameIDFormat + "\">" + req.user.nameID + "</saml:NameID>" +
"</samlp:LogoutRequest>";
return request;
};
SAML.prototype.requestToUrl = function (request, operation, callback) {
var self = this;
zlib.deflateRaw(request, function (err, buffer) {
if (err) {
return callback(err);
}
var base64 = buffer.toString('base64');
var target = self.options.entryPoint + '?';
if (operation === 'logout') {
if (self.options.logoutUrl) {
target = self.options.logoutUrl + '?';
}
}
var samlRequest = {
SAMLRequest: base64
};
if (self.options.privateCert) {
samlRequest.SigAlg = 'http://www.w3.org/2000/09/xmldsig#rsa-sha1';
samlRequest.Signature = self.signRequest(querystring.stringify(samlRequest));
}
target += querystring.stringify(samlRequest);
callback(null, target);
});
};
SAML.prototype.getAuthorizeUrl = function (req, callback) {
var request = this.generateAuthorizeRequest(req);
this.requestToUrl(request, 'authorize', callback);
};
SAML.prototype.getLogoutUrl = function (req, callback) {
var request = this.generateLogoutRequest(req);
this.requestToUrl(request, 'logout', callback);
};
SAML.prototype.certToPEM = function (cert) {
cert = cert.match(/.{1,64}/g).join('\n');
cert = "-----BEGIN CERTIFICATE-----\n" + cert;
cert = cert + "\n-----END CERTIFICATE-----\n";
return cert;
};
SAML.prototype.validateSignature = function (xml, cert) {
var self = this;
var doc = new xmldom.DOMParser().parseFromString(xml);
// return true;
var signature = xmlCrypto.xpath(doc, "//*[local-name(.)='Signature' and namespace-uri(.)='http://www.w3.org/2000/09/xmldsig#']")[0];
var sig = new xmlCrypto.SignedXml();
sig.keyInfoProvider = {
getKeyInfo: function (key) {
return "<X509Data></X509Data>";
},
getKey: function (keyInfo) {
return self.certToPEM(cert);
}
};
sig.loadSignature(signature.toString());
var res = sig.checkSignature(xml);
if (!res) console.log(sig.validationErrors)
return res;
};
SAML.prototype.getElement = function (parentElement, elementName) {
if (parentElement['saml:' + elementName]) {
return parentElement['saml:' + elementName];
} else if (parentElement['samlp:' + elementName]) {
return parentElement['samlp:' + elementName];
} else if (parentElement['saml2p:' + elementName]) {
return parentElement['saml2p:' + elementName];
} else if (parentElement['saml2:' + elementName]) {
return parentElement['saml2:' + elementName];
}
return parentElement[elementName];
};
SAML.prototype.validateResponse = function (samlResponse, callback) {
var self = this;
var xml = new Buffer(samlResponse, 'base64').toString('ascii');
var options = {
key: self.options.privateCert
}
var domdoc = new dom().parseFromString(xml);
var select = xpath.useNamespaces({"xenc": "http://www.w3.org/2001/04/xmlenc#"});
var result = select('//xenc:EncryptedData', domdoc)[0];
var assrt = new serializer().serializeToString(result);
xmlenc.decrypt(assrt, options, function(err, result) {
xml = result;
});
var parser = new xml2js.Parser({explicitRoot: true});
parser.parseString(xml, function (err, doc) {
// Verify signature
if (self.options.cert && !self.validateSignature(xml, self.options.cert)) {
return callback(new Error('Invalid signature'), null, false);
}
// var response = self.getElement(doc, 'Response');
// if (response) {
var assertion = [self.getElement(doc, 'Assertion')];
if (!assertion) {
return callback(new Error('Missing SAML assertion'), null, false);
}
var profile = {};
var issuer = self.getElement(assertion[0], 'Issuer');
if (issuer) {
profile.issuer = issuer[0].$;
}
var subject = self.getElement(assertion[0], 'Subject');
if (subject) {
var nameID = self.getElement(subject[0], 'NameID');
if (nameID) {
profile.nameID = nameID[0]._;
if (nameID[0].$.Format) {
profile.nameIDFormat = nameID[0].$.Format;
}
}
}
var attributeStatement = self.getElement(assertion[0], 'AttributeStatement');
if (!attributeStatement) {
return callback(new Error('Missing AttributeStatement'), null, false);
}
// console.log(util.inspect(assertion, showHidden=false, depth=20, colorize=true));
// console.log("\n");
var attributes = self.getElement(attributeStatement[0], 'Attribute');
if (attributes) {
attributes.forEach(function (attribute) {
var value = self.getElement(attribute, 'AttributeValue');
/*
console.log(attribute);
console.log("\n");
console.log(value);
console.log("\n\n\n");
*/
if(value.length > 1) {
profile[attribute.$.FriendlyName] = [];
value.forEach(function(val) {
if (typeof value[0] === 'string') {
profile[attribute.$.FriendlyName].push(val);
} else {
profile[attribute.$.FriendlyName].push(val._);
}
});
} else {
if (typeof value[0] === 'string') {
profile[attribute.$.FriendlyName] = value[0];
} else {
profile[attribute.$.FriendlyName] = value[0]._;
}
}
});
}
if (!profile.mail && profile['urn:oid:0.9.2342.19200300.100.1.3']) {
// See http://www.incommonfederation.org/attributesummary.html for definition of attribute OIDs
profile.mail = profile['urn:oid:0.9.2342.19200300.100.1.3'];
}
if (!profile.email && profile.mail) {
profile.email = profile.mail;
}
return callback(null, profile, false);
/*
} else {
var logoutResponse = self.getElement(doc, 'LogoutResponse');
if (logoutResponse) {
callback(null, null, true);
} else {
return callback(new Error('Unknown SAML response message'), null, false);
}
}
*/
});
};
exports.SAML = SAML;
@markstos
Copy link

markstos commented Sep 4, 2019

The current version of passport-saml supports Shibboleth (I use it). What was customized here?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment