Implement password reset

This commit is contained in:
Clement Michaud 2017-01-27 01:20:03 +01:00
parent 320998ef78
commit 05046338ed
40 changed files with 1308 additions and 427 deletions

View File

@ -4,6 +4,8 @@ debug_level: info
ldap:
url: ldap://ldap
base_dn: ou=users,dc=example,dc=com
user: cn=admin,dc=example,dc=com
password: password
# Will be per user soon
totp_secret: GRWGIJS6IRHVEODVNRCXCOBMJ5AGC6ZE

View File

@ -3,7 +3,7 @@
<title>Home page</title>
</head>
<body>
You need to <a href="/auth/login?redirect=/">log in</a> to access the <a href="/secret.html">secret</a>!<br/><br/>
You can also log off by visiting the following <a href="/auth/logout?redirect=/">link</a>.
You need to <a href="/authentication/login?redirect=/">log in</a> to access the <a href="/secret.html">secret</a>!<br/><br/>
You can also log off by visiting the following <a href="/authentication/logout?redirect=/">link</a>.
</body>
</html>

View File

@ -34,19 +34,31 @@ http {
error_page 401 = @error401;
location @error401 {
return 302 https://localhost:8080/auth/login?redirect=$request_uri;
return 302 https://localhost:8080/authentication/login?redirect=$request_uri;
}
location /auth/ {
location /authentication/ {
proxy_set_header X-Original-URI $request_uri;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_pass http://auth/;
proxy_pass http://auth/authentication/;
}
location /authentication/js/ {
proxy_pass http://auth/js/;
}
location /authentication/img/ {
proxy_pass http://auth/img/;
}
location /authentication/css/ {
proxy_pass http://auth/css/;
}
location = /secret.html {
auth_request /auth/verify;
auth_request /authentication/verify;
auth_request_set $user $upstream_http_x_remote_user;
proxy_set_header X-Forwarded-User $user;

View File

@ -23,9 +23,11 @@
"authdog": "^0.1.1",
"bluebird": "^3.4.7",
"body-parser": "^1.15.2",
"dovehash": "0.0.5",
"ejs": "^2.5.5",
"express": "^4.14.0",
"express-session": "^1.14.2",
"jshashes": "^1.0.6",
"ldapjs": "^1.0.1",
"nedb": "^1.8.0",
"nodemailer": "^2.7.0",

View File

@ -1,8 +1,12 @@
process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
var server = require('./lib/server');
var ldap = require('ldapjs');
var u2f = require('authdog');
var nodemailer = require('nodemailer');
var nedb = require('nedb');
var YAML = require('yamljs');
var config_path = process.argv[2];
@ -15,6 +19,8 @@ var config = {
totp_secret: yaml_config.totp_secret,
ldap_url: yaml_config.ldap.url || 'ldap://127.0.0.1:389',
ldap_users_dn: yaml_config.ldap.base_dn,
ldap_user: yaml_config.ldap.user,
ldap_password: yaml_config.ldap.password,
session_secret: yaml_config.session.secret,
session_max_age: yaml_config.session.expiration || 3600000, // in ms
store_directory: yaml_config.store_directory,
@ -30,4 +36,10 @@ var ldap_client = ldap.createClient({
reconnect: true
});
server.run(config, ldap_client, u2f);
var deps = {};
deps.u2f = u2f;
deps.nedb = nedb;
deps.nodemailer = nodemailer;
deps.ldap = ldap;
server.run(config, ldap_client, deps);

View File

@ -2,6 +2,8 @@
module.exports = {
LdapSearchError: LdapSearchError,
LdapBindError: LdapBindError,
IdentityError: IdentityError,
AccessDeniedError: AccessDeniedError
}
function LdapSearchError(message) {
@ -15,3 +17,15 @@ function LdapBindError(message) {
this.message = (message || "");
}
LdapBindError.prototype = Object.create(Error.prototype);
function IdentityError(message) {
this.name = "IdentityError";
this.message = (message || "");
}
IdentityError.prototype = Object.create(Error.prototype);
function AccessDeniedError(message) {
this.name = "AccessDeniedError";
this.message = (message || "");
}
AccessDeniedError.prototype = Object.create(Error.prototype);

141
src/lib/identity_check.js Normal file
View File

@ -0,0 +1,141 @@
var objectPath = require('object-path');
var randomstring = require('randomstring');
var Promise = require('bluebird');
var util = require('util');
var exceptions = require('./exceptions');
module.exports = identity_check;
// IdentityCheck class
function IdentityCheck(user_data_store, email_sender, logger) {
this._user_data_store = user_data_store;
this._email_sender = email_sender;
this._logger = logger;
}
IdentityCheck.prototype.issue_token = function(userid, email, content, logger) {
var five_minutes = 4 * 60 * 1000;
var token = randomstring.generate({ length: 64 });
var that = this;
this._logger.debug('identity_check: issue identity token %s for 5 minutes', token);
return this._user_data_store.issue_identity_check_token(userid, token, content, five_minutes)
.then(function() {
that._logger.debug('identity_check: send email to %s', email);
return that._send_identity_check_email(email, token);
})
}
IdentityCheck.prototype._send_identity_check_email = function(email, token) {
var url = util.format('%s?identity_token=%s', email.hook_url, token);
var email_content = util.format('<a href="%s">Register</a>', url);
return this._email_sender.send(email.to, email.subject, email_content);
}
IdentityCheck.prototype.consume_token = function(token, logger) {
this._logger.debug('identity_check: consume token %s', token);
return this._user_data_store.consume_identity_check_token(token)
}
// The identity_check middleware that allows the user two perform a two step validation
// using the user email
function identity_check(app, endpoint, icheck_interface) {
app.get(endpoint, identity_check_get(endpoint, icheck_interface));
app.post(endpoint, identity_check_post(endpoint, icheck_interface));
}
function identity_check_get(endpoint, icheck_interface) {
return function(req, res) {
var logger = req.app.get('logger');
var identity_token = objectPath.get(req, 'query.identity_token');
logger.info('GET identity_check: identity token provided is %s', identity_token);
if(!identity_token) {
res.status(403);
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);
identity_check.consume_token(identity_token, logger)
.then(function(content) {
objectPath.set(req, 'session.auth_session.identity_check', {});
req.session.auth_session.identity_check.challenge = icheck_interface.challenge;
req.session.auth_session.identity_check.userid = content.userid;
res.render(icheck_interface.render_template);
}, function(err) {
logger.error('GET identity_check: Error while consuming token %s', err);
throw new exceptions.AccessDeniedError('Access denied');
})
.catch(exceptions.AccessDeniedError, function(err) {
logger.error('GET identity_check: Access Denied %s', err);
res.status(403);
res.send();
  })
.catch(function(err) {
logger.error('GET identity_check: Internal error %s', err);
res.status(500);
res.send();
});
}
}
function identity_check_post(endpoint, icheck_interface) {
return function(req, res) {
var logger = req.app.get('logger');
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);
var userid, email_address;
icheck_interface.pre_check_callback(req)
.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');
}
var email = {};
email.to = email_address;
email.subject = 'Identity Verification';
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');
})
.then(function() {
res.status(204);
res.send();
})
.catch(exceptions.IdentityError, function(err) {
logger.error('POST identity_check: %s', err);
res.status(400);
res.send();
return;
})
.catch(exceptions.AccessDeniedError, function(err) {
logger.error('POST identity_check: %s', err);
res.status(403);
res.send();
return;
})
.catch(function(err) {
logger.error('POST identity_check: %s', err);
res.status(500);
res.send();
});
}
}

View File

@ -1,18 +1,23 @@
module.exports = {
validate: validateCredentials,
get_email: retrieve_email
get_email: retrieve_email,
update_password: update_password
}
var util = require('util');
var Promise = require('bluebird');
var exceptions = require('./exceptions');
var Hashes = require('jshashes')
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);
throw new exceptions.LdapBindError(err.message);
});
}
@ -33,14 +38,33 @@ function retrieve_email(ldap_client, username, users_dn) {
doc = entry.object;
});
res.on('error', function(err) {
reject(new exceptions.LdapSearchError(err.message));
reject(new exceptions.LdapSearchError(err));
});
res.on('end', function(result) {
resolve(doc);
});
})
.catch(function(err) {
reject(new exceptions.LdapSearchError(err.message));
reject(new exceptions.LdapSearchError(err));
});
});
}
function update_password(ldap_client, ldap, username, new_password, config) {
var userDN = util.format("cn=%s,%s", username, config.ldap_users_dn);
var encoded_password = Dovehash.encode('SSHA', new_password);
var change = new ldap.Change({
operation: 'replace',
modification: {
userPassword: encoded_password
}
});
var modify_promised = Promise.promisify(ldap_client.modify, { context: ldap_client });
var bind_promised = Promise.promisify(ldap_client.bind, { context: ldap_client });
return bind_promised(config.ldap_user, config.ldap_password)
.then(function() {
return modify_promised(userDN, change);
});
}

