Socket
Socket
Sign inDemoInstall

passport-saml

Package Overview
Dependencies
11
Maintainers
2
Versions
68
Alerts
File Explorer

Advanced tools

Install Socket

Detect and block malicious and high-risk dependencies

Install

Comparing version 0.0.4 to 0.1.0

.travis.yml

408

lib/passport-saml/saml.js

@@ -7,3 +7,11 @@ var zlib = require('zlib');

var querystring = require('querystring');
var xmlbuilder = require('xmlbuilder');
// Patch the xml-crypto envelope transform, which should remove our specific signature, but is
// currently removing the first signature it finds in the whole doc. TODO: submit a pull
// request to xml-crypto.
var patchedEnvelopedSignature = require('./patched-enveloped-signature.js');
xmlCrypto.SignedXml.CanonicalizationAlgorithms['http://www.w3.org/2000/09/xmldsig#enveloped-signature'] =
patchedEnvelopedSignature.EnvelopedSignature;
var SAML = function (options) {

@@ -48,3 +56,3 @@ this.options = this.initialize(options);

var date = new Date();
return date.getUTCFullYear() + '-' + ('0' + (date.getUTCMonth()+1)).slice(-2) + '-' + ('0' + date.getUTCDate()).slice(-2) + 'T' + ('0' + (date.getUTCHours()+2)).slice(-2) + ":" + ('0' + date.getUTCMinutes()).slice(-2) + ":" + ('0' + date.getUTCSeconds()).slice(-2) + "Z";
return date.getUTCFullYear() + '-' + ('0' + (date.getUTCMonth()+1)).slice(-2) + '-' + ('0' + date.getUTCDate()).slice(-2) + 'T' + ('0' + date.getUTCHours()).slice(-2) + ":" + ('0' + date.getUTCMinutes()).slice(-2) + ":" + ('0' + date.getUTCSeconds()).slice(-2) + "Z";
};

@@ -56,7 +64,8 @@

return signer.sign(this.options.privateCert, 'base64');
}
};
SAML.prototype.generateAuthorizeRequest = function (req) {
SAML.prototype.generateAuthorizeRequest = function (req, isPassive) {
var id = "_" + this.generateUniqueID();
var instant = this.generateInstant();
var callbackUrl;

@@ -67,22 +76,41 @@ // Post-auth destination

} else {
var callbackUrl = this.options.protocol + req.headers.host + this.options.path;
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";
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',
'#text': this.options.issuer
},
'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',
'#text': 'urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport'
}
}
}
};
if (isPassive)
request['samlp:AuthnRequest']['@IsPassive'] = true;
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:AuthnRequest']['samlp:NameIDPolicy'] = {
'@xmlns:samlp': 'urn:oasis:names:tc:SAML:2.0:protocol',
'@Format': this.options.identifierFormat,
'@AllowCreate': 'true'
};
}
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;
return xmlbuilder.create(request).end();
};

@@ -94,20 +122,53 @@

