Registration process sends an email to allow user to register its U2F device

pull/7/head
Clement Michaud 2017-01-22 17:54:45 +01:00
parent 3d82cef30b
commit d3db94105e
39 changed files with 1012 additions and 219 deletions

4
.gitignore vendored
View File

@ -6,3 +6,7 @@ coverage/
*.swp
*.sh
config.yml
npm-debug.log

View File

@ -10,6 +10,7 @@ COPY src /usr/src
ENV PORT=80
EXPOSE 80
VOLUME /etc/auth-server
VOLUME /var/lib/auth-server
CMD ["node", "index.js"]
CMD ["node", "index.js", "/etc/auth-server/config.yml"]

View File

@ -0,0 +1,19 @@
ldap:
url: ldap://ldap
base_dn: ou=users,dc=example,dc=com
# Will be per user soon
totp_secret: GRWGIJS6IRHVEODVNRCXCOBMJ5AGC6ZE
session:
secret: unsecure_secret
expiration: 3600000
store_directory: /var/lib/auth-server
notifier:
gmail:
username: user@example.com
password: yourpassword

View File

@ -7,3 +7,10 @@ services:
- ./src/views:/usr/src/views
- ./src/public_html:/usr/src/public_html
ldap-admin:
image: osixia/phpldapadmin:0.6.11
ports:
- 9090:80
environment:
- PHPLDAPADMIN_LDAP_HOSTS=ldap
- PHPLDAPADMIN_HTTPS=false

View File

@ -3,33 +3,30 @@ version: '2'
services:
auth:
build: .
environment:
- LDAP_URL=ldap://ldap
- LDAP_USERS_DN=dc=example,dc=com
- TOTP_SECRET=GRWGIJS6IRHVEODVNRCXCOBMJ5AGC6ZE
- SESSION_SECRET=unsecure_secret
- SESSION_EXPIRATION_TIME=3600000
- STORE_DIRECTORY=/var/lib/auth-server
depends_on:
- ldap
restart: always
volumes:
- ./config.yml:/etc/auth-server/config.yml:ro
ldap:
image: osixia/openldap:1.1.7
image: dinkel/openldap
environment:
- LDAP_ORGANISATION=MyCompany
- LDAP_DOMAIN=example.com
- LDAP_ADMIN_PASSWORD=password
- SLAPD_ORGANISATION=MyCompany
- SLAPD_DOMAIN=example.com
- SLAPD_PASSWORD=password
expose:
- "389"
volumes:
- ./example/ldap:/etc/ldap.dist/prepopulate
nginx:
image: nginx:alpine
volumes:
- ./nginx_conf/nginx.conf:/etc/nginx/nginx.conf
- ./nginx_conf/index.html:/usr/share/nginx/html/index.html
- ./nginx_conf/secret.html:/usr/share/nginx/html/secret.html
- ./nginx_conf/ssl:/etc/ssl
- ./example/nginx_conf/nginx.conf:/etc/nginx/nginx.conf
- ./example/nginx_conf/index.html:/usr/share/nginx/html/index.html
- ./example/nginx_conf/secret.html:/usr/share/nginx/html/secret.html
- ./example/nginx_conf/ssl:/etc/ssl
depends_on:
- auth
ports:

View File

@ -0,0 +1,31 @@
dn: ou=groups,dc=example,dc=com
objectclass: organizationalUnit
objectclass: top
ou: groups
dn: ou=users,dc=example,dc=com
objectclass: organizationalUnit
objectclass: top
ou: users
dn: cn=user,ou=groups,dc=example,dc=com
cn: user
gidnumber: 502
objectclass: posixGroup
objectclass: top
dn: cn=user,ou=users,dc=example,dc=com
cn: user
gidnumber: 500
givenname: user
homedirectory: /home/user1
loginshell: /bin/sh
objectclass: inetOrgPerson
objectclass: posixAccount
objectclass: top
mail: user@example.com
sn: User
uid: user
uidnumber: 1000
userpassword: {SHA}W6ph5Mm5Pz8GgiULbPgzG37mj9g=

View File