View File

@ -1,7 +1,9 @@
var first_factor = require('./routes/first_factor');
var second_factor = require('./routes/second_factor');
var reset_password = require('./routes/reset_password');
var verify = require('./routes/verify');
var u2f_register_handler = require('./routes/u2f_register_handler');
var objectPath = require('object-path');
module.exports = {
@ -9,7 +11,9 @@ module.exports = {
logout: serveLogout,
verify: verify,
first_factor: first_factor,
second_factor: second_factor
second_factor: second_factor,
reset_password: reset_password,
u2f_register: u2f_register_handler
}
function serveLogin(req, res) {
@ -18,7 +22,6 @@ function serveLogin(req, res) {
req.session.auth_session.first_factor = false;
req.session.auth_session.second_factor = false;
}
res.render('login');
}

View File

@ -12,15 +12,8 @@ function replyWithUnauthorized(res) {
function first_factor(req, res) {
var logger = req.app.get('logger');
if(!objectPath.has(req, 'session.auth_session.second_factor')) {
logger.error('1st factor: Session is missing.');
replyWithUnauthorized(res);
}
var username = req.body.username;
var password = req.body.password;
console.log('Start authentication of user %s', username);
if(!username || !password) {
replyWithUnauthorized(res);
return;
@ -34,29 +27,31 @@ function first_factor(req, res) {
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)
.then(function() {
req.session.auth_session.userid = username;
req.session.auth_session.first_factor = true;
objectPath.set(req, 'session.auth_session.userid', username);
objectPath.set(req, 'session.auth_session.first_factor', true);
logger.info('1st factor: LDAP binding successful');
logger.debug('1st factor: Retrieve email from LDAP');
return ldap.get_email(ldap_client, username, config.ldap_users_dn)
})
.then(function(doc) {
logger.debug('1st factor: document=%s', JSON.stringify(doc));
var email = objectPath.get(doc, 'mail');
logger.debug('1st factor: document=%s', JSON.stringify(doc));
logger.debug('1st factor: Retrieved email is %s', email);
req.session.auth_session.email = email;
objectPath.set(req, 'session.auth_session.email', email);
res.status(204);
res.send();
})
.catch(exceptions.LdapSearchError, function(err) {
logger.info('1st factor: Unable to retrieve email from LDAP');
logger.info('1st factor: Unable to retrieve email from LDAP', err);
res.status(500);
res.send();
})
.catch(exceptions.LdapBindError, function(err) {
logger.info('1st factor: LDAP binding failed');
logger.info('1st factor: LDAP binding failed', err);
replyWithUnauthorized(res);
})
.catch(function(err) {

View File

@ -0,0 +1,72 @@
var objectPath = require('object-path');
var ldap = require('../ldap');
var CHALLENGE = 'reset-password';
var icheck_interface = {
challenge: CHALLENGE,
render_template: 'reset-password',
pre_check_callback: pre_check,
}
module.exports = {
icheck_interface: icheck_interface,
post: protect(post)
}
function pre_check(req) {
var userid = objectPath.get(req, 'body.userid');
if(!userid) {
return Promise.reject('No user id provided');
}
var ldap_client = req.app.get('ldap client');
var config = req.app.get('config');
return ldap.get_email(ldap_client, userid, config.ldap_users_dn)
.then(function(doc) {
var email = objectPath.get(doc, 'mail');
var identity = {}
identity.email = email;
identity.userid = userid;
return Promise.resolve(identity);
})
}
function protect(fn) {
return function(req, res) {
var challenge = objectPath.get(req, 'session.auth_session.identity_check.challenge');
if(challenge != CHALLENGE) {
res.status(403);
res.send();
return;
}
fn(req, res);
  }
}
function post(req, res) {
var logger = req.app.get('logger');
var ldapjs = req.app.get('ldap');
var ldap_client = req.app.get('ldap client');
var new_password = objectPath.get(req, 'body.password');
var userid = objectPath.get(req, 'session.auth_session.identity_check.userid');
var config = req.app.get('config');
logger.info('POST reset-password: User %s wants to reset his/her password', userid);
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', {});
res.status(204);
res.send();
})
.catch(function(err) {
logger.error('POST reset-password: Error while resetting the password of user %s. %s', userid, err);
res.status(500);
res.send();
});
}

View File

@ -1,6 +1,7 @@
var u2f_register = require('./u2f_register');
var u2f_common = require('./u2f_common');
var objectPath = require('object-path');
module.exports = {
register_request: u2f_register.register_request,
@ -12,41 +13,18 @@ module.exports = {
sign: sign,
}
var objectPath = require('object-path');
function retrieveU2fMeta(req, user_data_storage) {
function retrieve_u2f_meta(req, user_data_storage) {
var userid = req.session.auth_session.userid;
var appid = u2f_common.extract_app_id(req);
return user_data_storage.get_u2f_meta(userid, appid);
}
function startU2fAuthentication(u2f, appid, meta) {
return new Promise(function(resolve, reject) {
u2f.startAuthentication(appid, [meta])
.then(function(authRequest) {
resolve(authRequest);
}, function(err) {
reject(err);
});
});
}
function finishU2fAuthentication(u2f, authRequest, data, meta) {
return new Promise(function(resolve, reject) {
u2f.finishAuthentication(authRequest, data, [meta])
.then(function(authenticationStatus) {
resolve(authenticationStatus);
}, function(err) {
reject(err);
})
});
}
function sign_request(req, res) {
var logger = req.app.get('logger');
var user_data_storage = req.app.get('user data store');
retrieveU2fMeta(req, user_data_storage)
retrieve_u2f_meta(req, user_data_storage)
.then(function(doc) {
if(!doc) {
u2f_common.reply_with_missing_registration(res);
@ -57,7 +35,7 @@ function sign_request(req, res) {
var meta = doc.meta;
var appid = u2f_common.extract_app_id(req);
logger.info('U2F sign_request: Start authentication');
return startU2fAuthentication(u2f, appid, meta);
return u2f.startAuthentication(appid, [meta])
})
.then(function(authRequest) {
logger.info('U2F sign_request: Store authentication request and reply');
@ -67,7 +45,8 @@ function sign_request(req, res) {
})
.catch(function(err) {
logger.info('U2F sign_request: %s', err);
u2f_common.reply_with_unauthorized(res);
res.status(500);
res.send();
});
}
@ -81,14 +60,14 @@ function sign(req, res) {
var logger = req.app.get('logger');
var user_data_storage = req.app.get('user data store');
retrieveU2fMeta(req, user_data_storage)
retrieve_u2f_meta(req, user_data_storage)
.then(function(doc) {
var appid = u2f_common.extract_app_id(req);
var u2f = req.app.get('u2f');
var authRequest = req.session.auth_session.sign_request;
var meta = doc.meta;
logger.info('U2F sign: Finish authentication');
return finishU2fAuthentication(u2f, authRequest, req.body, meta);
return u2f.finishAuthentication(authRequest, req.body, [meta])
})
.then(function(authenticationStatus) {
logger.info('U2F sign: Authentication successful');
@ -98,7 +77,7 @@ function sign(req, res) {
})
.catch(function(err) {
logger.error('U2F sign: %s', err);
res.status(401);
res.status(500);
res.send();
});
}

View File

@ -13,8 +13,15 @@ var u2f_common = require('./u2f_common');
var Promise = require('bluebird');
function register_request(req, res) {
var u2f = req.app.get('u2f');
var logger = req.app.get('logger');
var challenge = objectPath.get(req, 'session.auth_session.identity_check.challenge');
if(challenge != 'u2f-register') {
res.status(403);
res.send();
return;
}
var u2f = req.app.get('u2f');
var appid = u2f_common.extract_app_id(req);
logger.debug('U2F register_request: headers=%s', JSON.stringify(req.headers));
@ -28,19 +35,23 @@ function register_request(req, res) {
})
.catch(function(err) {
logger.error('U2F register_request: %s', err);
u2f_common.reply_with_internal_error(res, 'Unable to complete the registration');
res.status(500);
res.send('Unable to start registration request');
});
}
function register(req, res) {
if(!objectPath.has(req, 'session.auth_session.register_request')) {
u2f_common.reply_with_unauthorized(res);
var registrationRequest = objectPath.get(req, 'session.auth_session.register_request');
var challenge = objectPath.get(req, 'session.auth_session.identity_check.challenge');
if(!(registrationRequest && challenge == 'u2f-register')) {
res.status(403);
res.send();
return;
}
var user_data_storage = req.app.get('user data store');
var u2f = req.app.get('u2f');
var registrationRequest = req.session.auth_session.register_request;
var userid = req.session.auth_session.userid;
var appid = u2f_common.extract_app_id(req);
var logger = req.app.get('logger');
@ -65,7 +76,8 @@ function register(req, res) {
})
.catch(function(err) {
logger.error('U2F register: %s', err);
u2f_common.reply_with_unauthorized(res);
res.status(500);
res.send('Unable to register');
});
}

View File

@ -1,93 +1,36 @@
module.exports = {
get: register_handler_get,
post: register_handler_post
}
var objectPath = require('object-path');
var randomstring = require('randomstring');
var Promise = require('bluebird');
var util = require('util');
var u2f_common = require('./u2f_common');
var CHALLENGE = 'u2f-register';
function register_handler_get(req, res) {
var logger = req.app.get('logger');
logger.info('U2F register_handler: Continue registration process');
var icheck_interface = {
challenge: CHALLENGE,
render_template: 'u2f-register',
pre_check_callback: pre_check,
}
var registration_token = objectPath.get(req, 'query.registration_token');
logger.debug('U2F register_handler: registration_token=%s', registration_token);
module.exports = {
icheck_interface: icheck_interface,
}
if(!registration_token) {
res.status(403);
res.send();
return;
function pre_check(req) {
var first_factor_passed = objectPath.get(req, 'session.auth_session.first_factor');
if(!first_factor_passed) {
return Promise.reject('Authentication required before issuing a u2f registration request');
}
var user_data_store = req.app.get('user data store');
logger.debug('U2F register_handler: verify token validity and consume it');
user_data_store.consume_u2f_registration_token(registration_token)
.then(function() {
res.render('u2f_register');
})
.catch(function(err) {
res.status(403);
res.send();
});
}
function send_u2f_registration_email(email_sender, original_url, email, token) {
var url = util.format('%s?registration_token=%s', original_url, token);
var email_content = util.format('<a href="%s">Register</a>', url);
return email_sender.send(email, 'U2F Registration', email_content);
}
function register_handler_post(req, res) {
var logger = req.app.get('logger');
logger.info('U2F register_handler: Starting registration process');
logger.debug('U2F register_request: headers=%s', JSON.stringify(req.headers));
var userid = objectPath.get(req, 'session.auth_session.userid');
var email = objectPath.get(req, 'session.auth_session.email');
var first_factor_passed = objectPath.get(req, 'session.auth_session.first_factor');
// the user needs to have validated the first factor
if(!(userid && first_factor_passed)) {
var error = 'You need to be authenticated to register';
logger.error('U2F register_handler: %s', error);
res.status(403);
res.send(error);
return;
if(!(userid && email)) {
return Promise.reject('User ID or email is missing');
}
if(!email) {
var error = util.format('No email has been found for user %s', userid);
logger.error('U2F register_handler: %s', error);
res.status(400);
res.send(error);
return;
}
var five_minutes = 4 * 60 * 1000;
var user_data_store = req.app.get('user data store');
var token = randomstring.generate({ length: 64 });
logger.debug('U2F register_request: issue u2f registration token %s for 5 minutes', token);
user_data_store.save_u2f_registration_token(userid, token, five_minutes)
.then(function() {
logger.debug('U2F register_request: Send u2f registration email to %s', email);
var email_sender = req.app.get('email sender');
var original_url = u2f_common.extract_original_url(req);
return send_u2f_registration_email(email_sender, original_url, email, token);
})
.then(function() {
res.status(204);
res.send();
})
.catch(function(err) {
logger.error('U2F register_handler: %s', err);
res.status(500);
res.send();
});
var identity = {};
identity.email = email;
identity.userid = userid;
return Promise.resolve(identity);
}

View File

@ -22,8 +22,6 @@ function verify_filter(req, res) {
}
function verify(req, res) {
console.log('Verify authentication');
verify_filter(req, res)
.then(function() {
res.status(204);

View File

@ -11,12 +11,11 @@ var speakeasy = require('speakeasy');
var path = require('path');
var session = require('express-session');
var winston = require('winston');
var DataStore = require('nedb');
var nodemailer = require('nodemailer');
var UserDataStore = require('./user_data_store');
var EmailSender = require('./email_sender');
var identity_check = require('./identity_check');
function run(config, ldap_client, u2f, fn) {
function run(config, ldap_client, deps, fn) {
var view_directory = path.resolve(__dirname, '../views');
var public_html_directory = path.resolve(__dirname, '../public_html');
var datastore_options = {};
@ -47,32 +46,39 @@ function run(config, ldap_client, u2f, fn) {
winston.level = config.debug_level || 'info';
app.set('logger', winston);
app.set('ldap', deps.ldap);
app.set('ldap client', ldap_client);
app.set('totp engine', speakeasy);
app.set('u2f', u2f);
app.set('user data store', new UserDataStore(DataStore, datastore_options));
app.set('email sender', new EmailSender(nodemailer, email_options));
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('config', config);
var base_endpoint = '/authentication';
// web pages
app.get ('/login', routes.login);
app.get ('/logout', routes.logout);
app.get (base_endpoint + '/login', routes.login);
app.get (base_endpoint + '/logout', routes.logout);
app.get ('/u2f-register', routes.second_factor.u2f.register_handler_get);
app.post ('/u2f-register', routes.second_factor.u2f.register_handler_post);
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
app.post (base_endpoint + '/new-password', routes.reset_password.post);
// verify authentication
app.get ('/verify', routes.verify);
app.get (base_endpoint + '/verify', routes.verify);
// Authentication process
app.post ('/1stfactor', routes.first_factor);
app.post ('/2ndfactor/totp', routes.second_factor.totp);
app.post (base_endpoint + '/1stfactor', routes.first_factor);
app.post (base_endpoint + '/2ndfactor/totp', routes.second_factor.totp);
app.get ('/2ndfactor/u2f/register_request', routes.second_factor.u2f.register_request);
app.post ('/2ndfactor/u2f/register', routes.second_factor.u2f.register);
app.get (base_endpoint + '/2ndfactor/u2f/register_request', routes.second_factor.u2f.register_request);
app.post (base_endpoint + '/2ndfactor/u2f/register', routes.second_factor.u2f.register);
app.get ('/2ndfactor/u2f/sign_request', routes.second_factor.u2f.sign_request);
app.post ('/2ndfactor/u2f/sign', routes.second_factor.u2f.sign);
app.get (base_endpoint + '/2ndfactor/u2f/sign_request', routes.second_factor.u2f.sign_request);
app.post (base_endpoint + '/2ndfactor/u2f/sign', routes.second_factor.u2f.sign);
return app.listen(config.port, function(err) {
console.log('Listening on %d...', config.port);

View File

@ -6,8 +6,8 @@ var path = require('path');
function UserDataStore(DataStore, options) {
this._u2f_meta_collection = create_collection('u2f_meta', options, DataStore);
this._u2f_registration_tokens_collection =
create_collection('u2f_registration_tokens', options, DataStore);
this._identity_check_tokens_collection =
create_collection('identity_check_tokens', options, DataStore);
}
function create_collection(name, options, DataStore) {
@ -42,35 +42,41 @@ UserDataStore.prototype.get_u2f_meta = function(userid, app_id) {
return this._u2f_meta_collection.findOneAsync(filter);
}
UserDataStore.prototype.save_u2f_registration_token = function(userid, token, max_age) {
UserDataStore.prototype.issue_identity_check_token = function(userid, token, data, max_age) {
var newDocument = {};
newDocument.userid = userid;
newDocument.token = token;
newDocument.content = { userid: userid, data: data };
newDocument.max_date = new Date(new Date().getTime() + max_age);
return this._u2f_registration_tokens_collection.insertAsync(newDocument);
return this._identity_check_tokens_collection.insertAsync(newDocument);
}
UserDataStore.prototype.consume_u2f_registration_token = function(token) {
UserDataStore.prototype.consume_identity_check_token = function(token) {
var query = {};
query.token = token;
var that = this;
var doc_content;
return this._u2f_registration_tokens_collection.findOneAsync(query)
return this._identity_check_tokens_collection.findOneAsync(query)
.then(function(doc) {
if(!doc) {
return Promise.reject('Registration token does not exist');
}
if(!doc) {
return Promise.reject('Registration token does not exist');
}
var max_date = doc.max_date;
var current_date = new Date();
if(current_date > max_date) {
return Promise.reject('Registration token is not valid anymore');
}
var max_date = doc.max_date;
var current_date = new Date();
if(current_date > max_date) {
return Promise.reject('Registration token is not valid anymore');
}
return Promise.resolve();
doc_content = doc.content;
return Promise.resolve();
})
.then(function() {
return that._u2f_registration_tokens_collection.removeAsync(query);
});
return that._identity_check_tokens_collection.removeAsync(query);
})
.then(function() {
return Promise.resolve(doc_content);
})
}

View File

@ -43,6 +43,8 @@ body {
.login h2 { color: #fff; text-shadow: 0 0 10px rgba(0,0,0,0.3); letter-spacing:1px; text-align:center; font-size: 1em; }
.login p { color: #fff; text-shadow: 0 0 10px rgba(0,0,0,0.3); letter-spacing:1px; text-align:center; }
input {
width: 100%;
margin-bottom: 10px;
@ -98,6 +100,6 @@ input:focus { box-shadow: inset 0 -5px 45px rgba(100,100,100,0.4), 0 1px 1px rgb
float: right;
}
#second-factor #u2f button {
button {
margin-top: 5px;
}

View File

@ -30,6 +30,11 @@ function onLoginButtonClicked() {
});
}
function onResetPasswordButtonClicked() {
var r = '/authentication/reset-password-form';
window.location.replace(r);
}
function onTotpSignButtonClicked() {
var token = $('#totp-token').val();
validateSecondFactorTotp(token, function(err) {
@ -64,7 +69,7 @@ function onU2fRegistrationButtonClicked() {
function askForU2fRegistration(fn) {
$.ajax({
type: 'POST',
url: '/auth/u2f-register'
url: '/authentication/u2f-register'
})
.done(function(data) {
fn(undefined, data);
@ -91,7 +96,7 @@ function finishU2fAuthentication(url, responseData, fn) {
}
function startU2fAuthentication(fn, timeout) {
$.get('/auth/2ndfactor/u2f/sign_request', {}, null, 'json')
$.get('/authentication/2ndfactor/u2f/sign_request', {}, null, 'json')
.done(function(signResponse) {
var registeredKeys = signResponse.registeredKeys;
$.notify('Please touch the token', 'info');
@ -104,7 +109,7 @@ function startU2fAuthentication(fn, timeout) {
if (response.errorCode) {
fn(response);
} else {
finishU2fAuthentication('/auth/2ndfactor/u2f/sign', response, fn);
finishU2fAuthentication('/authentication/2ndfactor/u2f/sign', response, fn);
}
},
timeout
@ -116,7 +121,7 @@ function startU2fAuthentication(fn, timeout) {
}
function validateSecondFactorTotp(token, fn) {
$.post('/auth/2ndfactor/totp', {
$.post('/authentication/2ndfactor/totp', {
token: token,
})
.done(function() {
@ -128,7 +133,7 @@ function validateSecondFactorTotp(token, fn) {
}
function validateFirstFactor(username, password, fn) {
$.post('/auth/1stfactor', {
$.post('/authentication/1stfactor', {
username: username,
password: password,
})
@ -200,7 +205,6 @@ function hideSecondFactorLayout() {
function setupFirstFactorLoginButton() {
$('#first-factor #login-button').on('click', onLoginButtonClicked);
setupEnterKeypressListener('#login-form', onLoginButtonClicked);
$('#first-factor #information').hide();
}
function cleanupFirstFactorLoginButton() {
@ -221,10 +225,15 @@ function setupU2fRegistrationButton() {
$('#second-factor #u2f-register-button').on('click', onU2fRegistrationButtonClicked);
}
function setupResetPasswordButton() {
$('#first-factor #reset-password-button').on('click', onResetPasswordButtonClicked);
}
function enterFirstFactor() {
showFirstFactorLayout();
hideSecondFactorLayout();
setupFirstFactorLoginButton();
setupResetPasswordButton();
}
function enterSecondFactor() {

View File

@ -0,0 +1,47 @@
(function() {
function setupEnterKeypressListener(filter, fn) {
$(filter).on('keydown', 'input', function (e) {
var key = e.which;
switch (key) {
case 13: // enter key code
fn();
break;
default:
break;
}
});
}
function onResetPasswordButtonClicked() {
var username = $('#username').val();
if(!username) {
$.notify('You must provide your username to reset your password.', 'warn');
return;
}
$.post('/authentication/reset-password', {
userid: username,
})
.done(function() {
$.notify('An email has been sent. Click on the link to change your password', 'success');
setTimeout(function() {
window.location.replace('/authentication/login');
}, 1000);
})
.fail(function() {
$.notify('Are you sure this is your username?', 'warn');
});
}
function setupResetPasswordButton() {
$('#reset-password-button').on('click', onResetPasswordButtonClicked);
}
$(document).ready(function() {
setupResetPasswordButton();
setupEnterKeypressListener('#reset-password-form', onResetPasswordButtonClicked);
});
})();

View File

@ -0,0 +1,51 @@
(function() {
function setupEnterKeypressListener(filter, fn) {
$(filter).on('keydown', 'input', function (e) {
var key = e.which;
switch (key) {
case 13: // enter key code
fn();
break;
default:
break;
}
});
}
function onResetPasswordButtonClicked() {
var password1 = $('#password1').val();
var password2 = $('#password2').val();
if(!password1 || !password2) {
$.notify('You must enter your new password twice.', 'warn');
return;
}
if(password1 != password2) {
$.notify('The passwords are different', 'warn');
return;
}
$.post('/authentication/new-password', {
password: password1,
})
.done(function() {
$.notify('Your password has been changed. Please login again', 'success');
window.location.replace('/authentication/login');
})
.fail(function() {
$.notify('An error occurred during password change.', 'warn');
});
}
function setupResetPasswordButton() {
$('#reset-password-button').on('click', onResetPasswordButtonClicked);
}
$(document).ready(function() {
setupResetPasswordButton();
setupEnterKeypressListener('#reset-password-form', onResetPasswordButtonClicked);
});
})();

View File

@ -4,7 +4,6 @@ params={};
location.search.replace(/[?&]+([^=&]+)=([^&]*)/gi,function(s,k,v){params[k]=v});
function finishRegister(url, responseData, fn) {
console.log(responseData);
$.ajax({
type: 'POST',
url: url,
@ -21,7 +20,7 @@ function finishRegister(url, responseData, fn) {
}
function startRegister(fn, timeout) {
$.get('/auth/2ndfactor/u2f/register_request', {}, null, 'json')
$.get('/authentication/2ndfactor/u2f/register_request', {}, null, 'json')
.done(function(startRegisterResponse) {
u2f.register(
startRegisterResponse.appId,
@ -31,7 +30,7 @@ function startRegister(fn, timeout) {
if (response.errorCode) {
fn(response.errorCode);
} else {
finishRegister('/auth/2ndfactor/u2f/register', response, fn);
finishRegister('/authentication/2ndfactor/u2f/register', response, fn);
}
},
timeout

1
src/views/head.ejs Normal file
View File

@ -0,0 +1 @@
<link rel="stylesheet" type="text/css" href="css/login.css">

View File

@ -1,7 +1,7 @@
<!DOCTYPE html>
<head>
<title>Login Portal</title>
<link rel="stylesheet" type="text/css" href="login.css">
<title>Login</title>
<% include head %>
</head>
<body>
<div id="first-factor" class="login">
@ -10,6 +10,7 @@
<input type="text" name="username" id="username" placeholder="Username" required="required" />
<input type="password" name="password" id="password" placeholder="Password" required="required" />
<button type="button" id="login-button" class="btn btn-primary btn-block btn-large">Enter</button>
<button type="button" id="reset-password-button" class="btn btn-primary btn-block btn-large">Reset password</button>
</div>
</div>
@ -27,8 +28,7 @@
</div>
</div>
</body>
<script src="jquery.min.js"></script>
<script src="notify.min.js"></script>
<script src="u2f-api.js"></script>
<script src="login.js"></script>
<% include scripts %>
<script src="js/u2f-api.js"></script>
<script src="js/login.js"></script>
</html>

View File

@ -0,0 +1,18 @@
<!DOCTYPE html>
<head>
<title>Reset Password</title>
<% include head %>
</head>
<body>
<div id="reset-password" class="login">
<h1>Reset your password</h1>
<p>What's your username? You will receive an email to change your password</p>
<div id="reset-password-form">
<input type="text" name="username" id="username" placeholder="Your username" required="required" />
<button type="button" id="reset-password-button" class="btn btn-primary btn-block btn-large">Reset Password</button>
</div>
</div>
</body>
<% include scripts %>
<script src="js/reset-password-form.js"></script>
</html>

View File

@ -0,0 +1,19 @@
<!DOCTYPE html>
<head>
<title>Reset Password</title>
<% include head %>
</head>
<body>
<div id="reset-password" class="login">
<h1>Reset your password</h1>
<p>Please type your new password.</p>
<div id="reset-password-form">
<input type="password" name="password1" id="password1" placeholder="New password" required="required" />
<input type="password" name="password2" id="password2" placeholder="Password confirmation" required="required" />
<button type="button" id="reset-password-button" class="btn btn-primary btn-block btn-large">Confirm</button>
</div>
</div>
</body>
<% include scripts %>
<script src="js/reset-password.js"></script>
</html>

2
src/views/scripts.ejs Normal file
View File

@ -0,0 +1,2 @@
<script src="js/jquery.min.js"></script>
<script src="js/notify.min.js"></script>

View File

@ -1,7 +1,7 @@
<!DOCTYPE html>
<head>
<title>FIDO U2F Registration</title>
<link rel="stylesheet" type="text/css" href="login.css">
<% include head %>
</head>
<body>
<div class="login">
@ -10,8 +10,7 @@
</div>
</body>
<script src="jquery.min.js"></script>
<script src="notify.min.js"></script>
<script src="u2f-api.js"></script>
<script src="u2f-register.js"></script>
<% include scripts %>
<script src="js/u2f-api.js"></script>
<script src="js/u2f-register.js"></script>
</html>

130
test/unitary/requests.js Normal file
View File

@ -0,0 +1,130 @@
var Promise = require('bluebird');
var request = Promise.promisifyAll(require('request'));
var assert = require('assert');
module.exports = function(port) {
var PORT = port;
var BASE_URL = 'http://localhost:' + PORT;
function execute_reset_password(jar, transporter, user, new_password) {
return request.postAsync({
url: BASE_URL + '/authentication/reset-password',
jar: jar,
form: { userid: user }
})
.then(function(res) {
assert.equal(res.statusCode, 204);
var html_content = transporter.sendMail.getCall(0).args[0].html;
var regexp = /identity_token=([a-zA-Z0-9]+)/;
var token = regexp.exec(html_content)[1];
// console.log(html_content, token);
return request.getAsync({
url: BASE_URL + '/authentication/reset-password?identity_token=' + token,
jar: jar
})
})
.then(function(res) {
assert.equal(res.statusCode, 200);
return request.postAsync({
url: BASE_URL + '/authentication/new-password',
jar: jar,
form: {
password: new_password
}
});
});
}
function execute_totp(jar, token) {
return request.postAsync({
url: BASE_URL + '/authentication/2ndfactor/totp',
jar: jar,
form: {
token: token
}
});
}
function execute_u2f_authentication(jar) {
return request.getAsync({
url: BASE_URL + '/authentication/2ndfactor/u2f/sign_request',
jar: jar
})
.then(function(res) {
assert.equal(res.statusCode, 200);
return request.postAsync({
url: BASE_URL + '/authentication/2ndfactor/u2f/sign',
jar: jar,
form: {
}
});
});
}
function execute_verification(jar) {
return request.getAsync({ url: BASE_URL + '/authentication/verify', jar: jar })
}
function execute_login(jar) {
return request.getAsync({ url: BASE_URL + '/authentication/login', jar: jar })
}
function execute_u2f_registration(jar, transporter) {
return request.postAsync({
url: BASE_URL + '/authentication/u2f-register',
jar: jar
})
.then(function(res) {
assert.equal(res.statusCode, 204);
var html_content = transporter.sendMail.getCall(0).args[0].html;
var regexp = /identity_token=([a-zA-Z0-9]+)/;
var token = regexp.exec(html_content)[1];
// console.log(html_content, token);
return request.getAsync({
url: BASE_URL + '/authentication/u2f-register?identity_token=' + token,
jar: jar
})
})
.then(function(res) {
assert.equal(res.statusCode, 200);
return request.getAsync({
url: BASE_URL + '/authentication/2ndfactor/u2f/register_request',
jar: jar,
});
})
.then(function(res) {
assert.equal(res.statusCode, 200);
return request.postAsync({
url: BASE_URL + '/authentication/2ndfactor/u2f/register',
jar: jar,
form: {
s: 'test'
}
});
});
}
function execute_first_factor(jar) {
return request.postAsync({
url: BASE_URL + '/authentication/1stfactor',
jar: jar,
form: {
username: 'test_ok',
password: 'password'
}
});
}
return {
login: execute_login,
verify: execute_verification,
reset_password: execute_reset_password,
u2f_authentication: execute_u2f_authentication,
u2f_registration: execute_u2f_registration,
first_factor: execute_first_factor,
totp: execute_totp,
}
}

View File

@ -0,0 +1,140 @@
var sinon = require('sinon');
var winston = require('winston');
var reset_password = require('../../../src/lib/routes/reset_password');
var assert = require('assert');
describe('test reset password', function() {
var req, res;
var user_data_store;
var ldap_client;
var ldap;
beforeEach(function() {
req = {}
req.body = {};
req.body.userid = 'user';
req.app = {};
req.app.get = sinon.stub();
req.app.get.withArgs('logger').returns(winston);
req.session = {};
req.session.auth_session = {};
req.session.auth_session.userid = 'user';
req.session.auth_session.email = 'user@example.com';
req.session.auth_session.first_factor = true;
req.session.auth_session.second_factor = false;
req.headers = {};
req.headers.host = 'localhost';
var options = {};
options.inMemoryOnly = true;
user_data_store = {};
user_data_store.set_u2f_meta = sinon.stub().returns(Promise.resolve({}));
user_data_store.get_u2f_meta = sinon.stub().returns(Promise.resolve({}));
user_data_store.issue_identity_check_token = sinon.stub().returns(Promise.resolve({}));
user_data_store.consume_identity_check_token = sinon.stub().returns(Promise.resolve({}));
req.app.get.withArgs('user data store').returns(user_data_store);
ldap = {};
ldap.Change = sinon.spy();
req.app.get.withArgs('ldap').returns(ldap);
ldap_client = {};
ldap_client.bind = sinon.stub();
ldap_client.search = sinon.stub();
ldap_client.modify = sinon.stub();
req.app.get.withArgs('ldap client').returns(ldap_client);
config = {};
config.ldap_users_dn = 'dc=example,dc=com';
req.app.get.withArgs('config').returns(config);
res = {};
res.send = sinon.spy();
res.json = sinon.spy();
res.status = sinon.spy();
});
describe('test reset password identity pre check', test_reset_password_check);
describe('test reset password post', test_reset_password_post);
function test_reset_password_check() {
it('should fail when no userid is provided', function(done) {
req.body.userid = undefined;
reset_password.icheck_interface.pre_check_callback(req)
.catch(function(err) {
done();
});
});
it('should fail if ldap fail', function(done) {
ldap_client.search.yields('Internal error');
reset_password.icheck_interface.pre_check_callback(req)
.catch(function(err) {
done();
});
});
it('should returns identity when ldap replies', function(done) {
var doc = {};
doc.object = {};
doc.object.email = 'test@example.com';
doc.object.userid = 'user';
var res = {};
res.on = sinon.stub();
res.on.withArgs('searchEntry').yields(doc);
res.on.withArgs('end').yields();
ldap_client.search.yields(undefined, res);
reset_password.icheck_interface.pre_check_callback(req)
.then(function() {
done();
});
});
}
function test_reset_password_post() {
it('should update the password', 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';
req.body = {};
req.body.password = 'new-password';
ldap_client.modify.yields(undefined);
ldap_client.bind.yields(undefined);
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);
done();
});
reset_password.post(req, res);
});
it('should fail if identity_challenge does not exist', function(done) {
req.session.auth_session.identity_check = {};
req.session.auth_session.identity_check.challenge = undefined;
res.send = sinon.spy(function() {
assert.equal(res.status.getCall(0).args[0], 403);
done();
});
reset_password.post(req, res);
});
it('should fail when ldap fails', function(done) {
req.session.auth_session.identity_check = {};
req.session.auth_session.identity_check.challenge = 'reset-password';
req.body = {};
req.body.password = 'new-password';
ldap_client.bind.yields(undefined);
ldap_client.modify.yields('Internal error with LDAP');
res.send = sinon.spy(function() {
assert.equal(res.status.getCall(0).args[0], 500);
done();
});
reset_password.post(req, res);
});
}
});

View File

@ -19,6 +19,9 @@ describe('test u2f routes', function() {
req.session.auth_session.userid = 'user';
req.session.auth_session.first_factor = true;
req.session.auth_session.second_factor = false;
req.session.auth_session.identity_check = {};
req.session.auth_session.identity_check.challenge = 'u2f-register';
req.session.auth_session.register_request = {};
req.headers = {};
req.headers.host = 'localhost';
@ -73,6 +76,15 @@ describe('test u2f routes', function() {
req.app.get.withArgs('u2f').returns(u2f_mock);
u2f.register_request(req, res);
});
it('should return forbidden if identity has not been verified', function(done) {
res.send = sinon.spy(function(data) {
assert.equal(403, res.status.getCall(0).args[0]);
done();
});
req.session.auth_session.identity_check = undefined;
u2f.register_request(req, res);
});
}
function test_registration() {
@ -97,7 +109,7 @@ describe('test u2f routes', function() {
it('should return unauthorized on finishRegistration error', function(done) {
res.send = sinon.spy(function(data) {
assert.equal(401, res.status.getCall(0).args[0]);
assert.equal(500, res.status.getCall(0).args[0]);
done();
});
var user_key_container = {};
@ -110,9 +122,9 @@ describe('test u2f routes', function() {
u2f.register(req, res);
});
it('should return unauthorized error when no auth request has been initiated', function(done) {
it('should return forbidden error when no auth request has been initiated', function(done) {
res.send = sinon.spy(function(data) {
assert.equal(401, res.status.getCall(0).args[0]);
assert.equal(403, res.status.getCall(0).args[0]);
done();
});
var user_key_container = {};
@ -120,9 +132,19 @@ describe('test u2f routes', function() {
u2f_mock.finishRegistration = sinon.stub();
u2f_mock.finishRegistration.returns(Promise.resolve());
req.session.auth_session.register_request = undefined;
req.app.get.withArgs('u2f').returns(u2f_mock);
u2f.register(req, res);
});
it('should return forbidden error when identity has not been verified', function(done) {
res.send = sinon.spy(function(data) {
assert.equal(403, res.status.getCall(0).args[0]);
done();
});
req.session.auth_session.identity_check = undefined;
u2f.register(req, res);
});
}
function test_signing_request() {
@ -148,7 +170,7 @@ describe('test u2f routes', function() {
it('should return unauthorized error on registration request error', function(done) {
res.send = sinon.spy(function(data) {
assert.equal(401, res.status.getCall(0).args[0]);
assert.equal(500, res.status.getCall(0).args[0]);
done();
});
var user_key_container = {};
@ -209,7 +231,7 @@ describe('test u2f routes', function() {
it('should return unauthorized error on registration request internal error', function(done) {
res.send = sinon.spy(function(data) {
assert.equal(401, res.status.getCall(0).args[0]);
assert.equal(500, res.status.getCall(0).args[0]);
done();
});
var user_key_container = {};

View File

@ -1,10 +1,9 @@
var sinon = require('sinon');
var winston = require('winston');
var u2f_register = require('../../../src/lib/routes/u2f_register');
var u2f_register = require('../../../src/lib/routes/u2f_register_handler');
var assert = require('assert');
describe('test register handle', function() {
describe('test register handler', function() {
var req, res;
var user_data_store;
@ -28,88 +27,52 @@ describe('test register handle', function() {
user_data_store = {};
user_data_store.set_u2f_meta = sinon.stub().returns(Promise.resolve({}));
user_data_store.get_u2f_meta = sinon.stub().returns(Promise.resolve({}));
user_data_store.save_u2f_registration_token = sinon.stub().returns(Promise.resolve({}));
user_data_store.consume_u2f_registration_token = sinon.stub().returns(Promise.resolve({}));
user_data_store.issue_identity_check_token = sinon.stub().returns(Promise.resolve({}));
user_data_store.consume_identity_check_token = sinon.stub().returns(Promise.resolve({}));
req.app.get.withArgs('user data store').returns(user_data_store);
res = {};
res.send = sinon.spy();
res.json = sinon.spy();
res.status = sinon.spy();
})
});
describe('test u2f registration check', test_registration_check);
describe('test registration handler (POST)', test_registration_handler_post);
describe('test registration handler (GET)', test_registration_handler_get);
function test_registration_handler_post() {
it('should issue a registration token', function(done) {
res.send = sinon.spy(function() {
assert.equal(204, res.status.getCall(0).args[0]);
assert.equal('user', user_data_store.save_u2f_registration_token.getCall(0).args[0]);
assert.equal(4 * 60 * 1000, user_data_store.save_u2f_registration_token.getCall(0).args[2]);
function test_registration_check() {
it('should fail if first_factor has not been passed', function(done) {
req.session.auth_session.first_factor = false;
u2f_register.icheck_interface.pre_check_callback(req)
.catch(function(err) {
done();
});
var email_sender = {};
email_sender.send = sinon.stub().returns(Promise.resolve());
req.app.get.withArgs('email sender').returns(email_sender);
u2f_register.register_handler_post(req, res);
});
it('should fail during issuance of a registration token', function(done) {
res.send = sinon.spy(function() {
assert.equal(500, res.status.getCall(0).args[0]);
it('should fail if userid is missing', function(done) {
req.session.auth_session.first_factor = false;
req.session.auth_session.userid = undefined;
u2f_register.icheck_interface.pre_check_callback(req)
.catch(function(err) {
done();
});
user_data_store.save_u2f_registration_token = sinon.stub().returns(Promise.reject('Error'));
u2f_register.register_handler_post(req, res);
});
it('should send bad request if no email has been found for the given user', function(done) {
res.send = sinon.spy(function() {
assert.equal(400, res.status.getCall(0).args[0]);
done();
});
it('should fail if email is missing', function(done) {
req.session.auth_session.first_factor = false;
req.session.auth_session.email = undefined;
var email_sender = {};
email_sender.send = sinon.stub().returns(Promise.resolve());
req.app.get.withArgs('email sender').returns(email_sender);
u2f_register.register_handler_post(req, res);
});
}
function test_registration_handler_get() {
it('should send forbidden if no registration_token has been provided', function(done) {
res.send = sinon.spy(function() {
assert.equal(403, res.status.getCall(0).args[0]);
u2f_register.icheck_interface.pre_check_callback(req)
.catch(function(err) {
done();
});
u2f_register.register_handler_get(req, res);
});
it('should render the u2f-register view when registration token is still valid', function(done) {
res.render = sinon.spy(function(data) {
assert.equal('u2f_register', data);
it('should succeed if first factor passed, userid and email are provided', function(done) {
u2f_register.icheck_interface.pre_check_callback(req)
.then(function(err) {
done();
});
req.query = {};
req.query.registration_token = 'token';
u2f_register.register_handler_get(req, res);
});
it('should send forbidden status when registration token is not valid', function(done) {
res.send = sinon.spy(function(data) {
assert.equal(403, res.status.getCall(0).args[0]);
done();
});
req.params = {};
req.params.registration_token = 'token';
user_data_store.consume_u2f_registration_token = sinon.stub().returns(Promise.reject('Not valid anymore'));
u2f_register.register_handler_get(req, res);
});
}
});

View File

@ -1,18 +1,20 @@
var server = require('../../src/lib/server');
var request = require('request');
var Promise = require('bluebird');
var request = Promise.promisifyAll(require('request'));
var assert = require('assert');
var speakeasy = require('speakeasy');
var sinon = require('sinon');
var Promise = require('bluebird');
var tmp = require('tmp');
var request = Promise.promisifyAll(request);
var nedb = require('nedb');
var PORT = 8050;
var BASE_URL = 'http://localhost:' + PORT;
var requests = require('./requests')(PORT);
describe('test data persistence', function() {
var u2f;
var tmpDir;
@ -73,38 +75,52 @@ describe('test data persistence', function() {
u2f.finishRegistration.returns(Promise.resolve(sign_status));
u2f.startAuthentication.returns(Promise.resolve(registration_request));
u2f.finishAuthentication.returns(Promise.resolve(registration_status));
var nodemailer = {};
var transporter = {
sendMail: sinon.stub().yields()
};
nodemailer.createTransport = sinon.spy(function() {
return transporter;
});
var deps = {};
deps.u2f = u2f;
deps.nedb = nedb;
deps.nodemailer = nodemailer;
var j1 = request.jar();
var j2 = request.jar();
return start_server(config, ldap_client, u2f)
return start_server(config, ldap_client, deps)
.then(function(s) {
server = s;
return execute_login(j1);
return requests.login(j1);
})
.then(function(res) {
return execute_first_factor(j1);
return requests.first_factor(j1);
})
.then(function() {
return execute_u2f_registration(j1);
return requests.u2f_registration(j1, transporter);
})
.then(function() {
return execute_u2f_authentication(j1);
return requests.u2f_authentication(j1);
})
.then(function() {
return stop_server(server);
})
.then(function() {
return start_server(config, ldap_client, u2f)
return start_server(config, ldap_client, deps)
})
.then(function(s) {
server = s;
return execute_login(j2);
return requests.login(j2);
})
.then(function() {
return execute_first_factor(j2);
return requests.first_factor(j2);
})
.then(function() {
return execute_u2f_authentication(j2);
return requests.u2f_authentication(j2);
})
.then(function(res) {
assert.equal(204, res.statusCode);
@ -117,9 +133,9 @@ describe('test data persistence', function() {
});
});
function start_server(config, ldap_client, u2f) {
function start_server(config, ldap_client, deps) {
return new Promise(function(resolve, reject) {
var s = server.run(config, ldap_client, u2f);
var s = server.run(config, ldap_client, deps);
resolve(s);
});
}
@ -130,55 +146,4 @@ describe('test data persistence', function() {
resolve();
});
}
function execute_first_factor(jar) {
return request.postAsync({
url: BASE_URL + '/1stfactor',
jar: jar,
form: {
username: 'test_ok',
password: 'password'
}
});
}
function execute_u2f_registration(jar) {
return request.getAsync({
url: BASE_URL + '/2ndfactor/u2f/register_request',
jar: jar
})
.then(function(res) {
return request.postAsync({
url: BASE_URL + '/2ndfactor/u2f/register',
jar: jar,
form: {
s: 'test'
}
});
});
}
function execute_u2f_authentication(jar) {
return request.getAsync({
url: BASE_URL + '/2ndfactor/u2f/sign_request',
jar: jar
})
.then(function() {
return request.postAsync({
url: BASE_URL + '/2ndfactor/u2f/sign',
jar: jar,
form: {
s: 'test'
}
});
});
}
function execute_verification(jar) {
return request.getAsync({ url: BASE_URL + '/verify', jar: jar })
}
function execute_login(jar) {
return request.getAsync({ url: BASE_URL + '/login', jar: jar })
}
});

View File

@ -0,0 +1,208 @@
var sinon = require('sinon');
var identity_check = require('../../src/lib/identity_check');
var exceptions = require('../../src/lib/exceptions');
var assert = require('assert');
var winston = require('winston');
var Promise = require('bluebird');
describe('test identity check process', function() {
var req, res, app, icheck_interface;
var user_data_store;
var email_sender;
beforeEach(function() {
req = {};
res = {};
app = {};
icheck_interface = {};
icheck_interface.pre_check_callback = sinon.stub();
user_data_store = {};
user_data_store.issue_identity_check_token = sinon.stub();
user_data_store.issue_identity_check_token.returns(Promise.resolve());
user_data_store.consume_identity_check_token = sinon.stub();
user_data_store.consume_identity_check_token.returns(Promise.resolve({ userid: 'user' }));
email_sender = {};
email_sender.send = sinon.stub();
email_sender.send = sinon.stub().returns(Promise.resolve());
req.headers = {};
req.session = {};
req.session.auth_session = {};
req.query = {};
req.app = {};
req.app.get = sinon.stub();
req.app.get.withArgs('logger').returns(winston);
req.app.get.withArgs('user data store').returns(user_data_store);
req.app.get.withArgs('email sender').returns(email_sender);
res.status = sinon.spy();
res.send = sinon.spy();
res.redirect = sinon.spy();
res.render = sinon.spy();
app.get = sinon.spy();
app.post = sinon.spy();
});
it('should register a POST and GET endpoint', function() {
var app = {};
app.get = sinon.spy();
app.post = sinon.spy();
var endpoint = '/test';
var icheck_interface = {};
identity_check(app, endpoint, icheck_interface);
assert(app.get.calledOnce);
assert(app.get.calledWith(endpoint));
assert(app.post.calledOnce);
assert(app.post.calledWith(endpoint));
});
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';
icheck_interface.pre_check_callback.returns(Promise.reject('No access'));
identity_check(app, endpoint, icheck_interface);
res.send = sinon.spy(function() {
assert.equal(res.status.getCall(0).args[0], 403);
done();
});
var handler = app.post.getCall(0).args[1];
handler(req, res);
});
it('should send 400 if email is missing in provided identity', function(done) {
var endpoint = '/protected';
var identity = { userid: 'abc' };
icheck_interface.pre_check_callback.returns(Promise.resolve(identity));
identity_check(app, endpoint, icheck_interface);
res.send = sinon.spy(function() {
assert.equal(res.status.getCall(0).args[0], 400);
done();
});
var handler = app.post.getCall(0).args[1];
handler(req, res);
});
it('should send 400 if userid is missing in provided identity', function(done) {
var endpoint = '/protected';
var identity = { email: 'abc@example.com' };
icheck_interface.pre_check_callback.returns(Promise.resolve(identity));
identity_check(app, endpoint, icheck_interface);
res.send = sinon.spy(function() {
assert.equal(res.status.getCall(0).args[0], 400);
done();
});
var handler = app.post.getCall(0).args[1];
handler(req, res);
});
it('should issue a token, send an email and return 204', function(done) {
var endpoint = '/protected';
var identity = { userid: 'user', email: 'abc@example.com' };
req.headers.host = 'localhost';
req.headers['x-original-uri'] = '/auth/test';
icheck_interface.pre_check_callback.returns(Promise.resolve(identity));
identity_check(app, endpoint, icheck_interface);
res.send = sinon.spy(function() {
assert.equal(res.status.getCall(0).args[0], 204);
assert(email_sender.send.calledOnce);
assert(user_data_store.issue_identity_check_token.calledOnce);
assert.equal(user_data_store.issue_identity_check_token.getCall(0).args[0], 'user');
assert.equal(user_data_store.issue_identity_check_token.getCall(0).args[3], 240000);
assert(email_sender.send.getCall(0).args[2].startsWith('<a href="https://localhost/auth/test?identity_token='));
done();
});
var handler = app.post.getCall(0).args[1];
handler(req, res);
});
}
function test_get_handler() {
it('should send 403 if no identity_token is provided', function(done) {
var endpoint = '/protected';
identity_check(app, endpoint, icheck_interface);
res.send = sinon.spy(function() {
assert.equal(res.status.getCall(0).args[0], 403);
done();
});
var handler = app.get.getCall(0).args[1];
handler(req, res);
});
it('should render template if identity_token is provided and still valid', function(done) {
req.query.identity_token = 'token';
var endpoint = '/protected';
icheck_interface.render_template = 'template';
identity_check(app, endpoint, icheck_interface);
res.render = sinon.spy(function(template) {
assert.equal(template, 'template');
done();
});
var handler = app.get.getCall(0).args[1];
handler(req, res);
});
it('should return 403 if identity_token is provided but invalid', function(done) {
req.query.identity_token = 'token';
var endpoint = '/protected';
icheck_interface.render_template = 'template';
user_data_store.consume_identity_check_token
.returns(Promise.reject('Invalid token'));
identity_check(app, endpoint, icheck_interface);
res.send = sinon.spy(function(template) {
assert.equal(res.status.getCall(0).args[0], 403);
done();
});
var handler = app.get.getCall(0).args[1];
handler(req, res);
});
it('should set the identity_check session object even if session does not exist yet', function(done) {
req.query.identity_token = 'token';
var endpoint = '/protected';
req.session = {};
icheck_interface.render_template = 'template';
identity_check(app, endpoint, icheck_interface);
res.render = sinon.spy(function(template) {
assert.equal(req.session.auth_session.identity_check.userid, 'user');
assert.equal(template, 'template');
done();
});
var handler = app.get.getCall(0).args[1];
handler(req, res);
});
}
});

View File

@ -11,12 +11,15 @@ describe('test ldap validation', function() {
beforeEach(function() {
ldap_client = {
bind: sinon.stub(),
search: sinon.stub()
search: sinon.stub(),
modify: sinon.stub(),
Change: sinon.spy()
}
});
describe('test binding', test_binding);
describe('test get email', test_get_email);
describe('test update password', test_update_password);
function test_binding() {
function test_validate() {
@ -85,5 +88,53 @@ describe('test ldap validation', function() {
})
});
}
function test_update_password() {
it('should update the password successfully', function(done) {
var change = {};
change.operation = 'replace';
change.modification = {};
change.modification.userPassword = 'new-password';
var config = {};
config.ldap_users_dn = 'dc=example,dc=com';
config.ldap_user = 'admin';
var userdn = 'cn=user,dc=example,dc=com';
var ldapjs = {};
ldapjs.Change = sinon.spy();
ldap_client.bind.yields(undefined);
ldap_client.modify.yields(undefined);
ldap.update_password(ldap_client, ldapjs, 'user', 'new-password', config)
.then(function() {
assert.deepEqual(ldap_client.modify.getCall(0).args[0], userdn);
assert.deepEqual(ldapjs.Change.getCall(0).args[0].operation, change.operation);
var userPassword = ldapjs.Change.getCall(0).args[0].modification.userPassword;
assert(/{SSHA}/.test(userPassword));
done();
})
});
it('should fail when ldap throws an error', function(done) {
ldap_client.bind.yields(undefined);
ldap_client.modify.yields('Error');
var config = {};
config.ldap_users_dn = 'dc=example,dc=com';
config.ldap_user = 'admin';
var ldapjs = {};
ldapjs.Change = sinon.spy();
ldap.update_password(ldap_client, ldapjs, 'user', 'new-password', config)
.catch(function() {
done();
})
});
}
});

View File

@ -1,30 +1,39 @@
var server = require('../../src/lib/server');
var request = require('request');
var Promise = require('bluebird');
var request = Promise.promisifyAll(require('request'));
var assert = require('assert');
var speakeasy = require('speakeasy');
var sinon = require('sinon');
var Promise = require('bluebird');
var request = Promise.promisifyAll(request);
var BASE_URL = 'http://localhost:8090';
var PORT = 8090;
var BASE_URL = 'http://localhost:' + PORT;
var requests = require('./requests')(PORT);
describe('test the server', function() {
var _server
var u2f;
var deps;
var u2f, nedb;
var transporter;
var collection;
var ldap_client = {
bind: sinon.stub(),
search: sinon.stub()
search: sinon.stub(),
modify: sinon.stub(),
};
var ldap = {
Change: sinon.spy()
}
beforeEach(function(done) {
var config = {
port: 8090,
port: PORT,
totp_secret: 'totp_secret',
ldap_url: 'ldap://127.0.0.1:389',
ldap_users_dn: 'ou=users,dc=example,dc=com',
ldap_user: 'cn=admin,dc=example,dc=com',
ldap_password: 'password',
session_secret: 'session_secret',
session_max_age: 50000,
gmail: {
@ -39,6 +48,23 @@ 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;
});
transporter = {};
transporter.sendMail = sinon.stub().yields();
var nodemailer = {};
nodemailer.createTransport = sinon.spy(function() {
return transporter;
  });
var search_doc = {
object: {
mail: 'test_ok@example.com'
@ -52,10 +78,20 @@ describe('test the server', function() {
ldap_client.bind.withArgs('cn=test_ok,ou=users,dc=example,dc=com',
'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');
_server = server.run(config, ldap_client, u2f, function() {
ldap_client.modify.yields(undefined);
var deps = {};
deps.u2f = u2f;
deps.nedb = nedb;
deps.nodemailer = nodemailer;
deps.ldap = ldap;
_server = server.run(config, ldap_client, deps, function() {
done();
});
});
@ -74,11 +110,12 @@ describe('test the server', function() {
describe('test authentication and verification', function() {
test_authentication();
test_reset_password();
});
function test_login() {
it('should serve the login page', function(done) {
request.getAsync(BASE_URL + '/login')
request.getAsync(BASE_URL + '/authentication/login')
.then(function(response) {
assert.equal(response.statusCode, 200);
done();
@ -88,7 +125,7 @@ describe('test the server', function() {
function test_logout() {
it('should logout and redirect to /', function(done) {
request.getAsync(BASE_URL + '/logout')
request.getAsync(BASE_URL + '/authentication/logout')
.then(function(response) {
assert.equal(response.req.path, '/');
done();
@ -98,7 +135,7 @@ describe('test the server', function() {
function test_authentication() {
it('should return status code 401 when user is not authenticated', function() {
return request.getAsync({ url: BASE_URL + '/verify' })
return request.getAsync({ url: BASE_URL + '/authentication/verify' })
.then(function(response) {
assert.equal(response.statusCode, 401);
return Promise.resolve();
@ -111,31 +148,18 @@ describe('test the server', function() {
encoding: 'base32'
});
var j = request.jar();
return request.getAsync({ url: BASE_URL + '/login', jar: j })
return requests.login(j)
.then(function(res) {
assert.equal(res.statusCode, 200, 'get login page failed');
return request.postAsync({
url: BASE_URL + '/1stfactor',
jar: j,
form: {
username: 'test_ok',
password: 'password'
}
});
return requests.first_factor(j);
})
.then(function(res) {
assert.equal(res.statusCode, 204, 'first factor failed');
return request.postAsync({
url: BASE_URL + '/2ndfactor/totp',
jar: j,
form: {
token: real_token
}
});
return requests.totp(j, real_token);
})
.then(function(res) {
assert.equal(res.statusCode, 204, 'second factor failed');
return request.getAsync({ url: BASE_URL + '/verify', jar: j })
return requests.verify(j);
})
.then(function(res) {
assert.equal(res.statusCode, 204, 'verify failed');
@ -149,35 +173,22 @@ describe('test the server', function() {
encoding: 'base32'
});
var j = request.jar();
return request.getAsync({ url: BASE_URL + '/login', jar: j })
return requests.login(j)
.then(function(res) {
assert.equal(res.statusCode, 200, 'get login page failed');
return request.postAsync({
url: BASE_URL + '/1stfactor',
jar: j,
form: {
username: 'test_ok',
password: 'password'
}
});
return requests.first_factor(j);
})
.then(function(res) {
assert.equal(res.statusCode, 204, 'first factor failed');
return request.postAsync({
url: BASE_URL + '/2ndfactor/totp',
jar: j,
form: {
token: real_token
}
});
return requests.totp(j, real_token);
})
.then(function(res) {
assert.equal(res.statusCode, 204, 'second factor failed');
return request.getAsync({ url: BASE_URL + '/login', jar: j })
return requests.login(j);
})
.then(function(res) {
assert.equal(res.statusCode, 200, 'login page loading failed');
return request.getAsync({ url: BASE_URL + '/verify', jar: j })
return requests.verify(j);
})
.then(function(res) {
assert.equal(res.statusCode, 204, 'verify failed');
@ -197,57 +208,29 @@ describe('test the server', function() {
u2f.finishRegistration.returns(Promise.resolve(sign_status));
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 request.getAsync({ url: BASE_URL + '/login', jar: j })
return requests.login(j)
.then(function(res) {
assert.equal(res.statusCode, 200, 'get login page failed');
return request.postAsync({
url: BASE_URL + '/1stfactor',
jar: j,
form: {
username: 'test_ok',
password: 'password'
}
});
return requests.first_factor(j);
})
.then(function(res) {
assert.equal(res.statusCode, 204, 'first factor failed');
return request.getAsync({
url: BASE_URL + '/2ndfactor/u2f/register_request',
jar: j
});
})
.then(function(res) {
assert.equal(res.statusCode, 200, 'second factor, start register failed');
return request.postAsync({
url: BASE_URL + '/2ndfactor/u2f/register',
jar: j,
form: {
s: 'test'
}
});
return requests.u2f_registration(j, transporter);
})
.then(function(res) {
assert.equal(res.statusCode, 204, 'second factor, finish register failed');
return request.getAsync({
url: BASE_URL + '/2ndfactor/u2f/sign_request',
jar: j
});
})
.then(function(res) {
assert.equal(res.statusCode, 200, 'second factor, start sign failed');
return request.postAsync({
url: BASE_URL + '/2ndfactor/u2f/sign',
jar: j,
form: {
s: 'test'
}
});
return requests.u2f_authentication(j);
})
.then(function(res) {
assert.equal(res.statusCode, 204, 'second factor, finish sign failed');
return request.getAsync({ url: BASE_URL + '/verify', jar: j })
return requests.verify(j);
})
.then(function(res) {
assert.equal(res.statusCode, 204, 'verify failed');
@ -255,5 +238,29 @@ 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) {
assert.equal(res.statusCode, 200, 'get login page failed');
return requests.first_factor(j);
})
.then(function(res) {
assert.equal(res.statusCode, 204, 'first factor failed');
return requests.reset_password(j, transporter, 'user', 'new-password');
})
.then(function(res) {
assert.equal(res.statusCode, 204, 'second factor, finish register failed');
return Promise.resolve();
});
});
}
});

View File

@ -84,11 +84,13 @@ function test_u2f_registration_token() {
var userid = 'user';
var token = 'token';
var max_age = 60;
var content = 'abc';
return data_store.save_u2f_registration_token(userid, token, max_age)
return data_store.issue_identity_check_token(userid, token, content, max_age)
.then(function(document) {
assert.equal(userid, document.userid);
assert.equal(token, document.token);
assert.equal(document.userid, userid);
assert.equal(document.token, token);
assert.deepEqual(document.content, { userid: 'user', data: content });
assert('max_date' in document);
assert('_id' in document);
return Promise.resolve();
@ -109,9 +111,9 @@ function test_u2f_registration_token() {
var token = 'token';
var max_age = 50;
data_store.save_u2f_registration_token(userid, token, max_age)
data_store.issue_identity_check_token(userid, token, {}, max_age)
.then(function(document) {
return data_store.consume_u2f_registration_token(token);
return data_store.consume_identity_check_token(token);
})
.then(function() {
done();
@ -131,12 +133,12 @@ function test_u2f_registration_token() {
var token = 'token';
var max_age = 50;
data_store.save_u2f_registration_token(userid, token, max_age)
data_store.issue_identity_check_token(userid, token, {}, max_age)
.then(function(document) {
return data_store.consume_u2f_registration_token(token);
return data_store.consume_identity_check_token(token);
})
.then(function(document) {
return data_store.consume_u2f_registration_token(token);
return data_store.consume_identity_check_token(token);
})
.catch(function(err) {
console.error(err);
@ -152,7 +154,7 @@ function test_u2f_registration_token() {
var token = 'token';
return data_store.consume_u2f_registration_token(token)
return data_store.consume_identity_check_token(token)
.then(function(document) {
return Promise.reject();
})
@ -172,14 +174,39 @@ function test_u2f_registration_token() {
var max_age = 60;
MockDate.set('1/1/2000');
data_store.save_u2f_registration_token(userid, token, max_age)
data_store.issue_identity_check_token(userid, token, {}, max_age)
.then(function() {
MockDate.set('1/2/2000');
return data_store.consume_u2f_registration_token(token);
return data_store.consume_identity_check_token(token);
})
.catch(function(err) {
MockDate.reset();
done();
});
});
it('should save the userid and some data with the token', function(done) {
var options = {};
options.inMemoryOnly = true;
var data_store = new UserDataStore(DataStore, options);
var userid = 'user';
var token = 'token';
var max_age = 60;
MockDate.set('1/1/2000');
var data = 'abc';
data_store.issue_identity_check_token(userid, token, data, max_age)
.then(function() {
return data_store.consume_identity_check_token(token);
})
.then(function(content) {
var expected_content = {};
expected_content.userid = 'user';
expected_content.data = 'abc';
assert.deepEqual(content, expected_content);
done();
})
});
}