From d21164af5800b53fc223fd9e4b0ad25249c8bca1 Mon Sep 17 00:00:00 2001 From: Clement Michaud Date: Thu, 19 Jan 2017 01:01:37 +0100 Subject: [PATCH] Validate first factor through a post request --- package.json | 1 + src/lib/authentication.js | 39 --------- src/lib/{ldap_checker.js => ldap.js} | 7 +- src/lib/routes.js | 20 ++--- src/lib/routes/first_factor.js | 30 +++++++ src/lib/server.js | 3 +- test/unitary/routes/test_first_factor.js | 58 +++++++++++++ test/unitary/test_authentication.js | 65 ++++---------- test/unitary/test_ldap_checker.js | 8 +- test/unitary/test_server.js | 105 +++++++++++++---------- 10 files changed, 184 insertions(+), 152 deletions(-) rename src/lib/{ldap_checker.js => ldap.js} (52%) create mode 100644 src/lib/routes/first_factor.js create mode 100644 test/unitary/routes/test_first_factor.js diff --git a/package.json b/package.json index 7fd7ba080..c0d93b5e0 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "url": "https://github.com/clems4ever/two-factor-auth-server/issues" }, "dependencies": { + "bluebird": "^3.4.7", "body-parser": "^1.15.2", "cookie-parser": "^1.4.3", "ejs": "^2.5.5", diff --git a/src/lib/authentication.js b/src/lib/authentication.js index cf8e66f10..ee45da350 100644 --- a/src/lib/authentication.js +++ b/src/lib/authentication.js @@ -1,52 +1,13 @@ module.exports = { - 'authenticate': authenticate, 'verify': verify_authentication } var objectPath = require('object-path'); -var ldap_checker = require('./ldap_checker'); var totp_checker = require('./totp_checker'); var replies = require('./replies'); -var Q = require('q'); var utils = require('./utils'); - -function authenticate(req, res) { - var defer = Q.defer(); - var username = req.body.username; - var password = req.body.password; - var token = req.body.token; - console.log('Start authentication of user %s', username); - - if(!username || !password || !token) { - replies.authentication_failed(res); - return; - } - - var jwt_engine = req.app.get('jwt engine'); - var ldap_client = req.app.get('ldap client'); - var totp_engine = req.app.get('totp engine'); - var config = req.app.get('config'); - - var totp_promise = totp_checker.validate(totp_engine, token, config.totp_secret); - var credentials_promise = ldap_checker.validate(ldap_client, username, password, config.ldap_users_dn); - - Q.all([totp_promise, credentials_promise]) - .then(function() { - var token = jwt_engine.sign({ user: username }, config.jwt_expiration_time); - replies.authentication_succeeded(res, username, token); - console.log('Authentication succeeded'); - defer.resolve(); - }) - .fail(function(err1, err2) { - console.log('Authentication failed', err1, err2); - replies.authentication_failed(res); - defer.reject(); - }); - return defer.promise; -} - function verify_authentication(req, res) { console.log('Verify authentication'); diff --git a/src/lib/ldap_checker.js b/src/lib/ldap.js similarity index 52% rename from src/lib/ldap_checker.js rename to src/lib/ldap.js index 8f416a76a..77a157872 100644 --- a/src/lib/ldap_checker.js +++ b/src/lib/ldap.js @@ -3,12 +3,11 @@ module.exports = { 'validate': validateCredentials } -var Q = require('q'); var util = require('util'); -var utils = require('./utils'); +var Promise = require('bluebird'); function validateCredentials(ldap_client, username, password, users_dn) { - var userDN = util.format("cn=%s,%s", username, users_dn); - var bind_promised = utils.promisify(ldap_client.bind, ldap_client); + var userDN = util.format("binding entry cn=%s,%s", username, users_dn); + var bind_promised = Promise.promisify(ldap_client.bind, ldap_client); return bind_promised(userDN, password); } diff --git a/src/lib/routes.js b/src/lib/routes.js index 49120c9af..ada664bf0 100644 --- a/src/lib/routes.js +++ b/src/lib/routes.js @@ -1,20 +1,18 @@ +var first_factor = require('./routes/first_factor'); + module.exports = { - 'auth': serveAuth, - 'login': serveLogin, - 'logout': serveLogout + auth: serveAuth, + login: serveLogin, + logout: serveLogout, + first_factor: first_factor } var authentication = require('./authentication'); var replies = require('./replies'); function serveAuth(req, res) { - if(req.method == 'POST') { - serveAuthPost(req, res); - } - else { - serveAuthGet(req, res); - } + serveAuthGet(req, res); } function serveAuthGet(req, res) { @@ -28,10 +26,6 @@ function serveAuthGet(req, res) { }); } -function serveAuthPost(req, res) { - authentication.authenticate(req, res); -} - function serveLogin(req, res) { res.render('login'); } diff --git a/src/lib/routes/first_factor.js b/src/lib/routes/first_factor.js new file mode 100644 index 000000000..4c4ca779a --- /dev/null +++ b/src/lib/routes/first_factor.js @@ -0,0 +1,30 @@ + +module.exports = first_factor; + +var ldap = require('../ldap'); + +function first_factor(req, res) { + var username = req.body.username; + var password = req.body.password; + console.log('Start authentication of user %s', username); + + if(!username || !password) { + replies.authentication_failed(res); + return; + } + + var ldap_client = req.app.get('ldap client'); + var config = req.app.get('config'); + + ldap.validate(ldap_client, username, password, config.ldap_users_dn) + .then(function() { + res.status(204); + res.send(); + console.log('LDAP binding successful'); + }) + .error(function(err) { + res.status(401); + res.send(); + console.log('LDAP binding failed:', err); + }); +} diff --git a/src/lib/server.js b/src/lib/server.js index 73c6bb220..526297367 100644 --- a/src/lib/server.js +++ b/src/lib/server.js @@ -33,7 +33,8 @@ function run(config, ldap_client) { app.get ('/logout', routes.logout); app.get ('/_auth', routes.auth); - app.post ('/_auth', routes.auth); + + app.post ('/_auth/1stfactor', routes.first_factor); app.listen(config.port, function(err) { console.log('Listening on %d...', config.port); diff --git a/test/unitary/routes/test_first_factor.js b/test/unitary/routes/test_first_factor.js new file mode 100644 index 000000000..af8018739 --- /dev/null +++ b/test/unitary/routes/test_first_factor.js @@ -0,0 +1,58 @@ + +var sinon = require('sinon'); +var Promise = require('bluebird'); +var assert = require('assert'); +var first_factor = require('../../../src/lib/routes/first_factor'); + +describe('test the first factor validation route', function() { + it('should return status code 204 when LDAP binding succeeds', function() { + return test_first_factor_promised({ error: undefined, data: undefined }, 204); + }); + + it('should return status code 401 when LDAP binding fails', function() { + return test_first_factor_promised({ error: 'ldap failed', data: undefined }, 401); + }); +}); + + +function test_first_factor_promised(bind_params, statusCode) { + return new Promise(function(resolve, reject) { + test_first_factor(bind_params, statusCode, resolve, reject); + }); +} + +function test_first_factor(bind_params, statusCode, resolve, reject) { + var send = sinon.spy(function(data) { + resolve(); + }); + var status = sinon.spy(function(code) { + assert.equal(code, statusCode); + }); + + var bind_mock = sinon.stub().yields(bind_params.error, bind_params.data); + var ldap_interface_mock = { + bind: bind_mock + } + var config = { + ldap_users_dn: 'dc=example,dc=com' + } + + var app_get = sinon.stub(); + app_get.withArgs('ldap client').returns(ldap_interface_mock); + app_get.withArgs('config').returns(ldap_interface_mock); + var req = { + app: { + get: app_get + }, + body: { + username: 'username', + password: 'password' + } + } + var res = { + send: send, + status: status + } + + first_factor(req, res); +} diff --git a/test/unitary/test_authentication.js b/test/unitary/test_authentication.js index 03bea70e4..1c81c89d0 100644 --- a/test/unitary/test_authentication.js +++ b/test/unitary/test_authentication.js @@ -75,59 +75,30 @@ function create_mocks() { } } -describe('test jwt', function() { - describe('test authentication', function() { - it('should authenticate user successfuly', function(done) { - var jwt_token = 'jwt_token'; - var clock = sinon.useFakeTimers(); - var mocks = create_mocks(); - authentication.authenticate(mocks.req, mocks.res) - .then(function() { - clock.restore(); - assert(mocks.res.status.calledWith(200)); - assert(mocks.res.send.calledWith(jwt_token)); - done(); - }) +describe('test authentication token verification', function() { + it('should be already authenticated', function(done) { + var mocks = create_mocks(); + var data = { user: 'username' }; + mocks.req.app.get.withArgs('jwt engine').returns({ + verify: sinon.promise().resolves(data) }); - it('should fail authentication', function(done) { - var clock = sinon.useFakeTimers(); - var mocks = create_mocks(); - mocks.totp.returns('wrong token'); - authentication.authenticate(mocks.req, mocks.res) - .fail(function(err) { - clock.restore(); - done(); - }) + authentication.verify(mocks.req, mocks.res) + .then(function(actual_data) { + assert.equal(actual_data, data); + done(); }); }); - - describe('test verify authentication', function() { - it('should be already authenticated', function(done) { - var mocks = create_mocks(); - var data = { user: 'username' }; - mocks.req.app.get.withArgs('jwt engine').returns({ - verify: sinon.promise().resolves(data) - }); - - authentication.verify(mocks.req, mocks.res) - .then(function(actual_data) { - assert.equal(actual_data, data); - done(); - }); + it('should not be already authenticated', function(done) { + var mocks = create_mocks(); + var data = { user: 'username' }; + mocks.req.app.get.withArgs('jwt engine').returns({ + verify: sinon.promise().rejects('Error with JWT token') }); - - it('should not be already authenticated', function(done) { - var mocks = create_mocks(); - var data = { user: 'username' }; - mocks.req.app.get.withArgs('jwt engine').returns({ - verify: sinon.promise().rejects('Error with JWT token') - }); - return authentication.verify(mocks.req, mocks.res, mocks.args) - .fail(function() { - done(); - }); + return authentication.verify(mocks.req, mocks.res, mocks.args) + .fail(function() { + done(); }); }); }); diff --git a/test/unitary/test_ldap_checker.js b/test/unitary/test_ldap_checker.js index 8dfd65228..9cc023cda 100644 --- a/test/unitary/test_ldap_checker.js +++ b/test/unitary/test_ldap_checker.js @@ -1,5 +1,5 @@ -var ldap_checker = require('../../src/lib/ldap_checker'); +var ldap = require('../../src/lib/ldap'); var sinon = require('sinon'); var sinonPromise = require('sinon-promise'); @@ -17,10 +17,10 @@ function test_validate(bind_mock) { bind: bind_mock } - return ldap_checker.validate(ldap_client_mock, username, password, ldap_url, users_dn); + return ldap.validate(ldap_client_mock, username, password, ldap_url, users_dn); } -describe('test ldap checker', function() { +describe('test ldap validation', function() { it('should bind the user if good credentials provided', function() { var bind_mock = sinon.mock().yields(); return test_validate(bind_mock); @@ -29,7 +29,7 @@ describe('test ldap checker', function() { it('should not bind the user if wrong credentials provided', function() { var bind_mock = sinon.mock().yields('wrong credentials'); var promise = test_validate(bind_mock); - return promise.fail(autoResolving); + return promise.error(autoResolving); }); }); diff --git a/test/unitary/test_server.js b/test/unitary/test_server.js index 18e030e9d..f2c4be57a 100644 --- a/test/unitary/test_server.js +++ b/test/unitary/test_server.js @@ -6,6 +6,9 @@ var request = require('request'); var assert = require('assert'); var speakeasy = require('speakeasy'); var sinon = require('sinon'); +var Promise = require('bluebird'); + +var request = Promise.promisifyAll(request); var BASE_URL = 'http://localhost:8090'; @@ -46,8 +49,8 @@ describe('test the server', function() { test_get_auth(jwt); }); - describe('test POST /_auth', function() { - test_post_auth(jwt); + describe('test POST /_auth/1stfactor', function() { + test_post_auth_1st_factor(); }); }); @@ -95,50 +98,64 @@ function test_get_auth(jwt) { }); } -function test_post_auth() { - it('should return the JWT token when authentication is successful', function(done) { - var clock = sinon.useFakeTimers(); - var real_token = speakeasy.totp({ - secret: 'totp_secret', - encoding: 'base32' - }); - var expectedJwt = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoidGVzdF9vayIsImlhdCI6MCwiZXhwIjozNjAwfQ.ihvaljGjO5h3iSO_h3PkNNSCYeePyB8Hr5lfVZZYyrQ'; - - request.post(BASE_URL + '/_auth', { +function test_post_auth_1st_factor() { + it('should return status code 204 when ldap bind is successful', function() { + request.postAsync(BASE_URL + '/_auth/1stfactor', { form: { - username: 'test_ok', - password: 'password', - token: real_token - } - }, - function (error, response, body) { - if (!error && response.statusCode == 200) { - assert.equal(body, expectedJwt); - clock.restore(); - done(); - } - }); - }); - - it('should return invalid authentication status code', function(done) { - var clock = sinon.useFakeTimers(); - var real_token = speakeasy.totp({ - secret: 'totp_secret', - encoding: 'base32' - }); - var data = { - form: { - username: 'test_nok', - password: 'password', - token: real_token - } - } - - request.post(BASE_URL + '/_auth', data, function (error, response, body) { - if(response.statusCode == 401) { - clock.restore(); - done(); + username: 'username', + password: 'password' } + }) + .then(function(response) { + assert.equal(response.statusCode, 204); + return Promise.resolve(); }); }); } +// function test_post_auth_totp() { +// it('should return the JWT token when authentication is successful', function(done) { +// var clock = sinon.useFakeTimers(); +// var real_token = speakeasy.totp({ +// secret: 'totp_secret', +// encoding: 'base32' +// }); +// var expectedJwt = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoidGVzdF9vayIsImlhdCI6MCwiZXhwIjozNjAwfQ.ihvaljGjO5h3iSO_h3PkNNSCYeePyB8Hr5lfVZZYyrQ'; +// +// request.post(BASE_URL + '/_auth/totp', { +// form: { +// username: 'test_ok', +// password: 'password', +// token: real_token +// } +// }, +// function (error, response, body) { +// if (!error && response.statusCode == 200) { +// assert.equal(body, expectedJwt); +// clock.restore(); +// done(); +// } +// }); +// }); +// +// it('should return invalid authentication status code', function(done) { +// var clock = sinon.useFakeTimers(); +// var real_token = speakeasy.totp({ +// secret: 'totp_secret', +// encoding: 'base32' +// }); +// var data = { +// form: { +// username: 'test_nok', +// password: 'password', +// token: real_token +// } +// } +// +// request.post(BASE_URL + '/_auth/totp', data, function (error, response, body) { +// if(response.statusCode == 401) { +// clock.restore(); +// done(); +// } +// }); +// }); +// }