SAML = function (options) {
var self = this;
this.options = this.initialize(options);
this.cacheProvider = this.options.cacheProvider;
}...
}
this.name = 'saml';
passport.Strategy.call(this);
this._verify = verify;
this._saml = new saml.SAML(options);
this._passReqToCallback = !!options.passReqToCallback;
this._authnRequestBinding = options.authnRequestBinding || 'HTTP-Redirect';
}
util.inherits(Strategy, passport.Strategy);
Strategy.prototype.authenticate = function (req, options) {
...function Strategy(options, verify) {
if (typeof options == 'function') {
verify = options;
options = {};
}
if (!verify) {
throw new Error('SAML authentication strategy requires a verify function');
}
this.name = 'saml';
passport.Strategy.call(this);
this._verify = verify;
this._saml = new saml.SAML(options);
this._passReqToCallback = !!options.passReqToCallback;
this._authnRequestBinding = options.authnRequestBinding || 'HTTP-Redirect';
}n/a
function Strategy() {
}n/a
SAML = function (options) {
var self = this;
this.options = this.initialize(options);
this.cacheProvider = this.options.cacheProvider;
}...
}
this.name = 'saml';
passport.Strategy.call(this);
this._verify = verify;
this._saml = new saml.SAML(options);
this._passReqToCallback = !!options.passReqToCallback;
this._authnRequestBinding = options.authnRequestBinding || 'HTTP-Redirect';
}
util.inherits(Strategy, passport.Strategy);
Strategy.prototype.authenticate = function (req, options) {
...certToPEM = function (cert) {
cert = cert.match(/.{1,64}/g).join('\n');
if (cert.indexOf('-BEGIN CERTIFICATE-') === -1)
cert = "-----BEGIN CERTIFICATE-----\n" + cert;
if (cert.indexOf('-END CERTIFICATE-') === -1)
cert = cert + "\n-----END CERTIFICATE-----\n";
return cert;
}...
it('#certToPEM should generate valid certificate', function(done){
var samlConfig = {
entryPoint: 'https://app.onelogin.com/trust/saml2/http-post/sso/371755',
cert: '-----BEGIN CERTIFICATE-----MIIEFzCCAv+gAwIBAgIUFJsUjPM7AmWvNtEvULSHlTTMiLQwDQYJKoZIhvcNAQEFBQAwWDELMAkGA1UEBhMCVVMxETAPBgNVBAoMCFN1YnNwYWNlMRUwEwYDVQQLDAxPbmVMb2dpbiBJZFAxHzAdBgNVBAMMFk9uZUxvZ2luIEFjY291bnQgNDIzNDkwHhcNMTQwNTEzMTgwNjEyWhcNMTkwNTE0MTgwNjEyWjBYMQswCQYDVQQGEwJVUzERMA8GA1UECgwIU3Vic3BhY2UxFTATBgNVBAsMDE9uZUxvZ2luIElkUDEfMB0GA1UEAwwWT25lTG9naW4gQWNjb3VudCA0MjM0OTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKrAzJdY9FzFLt5blArJfPzgi87EnFGlTfcV5T1TUDwLBlDkY
/0ZGKnMOpf3D7ie2C4pPFOImOogcM5kpDDL7qxTXZ1ewXVyjBdMu29NG2C6NzWeQTUMUji01EcHkC8o+Pts8ANiNOYcjxEeyhEyzJKgEizblYzMMKzdrOET6QuqWo3C83K
+5+5dsjDn1ooKGRwj3HvgsYcFrQl9NojgQFjoobwsiE/7A+OJhLpBcy/nSVgnoJaMfrO+JsnukZPztbntLvOl56+Vra0N8n5NAYhaSayPiv/ayhjVgjfXd1tjMVTOiDknUOwizZuJ1Y3QH94vUtBgp0WBpBSs
/xMyTs8CAwEAAaOB2DCB1TAMBgNVHRMBAf8EAjAAMB0GA1UdDgQWBBRQO4WpM5fWwxib49WTuJkfYDbxODCBlQYDVR0jBIGNMIGKgBRQO4WpM5fWwxib49WTuJkfYDbxOKFcpFowWDELMAkGA1UEBhMCVVMxETAPBgNVBAoMCFN1YnNwYWNlMRUwEwYDVQQLDAxPbmVMb2dpbiBJZFAxHzAdBgNVBAMMFk9uZUxvZ2luIEFjY291bnQgNDIzNDmCFBSbFIzzOwJlrzbRL1C0h5U0zIi0MA4GA1UdDwEB
/wQEAwIHgDANBgkqhkiG9w0BAQUFAAOCAQEACdDAAoaZFCEY5pmfwbKuKrXtO5iE8lWtiCPjCZEUuT6bXRNcqrdnuV/EAfX9WQoXjalPi0eM78zKmbvRGSTUHwWw49RHjFfeJUKvHNeNnFgTXDjEPNhMvh69kHm453lFRmB
+kk6yjtXRZaQEwS8Uuo2Ot+krgNbl6oTBZJ0AHH1MtZECDloms1Km7zsK8wAi5i8TVIKkVr5b2VlhrLgFMvzZ5ViAxIMGB6w47yY4QGQB/5Q8ya9hBs9vkn+wubA+yr4j14JXZ7blVKDSTYva65Ea
+PqHyrp+Wnmnbw2ObS7iWexiTy1jD3G0R2avDBFjM8Fj5DbfufsE1b0U10RTtg==-----END CERTIFICATE-----',
acceptedClockSkewMs: -1
};
var samlObj = new SAML( samlConfig );
var certificate = samlObj.certToPEM(samlConfig.cert);
if (certificate.match(/BEGIN/g).length == 1
&& certificate.match(/END/g).length == 1){
done();
} else {
done('Certificate should have only 1 BEGIN and 1 END block');
}
...checkTimestampsValidityError = function (nowMs, notBefore, notOnOrAfter) {
var self = this;
if (self.options.acceptedClockSkewMs == -1)
return null;
if (notBefore) {
var notBeforeMs = Date.parse(notBefore);
if (nowMs + self.options.acceptedClockSkewMs < notBeforeMs)
return new Error('SAML assertion not yet valid');
}
if (notOnOrAfter) {
var notOnOrAfterMs = Date.parse(notOnOrAfter);
if (nowMs - self.options.acceptedClockSkewMs >= notOnOrAfterMs)
return new Error('SAML assertion expired');
}
return null;
}...
}
if (subjectConfirmation) {
if (confirmData && confirmData.$) {
var subjectNotBefore = confirmData.$.NotBefore;
var subjectNotOnOrAfter = confirmData.$.NotOnOrAfter;
var subjErr = self.checkTimestampsValidityError(
nowMs, subjectNotBefore, subjectNotOnOrAfter);
if (subjErr) {
throw subjErr;
}
}
}
}
...generateAuthorizeRequest = function (req, isPassive, callback) {
var self = this;
var id = "_" + self.generateUniqueID();
var instant = self.generateInstant();
var forceAuthn = self.options.forceAuthn || false;
Q.fcall(function() {
if(self.options.validateInResponseTo) {
return Q.ninvoke(self.cacheProvider, 'save', id, instant);
} else {
return Q();
}
})
.then(function(){
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': self.getCallbackUrl(req),
'@Destination': self.options.entryPoint,
'saml:Issuer' : {
'@xmlns:saml' : 'urn:oasis:names:tc:SAML:2.0:assertion',
'#text': self.options.issuer
}
}
};
if (isPassive)
request['samlp:AuthnRequest']['@IsPassive'] = true;
if (forceAuthn) {
request['samlp:AuthnRequest']['@ForceAuthn'] = true;
}
if (self.options.identifierFormat) {
request['samlp:AuthnRequest']['samlp:NameIDPolicy'] = {
'@xmlns:samlp': 'urn:oasis:names:tc:SAML:2.0:protocol',
'@Format': self.options.identifierFormat,
'@AllowCreate': 'true'
};
}
if (!self.options.disableRequestedAuthnContext) {
request['samlp:AuthnRequest']['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': self.options.authnContext
}
};
}
if (self.options.attributeConsumingServiceIndex) {
request['samlp:AuthnRequest']['@AttributeConsumingServiceIndex'] = self.options.attributeConsumingServiceIndex;
}
callback(null, xmlbuilder.create(request).end());
})
.fail(function(err){
callback(err);
})
.done();
}...
});
return additionalParams;
};
SAML.prototype.getAuthorizeUrl = function (req, callback) {
var self = this;
self.generateAuthorizeRequest(req, self.options.passive, function(err, request){
if (err)
return callback(err);
var operation = 'authorize';
self.requestToUrl(request, null, operation, self.getAdditionalParams(req, operation), callback);
});
};
...generateInstant = function () {
return new Date().toISOString();
}...
signer.update(querystring.stringify(samlMessageToSign));
samlMessage.Signature = signer.sign(this.options.privateCert, 'base64');
};
SAML.prototype.generateAuthorizeRequest = function (req, isPassive, callback) {
var self = this;
var id = "_" + self.generateUniqueID();
var instant = self.generateInstant();
var forceAuthn = self.options.forceAuthn || false;
Q.fcall(function() {
if(self.options.validateInResponseTo) {
return Q.ninvoke(self.cacheProvider, 'save', id, instant);
} else {
return Q();
...generateLogoutRequest = function (req) {
var id = "_" + this.generateUniqueID();
var instant = this.generateInstant();
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.logoutUrl,
'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
}
}
};
if (typeof(req.user.nameQualifier) !== 'undefined') {
request['samlp:LogoutRequest']['saml:NameID']['@NameQualifier'] = req.user.nameQualifier;
}
if (typeof(req.user.spNameQualifier) !== 'undefined') {
request['samlp:LogoutRequest']['saml:NameID']['@SPNameQualifier'] = req.user.spNameQualifier;
}
if (req.user.sessionIndex) {
request['samlp:LogoutRequest']['saml2p:SessionIndex'] = {
'@xmlns:saml2p': 'urn:oasis:names:tc:SAML:2.0:protocol',
'#text': req.user.sessionIndex
};
}
return xmlbuilder.create(request).end();
}...
Destination: 'foo' },
'saml:Issuer':
[ { _: 'onelogin_saml',
'$': { 'xmlns:saml': 'urn:oasis:names:tc:SAML:2.0:assertion' } } ],
'saml:NameID': [ { _: 'bar', '$': { Format: 'foo' } } ] } };
var samlObj = new SAML( { entryPoint: "foo" } );
var logoutRequest = samlObj.generateLogoutRequest({
user: {
nameIDFormat: 'foo',
nameID: 'bar'
}
});
parseString( logoutRequest, function( err, doc ) {
delete doc['samlp:LogoutRequest']['$']["ID"];
...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,
'@Destination': this.options.logoutUrl,
'@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();
}...
//IssueInstant: '2014-05-29T01:11:32Z',
Destination: 'foo',
InResponseTo: 'quux' },
'saml:Issuer': [ 'onelogin_saml' ],
'samlp:Status': [ { 'samlp:StatusCode': [ { '$': { Value: 'urn:oasis:names:tc:SAML:2.0
:status:Success' } } ] } ] } };
var samlObj = new SAML( { entryPoint: "foo" } );
var logoutRequest = samlObj.generateLogoutResponse({}, { ID: "quux" });
parseString( logoutRequest, function( err, doc ) {
delete doc['samlp:LogoutResponse']['$']["ID"];
delete doc['samlp:LogoutResponse']['$']["IssueInstant"];
doc.should.eql( expectedResponse );
done();
});
});
...generateServiceProviderMetadata = function ( decryptionCert ) {
var metadata = {
'EntityDescriptor' : {
'@xmlns': 'urn:oasis:names:tc:SAML:2.0:metadata',
'@xmlns:ds': 'http://www.w3.org/2000/09/xmldsig#',
'@entityID': this.options.issuer,
'@ID': this.options.issuer.replace(/\W/g, '_'),
'SPSSODescriptor' : {
'@protocolSupportEnumeration': 'urn:oasis:names:tc:SAML:2.0:protocol',
},
}
};
if (this.options.decryptionPvk) {
if (!decryptionCert) {
throw new Error(
"Missing decryptionCert while generating metadata for decrypting service provider");
}
decryptionCert = decryptionCert.replace( /-+BEGIN CERTIFICATE-+\r?\n?/, '' );
decryptionCert = decryptionCert.replace( /-+END CERTIFICATE-+\r?\n?/, '' );
decryptionCert = decryptionCert.replace( /\r\n/g, '\n' );
metadata.EntityDescriptor.SPSSODescriptor.KeyDescriptor = {
'ds:KeyInfo' : {
'ds:X509Data' : {
'ds:X509Certificate': {
'#text': decryptionCert
}
}
},
'#list' : [
// this should be the set that the xmlenc library supports
{ 'EncryptionMethod': { '@Algorithm': 'http://www.w3.org/2001/04/xmlenc#aes256-cbc' } },
{ 'EncryptionMethod': { '@Algorithm': 'http://www.w3.org/2001/04/xmlenc#aes128-cbc' } },
{ 'EncryptionMethod': { '@Algorithm': 'http://www.w3.org/2001/04/xmlenc#tripledes-cbc' } },
]
};
}
if (this.options.logoutCallbackUrl) {
metadata.EntityDescriptor.SPSSODescriptor.SingleLogoutService = {
'@Binding': 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST',
'@Location': this.options.logoutCallbackUrl
};
}
metadata.EntityDescriptor.SPSSODescriptor.NameIDFormat = this.options.identifierFormat;
metadata.EntityDescriptor.SPSSODescriptor.AssertionConsumerService = {
'@index': '1',
'@isDefault': 'true',
'@Binding': 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST',
'@Location': this.getCallbackUrl({})
};
return xmlbuilder.create(metadata).end({ pretty: true, indent: ' ', newline: '\n' });
}...
});
});
describe( 'generateServiceProviderMetadata tests /', function() {
function testMetadata( samlConfig, expectedMetadata ) {
var samlObj = new SAML( samlConfig );
var decryptionCert = fs.readFileSync(__dirname + '/static/testshib encryption cert.pem', 'utf-8');
var metadata = samlObj.generateServiceProviderMetadata( decryptionCert );
// splits are to get a nice diff if they don't match for some reason
metadata.split( '\n' ).should.eql( expectedMetadata.split( '\n' ) );
// verify that we are exposed through Strategy as well
var strategy = new SamlStrategy( samlConfig, function() {} );
metadata = strategy.generateServiceProviderMetadata( decryptionCert );
metadata.split( '\n' ).should.eql( expectedMetadata.split( '\n' ) );
...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;
}...
}
signer.update(querystring.stringify(samlMessageToSign));
samlMessage.Signature = signer.sign(this.options.privateCert, 'base64');
};
SAML.prototype.generateAuthorizeRequest = function (req, isPassive, callback) {
var self = this;
var id = "_" + self.generateUniqueID();
var instant = self.generateInstant();
var forceAuthn = self.options.forceAuthn || false;
Q.fcall(function() {
if(self.options.validateInResponseTo) {
return Q.ninvoke(self.cacheProvider, 'save', id, instant);
} else {
...getAdditionalParams = function (req, operation) {
var additionalParams = {};
var RelayState = req.query && req.query.RelayState || req.body && req.body.RelayState;
if (RelayState) {
additionalParams.RelayState = RelayState;
}
var optionsAdditionalParams = this.options.additionalParams || {};
Object.keys(optionsAdditionalParams).forEach(function(k) {
additionalParams[k] = optionsAdditionalParams[k];
});
var optionsAdditionalParamsForThisOperation = {};
if (operation == "authorize") {
optionsAdditionalParamsForThisOperation = this.options.additionalAuthorizeParams || {};
}
if (operation == "logout") {
optionsAdditionalParamsForThisOperation = this.options.additionalLogoutParams || {};
}
Object.keys(optionsAdditionalParamsForThisOperation).forEach(function(k) {
additionalParams[k] = optionsAdditionalParamsForThisOperation[k];
});
return additionalParams;
}...
it ( 'should not pass any additional params by default', function( done ) {
var samlConfig = {
entryPoint: 'https://app.onelogin.com/trust/saml2/http-post/sso/371755',
};
var samlObj = new SAML( samlConfig );
['logout', 'authorize'].forEach( function( operation ) {
var additionalParams = samlObj.getAdditionalParams({}, operation);
additionalParams.should.be.empty
});
done();
});
it ( 'should not pass any additional params by default apart from the RelayState in request query', function( done ) {
...getAuthorizeForm = function (req, callback) {
var self = this;
// The quoteattr() function is used in a context, where the result will not be evaluated by javascript
// but must be interpreted by an XML or HTML parser, and it must absolutely avoid breaking the syntax
// of an element attribute.
var quoteattr = function(s, preserveCR) {
preserveCR = preserveCR ? ' ' : '\n';
return ('' + s) // Forces the conversion to string.
.replace(/&/g, '&') // This MUST be the 1st replacement.
.replace(/'/g, ''') // The 4 other predefined entities, required.
.replace(/"/g, '"')
.replace(/</g, '<')
.replace(/>/g, '>')
// Add other replacements here for HTML only
// Or for XML, only if the named entities are defined in its DTD.
.replace(/\r\n/g, preserveCR) // Must be before the next replacement.
.replace(/[\r\n]/g, preserveCR);
};
var getAuthorizeFormHelper = function(err, buffer) {
if (err) {
return callback(err);
}
var operation = 'authorize';
var additionalParameters = self.getAdditionalParams(req, operation);
var samlMessage = {
SAMLRequest: buffer.toString('base64')
};
Object.keys(additionalParameters).forEach(function(k) {
samlMessage[k] = additionalParameters[k] || '';
});
var formInputs = Object.keys(samlMessage).map(function(k) {
return '<input type="hidden" name="' + k + '" value="' + quoteattr(samlMessage[k]) + '" />';
}).join('\r\n');
callback(null, [
'<!DOCTYPE html>',
'<html>',
'<head>',
'<meta charset="utf-8">',
'<meta http-equiv="x-ua-compatible" content="ie=edge">',
'</head>',
'<body onload="document.forms[0].submit()">',
'<noscript>',
'<p><strong>Note:</strong> Since your browser does not support JavaScript, you must press the button below once to proceed
.</p>',
'</noscript>',
'<form method="post" action="' + encodeURI(self.options.entryPoint) + '">',
formInputs,
'<input type="submit" value="Submit" />',
'</form>',
'<script>document.forms[0].style.display="none";</script>', // Hide the form if JavaScript is enabled
'</body>',
'</html>'
].join('\r\n'));
};
self.generateAuthorizeRequest(req, self.options.passive, function(err, request) {
if (err) {
return callback(err);
}
if (self.options.skipRequestCompression) {
getAuthorizeFormHelper(null, new Buffer(request, 'utf8'));
} else {
zlib.deflateRaw(request, getAuthorizeFormHelper);
}
});
}...
this._saml.validatePostResponse(req.body, validateCallback);
} else if (req.body && req.body.SAMLRequest) {
this._saml.validatePostRequest(req.body, validateCallback);
} else {
var requestHandler = {
'login-request': function() {
if (self._authnRequestBinding === 'HTTP-POST') {
this._saml.getAuthorizeForm(req, function(err, data) {
if (err) {
self.error(err);
} else {
var res = req.res;
res.send(data);
}
});
...getAuthorizeUrl = function (req, callback) {
var self = this;
self.generateAuthorizeRequest(req, self.options.passive, function(err, request){
if (err)
return callback(err);
var operation = 'authorize';
self.requestToUrl(request, null, operation, self.getAdditionalParams(req, operation), callback);
});
}...
protocol: 'https',
headers: {
host: 'examplesp.com'
}
}
});
it('calls callback with right host', function(done) {
saml.getAuthorizeUrl(req, function(err, target) {
url.parse(target).host.should.equal('exampleidp.com');
done();
});
});
it('calls callback with right protocol', function(done) {
saml.getAuthorizeUrl(req, function(err, target) {
url.parse(target).protocol.should.equal('https:');
...getCallbackUrl = function (req) {
// Post-auth destination
if (this.options.callbackUrl) {
return this.options.callbackUrl;
} else {
var host;
if (req.headers) {
host = req.headers.host;
} else {
host = this.options.host;
}
return this.getProtocol(req) + 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': self.getCallbackUrl(req),
'@Destination': self.options.entryPoint,
'saml:Issuer' : {
'@xmlns:saml' : 'urn:oasis:names:tc:SAML:2.0:assertion',
'#text': self.options.issuer
}
}
};
...getLogoutResponseUrl = function (req, callback) {
var response = this.generateLogoutResponse(req, req.samlLogoutRequest);
var operation = 'logout';
this.requestToUrl(null, response, operation, this.getAdditionalParams(req, operation), callback);
}...
return self.error(err);
}
if (loggedOut) {
req.logout();
if (profile) {
req.samlLogoutRequest = profile;
return self._saml.getLogoutResponseUrl(req, redirectIfSuccess);
}
return self.pass();
}
var verified = function (err, user, info) {
if (err) {
return self.error(err);
...getLogoutUrl = function (req, callback) {
var request = this.generateLogoutRequest(req);
var operation = 'logout';
this.requestToUrl(request, null, operation, this.getAdditionalParams(req, operation), callback);
}...
}
});
} else { // Defaults to HTTP-Redirect
this._saml.getAuthorizeUrl(req, redirectIfSuccess);
}
}.bind(self),
'logout-request': function() {
this._saml.getLogoutUrl(req, redirectIfSuccess);
}.bind(self)
}[options.samlFallback];
if (typeof requestHandler !== 'function') {
return self.fail();
}
...getProtocol = function (req) {
return this.options.protocol || (req.protocol || 'http').concat('://');
}...
} else {
var host;
if (req.headers) {
host = req.headers.host;
} else {
host = this.options.host;
}
return this.getProtocol(req) + host + this.options.path;
}
};
SAML.prototype.generateUniqueID = function () {
var chars = "abcdef0123456789";
var uniqueID = "";
for (var i = 0; i < 20; i++) {
...initialize = function (options) {
if (!options) {
options = {};
}
if (!options.path) {
options.path = '/saml/consume';
}
if (!options.host) {
options.host = 'localhost';
}
if (!options.issuer) {
options.issuer = 'onelogin_saml';
}
if (options.identifierFormat === undefined) {
options.identifierFormat = "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress";
}
if (options.authnContext === undefined) {
options.authnContext = "urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport";
}
if (!options.acceptedClockSkewMs) {
// default to no skew
options.acceptedClockSkewMs = 0;
}
if(!options.validateInResponseTo){
options.validateInResponseTo = false;
}
if(!options.requestIdExpirationPeriodMs){
options.requestIdExpirationPeriodMs = 28800000; // 8 hours
}
if(!options.cacheProvider){
options.cacheProvider = new InMemoryCacheProvider(
{keyExpirationPeriodMs: options.requestIdExpirationPeriodMs });
}
if (!options.logoutUrl) {
// Default to Entry Point
options.logoutUrl = options.entryPoint || '';
}
// sha1 or sha256
if (!options.signatureAlgorithm) {
options.signatureAlgorithm = 'sha1';
}
return options;
}...
var server;
function testForCheck(check) {
return function (done) {
var pp = new passport.Authenticator();
var app = express();
app.use(bodyParser.urlencoded({extended: false}));
app.use(pp.initialize());
var config = check.config;
config.callbackUrl = 'http://localhost:3033/login';
var profile = null;
pp.use(new SamlStrategy(config, function (_profile, done) {
profile = _profile;
done(null, { id: profile.nameID });
})
...processValidlySignedAssertion = function (xml, inResponseTo, callback) {
var self = this;
var msg;
var parserConfig = {
explicitRoot: true,
tagNameProcessors: [xml2js.processors.stripPrefix]
};
var nowMs = new Date().getTime();
var profile = {};
var assertion;
var parser = new xml2js.Parser(parserConfig);
Q.ninvoke(parser, 'parseString', xml)
.then(function(doc) {
assertion = doc.Assertion;
var issuer = assertion.Issuer;
if (issuer) {
profile.issuer = issuer[0];
}
var authnStatement = assertion.AuthnStatement;
if (authnStatement) {
if (authnStatement[0].$ && authnStatement[0].$.SessionIndex) {
profile.sessionIndex = authnStatement[0].$.SessionIndex;
}
}
var subject = assertion.Subject;
var subjectConfirmation, confirmData;
if (subject) {
var nameID = subject[0].NameID;
if (nameID) {
profile.nameID = nameID[0]._ || nameID[0];
if (nameID[0].$ && nameID[0].$.Format) {
profile.nameIDFormat = nameID[0].$.Format;
profile.nameQualifier = nameID[0].$.NameQualifier;
profile.spNameQualifier = nameID[0].$.SPNameQualifier;
}
}
subjectConfirmation = subject[0].SubjectConfirmation ?
subject[0].SubjectConfirmation[0] : null;
confirmData = subjectConfirmation && subjectConfirmation.SubjectConfirmationData ?
subjectConfirmation.SubjectConfirmationData[0] : null;
if (subject[0].SubjectConfirmation && subject[0].SubjectConfirmation.length > 1) {
msg = 'Unable to process multiple SubjectConfirmations in SAML assertion';
throw new Error(msg);
}
if (subjectConfirmation) {
if (confirmData && confirmData.$) {
var subjectNotBefore = confirmData.$.NotBefore;
var subjectNotOnOrAfter = confirmData.$.NotOnOrAfter;
var subjErr = self.checkTimestampsValidityError(
nowMs, subjectNotBefore, subjectNotOnOrAfter);
if (subjErr) {
throw subjErr;
}
}
}
}
// Test to see that if we have a SubjectConfirmation InResponseTo that it matches
// the 'InResponseTo' attribute set in the Response
if (self.options.validateInResponseTo) {
if (subjectConfirmation) {
if (confirmData && confirmData.$) {
var subjectInResponseTo = confirmData.$.InResponseTo;
if (inResponseTo && subjectInResponseTo && subjectInResponseTo != inResponseTo) {
return Q.ninvoke(self.cacheProvider, 'remove', inResponseTo)
.then(function(){
throw new Error('InResponseTo is not valid');
});
} else if (subjectInResponseTo) {
var foundValidInResponseTo = false;
return Q.ninvoke(self.cacheProvider, 'get', subjectInResponseTo)
.then(function(result){
if (result) {
var createdAt = new Date(result);
if (nowMs < createdAt.getTime() + self.options.requestIdExpirationPeriodMs)
foundValidInResponseTo = true;
}
return Q.ninvoke(self.cacheProvider, 'remove', inResponseTo );
})
.then(function(){
if (!foundValidInResponseTo) {
throw new Error('InResponseTo is not valid');
}
return Q();
});
}
}
} else {
return Q.ninvoke(self.cacheProvider, 'remove', inResponseTo);
}
} else {
return Q();
}
})
.then(function(){
var conditions = assertion.Conditions ? assertion.Conditions[0] : null;
if (assertion.Conditions && assertion.Conditions.length > 1) {
msg = 'Unable to process multiple conditions in SAML assertion';
throw new Error(msg);
}
if(conditions && conditions.$) {
var conErr = self.checkTimestampsValidityError(
nowMs, conditions.$.NotBefore, conditions.$.NotOnOrAfter);
if(conErr)
throw conErr; ......
if (assertions.length == 1) {
if (self.options.cert &&
!validSignature &&
!self.validateSignature(xml, assertions[0], self.options.cert)) {
throw new Error('Invalid signature');
}
return self.processValidlySignedAssertion(assertions[0].toString(), inResponseTo, callback
);
}
if (encryptedAssertions.length == 1) {
if (!self.options.decryptionPvk)
throw new Error('No decryption key for encrypted SAML response');
var encryptedDatas = xpath( encryptedAssertions[0], "./*[local-name()='EncryptedData']");
...requestToUrl = function (request, response, operation, additionalParameters, callback) {
var self = this;
if (self.options.skipRequestCompression)
requestToUrlHelper(null, new Buffer(request || response, 'utf8'));
else
zlib.deflateRaw(request || response, requestToUrlHelper);
function requestToUrlHelper(err, buffer) {
if (err) {
return callback(err);
}
var base64 = buffer.toString('base64');
var target = url.parse(self.options.entryPoint, true);
if (operation === 'logout') {
if (self.options.logoutUrl) {
target = url.parse(self.options.logoutUrl, true);
}
} else if (operation !== 'authorize') {
return callback(new Error("Unknown operation: "+operation));
}
var samlMessage = request ? {
SAMLRequest: base64
} : {
SAMLResponse: base64
};
Object.keys(additionalParameters).forEach(function(k) {
samlMessage[k] = additionalParameters[k];
});
if (self.options.privateCert) {
// sets .SigAlg and .Signature
self.signRequest(samlMessage);
}
Object.keys(samlMessage).forEach(function(k) {
target.query[k] = samlMessage[k];
});
// Delete 'search' to for pulling query string from 'query'
// https://nodejs.org/api/url.html#url_url_format_urlobj
delete target.search;
callback(null, url.format(target));
}
}...
SAML.prototype.getAuthorizeUrl = function (req, callback) {
var self = this;
self.generateAuthorizeRequest(req, self.options.passive, function(err, request){
if (err)
return callback(err);
var operation = 'authorize';
self.requestToUrl(request, null, operation, self.getAdditionalParams(req, operation
), callback);
});
};
SAML.prototype.getAuthorizeForm = function (req, callback) {
var self = this;
// The quoteattr() function is used in a context, where the result will not be evaluated by javascript
...signRequest = function (samlMessage) {
var signer;
var samlMessageToSign = {};
switch(this.options.signatureAlgorithm) {
case 'sha256':
samlMessage.SigAlg = 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha256';
signer = crypto.createSign('RSA-SHA256');
break;
default:
samlMessage.SigAlg = 'http://www.w3.org/2000/09/xmldsig#rsa-sha1';
signer = crypto.createSign('RSA-SHA1');
break;
}
if (samlMessage.SAMLRequest) {
samlMessageToSign.SAMLRequest = samlMessage.SAMLRequest;
}
if (samlMessage.SAMLResponse) {
samlMessageToSign.SAMLResponse = samlMessage.SAMLResponse;
}
if (samlMessage.RelayState) {
samlMessageToSign.RelayState = samlMessage.RelayState;
}
if (samlMessage.SigAlg) {
samlMessageToSign.SigAlg = samlMessage.SigAlg;
}
signer.update(querystring.stringify(samlMessageToSign));
samlMessage.Signature = signer.sign(this.options.privateCert, 'base64');
}...
};
Object.keys(additionalParameters).forEach(function(k) {
samlMessage[k] = additionalParameters[k];
});
if (self.options.privateCert) {
// sets .SigAlg and .Signature
self.signRequest(samlMessage);
}
Object.keys(samlMessage).forEach(function(k) {
target.query[k] = samlMessage[k];
});
// Delete 'search' to for pulling query string from 'query'
// https://nodejs.org/api/url.html#url_url_format_urlobj
...validatePostRequest = function (container, callback) {
var self = this;
var xml = new Buffer(container.SAMLRequest, 'base64').toString('utf8');
var dom = new xmldom.DOMParser().parseFromString(xml);
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);
}
// Check if this document has a valid top-level signature
if (self.options.cert && !self.validateSignature(xml, dom.documentElement, self.options.cert)) {
return callback(new Error('Invalid signature'));
}
processValidlySignedPostRequest(self, doc, callback);
});
}...
});
});
it('errors if bad xml', function(done) {
var body = {
SAMLRequest: "asdf"
};
samlObj.validatePostRequest(body, function(err) {
should.exist(err);
done();
});
});
it('errors if bad signature', function(done) {
var body = {
SAMLRequest: fs.readFileSync(__dirname + '/static/logout_request_with_bad_signature.xml', 'base64')
...validatePostResponse = function (container, callback) {
var self = this;
var xml = new Buffer(container.SAMLResponse, 'base64').toString('utf8');
var doc = new xmldom.DOMParser().parseFromString(xml);
var inResponseTo = xpath(doc, "/*[local-name()='Response']/@InResponseTo");
if(inResponseTo){
inResponseTo = inResponseTo.length ? inResponseTo[0].nodeValue : null;
}
Q.fcall(function(){
if(self.options.validateInResponseTo){
if (inResponseTo) {
return Q.ninvoke(self.cacheProvider, 'get', inResponseTo)
.then(function(result) {
if (!result)
throw new Error('InResponseTo is not valid');
return Q();
});
}
} else {
return Q();
}
})
.then(function() {
// 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 = xpath(doc, "/*[local-name()='Response']/*[local-name()='Assertion']");
var encryptedAssertions = xpath(doc,
"/*[local-name()='Response']/*[local-name()='EncryptedAssertion']");
if (assertions.length + encryptedAssertions.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
throw new Error('Invalid signature');
}
if (assertions.length == 1) {
if (self.options.cert &&
!validSignature &&
!self.validateSignature(xml, assertions[0], self.options.cert)) {
throw new Error('Invalid signature');
}
return self.processValidlySignedAssertion(assertions[0].toString(), inResponseTo, callback);
}
if (encryptedAssertions.length == 1) {
if (!self.options.decryptionPvk)
throw new Error('No decryption key for encrypted SAML response');
var encryptedDatas = xpath( encryptedAssertions[0], "./*[local-name()='EncryptedData']");
if (encryptedDatas.length != 1)
throw new Error('Invalid signature');
var encryptedDataXml = encryptedDatas[0].toString();
var xmlencOptions = { key: self.options.decryptionPvk };
return Q.ninvoke(xmlenc, 'decrypt', encryptedDataXml, xmlencOptions)
.then(function(decryptedXml) {
var decryptedDoc = new xmldom.DOMParser().parseFromString(decryptedXml);
var decryptedAssertions = xpath(decryptedDoc, "/*[local-name()='Assertion']");
if (decryptedAssertions.length != 1)
throw new Error('Invalid EncryptedAssertion content');
if (self.options.cert &&
!validSignature &&
!self.validateSignature(decryptedXml, decryptedAssertions[0], self.options.cert))
throw new Error('Invalid signature');
self.processValidlySignedAssertion(decryptedAssertions[0].toString(), inResponseTo, callback);
});
}
// If there's no assertion, fall back on xml2js response parsing for the status &
// LogoutResponse code.
var parserConfig = {
explicitRoot: true,
explicitCharkey: true,
tagNameProcessors: [xml2js.processors.stripPrefix]
};
var parser = new xml2js.Parser(parserConfig);
return Q.ninvoke( parser, 'parseString', xml)
.then(function(doc) {
var response = doc.Response;
if (response) {
var assertion = response.Assertion;
if (!assertion) {
var status = response.Status;
if (status) {
var statusCode = status[0].StatusCode;
if (statusCode && statusCode[0].$.Value === "urn:oasis:names:tc:SAML:2.0:status:Responder") {
var nestedStatusCode = statusCode[0].StatusCode;
if (nestedStatusCode && nestedStatusCode[0].$.Value === "urn:oasis:names:tc:SAML:2.0:status:NoPassive") {
if (self.options.cert && !validSignature) {
throw new Error('Invalid signature'); ......
it('response with error status message should generate appropriate error', function(done) {
var xml = '<?xml version="1.0" encoding="UTF-8"?><saml2p:Response xmlns:saml2p="urn
:oasis:names:tc:SAML:2.0:protocol" Destination="http://localhost/browserSamlLogin" ID="_6a377272c8662561acf1056274ef3f81
" InResponseTo="_4324fb0d00661146f7dc" IssueInstant="2014-07-02T18:16:31.278Z" Version="2.0"
x3e;<saml2:Issuer xmlns:saml2="urn:oasis:names:tc:SAML:2.0:assertion" Format="urn:oasis:names:tc:SAML:2.0:nameid
-format:entity">https://idp.testshib.org/idp/shibboleth</saml2:Issuer><saml2p:Status><saml2p:
StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Responder"><saml2p:StatusCode Value="urn:oasis:names
:tc:SAML:2.0:status:InvalidNameIDPolicy"/></saml2p:StatusCode><saml2p:StatusMessage>Required NameID
format not supported</saml2p:StatusMessage></saml2p:Status></saml2p:Response>';
var base64xml = new Buffer( xml ).toString('base64');
var container = { SAMLResponse: base64xml };
var samlObj = new SAML( {
cert: '-----BEGIN CERTIFICATE-----MIIEFzCCAv+gAwIBAgIUFJsUjPM7AmWvNtEvULSHlTTMiLQwDQYJKoZIhvcNAQEFBQAwWDELMAkGA1UEBhMCVVMxETAPBgNVBAoMCFN1YnNwYWNlMRUwEwYDVQQLDAxPbmVMb2dpbiBJZFAxHzAdBgNVBAMMFk9uZUxvZ2luIEFjY291bnQgNDIzNDkwHhcNMTQwNTEzMTgwNjEyWhcNMTkwNTE0MTgwNjEyWjBYMQswCQYDVQQGEwJVUzERMA8GA1UECgwIU3Vic3BhY2UxFTATBgNVBAsMDE9uZUxvZ2luIElkUDEfMB0GA1UEAwwWT25lTG9naW4gQWNjb3VudCA0MjM0OTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKrAzJdY9FzFLt5blArJfPzgi87EnFGlTfcV5T1TUDwLBlDkY
/0ZGKnMOpf3D7ie2C4pPFOImOogcM5kpDDL7qxTXZ1ewXVyjBdMu29NG2C6NzWeQTUMUji01EcHkC8o+Pts8ANiNOYcjxEeyhEyzJKgEizblYzMMKzdrOET6QuqWo3C83K
+5+5dsjDn1ooKGRwj3HvgsYcFrQl9NojgQFjoobwsiE/7A+OJhLpBcy/nSVgnoJaMfrO+JsnukZPztbntLvOl56+Vra0N8n5NAYhaSayPiv/ayhjVgjfXd1tjMVTOiDknUOwizZuJ1Y3QH94vUtBgp0WBpBSs
/xMyTs8CAwEAAaOB2DCB1TAMBgNVHRMBAf8EAjAAMB0GA1UdDgQWBBRQO4WpM5fWwxib49WTuJkfYDbxODCBlQYDVR0jBIGNMIGKgBRQO4WpM5fWwxib49WTuJkfYDbxOKFcpFowWDELMAkGA1UEBhMCVVMxETAPBgNVBAoMCFN1YnNwYWNlMRUwEwYDVQQLDAxPbmVMb2dpbiBJZFAxHzAdBgNVBAMMFk9uZUxvZ2luIEFjY291bnQgNDIzNDmCFBSbFIzzOwJlrzbRL1C0h5U0zIi0MA4GA1UdDwEB
/wQEAwIHgDANBgkqhkiG9w0BAQUFAAOCAQEACdDAAoaZFCEY5pmfwbKuKrXtO5iE8lWtiCPjCZEUuT6bXRNcqrdnuV/EAfX9WQoXjalPi0eM78zKmbvRGSTUHwWw49RHjFfeJUKvHNeNnFgTXDjEPNhMvh69kHm453lFRmB
+kk6yjtXRZaQEwS8Uuo2Ot+krgNbl6oTBZJ0AHH1MtZECDloms1Km7zsK8wAi5i8TVIKkVr5b2VlhrLgFMvzZ5ViAxIMGB6w47yY4QGQB/5Q8ya9hBs9vkn+wubA+yr4j14JXZ7blVKDSTYva65Ea
+PqHyrp+Wnmnbw2ObS7iWexiTy1jD3G0R2avDBFjM8Fj5DbfufsE1b0U10RTtg==-----END CERTIFICATE-----',
});
samlObj.validatePostResponse( container, function( err, profile, logout ) {
should.exist( err );
err.message.should.match( /Responder/ );
err.message.should.match( /Required NameID format not supported/ );
should.exist( err.statusXml );
done();
});
});
...validateSignature = function (fullXml, currentNode, cert) {
var self = this;
var xpathSigQuery = ".//*[local-name(.)='Signature' and " +
"namespace-uri(.)='http://www.w3.org/2000/09/xmldsig#']";
var signatures = 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];
var sig = new xmlCrypto.SignedXml();
sig.keyInfoProvider = {
getKeyInfo: function (key) {
return "<X509Data></X509Data>";
},
getKey: function (keyInfo) {
return self.certToPEM(cert);
}
};
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
var idAttribute = currentNode.getAttribute('ID') ? 'ID' : 'Id';
if (currentNode.getAttribute(idAttribute) != 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 = xpath(currentNode.ownerDocument,
"//*[@" + idAttribute + "='" + refId + "']");
if (totalReferencedNodes.length > 1)
return false;
return sig.checkSignature(fullXml);
}...
} else {
return Q();
}
})
.then(function() {
// 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 = xpath(doc, "/*[local-name()='Response']/*[local-name()='Assertion']");
var encryptedAssertions = xpath(doc,
"/*[local-name()='Response']/*[local-name()='EncryptedAssertion']");
...function Strategy(options, verify) {
if (typeof options == 'function') {
verify = options;
options = {};
}
if (!verify) {
throw new Error('SAML authentication strategy requires a verify function');
}
this.name = 'saml';
passport.Strategy.call(this);
this._verify = verify;
this._saml = new saml.SAML(options);
this._passReqToCallback = !!options.passReqToCallback;
this._authnRequestBinding = options.authnRequestBinding || 'HTTP-Redirect';
}n/a
function Strategy() {
}n/a
authenticate = function (req, options) {
var self = this;
options.samlFallback = options.samlFallback || 'login-request';
function validateCallback(err, profile, loggedOut) {
if (err) {
return self.error(err);
}
if (loggedOut) {
req.logout();
if (profile) {
req.samlLogoutRequest = profile;
return self._saml.getLogoutResponseUrl(req, redirectIfSuccess);
}
return self.pass();
}
var verified = function (err, user, info) {
if (err) {
return self.error(err);
}
if (!user) {
return self.fail(info);
}
self.success(user, info);
};
if (self._passReqToCallback) {
self._verify(req, profile, verified);
} else {
self._verify(profile, verified);
}
}
function redirectIfSuccess(err, url) {
if (err) {
self.error(err);
} 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 {
var requestHandler = {
'login-request': function() {
if (self._authnRequestBinding === 'HTTP-POST') {
this._saml.getAuthorizeForm(req, function(err, data) {
if (err) {
self.error(err);
} else {
var res = req.res;
res.send(data);
}
});
} else { // Defaults to HTTP-Redirect
this._saml.getAuthorizeUrl(req, redirectIfSuccess);
}
}.bind(self),
'logout-request': function() {
this._saml.getLogoutUrl(req, redirectIfSuccess);
}.bind(self)
}[options.samlFallback];
if (typeof requestHandler !== 'function') {
return self.fail();
}
requestHandler();
}
}...
### Provide the authentication callback
You need to provide a route corresponding to the `path` configuration parameter given to the strategy:
```javascript
app.post('/login/callback',
passport.authenticate('saml', { failureRedirect: '/', failureFlash
: true }),
function(req, res) {
res.redirect('/');
}
);
```
### Authenticate requests
...generateServiceProviderMetadata = function ( decryptionCert ) {
return this._saml.generateServiceProviderMetadata( decryptionCert );
}...
});
});
describe( 'generateServiceProviderMetadata tests /', function() {
function testMetadata( samlConfig, expectedMetadata ) {
var samlObj = new SAML( samlConfig );
var decryptionCert = fs.readFileSync(__dirname + '/static/testshib encryption cert.pem', 'utf-8');
var metadata = samlObj.generateServiceProviderMetadata( decryptionCert );
// splits are to get a nice diff if they don't match for some reason
metadata.split( '\n' ).should.eql( expectedMetadata.split( '\n' ) );
// verify that we are exposed through Strategy as well
var strategy = new SamlStrategy( samlConfig, function() {} );
metadata = strategy.generateServiceProviderMetadata( decryptionCert );
metadata.split( '\n' ).should.eql( expectedMetadata.split( '\n' ) );
...logout = function (req, callback) {
this._saml.getLogoutUrl(req, callback);
}...
function validateCallback(err, profile, loggedOut) {
if (err) {
return self.error(err);
}
if (loggedOut) {
req.logout();
if (profile) {
req.samlLogoutRequest = profile;
return self._saml.getLogoutResponseUrl(req, redirectIfSuccess);
}
return self.pass();
}
...function Strategy() {
}n/a
function Strategy() {
}n/a
authenticate = function (req, options) {
throw new Error('Strategy#authenticate must be overridden by subclass');
}...
### Provide the authentication callback
You need to provide a route corresponding to the `path` configuration parameter given to the strategy:
```javascript
app.post('/login/callback',
passport.authenticate('saml', { failureRedirect: '/', failureFlash
: true }),
function(req, res) {
res.redirect('/');
}
);
```
### Authenticate requests
...