//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',
'#text': this.options.issuer
},
'saml:NameID' : {
'@Format': req.user.nameIDFormat,
'#text': req.user.nameID
}
}
};
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;
}
return xmlbuilder.create(request).end();
};
SAML.prototype.requestToUrl = function (request, operation, callback) {
SAML.prototype.generateLogoutResponse = function (req, logoutRequest) {
var id = "_" + this.generateUniqueID();
var instant = this.generateInstant();
var request = {
'samlp:LogoutResponse' : {
'@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,
'@InResponseTo': logoutRequest.ID,
'saml:Issuer' : {
'#text': this.options.issuer
},
'samlp:Status': {
'samlp:StatusCode': {
'@Value': 'urn:oasis:names:tc:SAML:2.0:status:Success'
}
}
}
};
return xmlbuilder.create(request).end();
};
SAML.prototype.requestToUrl = function (request, response, operation, additionalParameters, callback) {
var self = this;
zlib.deflateRaw(request, function(err, buffer) {
zlib.deflateRaw(request || response, function(err, buffer) {
if (err) {

@@ -123,23 +184,30 @@ return callback(err);

target = self.options.logoutUrl + '?';
}
}
} else if (operation !== 'authorize') {
return callback(new Error("Unknown operation: "+operation));
}
var samlRequest = {
var samlMessage = request ? {
SAMLRequest: base64
} : {
SAMLResponse: base64
};
Object.keys(additionalParameters).forEach(function(k) {
samlMessage[k] = additionalParameters[k];
});
if (self.options.privateCert) {
samlRequest.SigAlg = 'http://www.w3.org/2000/09/xmldsig#rsa-sha1';
samlRequest.Signature = self.signRequest(querystring.stringify(samlRequest));
samlMessage.SigAlg = 'http://www.w3.org/2000/09/xmldsig#rsa-sha1';
samlMessage.Signature = self.signRequest(querystring.stringify(samlMessage));
}
target += querystring.stringify(samlRequest);
target += querystring.stringify(samlMessage);
callback(null, target);
});
}
};
SAML.prototype.getAuthorizeUrl = function (req, callback) {
var request = this.generateAuthorizeRequest(req);
this.requestToUrl(request, 'authorize', callback);
var request = this.generateAuthorizeRequest(req, this.options.passive);
var RelayState = req.query && req.query.RelayState || req.body && req.body.RelayState;
this.requestToUrl(request, null, 'authorize', RelayState ? { RelayState: RelayState } : {}, callback);
};

@@ -149,5 +217,11 @@

var request = this.generateLogoutRequest(req);
var RelayState = req.query && req.query.RelayState || req.body && req.body.RelayState;
this.requestToUrl(request, null, 'logout', RelayState ? { RelayState: RelayState } : {}, callback);
};
this.requestToUrl(request, 'logout', callback);
}
SAML.prototype.getLogoutResponseUrl = function(req, callback) {
var response = this.generateLogoutResponse(req, req.samlLogoutRequest);
var RelayState = req.query && req.query.RelayState || req.body && req.body.RelayState;
this.requestToUrl(null, response, 'logout', RelayState ? { RelayState: RelayState } : {}, callback);
};

