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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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() {

View File

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

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 {
login: execute_login,
verify: execute_verification,
@ -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,
}

View File

@ -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: {

View File

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

View File

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

View File

@ -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 = {};

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 GET', test_get_handler);
function test_post_handler() {
it('should send 403 if pre check rejects', function(done) {
var endpoint = '/protected';

View File

@ -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,14 +50,7 @@ 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();
})
});
}
});

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