Validate first factor through a post request

This commit is contained in:
Clement Michaud 2017-01-19 01:01:37 +01:00
parent a1a3da650c
commit d21164af58
10 changed files with 184 additions and 152 deletions

View File

@ -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",

View File

@ -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');

View File

@ -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);
}

View File

@ -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');
}

View File

@ -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);
});
}

View File

@ -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);

View File

@ -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);
}

View File

@ -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();
});
});
});

View File

@ -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);
});
});

View File

@ -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();
// }
// });
// });
// }