Implement authentication regulation

This commit is contained in:
Clement Michaud 2017-01-28 01:32:25 +01:00
parent 05046338ed
commit cb98f0454a
22 changed files with 445 additions and 88 deletions

View File

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

View File

@ -3,7 +3,8 @@ module.exports = {
LdapSearchError: LdapSearchError, LdapSearchError: LdapSearchError,
LdapBindError: LdapBindError, LdapBindError: LdapBindError,
IdentityError: IdentityError, IdentityError: IdentityError,
AccessDeniedError: AccessDeniedError AccessDeniedError: AccessDeniedError,
AuthenticationRegulationError: AuthenticationRegulationError,
} }
function LdapSearchError(message) { function LdapSearchError(message) {
@ -29,3 +30,9 @@ function AccessDeniedError(message) {
this.message = (message || ""); this.message = (message || "");
} }
AccessDeniedError.prototype = Object.create(Error.prototype); AccessDeniedError.prototype = Object.create(Error.prototype);
function AuthenticationRegulationError(message) {
this.name = "AuthenticationRegulationError";
this.message = (message || "");
}
AuthenticationRegulationError.prototype = Object.create(Error.prototype);

View File

@ -102,6 +102,7 @@ function identity_check_post(endpoint, icheck_interface) {
.then(function(identity) { .then(function(identity) {
email_address = objectPath.get(identity, 'email'); email_address = objectPath.get(identity, 'email');
userid = objectPath.get(identity, 'userid'); userid = objectPath.get(identity, 'userid');
if(!(email_address && userid)) { if(!(email_address && userid)) {
throw new exceptions.IdentityError('Missing user id or email address'); 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']); 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); return identity_check.issue_token(userid, email, undefined, logger);
}, function(err) { }, function(err) {
throw new exceptions.AccessDeniedError('Access denied'); throw new exceptions.AccessDeniedError();
}) })
.then(function() { .then(function() {
res.status(204); res.status(204);
res.send(); res.send();
}) })
.catch(exceptions.IdentityError, function(err) { .catch(exceptions.IdentityError, function(err) {
logger.error('POST identity_check: %s', err); logger.error('POST identity_check: IdentityError %s', err);
res.status(400); res.status(400);
res.send(); res.send();
return;
}) })
.catch(exceptions.AccessDeniedError, function(err) { .catch(exceptions.AccessDeniedError, function(err) {
logger.error('POST identity_check: %s', err); logger.error('POST identity_check: AccessDeniedError %s', err);
res.status(403); res.status(403);
res.send(); res.send();
return;
}) })
.catch(function(err) { .catch(function(err) {
logger.error('POST identity_check: %s', err); logger.error('POST identity_check: Error %s', err);
res.status(500); res.status(500);
res.send(); res.send();
}); });

View File

@ -14,7 +14,6 @@ var Dovehash = require('dovehash');
function validateCredentials(ldap_client, username, password, users_dn) { function validateCredentials(ldap_client, username, password, users_dn) {
var userDN = util.format("cn=%s,%s", username, users_dn); var userDN = util.format("cn=%s,%s", username, users_dn);
var bind_promised = Promise.promisify(ldap_client.bind, { context: ldap_client }); var bind_promised = Promise.promisify(ldap_client.bind, { context: ldap_client });
console.log(username, password);
return bind_promised(userDN, password) return bind_promised(userDN, password)
.error(function(err) { .error(function(err) {
console.error(err); console.error(err);

View File

@ -3,19 +3,14 @@ module.exports = denyNotLogged;
var objectPath = require('object-path'); var objectPath = require('object-path');
function replyWithUnauthorized(res) {
res.status(401);
res.send('Unauthorized access');
}
function denyNotLogged(next) { function denyNotLogged(next) {
return function(req, res) { return function(req, res) {
var auth_session = req.session.auth_session; var auth_session = req.session.auth_session;
var first_factor = objectPath.has(req, 'session.auth_session.first_factor') var first_factor = objectPath.has(req, 'session.auth_session.first_factor')
&& req.session.auth_session.first_factor; && req.session.auth_session.first_factor;
if(!first_factor) { if(!first_factor) {
replyWithUnauthorized(res); res.status(403);
console.log('Access to this route is denied'); res.send();
return; return;
} }

View File

@ -5,17 +5,13 @@ var exceptions = require('../exceptions');
var ldap = require('../ldap'); var ldap = require('../ldap');
var objectPath = require('object-path'); var objectPath = require('object-path');
function replyWithUnauthorized(res) {
res.status(401);
res.send();
}
function first_factor(req, res) { function first_factor(req, res) {
var logger = req.app.get('logger'); var logger = req.app.get('logger');
var username = req.body.username; var username = req.body.username;
var password = req.body.password; var password = req.body.password;
if(!username || !password) { if(!username || !password) {
replyWithUnauthorized(res); res.status(401);
res.send();
return; return;
} }
@ -23,12 +19,16 @@ function first_factor(req, res) {
var ldap_client = req.app.get('ldap client'); var ldap_client = req.app.get('ldap client');
var config = req.app.get('config'); 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: Start bind operation against LDAP');
logger.debug('1st factor: username=%s', username); logger.debug('1st factor: username=%s', username);
logger.debug('1st factor: base_dn=%s', config.ldap_users_dn); 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() { .then(function() {
objectPath.set(req, 'session.auth_session.userid', username); objectPath.set(req, 'session.auth_session.userid', username);
objectPath.set(req, 'session.auth_session.first_factor', true); 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); logger.debug('1st factor: Retrieved email is %s', email);
objectPath.set(req, 'session.auth_session.email', email); objectPath.set(req, 'session.auth_session.email', email);
regulator.mark(username, true);
res.status(204); res.status(204);
res.send(); res.send();
}) })
@ -51,10 +52,21 @@ function first_factor(req, res) {
res.send(); res.send();
}) })
.catch(exceptions.LdapBindError, function(err) { .catch(exceptions.LdapBindError, function(err) {
logger.info('1st factor: LDAP binding failed', err); logger.info('1st factor: LDAP binding failed');
replyWithUnauthorized(res); 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) { .catch(function(err) {
logger.debug('1st factor: Unhandled error %s', err); logger.debug('1st factor: Unhandled error %s', err);
res.status(500);
res.send('Internal error');
}); });
} }

View File

@ -1,6 +1,8 @@
var Promise = require('bluebird');
var objectPath = require('object-path'); var objectPath = require('object-path');
var ldap = require('../ldap'); var ldap = require('../ldap');
var exceptions = require('../exceptions');
var CHALLENGE = 'reset-password'; var CHALLENGE = 'reset-password';
var icheck_interface = { var icheck_interface = {
@ -17,7 +19,8 @@ module.exports = {
function pre_check(req) { function pre_check(req) {
var userid = objectPath.get(req, 'body.userid'); var userid = objectPath.get(req, 'body.userid');
if(!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'); var ldap_client = req.app.get('ldap client');
@ -31,7 +34,7 @@ function pre_check(req) {
identity.email = email; identity.email = email;
identity.userid = userid; identity.userid = userid;
return Promise.resolve(identity); return Promise.resolve(identity);
}) });
} }
function protect(fn) { function protect(fn) {
@ -59,7 +62,7 @@ function post(req, res) {
ldap.update_password(ldap_client, ldapjs, userid, new_password, config) ldap.update_password(ldap_client, ldapjs, userid, new_password, config)
.then(function() { .then(function() {
logger.info('POST reset-password: Password reset for user %s', userid); 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.status(204);
res.send(); res.send();
}) })

View File

@ -20,6 +20,7 @@ function retrieve_u2f_meta(req, user_data_storage) {
return user_data_storage.get_u2f_meta(userid, appid); return user_data_storage.get_u2f_meta(userid, appid);
} }
function sign_request(req, res) { function sign_request(req, res) {
var logger = req.app.get('logger'); var logger = req.app.get('logger');
var user_data_storage = req.app.get('user data store'); var user_data_storage = req.app.get('user data store');

View File

@ -71,6 +71,7 @@ function register(req, res) {
return user_data_storage.set_u2f_meta(userid, appid, meta); return user_data_storage.set_u2f_meta(userid, appid, meta);
}) })
.then(function() { .then(function() {
objectPath.set(req, 'session.auth_session.identity_check', undefined);
res.status(204); res.status(204);
res.send(); res.send();
}) })

View File

@ -13,6 +13,7 @@ var session = require('express-session');
var winston = require('winston'); var winston = require('winston');
var UserDataStore = require('./user_data_store'); var UserDataStore = require('./user_data_store');
var EmailSender = require('./email_sender'); var EmailSender = require('./email_sender');
var AuthenticationRegulator = require('./authentication_regulator');
var identity_check = require('./identity_check'); var identity_check = require('./identity_check');
function run(config, ldap_client, deps, fn) { 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 public_html_directory = path.resolve(__dirname, '../public_html');
var datastore_options = {}; var datastore_options = {};
datastore_options.directory = config.store_directory; datastore_options.directory = config.store_directory;
if(config.store_in_memory)
datastore_options.inMemory = true;
var email_options = {}; var email_options = {};
email_options.gmail = config.gmail; email_options.gmail = config.gmail;
@ -45,13 +48,19 @@ function run(config, ldap_client, deps, fn) {
winston.level = config.debug_level || 'info'; 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('logger', winston);
app.set('ldap', deps.ldap); app.set('ldap', deps.ldap);
app.set('ldap client', ldap_client); app.set('ldap client', ldap_client);
app.set('totp engine', speakeasy); app.set('totp engine', speakeasy);
app.set('u2f', deps.u2f); app.set('u2f', deps.u2f);
app.set('user data store', new UserDataStore(deps.nedb, datastore_options)); app.set('user data store', data_store);
app.set('email sender', new EmailSender(deps.nodemailer, email_options)); app.set('email sender', notifier);
app.set('authentication regulator', regulator);
app.set('config', config); app.set('config', config);
var base_endpoint = '/authentication'; 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 + '/u2f-register', routes.u2f_register.icheck_interface);
identity_check(app, base_endpoint + '/reset-password', routes.reset_password.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'); }); app.get (base_endpoint + '/reset-password-form', function(req, res) { res.render('reset-password-form'); });
// Reset the password // Reset the password

View File

@ -8,6 +8,8 @@ function UserDataStore(DataStore, options) {
this._u2f_meta_collection = create_collection('u2f_meta', options, DataStore); this._u2f_meta_collection = create_collection('u2f_meta', options, DataStore);
this._identity_check_tokens_collection = this._identity_check_tokens_collection =
create_collection('identity_check_tokens', options, DataStore); create_collection('identity_check_tokens', options, DataStore);
this._authentication_traces_collection =
create_collection('authentication_traces', options, DataStore);
} }
function create_collection(name, 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); 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) { UserDataStore.prototype.issue_identity_check_token = function(userid, token, data, max_age) {
var newDocument = {}; var newDocument = {};
newDocument.userid = userid; newDocument.userid = userid;

View File

@ -23,7 +23,7 @@ function onLoginButtonClicked() {
validateFirstFactor(username, password, function(err) { validateFirstFactor(username, password, function(err) {
if(err) { if(err) {
onFirstFactorFailure(); onFirstFactorFailure(err);
return; return;
} }
onFirstFactorSuccess(); onFirstFactorSuccess();
@ -91,7 +91,7 @@ function finishU2fAuthentication(url, responseData, fn) {
fn(undefined, data); fn(undefined, data);
}) })
.fail(function(xhr, status) { .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(); enterSecondFactor();
} }
function onFirstFactorFailure() { function onFirstFactorFailure(err) {
$('#password').val(''); $('#password').val('');
$('#token').val(''); $('#token').val('');
$.notify('Wrong credentials', 'error'); $.notify('Error during authentication: ' + err, 'error');
} }
function onAuthenticationSuccess() { function onAuthenticationSuccess() {
@ -183,7 +183,7 @@ function onU2fAuthenticationSuccess() {
} }
function onU2fAuthenticationFailure(err) { function onU2fAuthenticationFailure(err) {
$.notify('Problem authenticating with U2F.', 'error'); $.notify('Problem with U2F authentication. Did you register before authenticating?', 'warn');
} }
function showFirstFactorLayout() { function showFirstFactorLayout() {

View File

@ -28,7 +28,7 @@ describe('test the server', function() {
}); });
it('should serve the login page', function(done) { it('should serve the login page', function(done) {
getPromised(BASE_URL + '/auth/login?redirect=/') getPromised(BASE_URL + '/authentication/login?redirect=/')
.then(function(data) { .then(function(data) {
assert.equal(data.statusCode, 200); assert.equal(data.statusCode, 200);
done(); done();
@ -44,7 +44,7 @@ describe('test the server', function() {
}); });
it('should redirect when logout', function(done) { it('should redirect when logout', function(done) {
getPromised(BASE_URL + '/auth/logout?redirect=/') getPromised(BASE_URL + '/authentication/logout?redirect=/')
.then(function(data) { .then(function(data) {
assert.equal(data.statusCode, 200); assert.equal(data.statusCode, 200);
assert.equal(data.body, home_page); assert.equal(data.body, home_page);
@ -62,7 +62,7 @@ describe('test the server', function() {
}); });
it('should fail the first_factor login', function() { it('should fail the first_factor login', function() {
return postPromised(BASE_URL + '/auth/1stfactor', { return postPromised(BASE_URL + '/authentication/1stfactor', {
form: { form: {
username: 'user', username: 'user',
password: 'bad_password' password: 'bad_password'
@ -80,7 +80,7 @@ describe('test the server', function() {
encoding: 'base32' encoding: 'base32'
}); });
return postPromised(BASE_URL + '/auth/1stfactor', { return postPromised(BASE_URL + '/authentication/1stfactor', {
form: { form: {
username: 'user', username: 'user',
password: 'password', password: 'password',
@ -88,7 +88,7 @@ describe('test the server', function() {
}) })
.then(function(response) { .then(function(response) {
assert.equal(response.statusCode, 204); assert.equal(response.statusCode, 204);
return postPromised(BASE_URL + '/auth/2ndfactor/totp', { return postPromised(BASE_URL + '/authentication/2ndfactor/totp', {
form: { token: token } form: { token: token }
}); });
}) })
@ -116,7 +116,7 @@ describe('test the server', function() {
var is_secret_page_content = var is_secret_page_content =
(content.indexOf('This is a very important secret!') > -1); (content.indexOf('This is a very important secret!') > -1);
assert(is_secret_page_content); assert(is_secret_page_content);
return getPromised(BASE_URL + '/auth/logout') return getPromised(BASE_URL + '/authentication/logout')
}) })
.then(function(data) { .then(function(data) {
assert.equal(data.statusCode, 200); assert.equal(data.statusCode, 200);
@ -164,5 +164,5 @@ function getHomePage() {
} }
function getLoginPage() { function getLoginPage() {
return getPromised(BASE_URL + '/auth/login'); return getPromised(BASE_URL + '/authentication/login');
} }

View File

@ -116,6 +116,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 { return {
login: execute_login, login: execute_login,
verify: execute_verification, verify: execute_verification,
@ -123,6 +134,7 @@ module.exports = function(port) {
u2f_authentication: execute_u2f_authentication, u2f_authentication: execute_u2f_authentication,
u2f_registration: execute_u2f_registration, u2f_registration: execute_u2f_registration,
first_factor: execute_first_factor, first_factor: execute_first_factor,
failing_first_factor: execute_failing_first_factor,
totp: execute_totp, totp: execute_totp,
} }

View File

@ -6,11 +6,11 @@ var assert = require('assert');
var denyNotLogged = require('../../../src/lib/routes/deny_not_logged'); var denyNotLogged = require('../../../src/lib/routes/deny_not_logged');
describe('test not logged', function() { 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(); 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(); return test_auth_first_factor_not_validated();
}); });
@ -23,7 +23,7 @@ function test_auth_session_not_created() {
return new Promise(function(resolve, reject) { return new Promise(function(resolve, reject) {
var send = sinon.spy(resolve); var send = sinon.spy(resolve);
var status = sinon.spy(function(code) { var status = sinon.spy(function(code) {
assert.equal(401, code); assert.equal(403, code);
}); });
var req = { var req = {
session: {} session: {}
@ -42,7 +42,7 @@ function test_auth_first_factor_not_validated() {
return new Promise(function(resolve, reject) { return new Promise(function(resolve, reject) {
var send = sinon.spy(resolve); var send = sinon.spy(resolve);
var status = sinon.spy(function(code) { var status = sinon.spy(function(code) {
assert.equal(401, code); assert.equal(403, code);
}); });
var req = { var req = {
session: { session: {

View File

@ -4,11 +4,13 @@ var Promise = require('bluebird');
var assert = require('assert'); var assert = require('assert');
var winston = require('winston'); var winston = require('winston');
var first_factor = require('../../../src/lib/routes/first_factor'); var first_factor = require('../../../src/lib/routes/first_factor');
var exceptions = require('../../../src/lib/exceptions');
describe('test the first factor validation route', function() { describe('test the first factor validation route', function() {
var req, res; var req, res;
var ldap_interface_mock; var ldap_interface_mock;
var search_res_ok; var search_res_ok;
var regulator;
beforeEach(function() { beforeEach(function() {
ldap_interface_mock = { ldap_interface_mock = {
@ -31,10 +33,18 @@ describe('test the first factor validation route', function() {
}); });
ldap_interface_mock.search.yields(undefined, search_res_ok); 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(); var app_get = sinon.stub();
app_get.withArgs('ldap client').returns(ldap_interface_mock); app_get.withArgs('ldap client').returns(ldap_interface_mock);
app_get.withArgs('config').returns(ldap_interface_mock); app_get.withArgs('config').returns(ldap_interface_mock);
app_get.withArgs('logger').returns(winston); app_get.withArgs('logger').returns(winston);
app_get.withArgs('authentication regulator').returns(regulator);
req = { req = {
app: { app: {
@ -69,27 +79,36 @@ describe('test the first factor validation route', function() {
}); });
}); });
it('should return status code 401 when LDAP binding fails', function() { it('should return status code 401 when LDAP binding fails', function(done) {
return new Promise(function(resolve, reject) { res.send = sinon.spy(function(data) {
res.send = sinon.spy(function(data) { assert.equal(401, res.status.getCall(0).args[0]);
assert.equal(401, res.status.getCall(0).args[0]); assert.equal(regulator.mark.getCall(0).args[0], 'username');
resolve(); done();
});
ldap_interface_mock.bind.yields('Bad credentials');
first_factor(req, res);
}); });
ldap_interface_mock.bind.yields('Bad credentials');
first_factor(req, res);
}); });
it('should return status code 500 when LDAP binding fails', function() { it('should return status code 500 when LDAP binding throws', function(done) {
return new Promise(function(resolve, reject) { res.send = sinon.spy(function(data) {
res.send = sinon.spy(function(data) { assert.equal(500, res.status.getCall(0).args[0]);
assert.equal(500, res.status.getCall(0).args[0]); done();
resolve();
});
ldap_interface_mock.bind.yields(undefined);
ldap_interface_mock.search.yields('error');
first_factor(req, res);
}); });
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);
}); });
}); });

View File

@ -95,7 +95,7 @@ describe('test reset password', function() {
} }
function test_reset_password_post() { 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 = {};
req.session.auth_session.identity_check.userid = 'user'; req.session.auth_session.identity_check.userid = 'user';
req.session.auth_session.identity_check.challenge = 'reset-password'; req.session.auth_session.identity_check.challenge = 'reset-password';
@ -107,6 +107,7 @@ describe('test reset password', function() {
res.send = sinon.spy(function() { res.send = sinon.spy(function() {
assert.equal(ldap_client.modify.getCall(0).args[0], 'cn=user,dc=example,dc=com'); 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(res.status.getCall(0).args[0], 204);
assert.equal(req.session.auth_session, undefined);
done(); done();
}); });
reset_password.post(req, res); reset_password.post(req, res);

View File

@ -95,7 +95,8 @@ describe('test u2f routes', function() {
certificate: 'cert' certificate: 'cert'
}; };
res.send = sinon.spy(function(data) { 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(); done();
}); });
var u2f_mock = {}; var u2f_mock = {};

View File

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

View File

@ -68,7 +68,6 @@ describe('test identity check process', function() {
describe('test POST', test_post_handler); describe('test POST', test_post_handler);
describe('test GET', test_get_handler); describe('test GET', test_get_handler);
function test_post_handler() { function test_post_handler() {
it('should send 403 if pre check rejects', function(done) { it('should send 403 if pre check rejects', function(done) {
var endpoint = '/protected'; var endpoint = '/protected';

View File

@ -6,6 +6,7 @@ var request = Promise.promisifyAll(require('request'));
var assert = require('assert'); var assert = require('assert');
var speakeasy = require('speakeasy'); var speakeasy = require('speakeasy');
var sinon = require('sinon'); var sinon = require('sinon');
var MockDate = require('mockdate');
var PORT = 8090; var PORT = 8090;
var BASE_URL = 'http://localhost:' + PORT; var BASE_URL = 'http://localhost:' + PORT;
@ -36,6 +37,7 @@ describe('test the server', function() {
ldap_password: 'password', ldap_password: 'password',
session_secret: 'session_secret', session_secret: 'session_secret',
session_max_age: 50000, session_max_age: 50000,
store_in_memory: true,
gmail: { gmail: {
user: 'user@example.com', user: 'user@example.com',
pass: 'password' pass: 'password'
@ -48,14 +50,7 @@ describe('test the server', function() {
u2f.startAuthentication = sinon.stub(); u2f.startAuthentication = sinon.stub();
u2f.finishAuthentication = sinon.stub(); u2f.finishAuthentication = sinon.stub();
collection = {}; nedb = require('nedb');
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;
});
transporter = {}; transporter = {};
transporter.sendMail = sinon.stub().yields(); transporter.sendMail = sinon.stub().yields();
@ -80,10 +75,12 @@ describe('test the server', function() {
'password').yields(undefined); 'password').yields(undefined);
ldap_client.bind.withArgs('cn=admin,dc=example,dc=com', ldap_client.bind.withArgs('cn=admin,dc=example,dc=com',
'password').yields(undefined); 'password').yields(undefined);
ldap_client.search.yields(undefined, search_res);
ldap_client.bind.withArgs('cn=test_nok,ou=users,dc=example,dc=com', ldap_client.bind.withArgs('cn=test_nok,ou=users,dc=example,dc=com',
'password').yields('error'); 'password').yields('error');
ldap_client.modify.yields(undefined); ldap_client.modify.yields(undefined);
ldap_client.search.yields(undefined, search_res);
var deps = {}; var deps = {};
deps.u2f = u2f; deps.u2f = u2f;
@ -101,18 +98,97 @@ describe('test the server', function() {
  });   });
describe('test GET /login', function() { describe('test GET /login', function() {
test_login() test_login();
}); });
describe('test GET /logout', function() { 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() { describe('test authentication and verification', function() {
test_authentication(); test_authentication();
test_reset_password(); 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() { function test_login() {
it('should serve the login page', function(done) { it('should serve the login page', function(done) {
request.getAsync(BASE_URL + '/authentication/login') request.getAsync(BASE_URL + '/authentication/login')
@ -209,11 +285,6 @@ describe('test the server', function() {
u2f.startAuthentication.returns(Promise.resolve(registration_request)); u2f.startAuthentication.returns(Promise.resolve(registration_request));
u2f.finishAuthentication.returns(Promise.resolve(registration_status)); u2f.finishAuthentication.returns(Promise.resolve(registration_status));
collection.insert = sinon.spy(function(data, fn) {
collection.findOne.yields(undefined, data);
fn();
});
var j = request.jar(); var j = request.jar();
return requests.login(j) return requests.login(j)
.then(function(res) { .then(function(res) {
@ -241,11 +312,6 @@ describe('test the server', function() {
function test_reset_password() { function test_reset_password() {
it('should reset the password', function() { it('should reset the password', function() {
collection.insert = sinon.spy(function(data, fn) {
collection.findOne.yields(undefined, data);
fn();
});
var j = request.jar(); var j = request.jar();
return requests.login(j) return requests.login(j)
.then(function(res) { .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();
})
});
}
}); });

View File

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