@ -28,12 +28,16 @@
"express-session": "^1.14.2",
"ldapjs": "^1.0.1",
"nedb": "^1.8.0",
"nodemailer": "^2.7.0",
"object-path": "^0.11.3",
"randomstring": "^1.1.5",
"speakeasy": "^2.0.0",
"winston": "^2.3.1"
"winston": "^2.3.1",
"yamljs": "^0.2.8"
},
"devDependencies": {
"mocha": "^3.2.0",
"mockdate": "^2.0.1",
"request": "^2.79.0",
"should": "^11.1.1",
"sinon": "^1.17.6",

View File

@ -3,15 +3,25 @@ var server = require('./lib/server');
var ldap = require('ldapjs');
var u2f = require('authdog');
var YAML = require('yamljs');
var config_path = process.argv[2];
console.log('Parse configuration file: %s', config_path);
var yaml_config = YAML.load(config_path);
var config = {
port: process.env.PORT || 8080,
totp_secret: process.env.TOTP_SECRET,
ldap_url: process.env.LDAP_URL || 'ldap://127.0.0.1:389',
ldap_users_dn: process.env.LDAP_USERS_DN,
session_secret: process.env.SESSION_SECRET,
session_max_age: process.env.SESSION_MAX_AGE || 3600000, // in ms
store_directory: process.env.STORE_DIRECTORY
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,
session_secret: yaml_config.session.secret,
session_max_age: yaml_config.session.expiration || 3600000, // in ms
store_directory: yaml_config.store_directory,
gmail: {
user: yaml_config.notifier.gmail.username,
pass: yaml_config.notifier.gmail.password
}
}
var ldap_client = ldap.createClient({

View File

@ -0,0 +1,25 @@
module.exports = EmailSender;
var Promise = require('bluebird');
function EmailSender(nodemailer, options) {
var transporter = nodemailer.createTransport({
service: 'gmail',
auth: {
user: options.gmail.user,
pass: options.gmail.pass
}
});
this.transporter = Promise.promisifyAll(transporter);
}
EmailSender.prototype.send = function(to, subject, html) {
var mailOptions = {};
mailOptions.from = 'auth-server@open-intent.io';
mailOptions.to = to;
mailOptions.subject = subject;
mailOptions.html = html;
return this.transporter.sendMailAsync(mailOptions);
}

View File

@ -0,0 +1,17 @@
module.exports = {
LdapSearchError: LdapSearchError,
LdapBindError: LdapBindError,
}
function LdapSearchError(message) {
this.name = "LdapSearchError";
this.message = (message || "");
}
LdapSearchError.prototype = Object.create(Error.prototype);
function LdapBindError(message) {
this.name = "LdapBindError";
this.message = (message || "");
}
LdapBindError.prototype = Object.create(Error.prototype);

View File

@ -1,13 +1,46 @@
module.exports = {
'validate': validateCredentials
validate: validateCredentials,
get_email: retrieve_email
}
var util = require('util');
var Promise = require('bluebird');
var exceptions = require('./exceptions');
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 });
return bind_promised(userDN, password);
return bind_promised(userDN, password)
.error(function(err) {
throw new exceptions.LdapBindError(err.message);
});
}
function retrieve_email(ldap_client, username, users_dn) {
var userDN = util.format("cn=%s,%s", username, users_dn);
var search_promised = Promise.promisify(ldap_client.search, { context: ldap_client });
var query = {};
query.sizeLimit = 1;
query.attributes = ['mail'];
var base_dn = userDN;
return new Promise(function(resolve, reject) {
search_promised(base_dn, query)
.then(function(res) {
var doc;
res.on('searchEntry', function(entry) {
doc = entry.object;
});
res.on('error', function(err) {
reject(new exceptions.LdapSearchError(err.message));
});
res.on('end', function(result) {
resolve(doc);
});
})
.catch(function(err) {
reject(new exceptions.LdapSearchError(err.message));
});
});
}

View File

@ -2,6 +2,7 @@
var first_factor = require('./routes/first_factor');
var second_factor = require('./routes/second_factor');
var verify = require('./routes/verify');
var objectPath = require('object-path');
module.exports = {
login: serveLogin,
@ -12,9 +13,11 @@ module.exports = {
}
function serveLogin(req, res) {
req.session.auth_session = {};
req.session.auth_session.first_factor = false;
req.session.auth_session.second_factor = false;
if(!(objectPath.has(req, 'session.auth_session'))) {
req.session.auth_session = {};
req.session.auth_session.first_factor = false;
req.session.auth_session.second_factor = false;
}
res.render('login');
}

View File

@ -1,6 +1,7 @@
module.exports = first_factor;
var exceptions = require('../exceptions');
var ldap = require('../ldap');
var objectPath = require('object-path');
@ -10,7 +11,9 @@ 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);
}
@ -23,19 +26,40 @@ function first_factor(req, res) {
return;
}
logger.info('1st factor: Starting authentication of user "%s"', username);
var ldap_client = req.app.get('ldap client');
var config = req.app.get('config');
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;
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: Retrieved email is %s', email);
req.session.auth_session.email = email;
res.status(204);
res.send();
console.log('LDAP binding successful');
})
.catch(exceptions.LdapSearchError, function(err) {
logger.info('1st factor: Unable to retrieve email from LDAP');
res.status(500);
res.send();
})
.catch(exceptions.LdapBindError, function(err) {
logger.info('1st factor: LDAP binding failed');
replyWithUnauthorized(res);
})
.catch(function(err) {
replyWithUnauthorized(res);
console.log('LDAP binding failed:', err);
logger.debug('1st factor: Unhandled error %s', err);
});
}

