Last active
September 4, 2019 16:10
-
-
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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/">[email protected]</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; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
The current version of passport-saml supports Shibboleth (I use it). What was customized here?