diff --git a/Readme.md b/Readme.md index ade837383..2fe153e07 100644 --- a/Readme.md +++ b/Readme.md @@ -294,6 +294,26 @@ To change the HTTP statusCode of the response include it on the fault. The stat If `server.authenticate` is not defined then no authentication will take place. +Asynchronous authentication: +``` javascript + server = soap.listen(...) + server.authenticate = function(security, callback) { + var created, nonce, password, user, token; + token = security.UsernameToken, user = token.Username, + password = token.Password, nonce = token.Nonce, created = token.Created; + + myDatabase.getUser(user, function (err, dbUser) { + if (err || !dbUser) { + callback(false); + return; + } + + callback(password === soap.passwordDigest(nonce, created, dbUser.password)); + }); + }; +``` + +Synchronous authentication: ``` javascript server = soap.listen(...) server.authenticate = function(security) { diff --git a/lib/server.js b/lib/server.js index 92d8f2c06..f6edd2fc1 100644 --- a/lib/server.js +++ b/lib/server.js @@ -234,103 +234,149 @@ Server.prototype._process = function (input, req, callback) { bindings = this.wsdl.definitions.bindings, binding, method, methodName, serviceName, portName, - includeTimestamp = obj.Header && obj.Header.Security && obj.Header.Security.Timestamp; + includeTimestamp = obj.Header && obj.Header.Security && obj.Header.Security.Timestamp, + authenticate = self.authenticate || function defaultAuthenticate() { return true; }; - if (typeof self.authenticate === 'function') { - if (!obj.Header || !obj.Header.Security) { - throw new Error('No security header'); - } - if (!self.authenticate(obj.Header.Security)) { - throw new Error('Invalid username or password'); + function process() { + + if (typeof self.log === 'function') { + self.log("info", "Attempting to bind to " + pathname); } - } - if (typeof self.log === 'function') { - self.log("info", "Attempting to bind to " + pathname); - } + //Avoid Cannot convert undefined or null to object due to Object.keys(body) + //and throw more meaningful error + if (!body) { + throw new Error('Failed to parse the SOAP Message body'); + } - //Avoid Cannot convert undefined or null to object due to Object.keys(body) - //and throw more meaningful error - if (!body) { - throw new Error('Failed to parse the SOAP Message body'); - } + // use port.location and current url to find the right binding + binding = (function () { + var services = self.wsdl.definitions.services; + var firstPort; + var name; + for (name in services) { + serviceName = name; + var service = services[serviceName]; + var ports = service.ports; + for (name in ports) { + portName = name; + var port = ports[portName]; + var portPathname = url.parse(port.location).pathname.replace(/\/$/, ''); + + if (typeof self.log === 'function') { + self.log("info", "Trying " + portName + " from path " + portPathname); + } - // use port.location and current url to find the right binding - binding = (function (self) { - var services = self.wsdl.definitions.services; - var firstPort; - var name; - for (name in services) { - serviceName = name; - var service = services[serviceName]; - var ports = service.ports; - for (name in ports) { - portName = name; - var port = ports[portName]; - var portPathname = url.parse(port.location).pathname.replace(/\/$/, ''); + if (portPathname === pathname) + return port.binding; - if (typeof self.log === 'function') { - self.log("info", "Trying " + portName + " from path " + portPathname); + // The port path is almost always wrong for generated WSDLs + if (!firstPort) { + firstPort = port; + } } + } + return !firstPort ? void 0 : firstPort.binding; + })(); - if (portPathname === pathname) - return port.binding; + if (!binding) { + throw new Error('Failed to bind to WSDL'); + } - // The port path is almost always wrong for generated WSDLs - if (!firstPort) { - firstPort = port; - } + try { + if (binding.style === 'rpc') { + methodName = Object.keys(body)[0]; + + self.emit('request', obj, methodName); + if (headers) + self.emit('headers', headers, methodName); + + self._executeMethod({ + serviceName: serviceName, + portName: portName, + methodName: methodName, + outputName: methodName + 'Response', + args: body[methodName], + headers: headers, + style: 'rpc' + }, req, callback); + } else { + var messageElemName = (Object.keys(body)[0] === 'attributes' ? Object.keys(body)[1] : Object.keys(body)[0]); + var pair = binding.topElements[messageElemName]; + + self.emit('request', obj, pair.methodName); + if (headers) + self.emit('headers', headers, pair.methodName); + + self._executeMethod({ + serviceName: serviceName, + portName: portName, + methodName: pair.methodName, + outputName: pair.outputName, + args: body[messageElemName], + headers: headers, + style: 'document' + }, req, callback, includeTimestamp); } } - return !firstPort ? void 0 : firstPort.binding; - })(this); - - if (!binding) { - throw new Error('Failed to bind to WSDL'); - } + catch (error) { + if (error.Fault !== undefined) { + return self._sendError(error.Fault, callback, includeTimestamp); + } - try { - if (binding.style === 'rpc') { - methodName = Object.keys(body)[0]; - - self.emit('request', obj, methodName); - if (headers) - self.emit('headers', headers, methodName); - - self._executeMethod({ - serviceName: serviceName, - portName: portName, - methodName: methodName, - outputName: methodName + 'Response', - args: body[methodName], - headers: headers, - style: 'rpc' - }, req, callback); - } else { - var messageElemName = (Object.keys(body)[0] === 'attributes' ? Object.keys(body)[1] : Object.keys(body)[0]); - var pair = binding.topElements[messageElemName]; - - self.emit('request', obj, pair.methodName); - if (headers) - self.emit('headers', headers, pair.methodName); - - self._executeMethod({ - serviceName: serviceName, - portName: portName, - methodName: pair.methodName, - outputName: pair.outputName, - args: body[messageElemName], - headers: headers, - style: 'document' - }, req, callback, includeTimestamp); + throw error; } } - catch (error) { - if (error.Fault !== undefined) { - return self._sendError(error.Fault, callback, includeTimestamp); - } - throw error; + // Authentication + + if (typeof authenticate === 'function') { + + var authResultProcessed = false, + processAuthResult = function (authResult) { + + if (!authResultProcessed && (authResult || authResult === false)) { + + authResultProcessed = true; + + if (authResult) { + + try { + process(); + } catch (error) { + + if (error.Fault !== undefined) { + return self._sendError(error.Fault, callback, includeTimestamp); + } + + return self._sendError({ + Code: { + Value: 'SOAP-ENV:Server', + Subcode: { value: 'InternalServerError' } + }, + Reason: { Text: error.toString() }, + statusCode: 500 + }, callback, includeTimestamp); + } + + } else { + + return self._sendError({ + Code: { + Value: 'SOAP-ENV:Client', + Subcode: { value: 'AuthenticationFailure' } + }, + Reason: { Text: 'Invalid username or password' }, + statusCode: 401 + }, callback, includeTimestamp); + } + } + }; + + processAuthResult(authenticate(obj.Header && obj.Header.Security, processAuthResult)); + + } else { + throw new Error('Invalid authenticate function (not a function)'); } }; diff --git a/test/server-authentication-test.js b/test/server-authentication-test.js new file mode 100644 index 000000000..afa62ca49 --- /dev/null +++ b/test/server-authentication-test.js @@ -0,0 +1,148 @@ +"use strict"; + +var fs = require('fs'), + soap = require('..'), + assert = require('assert'), + request = require('request'), + http = require('http'), + lastReqAddress; + +var test = {}; +test.server = null; +test.authenticate = null; +test.authenticateProxy = function authenticate(security, callback) { + return test.authenticate(security, callback); +}; +test.service = { + StockQuoteService: { + StockQuotePort: { + GetLastTradePrice: function(args, cb, soapHeader) { + return { price: 19.56 }; + } + } + } +}; + +describe('SOAP Server', function() { + before(function(done) { + fs.readFile(__dirname + '/wsdl/strict/stockquote.wsdl', 'utf8', function(err, data) { + assert.ifError(err); + test.wsdl = data; + done(); + }); + }); + + beforeEach(function(done) { + test.server = http.createServer(function(req, res) { + res.statusCode = 404; + res.end(); + }); + + test.server.listen(15099, null, null, function() { + test.soapServer = soap.listen(test.server, '/stockquote', test.service, test.wsdl); + test.soapServer.authenticate = test.authenticateProxy; + test.baseUrl = + 'http://' + test.server.address().address + ":" + test.server.address().port; + + //windows return 0.0.0.0 as address and that is not + //valid to use in a request + if (test.server.address().address === '0.0.0.0' || test.server.address().address === '::') { + test.baseUrl = + 'http://127.0.0.1:' + test.server.address().port; + } + + done(); + }); + }); + + afterEach(function(done) { + test.server.close(function() { + test.server = null; + test.authenticate = null; + delete test.soapServer; + test.soapServer = null; + done(); + }); + }); + + it('should succeed on valid synchronous authentication', function(done) { + test.authenticate = function(security, callback) { + setTimeout(function delayed() { + callback(false); // Ignored + }, 10); + return true; + }; + + soap.createClient(test.baseUrl + '/stockquote?wsdl', function(err, client) { + assert.ifError(err); + + client.GetLastTradePrice({ tickerSymbol: 'AAPL'}, function(err, result) { + assert.ifError(err); + assert.equal(19.56, parseFloat(result.price)); + done(); + }); + }); + }); + + it('should succeed on valid asynchronous authentication', function(done) { + test.authenticate = function(security, callback) { + setTimeout(function delayed() { + callback(true); + }, 10); + return null; // Ignored + }; + + soap.createClient(test.baseUrl + '/stockquote?wsdl', function(err, client) { + assert.ifError(err); + + client.GetLastTradePrice({ tickerSymbol: 'AAPL'}, function(err, result) { + assert.ifError(err); + assert.equal(19.56, parseFloat(result.price)); + done(); + }); + }); + }); + + it('should fail on invalid synchronous authentication', function(done) { + test.authenticate = function(security, callback) { + setTimeout(function delayed() { + callback(true); // Ignored + }, 10); + return false; + }; + + soap.createClient(test.baseUrl + '/stockquote?wsdl', function(err, client) { + assert.ifError(err); + + client.GetLastTradePrice({ tickerSymbol: 'AAPL'}, function (err, result) { + assert.ok(err); + assert.ok(err.root.Envelope.Body.Fault.Code.Value); + assert.equal(err.root.Envelope.Body.Fault.Code.Value, 'SOAP-ENV:Client'); + assert.ok(err.root.Envelope.Body.Fault.Code.Subcode.value); + assert.equal(err.root.Envelope.Body.Fault.Code.Subcode.value, 'AuthenticationFailure'); + done(); + }); + }); + }); + + it('should fail on invalid asynchronous authentication', function(done) { + test.authenticate = function(security, callback) { + setTimeout(function delayed() { + callback(false); + }, 10); + }; + + soap.createClient(test.baseUrl + '/stockquote?wsdl', function(err, client) { + assert.ifError(err); + + client.GetLastTradePrice({ tickerSymbol: 'AAPL'}, function (err, result) { + assert.ok(err); + assert.ok(err.root.Envelope.Body.Fault.Code.Value); + assert.equal(err.root.Envelope.Body.Fault.Code.Value, 'SOAP-ENV:Client'); + assert.ok(err.root.Envelope.Body.Fault.Code.Subcode.value); + assert.equal(err.root.Envelope.Body.Fault.Code.Subcode.value, 'AuthenticationFailure'); + done(); + }); + }); + }); +});