@@ -161,10 +235,21 @@ SAML.prototype.certToPEM = function (cert) {

SAML.prototype.validateSignature = function (xml, cert) {
// This function checks that the |currentNode| in the |fullXml| document contains exactly 1 valid
// signature of the |currentNode|.
//
// See https://github.com/bergie/passport-saml/issues/19 for references to some of the attack
// vectors against SAML signature verification.
SAML.prototype.validateSignature = function (fullXml, currentNode, cert) {
var self = this;
var doc = new xmldom.DOMParser().parseFromString(xml);
var signature = xmlCrypto.xpath.SelectNodes(doc, "//*[local-name(.)='Signature' and namespace-uri(.)='http://www.w3.org/2000/09/xmldsig#']")[0];
var xpathSigQuery = ".//*[local-name(.)='Signature' and " +
"namespace-uri(.)='http://www.w3.org/2000/09/xmldsig#']";
var signatures = xmlCrypto.xpath(currentNode, xpathSigQuery);
// This function is expecting to validate exactly one signature, so if we find more or fewer
// than that, reject.
if (signatures.length != 1)
return false;
var signature = signatures[0].toString();
var sig = new xmlCrypto.SignedXml();
sig.keyInfoProvider = {
getKeyInfo: function (key) {
return "<X509Data></X509Data>"
return "<X509Data></X509Data>";
},

@@ -175,94 +260,205 @@ getKey: function (keyInfo) {

};
sig.loadSignature(signature.toString());
return sig.checkSignature(xml);
sig.loadSignature(signature);
// We expect each signature to contain exactly one reference to the top level of the xml we
// are validating, so if we see anything else, reject.
if (sig.references.length != 1 )
return false;
var refUri = sig.references[0].uri;
var refId = (refUri[0] === '#') ? refUri.substring(1) : refUri;
// If we can't find the reference at the top level, reject
if (currentNode.getAttribute('ID') != refId)
return false;
// If we find any extra referenced nodes, reject. (xml-crypto only verifies one digest, so
// multiple candidate references is bad news)
var totalReferencedNodes = xmlCrypto.xpath(currentNode.ownerDocument,
"//*[@ID='" + refId + "']");
if (totalReferencedNodes.length > 1)
return false;
return sig.checkSignature(fullXml);
};
SAML.prototype.getElement = function (parentElement, elementName) {
if (parentElement['saml:' + elementName]) {
return parentElement['saml:' + elementName];
} else if (parentElement['samlp:'+elementName]) {
return parentElement['samlp:'+elementName];
}
return parentElement[elementName];
}
SAML.prototype.validatePostResponse = function (container, callback) {
var self = this;
var xml = new Buffer(container.SAMLResponse, 'base64').toString('ascii');
var doc = new xmldom.DOMParser().parseFromString(xml);
SAML.prototype.validateResponse = function (samlResponse, callback) {
var self = this;
var xml = new Buffer(samlResponse, 'base64').toString('ascii');
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)) {
// Check if this document has a valid top-level signature
var validSignature = false;
if (self.options.cert && self.validateSignature(xml, doc.documentElement, self.options.cert)) {
validSignature = true;
}
var assertions = xmlCrypto.xpath(doc, "/*[local-name()='Response']/*[local-name()='Assertion']");
if (assertions.length > 1) {
// There's no reason I know of that we want to handle multiple assertions, and it seems like a
// potential risk vector for signature scope issues, so treat this as an invalid signature
return callback(new Error('Invalid signature'), null, false);
} else if (assertions.length == 1) {
if (self.options.cert &&
!validSignature &&
!self.validateSignature(xml, assertions[0], self.options.cert)) {
return callback(new Error('Invalid signature'), null, false);
}
return processValidlySignedAssertion(assertions[0].toString(), callback);
}
var response = self.getElement(doc, 'Response');
// If there's no assertion, and there is a top-level signature, fall back on xml2js response
// parsing for the passive status & LogoutResponse code.
if (self.options.cert && !validSignature) {
return callback(new Error('Invalid signature'), null, false);
}
var parserConfig = {
explicitRoot: true,
tagNameProcessors: [xml2js.processors.stripPrefix]
};
var parser = new xml2js.Parser(parserConfig);
parser.parseString(xml, function (err, doc) {
if (err) {
return callback(err, null, false);
}
var response = doc.Response;
if (response) {
var assertion = self.getElement(response, 'Assertion');
var assertion = response.Assertion;
if (!assertion) {
var status = response.Status;
if (status) {
status = status[0].StatusCode;
if (status && status[0].$.Value === "urn:oasis:names:tc:SAML:2.0:status:Responder") {
status = status[0].StatusCode;
if (status && status[0].$.Value === "urn:oasis:names:tc:SAML:2.0:status:NoPassive") {
return callback(null, null, false);
}
}
}
return callback(new Error('Missing SAML assertion'), null, false);
}
profile = {};
var issuer = self.getElement(assertion[0], 'Issuer');
if (issuer) {
profile.issuer = issuer[0];
} else {
var logoutResponse = doc.LogoutResponse;
if (logoutResponse){
callback(null, null, true);
} else {
return callback(new Error('Unknown SAML response message'), null, false);
}
}
});
};
var subject = self.getElement(assertion[0], 'Subject');
if (subject) {
var nameID = self.getElement(subject[0], 'NameID');
if (nameID) {
profile.nameID = nameID[0]["_"];
function processValidlySignedAssertion (xml, callback) {
var parserConfig = {
explicitRoot: true,
tagNameProcessors: [xml2js.processors.stripPrefix]
};
var parser = new xml2js.Parser(parserConfig);
parser.parseString(xml, function (err, doc) {
if (err) {
return callback(err, null, false);
}
if (nameID[0]['$'].Format) {
profile.nameIDFormat = nameID[0]['$'].Format;
}
var assertion = doc.Assertion;
var profile = {};
var issuer = assertion.Issuer;
if (issuer) {
profile.issuer = issuer[0];
}
var subject = assertion.Subject;
if (subject) {
var nameID = 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);
}
var attributeStatement = assertion.AttributeStatement;
if (attributeStatement) {
var attributes = attributeStatement[0].Attribute;
var attributes = self.getElement(attributeStatement[0], 'Attribute');
var attrValueMapper = function(value) {
return typeof value === 'string' ? value : value._;
};
if (attributes) {
attributes.forEach(function (attribute) {
var value = self.getElement(attribute, 'AttributeValue');
if (typeof value[0] === 'string') {
profile[attribute['$'].Name] = value[0];
var value = attribute.AttributeValue;
if (value.length === 1) {
profile[attribute.$.Name] = attrValueMapper(value[0]);
} else {
profile[attribute['$'].Name] = value[0]['_'];
profile[attribute.$.Name] = value.map(attrValueMapper);
}
});
}
}
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.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;
}
if (!profile.email && profile.mail) {
profile.email = profile.mail;
}
callback(null, profile, false);
} else {
var logoutResponse = self.getElement(doc, 'LogoutResponse');
callback(null, profile, false);
});
}
if (logoutResponse){
callback(null, null, true);
} else {
return callback(new Error('Unknown SAML response message'), null, false);
}
SAML.prototype.validatePostRequest = function (container, callback) {
var self = this;
var xml = new Buffer(container.SAMLRequest, 'base64').toString('ascii');
var parserConfig = {
explicitRoot: true,
tagNameProcessors: [xml2js.processors.stripPrefix]
};
var parser = new xml2js.Parser(parserConfig);
parser.parseString(xml, function (err, doc) {
if (err) {
return callback(err, null, false);
}
// Check if this document has a valid top-level signature
if (self.options.cert && !self.validateSignature(xml, self.options.cert)) {
return callback(new Error('Invalid signature'), null, false);
}
processValidlySignedPostRequest(self, doc, callback);
});
};
function processValidlySignedPostRequest(self, doc, callback) {
var request = doc.LogoutRequest;
if (request) {
var profile = {};
if (request.$.ID) {
profile.ID = request.$.ID;
} else {
return callback(new Error('Missing SAML LogoutRequest ID'), null, false);
}
var issuer = request.Issuer;
if (issuer) {
profile.issuer = issuer[0];
} else {
return callback(new Error('Missing SAML issuer'), null, false);
}
var nameID = request.NameID;
if (nameID) {
profile.nameID = nameID[0]._;
if (nameID[0].$.Format) {
profile.nameIDFormat = nameID[0].$.Format;
}
} else {
return callback(new Error('Missing SAML NameID'), null, false);
}
callback(null, profile, true);
} else {
return callback(new Error('Unknown SAML request message'), null, false);
}
}
exports.SAML = SAML;

@@ -27,7 +27,4 @@ var passport = require('passport');

var self = this;
if (req.body && req.body.SAMLResponse) {
// We have a response, get the user identity out of it
var response = req.body.SAMLResponse;
this._saml.validateResponse(response, function (err, profile, loggedOut) {
function validateCallback(err, profile, loggedOut) {
if (err) {

@@ -38,9 +35,8 @@ return self.error(err);

if (loggedOut) {
if (self._saml.options.logoutRedirect) {
self.redirect(self._saml.options.logoutRedirect);
return;
} else {
self.redirect("/");
req.logout();
if (profile) {
req.samlLogoutRequest = profile;
return self._saml.getLogoutResponseUrl(req, redirectIfSuccess);
}
return self.pass();
}

@@ -61,14 +57,30 @@

self._verify(profile, verified);
});
} else {
// Initiate new SAML authentication request
}
this._saml.getAuthorizeUrl(req, function (err, url) {
if (err) {
return self.fail();
}
function redirectIfSuccess(err, url) {
if (err) {
self.fail();
} else {
self.redirect(url);
});
}
}
if (req.body && req.body.SAMLResponse) {
this._saml.validatePostResponse(req.body, validateCallback);
} else if (req.body && req.body.SAMLRequest) {
this._saml.validatePostRequest(req.body, validateCallback);
} else if (options.samlFallback) {
// Initiate fallback redirection
var operation = {
'login-request': 'getAuthorizeUrl',
'logout-request': 'getLogoutUrl'
}[options.samlFallback];
if (!operation) {
return self.fail();
}
this._saml[operation](req, redirectIfSuccess);
} else {
return self.fail();
}
};

@@ -75,0 +87,0 @@

{
"name": "passport-saml",
"version": "0.0.4",
"version": "0.1.0",
"licenses": [{

@@ -16,19 +16,34 @@ "type": "MIT",

"contributors" : [
"Michael Bosworth"
"Michael Bosworth",
"Herbert Vojčík",
"Peter Loer"
],
"repository" : {
"type" : "git",
"url" : "https://github.com/bergie/passport-saml.git"
},
"main": "./lib/passport-saml",
"dependencies": {
"passport": "0.1.x",
"zlib": "1.0.x",
"xml2js": "0.2.0",
"passport": "0.2.x",
"xml2js": "0.4.x",
"xml-crypto": "0.0.x",
"xmldom": "0.1.x"
"xmldom": "0.1.x",
"xmlbuilder": "~2.2"
},
"devDependencies": {
"express": "2.5.x",
"ejs": "0.7.x"
"express": "3.x",
"body-parser": "*",
"ejs": "0.7.x",
"jshint": "*",
"request": "*",
"should": "*",
"mocha": "*"
},
"engines": {
"node": ">= 0.6.0"
"node": ">= 0.8.0"
},
"scripts" : {
"test": "mocha",
"jshint": "./node_modules/.bin/jshint lib"
}
}
SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap

Packages

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc