passport-saml
Advanced tools
Comparing version 0.0.3 to 0.0.4
@@ -56,3 +56,3 @@ var zlib = require('zlib'); | ||
SAML.prototype.generateRequest = function (req) { | ||
SAML.prototype.generateAuthorizeRequest = function (req) { | ||
var id = "_" + this.generateUniqueID(); | ||
@@ -69,7 +69,10 @@ var instant = this.generateInstant(); | ||
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 + "\">" + | ||
"<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:NameIDPolicy xmlns:samlp=\"urn:oasis:names:tc:SAML:2.0:protocol\" Format=\"" + this.options.identifierFormat + | ||
"\" AllowCreate=\"true\"></samlp:NameIDPolicy>\n"; | ||
} | ||
@@ -85,5 +88,23 @@ | ||
SAML.prototype.getAuthorizeUrl = function (req, callback) { | ||
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; | ||
var request = this.generateRequest(req); | ||
zlib.deflateRaw(request, function(err, buffer) { | ||
@@ -94,5 +115,11 @@ if (err) { | ||
var base64 = buffer.toString('base64'); | ||
var target = self.options.entryPoint + '?' | ||
var base64 = buffer.toString('base64'); | ||
var target = self.options.entryPoint + '?'; | ||
if (operation === 'logout') { | ||
if (self.options.logoutUrl) { | ||
target = self.options.logoutUrl + '?'; | ||
} | ||
} | ||
var samlRequest = { | ||
@@ -110,4 +137,16 @@ SAMLRequest: base64 | ||
}); | ||
} | ||
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) { | ||
@@ -123,3 +162,3 @@ cert = cert.match(/.{1,64}/g).join('\n'); | ||
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 signature = xmlCrypto.xpath.SelectNodes(doc, "//*[local-name(.)='Signature' and namespace-uri(.)='http://www.w3.org/2000/09/xmldsig#']")[0]; | ||
var sig = new xmlCrypto.SignedXml(); | ||
@@ -141,3 +180,5 @@ sig.keyInfoProvider = { | ||
return parentElement['saml:' + elementName]; | ||
} | ||
} else if (parentElement['samlp:'+elementName]) { | ||
return parentElement['samlp:'+elementName]; | ||
} | ||
return parentElement[elementName]; | ||
@@ -149,38 +190,75 @@ } | ||
var xml = new Buffer(samlResponse, 'base64').toString('ascii'); | ||
var parser = new xml2js.Parser(); | ||
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); | ||
return callback(new Error('Invalid signature'), null, false); | ||
} | ||
var assertion = self.getElement(doc, 'Assertion'); | ||
if (!assertion) { | ||
return callback(new Error('Missing SAML assertion'), null); | ||
} | ||
var response = self.getElement(doc, 'Response'); | ||
if (response) { | ||
var assertion = self.getElement(response, 'Assertion'); | ||
if (!assertion) { | ||
return callback(new Error('Missing SAML assertion'), null, false); | ||
} | ||
profile = {}; | ||
profile.issuer = self.getElement(assertion, 'Issuer'); | ||
profile = {}; | ||
var issuer = self.getElement(assertion[0], 'Issuer'); | ||
if (issuer) { | ||
profile.issuer = issuer[0]; | ||
} | ||
var attributeStatement = self.getElement(assertion, 'AttributeStatement'); | ||
var attributes = self.getElement(attributeStatement, 'Attribute'); | ||
attributes.forEach(function (attribute) { | ||
var value = self.getElement(attribute, 'AttributeValue'); | ||
if (typeof value === 'string') { | ||
profile[attribute['@'].Name] = value; | ||
return; | ||
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; | ||
} | ||
} | ||
} | ||
profile[attribute['@'].Name] = value['#']; | ||
}); | ||
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']; | ||
} | ||
var attributeStatement = self.getElement(assertion[0], 'AttributeStatement'); | ||
if (!attributeStatement) { | ||
return callback(new Error('Missing AttributeStatement'), null, false); | ||
} | ||
if (!profile.email && profile.mail) { | ||
profile.email = profile.mail; | ||
var attributes = self.getElement(attributeStatement[0], 'Attribute'); | ||
if (attributes) { | ||
attributes.forEach(function (attribute) { | ||
var value = self.getElement(attribute, 'AttributeValue'); | ||
if (typeof value[0] === 'string') { | ||
profile[attribute['$'].Name] = value[0]; | ||
} else { | ||
profile[attribute['$'].Name] = 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; | ||
} | ||
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); | ||
} | ||
} | ||
callback(null, profile); | ||
}); | ||
@@ -187,0 +265,0 @@ }; |
@@ -31,3 +31,3 @@ var passport = require('passport'); | ||
this._saml.validateResponse(response, function (err, profile) { | ||
this._saml.validateResponse(response, function (err, profile, loggedOut) { | ||
if (err) { | ||
@@ -37,2 +37,12 @@ return self.error(err); | ||
if (loggedOut) { | ||
if (self._saml.options.logoutRedirect) { | ||
self.redirect(self._saml.options.logoutRedirect); | ||
return; | ||
} else { | ||
self.redirect("/"); | ||
} | ||
} | ||
var verified = function (err, user, info) { | ||
@@ -65,2 +75,6 @@ if (err) { | ||
Strategy.prototype.logout = function(req, callback) { | ||
this._saml.getLogoutUrl(req, callback); | ||
}; | ||
module.exports = Strategy; |
{ | ||
"name": "passport-saml", | ||
"version": "0.0.3", | ||
"version": "0.0.4", | ||
"licenses": [{ | ||
@@ -22,3 +22,3 @@ "type": "MIT", | ||
"zlib": "1.0.x", | ||
"xml2js": "0.1.x", | ||
"xml2js": "0.2.0", | ||
"xml-crypto": "0.0.x", | ||
@@ -25,0 +25,0 @@ "xmldom": "0.1.x" |
@@ -6,4 +6,6 @@ Passport-SAML | ||
The code is based on Michael Bosworth's [express-saml](https://github.com/bozzltron/express-saml) library. | ||
The code was originally based on Michael Bosworth's [express-saml](https://github.com/bozzltron/express-saml) library. | ||
Passport-SAML has been tested to work with both [SimpleSAMLphp](http://simplesamlphp.org/) based Identity Providers, and with [Active Directory Federation Services](http://en.wikipedia.org/wiki/Active_Directory_Federation_Services). | ||
## Installation | ||
@@ -64,1 +66,33 @@ | ||
``` | ||
## Security and signatures | ||
Passport-SAML uses the HTTP Redirect Binding for its `AuthnRequest`s, and expects to receive the messages back via the HTTP POST binding. | ||
Authentication requests sent by Passport-SAML can be signed using RSA-SHA1. To sign them you need to provide a private key in the PEM format via the `privateCert` configuration key. For example: | ||
```javascript | ||
privateCert: fs.readFileSync('./cert.pem', 'utf-8') | ||
``` | ||
It is a good idea to validate the incoming SAML Responses. For this, you can provide the Identity Provider's certificate using the `cert` confguration key: | ||
```javascript | ||
cert: 'MIICizCCAfQCCQCY8tKaMc0BMjANBgkqh ... W==' | ||
``` | ||
## Usage with Active Directory Federation Services | ||
Here is a configuration that has been proven to work with ADFS: | ||
```javascript | ||
{ | ||
entryPoint: 'https://ad.example.net/adfs/ls/', | ||
issuer: 'https://your-app.example.net/login/callback', | ||
callbackUrl: 'https://your-app.example.net/login/callback', | ||
cert: 'MIICizCCAfQCCQCY8tKaMc0BMjANBgkqh ... W==', | ||
identifierFormat: null | ||
} | ||
``` | ||
Please note that ADFS needs to have a trust established to your service in order for this to work. |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
20396
383
97
+ Addedxml2js@0.2.0(transitive)
- Removedxml2js@0.1.14(transitive)
Updatedxml2js@0.2.0