View File

@ -5,8 +5,11 @@ var u2f = require('./u2f');
module.exports = {
totp: denyNotLogged(require('./totp')),
u2f: {
register_request: denyNotLogged(u2f.register_request),
register: denyNotLogged(u2f.register),
register_request: u2f.register_request,
register: u2f.register,
register_handler_get: u2f.register_handler_get,
register_handler_post: u2f.register_handler_post,
sign_request: denyNotLogged(u2f.sign_request),
sign: denyNotLogged(u2f.sign),
}

View File

@ -1,89 +1,22 @@
var u2f_register = require('./u2f_register');
var u2f_common = require('./u2f_common');
module.exports = {
register_request: register_request,
register: register,
register_request: u2f_register.register_request,
register: u2f_register.register,
register_handler_get: u2f_register.register_handler_get,
register_handler_post: u2f_register.register_handler_post,
sign_request: sign_request,
sign: sign,
}
var objectPath = require('object-path');
var util = require('util');
function replyWithInternalError(res, msg) {
res.status(500);
res.send(msg)
}
function replyWithMissingRegistration(res) {
res.status(401);
res.send('Please register before authenticate');
}
function replyWithUnauthorized(res) {
res.status(401);
res.send();
}
function extractAppId(req) {
return util.format('https://%s', req.headers.host);
}
function register_request(req, res) {
var u2f = req.app.get('u2f');
var logger = req.app.get('logger');
var appid = extractAppId(req);
logger.debug('U2F register_request: headers=%s', JSON.stringify(req.headers));
logger.info('U2F register_request: Starting registration');
u2f.startRegistration(appid, [])
.then(function(registrationRequest) {
logger.info('U2F register_request: Sending back registration request');
req.session.auth_session.register_request = registrationRequest;
res.status(200);
res.json(registrationRequest);
}, function(err) {
logger.error('U2F register_request: %s', err);
replyWithInternalError(res, 'Unable to complete the registration');
});
}
function register(req, res) {
if(!objectPath.has(req, 'session.auth_session.register_request')) {
replyWithUnauthorized(res);
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 = extractAppId(req);
var logger = req.app.get('logger');
logger.info('U2F register: Finishing registration');
logger.debug('U2F register: register_request=%s', JSON.stringify(registrationRequest));
logger.debug('U2F register: body=%s', JSON.stringify(req.body));
u2f.finishRegistration(registrationRequest, req.body)
.then(function(registrationStatus) {
logger.info('U2F register: Store registration and reply');
var meta = {
keyHandle: registrationStatus.keyHandle,
publicKey: registrationStatus.publicKey,
certificate: registrationStatus.certificate
}
user_data_storage.set_u2f_meta(userid, appid, meta);
res.status(204);
res.send();
}, function(err) {
logger.error('U2F register: %s', err);
replyWithInternalError(res, 'Unable to complete the registration');
});
}
function retrieveU2fMeta(req, user_data_storage) {
var userid = req.session.auth_session.userid;
var appid = extractAppId(req);
var appid = u2f_common.extract_app_id(req);
return user_data_storage.get_u2f_meta(userid, appid);
}
@ -116,13 +49,13 @@ function sign_request(req, res) {
retrieveU2fMeta(req, user_data_storage)
.then(function(doc) {
if(!doc) {
replyWithMissingRegistration(res);
u2f_common.reply_with_missing_registration(res);
return;
}
var u2f = req.app.get('u2f');
var meta = doc.meta;
var appid = extractAppId(req);
var appid = u2f_common.extract_app_id(req);
logger.info('U2F sign_request: Start authentication');
return startU2fAuthentication(u2f, appid, meta);
})
@ -134,14 +67,14 @@ function sign_request(req, res) {
})
.catch(function(err) {
logger.info('U2F sign_request: %s', err);
replyWithUnauthorized(res);
u2f_common.reply_with_unauthorized(res);
});
}
function sign(req, res) {
if(!objectPath.has(req, 'session.auth_session.sign_request')) {
replyWithUnauthorized(res);
u2f_common.reply_with_unauthorized(res);
return;
}
@ -150,7 +83,7 @@ function sign(req, res) {
retrieveU2fMeta(req, user_data_storage)
.then(function(doc) {
var appid = extractAppId(req);
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;

View File

@ -0,0 +1,38 @@
module.exports = {
extract_app_id: extract_app_id,
extract_original_url: extract_original_url,
extract_referrer: extract_referrer,
reply_with_internal_error: reply_with_internal_error,
reply_with_missing_registration: reply_with_missing_registration,
reply_with_unauthorized: reply_with_unauthorized
}
var util = require('util');
function extract_app_id(req) {
return util.format('https://%s', req.headers.host);
}
function extract_original_url(req) {
return util.format('https://%s%s', req.headers.host, req.headers['x-original-uri']);
}
function extract_referrer(req) {
return req.headers.referrer;
}
function reply_with_internal_error(res, msg) {
res.status(500);
res.send(msg)
}
function reply_with_missing_registration(res) {
res.status(401);
res.send('Please register before authenticate');
}
function reply_with_unauthorized(res) {
res.status(401);
res.send();
}

View File

@ -0,0 +1,71 @@
var u2f_register_handler = require('./u2f_register_handler');
module.exports = {
register_request: register_request,
register: register,
register_handler_get: u2f_register_handler.get,
register_handler_post: u2f_register_handler.post
}
var objectPath = require('object-path');
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 appid = u2f_common.extract_app_id(req);
logger.debug('U2F register_request: headers=%s', JSON.stringify(req.headers));
logger.info('U2F register_request: Starting registration');
u2f.startRegistration(appid, [])
.then(function(registrationRequest) {
logger.info('U2F register_request: Sending back registration request');
req.session.auth_session.register_request = registrationRequest;
res.status(200);
res.json(registrationRequest);
})
.catch(function(err) {
logger.error('U2F register_request: %s', err);
u2f_common.reply_with_internal_error(res, 'Unable to complete the registration');
});
}
function register(req, res) {
if(!objectPath.has(req, 'session.auth_session.register_request')) {
u2f_common.reply_with_unauthorized(res);
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');
logger.info('U2F register: Finishing registration');
logger.debug('U2F register: register_request=%s', JSON.stringify(registrationRequest));
logger.debug('U2F register: body=%s', JSON.stringify(req.body));
u2f.finishRegistration(registrationRequest, req.body)
.then(function(registrationStatus) {
logger.info('U2F register: Store registration and reply');
var meta = {
keyHandle: registrationStatus.keyHandle,
publicKey: registrationStatus.publicKey,
certificate: registrationStatus.certificate
}
return user_data_storage.set_u2f_meta(userid, appid, meta);
})
.then(function() {
res.status(204);
res.send();
})
.catch(function(err) {
logger.error('U2F register: %s', err);
u2f_common.reply_with_unauthorized(res);
});
}

View File

@ -0,0 +1,93 @@
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');
function register_handler_get(req, res) {
var logger = req.app.get('logger');
logger.info('U2F register_handler: Continue registration process');
var registration_token = objectPath.get(req, 'query.registration_token');
logger.debug('U2F register_handler: registration_token=%s', registration_token);
if(!registration_token) {
res.status(403);
res.send();
return;
}
var user_data_store = req.app.get('user data store');
logger.debug('U2F register_handler: verify token validity');
user_data_store.verify_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(!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();
});
}

View File

@ -12,7 +12,9 @@ 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');
function run(config, ldap_client, u2f, fn) {
var view_directory = path.resolve(__dirname, '../views');
@ -20,6 +22,9 @@ function run(config, ldap_client, u2f, fn) {
var datastore_options = {};
datastore_options.directory = config.store_directory;
var email_options = {};
email_options.gmail = config.gmail;
var app = express();
app.use(express.static(public_html_directory));
app.use(bodyParser.urlencoded({ extended: false }));
@ -46,18 +51,26 @@ function run(config, ldap_client, u2f, fn) {
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('config', config);
// web pages
app.get ('/login', routes.login);
app.get ('/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);
// verify authentication
app.get ('/verify', routes.verify);
// Authentication process
app.post ('/1stfactor', routes.first_factor);
app.post ('/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 ('/2ndfactor/u2f/sign_request', routes.second_factor.u2f.sign_request);
app.post ('/2ndfactor/u2f/sign', routes.second_factor.u2f.sign);

View File

@ -5,15 +5,20 @@ var Promise = require('bluebird');
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);
}
function create_collection(name, options, DataStore) {
var datastore_options = {};
if(options.directory)
datastore_options.filename = path.resolve(options.directory, 'u2f_meta');
datastore_options.filename = path.resolve(options.directory, name);
datastore_options.inMemoryOnly = options.inMemoryOnly || false;
datastore_options.autoload = true;
console.log(datastore_options);
this._u2f_meta_collection = Promise.promisifyAll(new DataStore(datastore_options));
return Promise.promisifyAll(new DataStore(datastore_options));
}
UserDataStore.prototype.set_u2f_meta = function(userid, app_id, meta) {
@ -37,3 +42,31 @@ 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) {
var newDocument = {};
newDocument.userid = userid;
newDocument.token = token;
newDocument.max_date = new Date(new Date().getTime() + max_age);
return this._u2f_registration_tokens_collection.insertAsync(newDocument);
}
UserDataStore.prototype.verify_u2f_registration_token = function(token) {
var query = {};
query.token = token;
return this._u2f_registration_tokens_collection.findOneAsync(query)
.then(function(doc) {
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');
}
return Promise.resolve();
});
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

View File

@ -42,27 +42,39 @@ function onTotpSignButtonClicked() {
}
function onU2fSignButtonClicked() {
startSecondFactorU2fSigning(function(err) {
startU2fAuthentication(function(err) {
if(err) {
onSecondFactorU2fSigningFailure();
onU2fAuthenticationFailure();
return;
}
onSecondFactorU2fSigningSuccess();
onU2fAuthenticationSuccess();
}, 120);
}
function onU2fRegisterButtonClicked() {
startSecondFactorU2fRegister(function(err) {
function onU2fRegistrationButtonClicked() {
askForU2fRegistration(function(err) {
if(err) {
onSecondFactorU2fRegisterFailure();
$.notify('Unable to send you an email', 'error');
return;
}
onSecondFactorU2fRegisterSuccess();
}, 120);
$.notify('An email has been sent to your email address', 'info');
});
}
function finishSecondFactorU2f(url, responseData, fn) {
console.log(responseData);
function askForU2fRegistration(fn) {
$.ajax({
type: 'POST',
url: '/auth/u2f-register'
})
.done(function(data) {
fn(undefined, data);
})
.fail(function(xhr, status) {
fn(status);
});
}
function finishU2fAuthentication(url, responseData, fn) {
$.ajax({
type: 'POST',
url: url,
@ -78,19 +90,11 @@ function finishSecondFactorU2f(url, responseData, fn) {
});
}
function startSecondFactorU2fSigning(fn, timeout) {
function startU2fAuthentication(fn, timeout) {
$.get('/auth/2ndfactor/u2f/sign_request', {}, null, 'json')
.done(function(signResponse) {
var registeredKeys = signResponse.registeredKeys;
$.notify('Please touch the token', 'information');
console.log(signResponse);
// Store sessionIds
// var sessionIds = {};
// for (var i = 0; i < registeredKeys.length; i++) {
// sessionIds[registeredKeys[i].keyHandle] = registeredKeys[i].sessionId;
// delete registeredKeys[i]['sessionId'];
// }
$.notify('Please touch the token', 'info');
u2f.sign(
signResponse.appId,
@ -100,8 +104,7 @@ function startSecondFactorU2fSigning(fn, timeout) {
if (response.errorCode) {
fn(response);
} else {
// response['sessionId'] = sessionIds[response.keyHandle];
finishSecondFactorU2f('/auth/2ndfactor/u2f/sign', response, fn);
finishU2fAuthentication('/auth/2ndfactor/u2f/sign', response, fn);
}
},
timeout
@ -112,28 +115,6 @@ function startSecondFactorU2fSigning(fn, timeout) {
});
}
function startSecondFactorU2fRegister(fn, timeout) {
$.get('/auth/2ndfactor/u2f/register_request', {}, null, 'json')
.done(function(startRegisterResponse) {
console.log(startRegisterResponse);
$.notify('Please touch the token', 'information');
u2f.register(
startRegisterResponse.appId,
startRegisterResponse.registerRequests,
startRegisterResponse.registeredKeys,
function (response) {
if (response.errorCode) {
fn(response.errorCode);
} else {
// response['sessionId'] = startRegisterResponse.clientData;
finishSecondFactorU2f('/auth/2ndfactor/u2f/register', response, fn);
}
},
timeout
);
});
}
function validateSecondFactorTotp(token, fn) {
$.post('/auth/2ndfactor/totp', {
token: token,
@ -146,7 +127,6 @@ function validateSecondFactorTotp(token, fn) {
});
}
function validateFirstFactor(username, password, fn) {
$.post('/auth/1stfactor', {
username: username,
@ -193,21 +173,11 @@ function onSecondFactorTotpFailure() {
$.notify('Wrong TOTP token', 'error');
}
function onSecondFactorU2fSigningSuccess() {
function onU2fAuthenticationSuccess() {
onAuthenticationSuccess();
}
function onSecondFactorU2fSigningFailure(err) {
console.error(err);
$.notify('Problem authenticating with U2F.', 'error');
}
function onSecondFactorU2fRegisterSuccess() {
$.notify('Registration succeeded. You can now sign in.', 'success');
}
function onSecondFactorU2fRegisterFailure(err) {
console.error(err);
function onU2fAuthenticationFailure(err) {
$.notify('Problem authenticating with U2F.', 'error');
}
@ -247,26 +217,23 @@ function setupU2fSignButton() {
setupEnterKeypressListener('#u2f', onU2fSignButtonClicked);
}
function setupU2fRegisterButton() {
$('#second-factor #u2f-register-button').on('click', onU2fRegisterButtonClicked);
setupEnterKeypressListener('#u2f', onU2fRegisterButtonClicked);
function setupU2fRegistrationButton() {
$('#second-factor #u2f-register-button').on('click', onU2fRegistrationButtonClicked);
}
function enterFirstFactor() {
// console.log('entering first factor');
showFirstFactorLayout();
hideSecondFactorLayout();
setupFirstFactorLoginButton();
}
function enterSecondFactor() {
// console.log('entering second factor');
hideFirstFactorLayout();
showSecondFactorLayout();
cleanupFirstFactorLoginButton();
setupTotpSignButton();
setupU2fSignButton();
setupU2fRegisterButton();
setupU2fRegistrationButton();
}
$(document).ready(function() {

View File

@ -0,0 +1,68 @@
(function() {
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,
data: JSON.stringify(responseData),
contentType: 'application/json',
dataType: 'json',
})
.done(function(data) {
fn(undefined, data);
})
.fail(function(xhr, status) {
$.notify('Error when finish U2F transaction' + status);
});
}
function startRegister(fn, timeout) {
$.get('/auth/2ndfactor/u2f/register_request', {}, null, 'json')
.done(function(startRegisterResponse) {
u2f.register(
startRegisterResponse.appId,
startRegisterResponse.registerRequests,
startRegisterResponse.registeredKeys,
function (response) {
if (response.errorCode) {
fn(response.errorCode);
} else {
finishRegister('/auth/2ndfactor/u2f/register', response, fn);
}
},
timeout
);
});
}
function redirect() {
var redirect_uri = '/';
if('redirect' in params) {
redirect_uri = params['redirect'];
}
window.location.replace(redirect_uri);
}
function onRegisterSuccess() {
redirect();
}
function onRegisterFailure(err) {
$.notify('Problem authenticating with U2F.', 'error');
}
$(document).ready(function() {
startRegister(function(err) {
if(err) {
onRegisterFailure(err);
return;
}
onRegisterSuccess();
}, 240);
});
})();

View File

@ -0,0 +1,17 @@
<!DOCTYPE html>
<head>
<title>FIDO U2F Registration</title>
<link rel="stylesheet" type="text/css" href="login.css">
</head>
<body>
<div class="login">
<h1>Touch the token</h1>
<img src="img/pendrive.png" alt="pendrive" />
</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>
</html>

View File

@ -64,12 +64,12 @@ describe('test the server', function() {
it('should fail the first_factor login', function() {
return postPromised(BASE_URL + '/auth/1stfactor', {
form: {
username: 'admin',
username: 'user',
password: 'bad_password'
}
})
.then(function(data) {
assert.equal(401, data.statusCode);
assert.equal(data.statusCode, 401);
return Promise.resolve();
});
});
@ -82,7 +82,7 @@ describe('test the server', function() {
return postPromised(BASE_URL + '/auth/1stfactor', {
form: {
username: 'admin',
username: 'user',
password: 'password',
}
})

View File

@ -2,24 +2,40 @@
var sinon = require('sinon');
var Promise = require('bluebird');
var assert = require('assert');
var winston = require('winston');
var first_factor = require('../../../src/lib/routes/first_factor');
describe('test the first factor validation route', function() {
var req, res;
var ldap_interface_mock;
var search_res_ok;
beforeEach(function() {
var bind_mock = sinon.stub();
ldap_interface_mock = {
bind: bind_mock
bind: sinon.stub(),
search: sinon.stub()
}
var config = {
ldap_users_dn: 'dc=example,dc=com'
}
var search_doc = {
object: {
mail: 'test_ok@example.com'
}
};
var search_res_ok = {};
search_res_ok.on = sinon.spy(function(event, fn) {
if(event != 'error') fn(search_doc);
});
ldap_interface_mock.search.yields(undefined, search_res_ok);
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);
req = {
app: {
get: app_get
@ -63,6 +79,18 @@ describe('test the first factor validation route', function() {
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);
});
});
});

View File

@ -95,7 +95,7 @@ describe('test u2f routes', function() {
u2f.register(req, res);
});
it('should return unauthorized error on registration request', function(done) {
it('should return unauthorized on finishRegistration error', function(done) {
res.send = sinon.spy(function(data) {
assert.equal(401, res.status.getCall(0).args[0]);
done();
@ -105,6 +105,7 @@ describe('test u2f routes', function() {
u2f_mock.finishRegistration = sinon.stub();
u2f_mock.finishRegistration.returns(Promise.reject('Internal error'));
req.session.auth_session.register_request = 'abc';
req.app.get.withArgs('u2f').returns(u2f_mock);
u2f.register(req, res);
});

View File

@ -0,0 +1,115 @@
var sinon = require('sinon');
var winston = require('winston');
var u2f_register = require('../../../src/lib/routes/u2f_register');
var assert = require('assert');
describe('test register handle', function() {
var req, res;
var user_data_store;
beforeEach(function() {
req = {}
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.save_u2f_registration_token = sinon.stub().returns(Promise.resolve({}));
user_data_store.verify_u2f_registration_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 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]);
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]);
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();
});
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]);
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);
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.verify_u2f_registration_token = sinon.stub().returns(Promise.reject('Not valid anymore'));
u2f_register.register_handler_get(req, res);
});
}
});

View File

@ -17,7 +17,8 @@ describe('test data persistence', function() {
var u2f;
var tmpDir;
var ldap_client = {
bind: sinon.stub()
bind: sinon.stub(),
search: sinon.stub()
};
var config;
@ -28,10 +29,23 @@ describe('test data persistence', function() {
u2f.startAuthentication = sinon.stub();
u2f.finishAuthentication = sinon.stub();
var search_doc = {
object: {
mail: 'test_ok@example.com'
}
};
var search_res = {};
search_res.on = sinon.spy(function(event, fn) {
if(event != 'error') fn(search_doc);
});
ldap_client.bind.withArgs('cn=test_ok,ou=users,dc=example,dc=com',
'password').yields(undefined);
ldap_client.bind.withArgs('cn=test_nok,ou=users,dc=example,dc=com',
'password').yields('error');
ldap_client.search.yields(undefined, search_res);
tmpDir = tmp.dirSync({ unsafeCleanup: true });
config = {
port: PORT,
@ -40,7 +54,8 @@ describe('test data persistence', function() {
ldap_users_dn: 'ou=users,dc=example,dc=com',
session_secret: 'session_secret',
session_max_age: 50000,
store_directory: tmpDir.name
store_directory: tmpDir.name,
gmail: { user: 'user@example.com', pass: 'password' }
};
});
@ -160,7 +175,7 @@ describe('test data persistence', function() {
}
function execute_verification(jar) {
return request.getAsync({ url: BASE_URL + '/_verify', jar: jar })
return request.getAsync({ url: BASE_URL + '/verify', jar: jar })
}
function execute_login(jar) {

View File

@ -0,0 +1,31 @@
var sinon = require('sinon');
var assert = require('assert');
var EmailSender = require('../../src/lib/email_sender');
describe('test email sender', function() {
it('should send an email', function() {
var nodemailer = {};
var transporter = {};
nodemailer.createTransport = sinon.stub().returns(transporter);
transporter.sendMail = sinon.stub().yields();
var options = {};
options.gmail = {};
options.gmail.user = 'test@gmail.com';
options.gmail.pass = 'test@gmail.com';
var sender = new EmailSender(nodemailer, options);
var to = 'example@gmail.com';
var subject = 'subject';
var content = 'content';
return sender.send(to, subject, content)
.then(function() {
assert.equal(to, transporter.sendMail.getCall(0).args[0].to);
assert.equal(subject, transporter.sendMail.getCall(0).args[0].subject);
assert.equal(content, transporter.sendMail.getCall(0).args[0].html);
return Promise.resolve();
});
});
});

View File

@ -10,43 +10,80 @@ describe('test ldap validation', function() {
beforeEach(function() {
ldap_client = {
bind: sinon.stub()
bind: sinon.stub(),
search: sinon.stub()
}
});
function test_validate() {
describe('test binding', test_binding);
describe('test get email', test_get_email);
function test_binding() {
function test_validate() {
var username = 'user';
var password = 'password';
var ldap_url = 'http://ldap';
var users_dn = 'dc=example,dc=com';
return ldap.validate(ldap_client, username, password, ldap_url, users_dn);
}
it('should bind the user if good credentials provided', function() {
ldap_client.bind.yields();
return test_validate();
});
// cover an issue with promisify context
it('should promisify correctly', function() {
function LdapClient() {
this.test = 'abc';
}
LdapClient.prototype.bind = function(username, password, fn) {
assert.equal('abc', this.test);
fn();
}
ldap_client = new LdapClient();
return test_validate();
});
it('should not bind the user if wrong credentials provided', function() {
ldap_client.bind.yields('wrong credentials');
var promise = test_validate();
return promise.catch(function() {
return Promise.resolve();
});
});
}
function test_get_email() {
it('should retrieve the email of an existing user', function() {
var expected_doc = {};
expected_doc.object = {};
expected_doc.object.mail = 'user@example.com';
var res_emitter = {};
res_emitter.on = sinon.spy(function(event, fn) {
if(event != 'error') fn(expected_doc)
});
it('should bind the user if good credentials provided', function() {
ldap_client.bind.yields();
return test_validate();
});
ldap_client.search.yields(undefined, res_emitter);
// cover an issue with promisify context
it('should promisify correctly', function() {
function LdapClient() {
this.test = 'abc';
}
LdapClient.prototype.bind = function(username, password, fn) {
assert.equal('abc', this.test);
fn();
}
ldap_client = new LdapClient();
return test_validate();
});
it('should not bind the user if wrong credentials provided', function() {
ldap_client.bind.yields('wrong credentials');
var promise = test_validate();
return promise.catch(function() {
return Promise.resolve();
return ldap.get_email(ldap_client, 'user', 'dc=example,dc=com')
.then(function(doc) {
assert.deepEqual(doc, expected_doc.object);
return Promise.resolve();
})
});
});
it('should fail on error with search method', function(done) {
var expected_doc = {};
expected_doc.mail = [];
expected_doc.mail.push('user@example.com');
ldap_client.search.yields('error');
ldap.get_email(ldap_client, 'user', 'dc=example,dc=com')
.catch(function() {
done();
})
});
}
});

View File

@ -15,7 +15,8 @@ describe('test the server', function() {
var _server
var u2f;
var ldap_client = {
bind: sinon.stub()
bind: sinon.stub(),
search: sinon.stub()
};
beforeEach(function(done) {
@ -25,7 +26,11 @@ describe('test the server', function() {
ldap_url: 'ldap://127.0.0.1:389',
ldap_users_dn: 'ou=users,dc=example,dc=com',
session_secret: 'session_secret',
session_max_age: 50000
session_max_age: 50000,
gmail: {
user: 'user@example.com',
pass: 'password'
}
};
u2f = {};
@ -34,8 +39,20 @@ describe('test the server', function() {
u2f.startAuthentication = sinon.stub();
u2f.finishAuthentication = sinon.stub();
var search_doc = {
object: {
mail: 'test_ok@example.com'
}
};
var search_res = {};
search_res.on = sinon.spy(function(event, fn) {
if(event != 'error') fn(search_doc);
});
ldap_client.bind.withArgs('cn=test_ok,ou=users,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() {
@ -126,6 +143,51 @@ describe('test the server', function() {
});
});
it('should keep session variables when login page is reloaded', function() {
var real_token = speakeasy.totp({
secret: 'totp_secret',
encoding: 'base32'
});
var j = request.jar();
return request.getAsync({ url: BASE_URL + '/login', jar: 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'
}
});
})
.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
}
});
})
.then(function(res) {
assert.equal(res.statusCode, 204, 'second factor failed');
return request.getAsync({ url: BASE_URL + '/login', jar: j })
})
.then(function(res) {
assert.equal(res.statusCode, 200, 'login page loading failed');
return request.getAsync({ url: BASE_URL + '/verify', jar: j })
})
.then(function(res) {
assert.equal(res.statusCode, 204, 'verify failed');
return Promise.resolve();
})
.catch(function(err) {
console.error(err);
  });
});
it('should return status code 204 when user is authenticated using u2f', function() {
var sign_request = {};
var sign_status = {};
@ -193,6 +255,5 @@ describe('test the server', function() {
});
});
}
});

View File

@ -3,8 +3,15 @@ var UserDataStore = require('../../src/lib/user_data_store');
var DataStore = require('nedb');
var assert = require('assert');
var Promise = require('bluebird');
var sinon = require('sinon');
var MockDate = require('mockdate');
describe('test user data store', function() {
describe('test u2f meta', test_u2f_meta);
describe('test u2f registration token', test_u2f_registration_token);
});
function test_u2f_meta() {
it('should save a u2f meta', function() {
var options = {};
options.inMemoryOnly = true;
@ -65,4 +72,91 @@ describe('test user data store', function() {
return Promise.resolve();
});
});
});
}
function test_u2f_registration_token() {
it('should save u2f registration token', function() {
var options = {};
options.inMemoryOnly = true;
var data_store = new UserDataStore(DataStore, options);
var userid = 'user';
var token = 'token';
var max_age = 60;
return data_store.save_u2f_registration_token(userid, token, max_age)
.then(function(document) {
assert.equal(userid, document.userid);
assert.equal(token, document.token);
assert('max_date' in document);
assert('_id' in document);
return Promise.resolve();
})
.catch(function(err) {
console.error(err);
return Promise.reject(err);
});
});
it('should save u2f registration token and verify it', function(done) {
var options = {};
options.inMemoryOnly = true;
var data_store = new UserDataStore(DataStore, options);
var userid = 'user';
var token = 'token';
var max_age = 50;
data_store.save_u2f_registration_token(userid, token, max_age)
.then(function(document) {
return data_store.verify_u2f_registration_token(token);
})
.then(function() {
done();
})
.catch(function(err) {
console.error(err);
});
});
it('should fail when token does not exist', function() {
var options = {};
options.inMemoryOnly = true;
var data_store = new UserDataStore(DataStore, options);
var token = 'token';
return data_store.verify_u2f_registration_token(token)
.then(function(document) {
return Promise.reject();
})
.catch(function(err) {
return Promise.resolve(err);
});
});
it('should fail when token expired', 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');
data_store.save_u2f_registration_token(userid, token, max_age)
.then(function() {
MockDate.set('1/2/2000');
return data_store.verify_u2f_registration_token(token);
})
.catch(function(err) {
MockDate.reset();
done();
});
});
}