From cb98f0454a6b599e98fcdc4b2d9ce22b41069fc7 Mon Sep 17 00:00:00 2001 From: Clement Michaud Date: Sat, 28 Jan 2017 01:32:25 +0100 Subject: [PATCH] Implement authentication regulation --- src/lib/authentication_regulator.js | 35 +++++ src/lib/exceptions.js | 9 +- src/lib/identity_check.js | 13 +- src/lib/ldap.js | 1 - src/lib/routes/deny_not_logged.js | 9 +- src/lib/routes/first_factor.js | 30 ++-- src/lib/routes/reset_password.js | 9 +- src/lib/routes/u2f.js | 1 + src/lib/routes/u2f_register.js | 1 + src/lib/server.js | 14 +- src/lib/user_data_store.js | 24 +++ src/public_html/js/login.js | 10 +- test/integration/test_server.js | 14 +- test/unitary/requests.js | 12 ++ test/unitary/routes/test_deny_not_logged.js | 8 +- test/unitary/routes/test_first_factor.js | 53 ++++--- test/unitary/routes/test_reset_password.js | 3 +- test/unitary/routes/test_u2f.js | 3 +- test/unitary/test_authentication_regulator.js | 70 +++++++++ test/unitary/test_identity_check.js | 1 - test/unitary/test_server.js | 144 +++++++++++++++--- .../test_authentication_audit.js | 69 +++++++++ 22 files changed, 445 insertions(+), 88 deletions(-) create mode 100644 src/lib/authentication_regulator.js create mode 100644 test/unitary/test_authentication_regulator.js create mode 100644 test/unitary/user_data_store/test_authentication_audit.js diff --git a/src/lib/authentication_regulator.js b/src/lib/authentication_regulator.js new file mode 100644 index 00000000..e7e22190 --- /dev/null +++ b/src/lib/authentication_regulator.js @@ -0,0 +1,35 @@ + +module.exports = AuthenticationRegulator; + +var exceptions = require('./exceptions'); +var Promise = require('bluebird'); + +function AuthenticationRegulator(user_data_store, lock_time_in_seconds) { + this._user_data_store = user_data_store; + this._lock_time_in_seconds = lock_time_in_seconds; +} + +// Mark authentication +AuthenticationRegulator.prototype.mark = function(userid, is_success) { + return this._user_data_store.save_authentication_trace(userid, '1stfactor', is_success); +} + +AuthenticationRegulator.prototype.regulate = function(userid) { + var that = this; + return this._user_data_store.get_last_authentication_traces(userid, '1stfactor', false, 3) + .then(function(docs) { + if(docs.length < 3) { + return Promise.resolve(); + } + + var oldest_doc = docs[2]; + var no_lock_min_date = new Date(new Date().getTime() - + that._lock_time_in_seconds * 1000); + + if(oldest_doc.date > no_lock_min_date) { + throw new exceptions.AuthenticationRegulationError(); + } + + return Promise.resolve(); + }); +} diff --git a/src/lib/exceptions.js b/src/lib/exceptions.js index 86f2e20a..7b5421ff 100644 --- a/src/lib/exceptions.js +++ b/src/lib/exceptions.js @@ -3,7 +3,8 @@ module.exports = { LdapSearchError: LdapSearchError, LdapBindError: LdapBindError, IdentityError: IdentityError, - AccessDeniedError: AccessDeniedError + AccessDeniedError: AccessDeniedError, + AuthenticationRegulationError: AuthenticationRegulationError, } function LdapSearchError(message) { @@ -29,3 +30,9 @@ function AccessDeniedError(message) { this.message = (message || ""); } AccessDeniedError.prototype = Object.create(Error.prototype); + +function AuthenticationRegulationError(message) { + this.name = "AuthenticationRegulationError"; + this.message = (message || ""); +} +AuthenticationRegulationError.prototype = Object.create(Error.prototype); diff --git a/src/lib/identity_check.js b/src/lib/identity_check.js index 076aab2c..96d1f9d8 100644 --- a/src/lib/identity_check.js +++ b/src/lib/identity_check.js @@ -61,7 +61,7 @@ function identity_check_get(endpoint, icheck_interface) { res.send(); return; } - + var email_sender = req.app.get('email sender'); var user_data_store = req.app.get('user data store'); var identity_check = new IdentityCheck(user_data_store, email_sender, logger); @@ -102,6 +102,7 @@ function identity_check_post(endpoint, icheck_interface) { .then(function(identity) { email_address = objectPath.get(identity, 'email'); userid = objectPath.get(identity, 'userid'); + if(!(email_address && userid)) { throw new exceptions.IdentityError('Missing user id or email address'); } @@ -112,26 +113,24 @@ function identity_check_post(endpoint, icheck_interface) { email.hook_url = util.format('https://%s%s', req.headers.host, req.headers['x-original-uri']); return identity_check.issue_token(userid, email, undefined, logger); }, function(err) { - throw new exceptions.AccessDeniedError('Access denied'); + throw new exceptions.AccessDeniedError(); }) .then(function() { res.status(204); res.send(); }) .catch(exceptions.IdentityError, function(err) { - logger.error('POST identity_check: %s', err); + logger.error('POST identity_check: IdentityError %s', err); res.status(400); res.send(); - return; }) .catch(exceptions.AccessDeniedError, function(err) { - logger.error('POST identity_check: %s', err); + logger.error('POST identity_check: AccessDeniedError %s', err); res.status(403); res.send(); - return; }) .catch(function(err) { - logger.error('POST identity_check: %s', err); + logger.error('POST identity_check: Error %s', err); res.status(500); res.send(); }); diff --git a/src/lib/ldap.js b/src/lib/ldap.js index 4b30e444..38bf6eb3 100644 --- a/src/lib/ldap.js +++ b/src/lib/ldap.js @@ -14,7 +14,6 @@ var Dovehash = require('dovehash'); function validateCredentials(ldap_client, username, password, users_dn) { var userDN = util.format("cn=%s,%s", username, users_dn); var bind_promised = Promise.promisify(ldap_client.bind, { context: ldap_client }); - console.log(username, password); return bind_promised(userDN, password) .error(function(err) { console.error(err); diff --git a/src/lib/routes/deny_not_logged.js b/src/lib/routes/deny_not_logged.js index 7aaf448b..d22faa03 100644 --- a/src/lib/routes/deny_not_logged.js +++ b/src/lib/routes/deny_not_logged.js @@ -3,19 +3,14 @@ module.exports = denyNotLogged; var objectPath = require('object-path'); -function replyWithUnauthorized(res) { - res.status(401); - res.send('Unauthorized access'); -} - function denyNotLogged(next) { return function(req, res) { var auth_session = req.session.auth_session; var first_factor = objectPath.has(req, 'session.auth_session.first_factor') && req.session.auth_session.first_factor; if(!first_factor) { - replyWithUnauthorized(res); - console.log('Access to this route is denied'); + res.status(403); + res.send(); return; } diff --git a/src/lib/routes/first_factor.js b/src/lib/routes/first_factor.js index 3c2b49f8..fa5a3c1b 100644 --- a/src/lib/routes/first_factor.js +++ b/src/lib/routes/first_factor.js @@ -5,17 +5,13 @@ var exceptions = require('../exceptions'); var ldap = require('../ldap'); var objectPath = require('object-path'); -function replyWithUnauthorized(res) { - res.status(401); - res.send(); -} - function first_factor(req, res) { var logger = req.app.get('logger'); var username = req.body.username; var password = req.body.password; if(!username || !password) { - replyWithUnauthorized(res); + res.status(401); + res.send(); return; } @@ -23,12 +19,16 @@ function first_factor(req, res) { var ldap_client = req.app.get('ldap client'); var config = req.app.get('config'); + var regulator = req.app.get('authentication regulator'); logger.debug('1st factor: Start bind operation against LDAP'); logger.debug('1st factor: username=%s', username); logger.debug('1st factor: base_dn=%s', config.ldap_users_dn); - ldap.validate(ldap_client, username, password, config.ldap_users_dn) + regulator.regulate(username) + .then(function() { + return ldap.validate(ldap_client, username, password, config.ldap_users_dn); + }) .then(function() { objectPath.set(req, 'session.auth_session.userid', username); objectPath.set(req, 'session.auth_session.first_factor', true); @@ -42,6 +42,7 @@ function first_factor(req, res) { logger.debug('1st factor: Retrieved email is %s', email); objectPath.set(req, 'session.auth_session.email', email); + regulator.mark(username, true); res.status(204); res.send(); }) @@ -51,10 +52,21 @@ function first_factor(req, res) { res.send(); }) .catch(exceptions.LdapBindError, function(err) { - logger.info('1st factor: LDAP binding failed', err); - replyWithUnauthorized(res); + logger.info('1st factor: LDAP binding failed'); + logger.debug('1st factor: LDAP binding failed due to ', err); + regulator.mark(username, false); + res.status(401); + res.send('Bad credentials'); + }) + .catch(exceptions.AuthenticationRegulationError, function(err) { + logger.info('1st factor: the regulator rejected the authentication of user %s', username); + logger.debug('1st factor: authentication rejected due to %s', err); + res.status(403); + res.send('Access has been restricted for a few minutes...'); }) .catch(function(err) { logger.debug('1st factor: Unhandled error %s', err); + res.status(500); + res.send('Internal error'); }); } diff --git a/src/lib/routes/reset_password.js b/src/lib/routes/reset_password.js index 9eef18dc..49b32e47 100644 --- a/src/lib/routes/reset_password.js +++ b/src/lib/routes/reset_password.js @@ -1,6 +1,8 @@ +var Promise = require('bluebird'); var objectPath = require('object-path'); var ldap = require('../ldap'); +var exceptions = require('../exceptions'); var CHALLENGE = 'reset-password'; var icheck_interface = { @@ -17,7 +19,8 @@ module.exports = { function pre_check(req) { var userid = objectPath.get(req, 'body.userid'); if(!userid) { - return Promise.reject('No user id provided'); + var err = new exceptions.AccessDeniedError(); + return Promise.reject(err); } var ldap_client = req.app.get('ldap client'); @@ -31,7 +34,7 @@ function pre_check(req) { identity.email = email; identity.userid = userid; return Promise.resolve(identity); - }) + }); } function protect(fn) { @@ -59,7 +62,7 @@ function post(req, res) { ldap.update_password(ldap_client, ldapjs, userid, new_password, config) .then(function() { logger.info('POST reset-password: Password reset for user %s', userid); - objectPath.set(req, 'session.auth_session', {}); + objectPath.set(req, 'session.auth_session', undefined); res.status(204); res.send(); }) diff --git a/src/lib/routes/u2f.js b/src/lib/routes/u2f.js index 15fb5641..00e8d929 100644 --- a/src/lib/routes/u2f.js +++ b/src/lib/routes/u2f.js @@ -20,6 +20,7 @@ function retrieve_u2f_meta(req, user_data_storage) { return user_data_storage.get_u2f_meta(userid, appid); } + function sign_request(req, res) { var logger = req.app.get('logger'); var user_data_storage = req.app.get('user data store'); diff --git a/src/lib/routes/u2f_register.js b/src/lib/routes/u2f_register.js index a6d0610b..aa1bfc97 100644 --- a/src/lib/routes/u2f_register.js +++ b/src/lib/routes/u2f_register.js @@ -71,6 +71,7 @@ function register(req, res) { return user_data_storage.set_u2f_meta(userid, appid, meta); }) .then(function() { + objectPath.set(req, 'session.auth_session.identity_check', undefined); res.status(204); res.send(); }) diff --git a/src/lib/server.js b/src/lib/server.js index 8c0e7ca6..83f5ca26 100644 --- a/src/lib/server.js +++ b/src/lib/server.js @@ -13,6 +13,7 @@ var session = require('express-session'); var winston = require('winston'); var UserDataStore = require('./user_data_store'); var EmailSender = require('./email_sender'); +var AuthenticationRegulator = require('./authentication_regulator'); var identity_check = require('./identity_check'); function run(config, ldap_client, deps, fn) { @@ -20,6 +21,8 @@ function run(config, ldap_client, deps, fn) { var public_html_directory = path.resolve(__dirname, '../public_html'); var datastore_options = {}; datastore_options.directory = config.store_directory; + if(config.store_in_memory) + datastore_options.inMemory = true; var email_options = {}; email_options.gmail = config.gmail; @@ -45,13 +48,19 @@ function run(config, ldap_client, deps, fn) { winston.level = config.debug_level || 'info'; + var five_minutes = 5 * 60; + var data_store = new UserDataStore(deps.nedb, datastore_options); + var regulator = new AuthenticationRegulator(data_store, five_minutes); + var notifier = new EmailSender(deps.nodemailer, email_options); + app.set('logger', winston); app.set('ldap', deps.ldap); app.set('ldap client', ldap_client); app.set('totp engine', speakeasy); app.set('u2f', deps.u2f); - app.set('user data store', new UserDataStore(deps.nedb, datastore_options)); - app.set('email sender', new EmailSender(deps.nodemailer, email_options)); + app.set('user data store', data_store); + app.set('email sender', notifier); + app.set('authentication regulator', regulator); app.set('config', config); var base_endpoint = '/authentication'; @@ -62,6 +71,7 @@ function run(config, ldap_client, deps, fn) { identity_check(app, base_endpoint + '/u2f-register', routes.u2f_register.icheck_interface); identity_check(app, base_endpoint + '/reset-password', routes.reset_password.icheck_interface); + app.get (base_endpoint + '/reset-password-form', function(req, res) { res.render('reset-password-form'); }); // Reset the password diff --git a/src/lib/user_data_store.js b/src/lib/user_data_store.js index 1816008b..01daa05e 100644 --- a/src/lib/user_data_store.js +++ b/src/lib/user_data_store.js @@ -8,6 +8,8 @@ function UserDataStore(DataStore, options) { this._u2f_meta_collection = create_collection('u2f_meta', options, DataStore); this._identity_check_tokens_collection = create_collection('identity_check_tokens', options, DataStore); + this._authentication_traces_collection = + create_collection('authentication_traces', options, DataStore); } function create_collection(name, options, DataStore) { @@ -42,6 +44,28 @@ UserDataStore.prototype.get_u2f_meta = function(userid, app_id) { return this._u2f_meta_collection.findOneAsync(filter); } +UserDataStore.prototype.save_authentication_trace = function(userid, type, is_success) { + var newDocument = {}; + newDocument.userid = userid; + newDocument.date = new Date(); + newDocument.is_success = is_success; + newDocument.type = type; + + return this._authentication_traces_collection.insertAsync(newDocument); +} + +UserDataStore.prototype.get_last_authentication_traces = function(userid, type, is_success, count) { + var query = {}; + query.userid = userid; + query.type = type; + query.is_success = is_success; + + var query = this._authentication_traces_collection.find(query) + .sort({ date: -1 }).limit(count); + var query_promisified = Promise.promisify(query.exec, { context: query }); + return query_promisified(); +} + UserDataStore.prototype.issue_identity_check_token = function(userid, token, data, max_age) { var newDocument = {}; newDocument.userid = userid; diff --git a/src/public_html/js/login.js b/src/public_html/js/login.js index 49333cb9..d73ba0f0 100644 --- a/src/public_html/js/login.js +++ b/src/public_html/js/login.js @@ -23,7 +23,7 @@ function onLoginButtonClicked() { validateFirstFactor(username, password, function(err) { if(err) { - onFirstFactorFailure(); + onFirstFactorFailure(err); return; } onFirstFactorSuccess(); @@ -91,7 +91,7 @@ function finishU2fAuthentication(url, responseData, fn) { fn(undefined, data); }) .fail(function(xhr, status) { - $.notify('Error when finish U2F transaction' + status); + $.notify('Error when finish U2F transaction', 'error'); }); } @@ -159,10 +159,10 @@ function onFirstFactorSuccess() { enterSecondFactor(); } -function onFirstFactorFailure() { +function onFirstFactorFailure(err) { $('#password').val(''); $('#token').val(''); - $.notify('Wrong credentials', 'error'); + $.notify('Error during authentication: ' + err, 'error'); } function onAuthenticationSuccess() { @@ -183,7 +183,7 @@ function onU2fAuthenticationSuccess() { } function onU2fAuthenticationFailure(err) { - $.notify('Problem authenticating with U2F.', 'error'); + $.notify('Problem with U2F authentication. Did you register before authenticating?', 'warn'); } function showFirstFactorLayout() { diff --git a/test/integration/test_server.js b/test/integration/test_server.js index 315f3c8b..69df3827 100644 --- a/test/integration/test_server.js +++ b/test/integration/test_server.js @@ -28,7 +28,7 @@ describe('test the server', function() { }); it('should serve the login page', function(done) { - getPromised(BASE_URL + '/auth/login?redirect=/') + getPromised(BASE_URL + '/authentication/login?redirect=/') .then(function(data) { assert.equal(data.statusCode, 200); done(); @@ -44,7 +44,7 @@ describe('test the server', function() { }); it('should redirect when logout', function(done) { - getPromised(BASE_URL + '/auth/logout?redirect=/') + getPromised(BASE_URL + '/authentication/logout?redirect=/') .then(function(data) { assert.equal(data.statusCode, 200); assert.equal(data.body, home_page); @@ -62,7 +62,7 @@ describe('test the server', function() { }); it('should fail the first_factor login', function() { - return postPromised(BASE_URL + '/auth/1stfactor', { + return postPromised(BASE_URL + '/authentication/1stfactor', { form: { username: 'user', password: 'bad_password' @@ -80,7 +80,7 @@ describe('test the server', function() { encoding: 'base32' }); - return postPromised(BASE_URL + '/auth/1stfactor', { + return postPromised(BASE_URL + '/authentication/1stfactor', { form: { username: 'user', password: 'password', @@ -88,7 +88,7 @@ describe('test the server', function() { }) .then(function(response) { assert.equal(response.statusCode, 204); - return postPromised(BASE_URL + '/auth/2ndfactor/totp', { + return postPromised(BASE_URL + '/authentication/2ndfactor/totp', { form: { token: token } }); }) @@ -116,7 +116,7 @@ describe('test the server', function() { var is_secret_page_content = (content.indexOf('This is a very important secret!') > -1); assert(is_secret_page_content); - return getPromised(BASE_URL + '/auth/logout') + return getPromised(BASE_URL + '/authentication/logout') }) .then(function(data) { assert.equal(data.statusCode, 200); @@ -164,5 +164,5 @@ function getHomePage() { } function getLoginPage() { - return getPromised(BASE_URL + '/auth/login'); + return getPromised(BASE_URL + '/authentication/login'); } diff --git a/test/unitary/requests.js b/test/unitary/requests.js index deb7a06c..dfa4e4d9 100644 --- a/test/unitary/requests.js +++ b/test/unitary/requests.js @@ -115,6 +115,17 @@ module.exports = function(port) { } }); } + + function execute_failing_first_factor(jar) { + return request.postAsync({ + url: BASE_URL + '/authentication/1stfactor', + jar: jar, + form: { + username: 'test_nok', + password: 'password' + } + }); + } return { login: execute_login, @@ -123,6 +134,7 @@ module.exports = function(port) { u2f_authentication: execute_u2f_authentication, u2f_registration: execute_u2f_registration, first_factor: execute_first_factor, + failing_first_factor: execute_failing_first_factor, totp: execute_totp, } diff --git a/test/unitary/routes/test_deny_not_logged.js b/test/unitary/routes/test_deny_not_logged.js index 04b003f7..48a7007c 100644 --- a/test/unitary/routes/test_deny_not_logged.js +++ b/test/unitary/routes/test_deny_not_logged.js @@ -6,11 +6,11 @@ var assert = require('assert'); var denyNotLogged = require('../../../src/lib/routes/deny_not_logged'); describe('test not logged', function() { - it('should return status code 401 when auth_session has not been previously created', function() { + it('should return status code 403 when auth_session has not been previously created', function() { return test_auth_session_not_created(); }); - it('should return status code 401 when auth_session has failed first factor', function() { + it('should return status code 403 when auth_session has failed first factor', function() { return test_auth_first_factor_not_validated(); }); @@ -23,7 +23,7 @@ function test_auth_session_not_created() { return new Promise(function(resolve, reject) { var send = sinon.spy(resolve); var status = sinon.spy(function(code) { - assert.equal(401, code); + assert.equal(403, code); }); var req = { session: {} @@ -42,7 +42,7 @@ function test_auth_first_factor_not_validated() { return new Promise(function(resolve, reject) { var send = sinon.spy(resolve); var status = sinon.spy(function(code) { - assert.equal(401, code); + assert.equal(403, code); }); var req = { session: { diff --git a/test/unitary/routes/test_first_factor.js b/test/unitary/routes/test_first_factor.js index 840a9b99..aabd085f 100644 --- a/test/unitary/routes/test_first_factor.js +++ b/test/unitary/routes/test_first_factor.js @@ -4,11 +4,13 @@ var Promise = require('bluebird'); var assert = require('assert'); var winston = require('winston'); var first_factor = require('../../../src/lib/routes/first_factor'); +var exceptions = require('../../../src/lib/exceptions'); describe('test the first factor validation route', function() { var req, res; var ldap_interface_mock; var search_res_ok; + var regulator; beforeEach(function() { ldap_interface_mock = { @@ -31,10 +33,18 @@ describe('test the first factor validation route', function() { }); ldap_interface_mock.search.yields(undefined, search_res_ok); + regulator = {}; + regulator.mark = sinon.stub(); + regulator.regulate = sinon.stub(); + + regulator.mark.returns(Promise.resolve()); + regulator.regulate.returns(Promise.resolve()); + var app_get = sinon.stub(); app_get.withArgs('ldap client').returns(ldap_interface_mock); app_get.withArgs('config').returns(ldap_interface_mock); app_get.withArgs('logger').returns(winston); + app_get.withArgs('authentication regulator').returns(regulator); req = { app: { @@ -69,27 +79,36 @@ describe('test the first factor validation route', function() { }); }); - it('should return status code 401 when LDAP binding fails', function() { - return new Promise(function(resolve, reject) { - res.send = sinon.spy(function(data) { - assert.equal(401, res.status.getCall(0).args[0]); - resolve(); - }); - ldap_interface_mock.bind.yields('Bad credentials'); - first_factor(req, res); + it('should return status code 401 when LDAP binding fails', function(done) { + res.send = sinon.spy(function(data) { + assert.equal(401, res.status.getCall(0).args[0]); + assert.equal(regulator.mark.getCall(0).args[0], 'username'); + done(); }); + ldap_interface_mock.bind.yields('Bad credentials'); + first_factor(req, res); }); - it('should return status code 500 when LDAP binding fails', function() { - return new Promise(function(resolve, reject) { - res.send = sinon.spy(function(data) { - assert.equal(500, res.status.getCall(0).args[0]); - resolve(); - }); - ldap_interface_mock.bind.yields(undefined); - ldap_interface_mock.search.yields('error'); - first_factor(req, res); + it('should return status code 500 when LDAP binding throws', function(done) { + res.send = sinon.spy(function(data) { + assert.equal(500, res.status.getCall(0).args[0]); + done(); }); + ldap_interface_mock.bind.yields(undefined); + ldap_interface_mock.search.yields('error'); + first_factor(req, res); + }); + + it('should return status code 403 when regulator rejects authentication', function(done) { + var err = new exceptions.AuthenticationRegulationError(); + regulator.regulate.returns(Promise.reject(err)); + res.send = sinon.spy(function(data) { + assert.equal(403, res.status.getCall(0).args[0]); + done(); + }); + ldap_interface_mock.bind.yields(undefined); + ldap_interface_mock.search.yields(undefined); + first_factor(req, res); }); }); diff --git a/test/unitary/routes/test_reset_password.js b/test/unitary/routes/test_reset_password.js index a92d56c5..8ceea448 100644 --- a/test/unitary/routes/test_reset_password.js +++ b/test/unitary/routes/test_reset_password.js @@ -95,7 +95,7 @@ describe('test reset password', function() { } function test_reset_password_post() { - it('should update the password', function(done) { + it('should update the password and reset auth_session for reauthentication', function(done) { req.session.auth_session.identity_check = {}; req.session.auth_session.identity_check.userid = 'user'; req.session.auth_session.identity_check.challenge = 'reset-password'; @@ -107,6 +107,7 @@ describe('test reset password', function() { res.send = sinon.spy(function() { assert.equal(ldap_client.modify.getCall(0).args[0], 'cn=user,dc=example,dc=com'); assert.equal(res.status.getCall(0).args[0], 204); + assert.equal(req.session.auth_session, undefined); done(); }); reset_password.post(req, res); diff --git a/test/unitary/routes/test_u2f.js b/test/unitary/routes/test_u2f.js index 401152d2..d8c65ae8 100644 --- a/test/unitary/routes/test_u2f.js +++ b/test/unitary/routes/test_u2f.js @@ -95,7 +95,8 @@ describe('test u2f routes', function() { certificate: 'cert' }; res.send = sinon.spy(function(data) { - assert('user', user_data_store.set_u2f_meta.getCall(0).args[0]) + assert.equal('user', user_data_store.set_u2f_meta.getCall(0).args[0]) + assert.equal(req.session.auth_session.identity_check, undefined); done(); }); var u2f_mock = {}; diff --git a/test/unitary/test_authentication_regulator.js b/test/unitary/test_authentication_regulator.js new file mode 100644 index 00000000..18b46c9e --- /dev/null +++ b/test/unitary/test_authentication_regulator.js @@ -0,0 +1,70 @@ + +var AuthenticationRegulator = require('../../src/lib/authentication_regulator'); +var UserDataStore = require('../../src/lib/user_data_store'); +var DataStore = require('nedb'); +var exceptions = require('../../src/lib/exceptions'); +var MockDate = require('mockdate'); + +describe('test authentication regulator', function() { + it('should mark 2 authentication and regulate (resolve)', function() { + var options = {}; + options.inMemoryOnly = true; + var data_store = new UserDataStore(DataStore, options); + var regulator = new AuthenticationRegulator(data_store, 10); + var user = 'user'; + + return regulator.mark(user, false) + .then(function() { + return regulator.mark(user, true); + }) + .then(function() { + return regulator.regulate(user); + }); + }); + + it('should mark 3 authentications and regulate (reject)', function(done) { + var options = {}; + options.inMemoryOnly = true; + var data_store = new UserDataStore(DataStore, options); + var regulator = new AuthenticationRegulator(data_store, 10); + var user = 'user'; + + regulator.mark(user, false) + .then(function() { + return regulator.mark(user, false); + }) + .then(function() { + return regulator.mark(user, false); + }) + .then(function() { + return regulator.regulate(user); + }) + .catch(exceptions.AuthenticationRegulationError, function() { + done(); + }) + }); + + it('should mark 3 authentications and regulate (resolve)', function(done) { + var options = {}; + options.inMemoryOnly = true; + var data_store = new UserDataStore(DataStore, options); + var regulator = new AuthenticationRegulator(data_store, 10); + var user = 'user'; + + MockDate.set('1/2/2000 00:00:00'); + regulator.mark(user, false) + .then(function() { + MockDate.set('1/2/2000 00:00:15'); + return regulator.mark(user, false); + }) + .then(function() { + return regulator.mark(user, false); + }) + .then(function() { + return regulator.regulate(user); + }) + .then(function() { + done(); + }) + }); +}); diff --git a/test/unitary/test_identity_check.js b/test/unitary/test_identity_check.js index ae2b571b..555cb197 100644 --- a/test/unitary/test_identity_check.js +++ b/test/unitary/test_identity_check.js @@ -68,7 +68,6 @@ describe('test identity check process', function() { describe('test POST', test_post_handler); describe('test GET', test_get_handler); - function test_post_handler() { it('should send 403 if pre check rejects', function(done) { var endpoint = '/protected'; diff --git a/test/unitary/test_server.js b/test/unitary/test_server.js index f6a9bde4..f6acfd73 100644 --- a/test/unitary/test_server.js +++ b/test/unitary/test_server.js @@ -6,6 +6,7 @@ var request = Promise.promisifyAll(require('request')); var assert = require('assert'); var speakeasy = require('speakeasy'); var sinon = require('sinon'); +var MockDate = require('mockdate'); var PORT = 8090; var BASE_URL = 'http://localhost:' + PORT; @@ -36,6 +37,7 @@ describe('test the server', function() { ldap_password: 'password', session_secret: 'session_secret', session_max_age: 50000, + store_in_memory: true, gmail: { user: 'user@example.com', pass: 'password' @@ -48,15 +50,8 @@ describe('test the server', function() { u2f.startAuthentication = sinon.stub(); u2f.finishAuthentication = sinon.stub(); - collection = {}; - collection.insert = sinon.stub().yields(undefined, 1); - collection.findOne = sinon.stub().yields(undefined, {}); - collection.update = sinon.stub().yields(undefined, {}); - collection.remove = sinon.stub().yields(undefined, 1); - nedb = sinon.spy(function() { - return collection; - }); - + nedb = require('nedb'); + transporter = {}; transporter.sendMail = sinon.stub().yields(); @@ -80,10 +75,12 @@ describe('test the server', function() { 'password').yields(undefined); ldap_client.bind.withArgs('cn=admin,dc=example,dc=com', 'password').yields(undefined); - ldap_client.search.yields(undefined, search_res); + ldap_client.bind.withArgs('cn=test_nok,ou=users,dc=example,dc=com', 'password').yields('error'); + ldap_client.modify.yields(undefined); + ldap_client.search.yields(undefined, search_res); var deps = {}; deps.u2f = u2f; @@ -101,18 +98,97 @@ describe('test the server', function() {   }); describe('test GET /login', function() { - test_login() + test_login(); }); describe('test GET /logout', function() { - test_logout() + test_logout(); + }); + + describe('test GET /reset-password-form', function() { + test_reset_password_form(); + }); + + describe('test endpoints locks', function() { + function should_post_and_reply_with(url, status_code) { + return request.postAsync(url).then(function(response) { + assert.equal(response.statusCode, status_code); + return Promise.resolve(); + }) + } + + function should_get_and_reply_with(url, status_code) { + return request.getAsync(url).then(function(response) { + assert.equal(response.statusCode, status_code); + return Promise.resolve(); + }) + } + + function should_post_and_reply_with_403(url) { + return should_post_and_reply_with(url, 403); +  } + function should_get_and_reply_with_403(url) { + return should_get_and_reply_with(url, 403); +  } + + function should_post_and_reply_with_401(url) { + return should_post_and_reply_with(url, 401); +  } + function should_get_and_reply_with_401(url) { + return should_get_and_reply_with(url, 401); +  } + + function should_get_and_post_reply_with_403(url) { + var p1 = should_post_and_reply_with_403(url); + var p2 = should_get_and_reply_with_403(url); + return Promise.all([p1, p2]); +  } + + it('should block /authentication/new-password', function() { + return should_post_and_reply_with_403(BASE_URL + '/authentication/new-password') + }); + + it('should block /authentication/u2f-register', function() { + return should_get_and_post_reply_with_403(BASE_URL + '/authentication/u2f-register'); + }); + + it('should block /authentication/reset-password', function() { + return should_get_and_post_reply_with_403(BASE_URL + '/authentication/reset-password'); + }); + + it('should block /authentication/2ndfactor/u2f/register_request', function() { + return should_get_and_reply_with_403(BASE_URL + '/authentication/2ndfactor/u2f/register_request'); + }); + + it('should block /authentication/2ndfactor/u2f/register', function() { + return should_post_and_reply_with_403(BASE_URL + '/authentication/2ndfactor/u2f/register'); + }); + + it('should block /authentication/2ndfactor/u2f/sign_request', function() { + return should_get_and_reply_with_403(BASE_URL + '/authentication/2ndfactor/u2f/sign_request'); + }); + + it('should block /authentication/2ndfactor/u2f/sign', function() { + return should_post_and_reply_with_403(BASE_URL + '/authentication/2ndfactor/u2f/sign'); + }); }); describe('test authentication and verification', function() { test_authentication(); test_reset_password(); + test_regulation(); }); + function test_reset_password_form() { + it('should serve the reset password form page', function(done) { + request.getAsync(BASE_URL + '/authentication/reset-password-form') + .then(function(response) { + assert.equal(response.statusCode, 200); + done(); + }); + }); + } + function test_login() { it('should serve the login page', function(done) { request.getAsync(BASE_URL + '/authentication/login') @@ -209,11 +285,6 @@ describe('test the server', function() { u2f.startAuthentication.returns(Promise.resolve(registration_request)); u2f.finishAuthentication.returns(Promise.resolve(registration_status)); - collection.insert = sinon.spy(function(data, fn) { - collection.findOne.yields(undefined, data); - fn(); - }); - var j = request.jar(); return requests.login(j) .then(function(res) { @@ -241,11 +312,6 @@ describe('test the server', function() { function test_reset_password() { it('should reset the password', function() { - collection.insert = sinon.spy(function(data, fn) { - collection.findOne.yields(undefined, data); - fn(); - }); - var j = request.jar(); return requests.login(j) .then(function(res) { @@ -262,5 +328,39 @@ describe('test the server', function() { }); }); } + + function test_regulation() { + it('should regulate authentication', function() { + var j = request.jar(); + MockDate.set('1/2/2017 00:00:00'); + return requests.login(j) + .then(function(res) { + assert.equal(res.statusCode, 200, 'get login page failed'); + return requests.failing_first_factor(j); + }) + .then(function(res) { + console.log('coucou'); + assert.equal(res.statusCode, 401, 'first factor failed'); + return requests.failing_first_factor(j); + }) + .then(function(res) { + assert.equal(res.statusCode, 401, 'first factor failed'); + return requests.failing_first_factor(j); + }) + .then(function(res) { + assert.equal(res.statusCode, 401, 'first factor failed'); + return requests.failing_first_factor(j); + }) + .then(function(res) { + assert.equal(res.statusCode, 403, 'first factor failed'); + MockDate.set('1/2/2017 00:30:00'); + return requests.failing_first_factor(j); + }) + .then(function(res) { + assert.equal(res.statusCode, 401, 'first factor failed'); + return Promise.resolve(); + }) + }); + } }); diff --git a/test/unitary/user_data_store/test_authentication_audit.js b/test/unitary/user_data_store/test_authentication_audit.js new file mode 100644 index 00000000..d317b480 --- /dev/null +++ b/test/unitary/user_data_store/test_authentication_audit.js @@ -0,0 +1,69 @@ + +var assert = require('assert'); +var Promise = require('bluebird'); +var sinon = require('sinon'); +var MockDate = require('mockdate'); +var UserDataStore = require('../../../src/lib/user_data_store'); +var DataStore = require('nedb'); + +describe('test user data store', function() { + describe('test authentication traces', test_authentication_traces); +}); + +function test_authentication_traces() { + it('should save an authentication trace in db', function() { + var options = {}; + options.inMemoryOnly = true; + + var data_store = new UserDataStore(DataStore, options); + var userid = 'user'; + var type = '1stfactor'; + var is_success = false; + return data_store.save_authentication_trace(userid, type, is_success) + .then(function(doc) { + assert('_id' in doc); + assert.equal(doc.userid, 'user'); + assert.equal(doc.is_success, false); + assert.equal(doc.type, '1stfactor'); + return Promise.resolve(); + }); + }); + + it('should return 3 last authentication traces', function() { + var options = {}; + options.inMemoryOnly = true; + + var data_store = new UserDataStore(DataStore, options); + var userid = 'user'; + var type = '1stfactor'; + var is_success = false; + MockDate.set('2/1/2000'); + return data_store.save_authentication_trace(userid, type, false) + .then(function(doc) { + MockDate.set('1/2/2000'); + return data_store.save_authentication_trace(userid, type, true); + }) + .then(function(doc) { + MockDate.set('1/7/2000'); + return data_store.save_authentication_trace(userid, type, false); + }) + .then(function(doc) { + MockDate.set('1/2/2000'); + return data_store.save_authentication_trace(userid, type, false); + }) + .then(function(doc) { + MockDate.set('1/5/2000'); + return data_store.save_authentication_trace(userid, type, false); + }) + .then(function(doc) { + return data_store.get_last_authentication_traces(userid, type, false, 3); + }) + .then(function(docs) { + assert.equal(docs.length, 3); + assert.deepEqual(docs[0].date, new Date('2/1/2000')); + assert.deepEqual(docs[1].date, new Date('1/7/2000')); + assert.deepEqual(docs[2].date, new Date('1/5/2000')); + return Promise.resolve(); + }) + }); +}