Registration process sends an email to allow user to register its U2F device
parent
3d82cef30b
commit
d3db94105e
|
@ -6,3 +6,7 @@ coverage/
|
||||||
*.swp
|
*.swp
|
||||||
|
|
||||||
*.sh
|
*.sh
|
||||||
|
|
||||||
|
config.yml
|
||||||
|
|
||||||
|
npm-debug.log
|
||||||
|
|
|
@ -10,6 +10,7 @@ COPY src /usr/src
|
||||||
ENV PORT=80
|
ENV PORT=80
|
||||||
EXPOSE 80
|
EXPOSE 80
|
||||||
|
|
||||||
|
VOLUME /etc/auth-server
|
||||||
VOLUME /var/lib/auth-server
|
VOLUME /var/lib/auth-server
|
||||||
|
|
||||||
CMD ["node", "index.js"]
|
CMD ["node", "index.js", "/etc/auth-server/config.yml"]
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -7,3 +7,10 @@ services:
|
||||||
- ./src/views:/usr/src/views
|
- ./src/views:/usr/src/views
|
||||||
- ./src/public_html:/usr/src/public_html
|
- ./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
|
||||||
|
|
|
@ -3,33 +3,30 @@ version: '2'
|
||||||
services:
|
services:
|
||||||
auth:
|
auth:
|
||||||
build: .
|
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:
|
depends_on:
|
||||||
- ldap
|
- ldap
|
||||||
restart: always
|
restart: always
|
||||||
|
volumes:
|
||||||
|
- ./config.yml:/etc/auth-server/config.yml:ro
|
||||||
|
|
||||||
ldap:
|
ldap:
|
||||||
image: osixia/openldap:1.1.7
|
image: dinkel/openldap
|
||||||
environment:
|
environment:
|
||||||
- LDAP_ORGANISATION=MyCompany
|
- SLAPD_ORGANISATION=MyCompany
|
||||||
- LDAP_DOMAIN=example.com
|
- SLAPD_DOMAIN=example.com
|
||||||
- LDAP_ADMIN_PASSWORD=password
|
- SLAPD_PASSWORD=password
|
||||||
expose:
|
expose:
|
||||||
- "389"
|
- "389"
|
||||||
|
volumes:
|
||||||
|
- ./example/ldap:/etc/ldap.dist/prepopulate
|
||||||
|
|
||||||
nginx:
|
nginx:
|
||||||
image: nginx:alpine
|
image: nginx:alpine
|
||||||
volumes:
|
volumes:
|
||||||
- ./nginx_conf/nginx.conf:/etc/nginx/nginx.conf
|
- ./example/nginx_conf/nginx.conf:/etc/nginx/nginx.conf
|
||||||
- ./nginx_conf/index.html:/usr/share/nginx/html/index.html
|
- ./example/nginx_conf/index.html:/usr/share/nginx/html/index.html
|
||||||
- ./nginx_conf/secret.html:/usr/share/nginx/html/secret.html
|
- ./example/nginx_conf/secret.html:/usr/share/nginx/html/secret.html
|
||||||
- ./nginx_conf/ssl:/etc/ssl
|
- ./example/nginx_conf/ssl:/etc/ssl
|
||||||
depends_on:
|
depends_on:
|
||||||
- auth
|
- auth
|
||||||
ports:
|
ports:
|
||||||
|
|
|
@ -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=
|
||||||
|
|
|
@ -28,12 +28,16 @@
|
||||||
"express-session": "^1.14.2",
|
"express-session": "^1.14.2",
|
||||||
"ldapjs": "^1.0.1",
|
"ldapjs": "^1.0.1",
|
||||||
"nedb": "^1.8.0",
|
"nedb": "^1.8.0",
|
||||||
|
"nodemailer": "^2.7.0",
|
||||||
"object-path": "^0.11.3",
|
"object-path": "^0.11.3",
|
||||||
|
"randomstring": "^1.1.5",
|
||||||
"speakeasy": "^2.0.0",
|
"speakeasy": "^2.0.0",
|
||||||
"winston": "^2.3.1"
|
"winston": "^2.3.1",
|
||||||
|
"yamljs": "^0.2.8"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"mocha": "^3.2.0",
|
"mocha": "^3.2.0",
|
||||||
|
"mockdate": "^2.0.1",
|
||||||
"request": "^2.79.0",
|
"request": "^2.79.0",
|
||||||
"should": "^11.1.1",
|
"should": "^11.1.1",
|
||||||
"sinon": "^1.17.6",
|
"sinon": "^1.17.6",
|
||||||
|
|
22
src/index.js
22
src/index.js
|
@ -3,15 +3,25 @@ var server = require('./lib/server');
|
||||||
|
|
||||||
var ldap = require('ldapjs');
|
var ldap = require('ldapjs');
|
||||||
var u2f = require('authdog');
|
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 = {
|
var config = {
|
||||||
port: process.env.PORT || 8080,
|
port: process.env.PORT || 8080,
|
||||||
totp_secret: process.env.TOTP_SECRET,
|
totp_secret: yaml_config.totp_secret,
|
||||||
ldap_url: process.env.LDAP_URL || 'ldap://127.0.0.1:389',
|
ldap_url: yaml_config.ldap.url || 'ldap://127.0.0.1:389',
|
||||||
ldap_users_dn: process.env.LDAP_USERS_DN,
|
ldap_users_dn: yaml_config.ldap.base_dn,
|
||||||
session_secret: process.env.SESSION_SECRET,
|
session_secret: yaml_config.session.secret,
|
||||||
session_max_age: process.env.SESSION_MAX_AGE || 3600000, // in ms
|
session_max_age: yaml_config.session.expiration || 3600000, // in ms
|
||||||
store_directory: process.env.STORE_DIRECTORY
|
store_directory: yaml_config.store_directory,
|
||||||
|
gmail: {
|
||||||
|
user: yaml_config.notifier.gmail.username,
|
||||||
|
pass: yaml_config.notifier.gmail.password
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var ldap_client = ldap.createClient({
|
var ldap_client = ldap.createClient({
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
|
@ -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);
|
|
@ -1,13 +1,46 @@
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
'validate': validateCredentials
|
validate: validateCredentials,
|
||||||
|
get_email: retrieve_email
|
||||||
}
|
}
|
||||||
|
|
||||||
var util = require('util');
|
var util = require('util');
|
||||||
var Promise = require('bluebird');
|
var Promise = require('bluebird');
|
||||||
|
var exceptions = require('./exceptions');
|
||||||
|
|
||||||
function validateCredentials(ldap_client, username, password, users_dn) {
|
function validateCredentials(ldap_client, username, password, users_dn) {
|
||||||
var userDN = util.format("cn=%s,%s", username, users_dn);
|
var userDN = util.format("cn=%s,%s", username, users_dn);
|
||||||
var bind_promised = Promise.promisify(ldap_client.bind, { context: ldap_client });
|
var bind_promised = Promise.promisify(ldap_client.bind, { context: ldap_client });
|
||||||
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));
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
var first_factor = require('./routes/first_factor');
|
var first_factor = require('./routes/first_factor');
|
||||||
var second_factor = require('./routes/second_factor');
|
var second_factor = require('./routes/second_factor');
|
||||||
var verify = require('./routes/verify');
|
var verify = require('./routes/verify');
|
||||||
|
var objectPath = require('object-path');
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
login: serveLogin,
|
login: serveLogin,
|
||||||
|
@ -12,9 +13,11 @@ module.exports = {
|
||||||
}
|
}
|
||||||
|
|
||||||
function serveLogin(req, res) {
|
function serveLogin(req, res) {
|
||||||
req.session.auth_session = {};
|
if(!(objectPath.has(req, 'session.auth_session'))) {
|
||||||
req.session.auth_session.first_factor = false;
|
req.session.auth_session = {};
|
||||||
req.session.auth_session.second_factor = false;
|
req.session.auth_session.first_factor = false;
|
||||||
|
req.session.auth_session.second_factor = false;
|
||||||
|
}
|
||||||
|
|
||||||
res.render('login');
|
res.render('login');
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
|
|
||||||
module.exports = first_factor;
|
module.exports = first_factor;
|
||||||
|
|
||||||
|
var exceptions = require('../exceptions');
|
||||||
var ldap = require('../ldap');
|
var ldap = require('../ldap');
|
||||||
var objectPath = require('object-path');
|
var objectPath = require('object-path');
|
||||||
|
|
||||||
|
@ -10,7 +11,9 @@ function replyWithUnauthorized(res) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function first_factor(req, res) {
|
function first_factor(req, res) {
|
||||||
|
var logger = req.app.get('logger');
|
||||||
if(!objectPath.has(req, 'session.auth_session.second_factor')) {
|
if(!objectPath.has(req, 'session.auth_session.second_factor')) {
|
||||||
|
logger.error('1st factor: Session is missing.');
|
||||||
replyWithUnauthorized(res);
|
replyWithUnauthorized(res);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -23,19 +26,40 @@ function first_factor(req, res) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logger.info('1st factor: Starting authentication of user "%s"', username);
|
||||||
|
|
||||||
var ldap_client = req.app.get('ldap client');
|
var ldap_client = req.app.get('ldap client');
|
||||||
var config = req.app.get('config');
|
var config = req.app.get('config');
|
||||||
|
|
||||||
|
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)
|
ldap.validate(ldap_client, username, password, config.ldap_users_dn)
|
||||||
.then(function() {
|
.then(function() {
|
||||||
req.session.auth_session.userid = username;
|
req.session.auth_session.userid = username;
|
||||||
req.session.auth_session.first_factor = true;
|
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.status(204);
|
||||||
res.send();
|
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) {
|
.catch(function(err) {
|
||||||
replyWithUnauthorized(res);
|
logger.debug('1st factor: Unhandled error %s', err);
|
||||||
console.log('LDAP binding failed:', err);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,8 +5,11 @@ var u2f = require('./u2f');
|
||||||
module.exports = {
|
module.exports = {
|
||||||
totp: denyNotLogged(require('./totp')),
|
totp: denyNotLogged(require('./totp')),
|
||||||
u2f: {
|
u2f: {
|
||||||
register_request: denyNotLogged(u2f.register_request),
|
register_request: u2f.register_request,
|
||||||
register: denyNotLogged(u2f.register),
|
register: u2f.register,
|
||||||
|
register_handler_get: u2f.register_handler_get,
|
||||||
|
register_handler_post: u2f.register_handler_post,
|
||||||
|
|
||||||
sign_request: denyNotLogged(u2f.sign_request),
|
sign_request: denyNotLogged(u2f.sign_request),
|
||||||
sign: denyNotLogged(u2f.sign),
|
sign: denyNotLogged(u2f.sign),
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,89 +1,22 @@
|
||||||
|
|
||||||
|
var u2f_register = require('./u2f_register');
|
||||||
|
var u2f_common = require('./u2f_common');
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
register_request: register_request,
|
register_request: u2f_register.register_request,
|
||||||
register: register,
|
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_request: sign_request,
|
||||||
sign: sign,
|
sign: sign,
|
||||||
}
|
}
|
||||||
|
|
||||||
var objectPath = require('object-path');
|
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) {
|
function retrieveU2fMeta(req, user_data_storage) {
|
||||||
var userid = req.session.auth_session.userid;
|
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);
|
return user_data_storage.get_u2f_meta(userid, appid);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -116,13 +49,13 @@ function sign_request(req, res) {
|
||||||
retrieveU2fMeta(req, user_data_storage)
|
retrieveU2fMeta(req, user_data_storage)
|
||||||
.then(function(doc) {
|
.then(function(doc) {
|
||||||
if(!doc) {
|
if(!doc) {
|
||||||
replyWithMissingRegistration(res);
|
u2f_common.reply_with_missing_registration(res);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var u2f = req.app.get('u2f');
|
var u2f = req.app.get('u2f');
|
||||||
var meta = doc.meta;
|
var meta = doc.meta;
|
||||||
var appid = extractAppId(req);
|
var appid = u2f_common.extract_app_id(req);
|
||||||
logger.info('U2F sign_request: Start authentication');
|
logger.info('U2F sign_request: Start authentication');
|
||||||
return startU2fAuthentication(u2f, appid, meta);
|
return startU2fAuthentication(u2f, appid, meta);
|
||||||
})
|
})
|
||||||
|
@ -134,14 +67,14 @@ function sign_request(req, res) {
|
||||||
})
|
})
|
||||||
.catch(function(err) {
|
.catch(function(err) {
|
||||||
logger.info('U2F sign_request: %s', err);
|
logger.info('U2F sign_request: %s', err);
|
||||||
replyWithUnauthorized(res);
|
u2f_common.reply_with_unauthorized(res);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function sign(req, res) {
|
function sign(req, res) {
|
||||||
if(!objectPath.has(req, 'session.auth_session.sign_request')) {
|
if(!objectPath.has(req, 'session.auth_session.sign_request')) {
|
||||||
replyWithUnauthorized(res);
|
u2f_common.reply_with_unauthorized(res);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -150,7 +83,7 @@ function sign(req, res) {
|
||||||
|
|
||||||
retrieveU2fMeta(req, user_data_storage)
|
retrieveU2fMeta(req, user_data_storage)
|
||||||
.then(function(doc) {
|
.then(function(doc) {
|
||||||
var appid = extractAppId(req);
|
var appid = u2f_common.extract_app_id(req);
|
||||||
var u2f = req.app.get('u2f');
|
var u2f = req.app.get('u2f');
|
||||||
var authRequest = req.session.auth_session.sign_request;
|
var authRequest = req.session.auth_session.sign_request;
|
||||||
var meta = doc.meta;
|
var meta = doc.meta;
|
||||||
|
|
|
@ -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();
|
||||||
|
}
|
|
@ -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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
|
@ -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();
|
||||||
|
});
|
||||||
|
}
|
|
@ -12,7 +12,9 @@ var path = require('path');
|
||||||
var session = require('express-session');
|
var session = require('express-session');
|
||||||
var winston = require('winston');
|
var winston = require('winston');
|
||||||
var DataStore = require('nedb');
|
var DataStore = require('nedb');
|
||||||
|
var nodemailer = require('nodemailer');
|
||||||
var UserDataStore = require('./user_data_store');
|
var UserDataStore = require('./user_data_store');
|
||||||
|
var EmailSender = require('./email_sender');
|
||||||
|
|
||||||
function run(config, ldap_client, u2f, fn) {
|
function run(config, ldap_client, u2f, fn) {
|
||||||
var view_directory = path.resolve(__dirname, '../views');
|
var view_directory = path.resolve(__dirname, '../views');
|
||||||
|
@ -20,6 +22,9 @@ function run(config, ldap_client, u2f, fn) {
|
||||||
var datastore_options = {};
|
var datastore_options = {};
|
||||||
datastore_options.directory = config.store_directory;
|
datastore_options.directory = config.store_directory;
|
||||||
|
|
||||||
|
var email_options = {};
|
||||||
|
email_options.gmail = config.gmail;
|
||||||
|
|
||||||
var app = express();
|
var app = express();
|
||||||
app.use(express.static(public_html_directory));
|
app.use(express.static(public_html_directory));
|
||||||
app.use(bodyParser.urlencoded({ extended: false }));
|
app.use(bodyParser.urlencoded({ extended: false }));
|
||||||
|
@ -46,18 +51,26 @@ function run(config, ldap_client, u2f, fn) {
|
||||||
app.set('totp engine', speakeasy);
|
app.set('totp engine', speakeasy);
|
||||||
app.set('u2f', u2f);
|
app.set('u2f', u2f);
|
||||||
app.set('user data store', new UserDataStore(DataStore, datastore_options));
|
app.set('user data store', new UserDataStore(DataStore, datastore_options));
|
||||||
|
app.set('email sender', new EmailSender(nodemailer, email_options));
|
||||||
app.set('config', config);
|
app.set('config', config);
|
||||||
|
|
||||||
|
// web pages
|
||||||
app.get ('/login', routes.login);
|
app.get ('/login', routes.login);
|
||||||
app.get ('/logout', routes.logout);
|
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);
|
app.get ('/verify', routes.verify);
|
||||||
|
|
||||||
|
// Authentication process
|
||||||
app.post ('/1stfactor', routes.first_factor);
|
app.post ('/1stfactor', routes.first_factor);
|
||||||
app.post ('/2ndfactor/totp', routes.second_factor.totp);
|
app.post ('/2ndfactor/totp', routes.second_factor.totp);
|
||||||
|
|
||||||
app.get ('/2ndfactor/u2f/register_request', routes.second_factor.u2f.register_request);
|
app.get ('/2ndfactor/u2f/register_request', routes.second_factor.u2f.register_request);
|
||||||
app.post ('/2ndfactor/u2f/register', routes.second_factor.u2f.register);
|
app.post ('/2ndfactor/u2f/register', routes.second_factor.u2f.register);
|
||||||
|
|
||||||
app.get ('/2ndfactor/u2f/sign_request', routes.second_factor.u2f.sign_request);
|
app.get ('/2ndfactor/u2f/sign_request', routes.second_factor.u2f.sign_request);
|
||||||
app.post ('/2ndfactor/u2f/sign', routes.second_factor.u2f.sign);
|
app.post ('/2ndfactor/u2f/sign', routes.second_factor.u2f.sign);
|
||||||
|
|
||||||
|
|
|
@ -5,15 +5,20 @@ var Promise = require('bluebird');
|
||||||
var path = require('path');
|
var path = require('path');
|
||||||
|
|
||||||
function UserDataStore(DataStore, options) {
|
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 = {};
|
var datastore_options = {};
|
||||||
|
|
||||||
if(options.directory)
|
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.inMemoryOnly = options.inMemoryOnly || false;
|
||||||
datastore_options.autoload = true;
|
datastore_options.autoload = true;
|
||||||
console.log(datastore_options);
|
return Promise.promisifyAll(new DataStore(datastore_options));
|
||||||
|
|
||||||
this._u2f_meta_collection = Promise.promisifyAll(new DataStore(datastore_options));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
UserDataStore.prototype.set_u2f_meta = function(userid, app_id, meta) {
|
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);
|
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 |
|
@ -42,27 +42,39 @@ function onTotpSignButtonClicked() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function onU2fSignButtonClicked() {
|
function onU2fSignButtonClicked() {
|
||||||
startSecondFactorU2fSigning(function(err) {
|
startU2fAuthentication(function(err) {
|
||||||
if(err) {
|
if(err) {
|
||||||
onSecondFactorU2fSigningFailure();
|
onU2fAuthenticationFailure();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
onSecondFactorU2fSigningSuccess();
|
onU2fAuthenticationSuccess();
|
||||||
}, 120);
|
}, 120);
|
||||||
}
|
}
|
||||||
|
|
||||||
function onU2fRegisterButtonClicked() {
|
function onU2fRegistrationButtonClicked() {
|
||||||
startSecondFactorU2fRegister(function(err) {
|
askForU2fRegistration(function(err) {
|
||||||
if(err) {
|
if(err) {
|
||||||
onSecondFactorU2fRegisterFailure();
|
$.notify('Unable to send you an email', 'error');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
onSecondFactorU2fRegisterSuccess();
|
$.notify('An email has been sent to your email address', 'info');
|
||||||
}, 120);
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function finishSecondFactorU2f(url, responseData, fn) {
|
function askForU2fRegistration(fn) {
|
||||||
console.log(responseData);
|
$.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({
|
$.ajax({
|
||||||
type: 'POST',
|
type: 'POST',
|
||||||
url: url,
|
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')
|
$.get('/auth/2ndfactor/u2f/sign_request', {}, null, 'json')
|
||||||
.done(function(signResponse) {
|
.done(function(signResponse) {
|
||||||
var registeredKeys = signResponse.registeredKeys;
|
var registeredKeys = signResponse.registeredKeys;
|
||||||
$.notify('Please touch the token', 'information');
|
$.notify('Please touch the token', 'info');
|
||||||
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'];
|
|
||||||
// }
|
|
||||||
|
|
||||||
u2f.sign(
|
u2f.sign(
|
||||||
signResponse.appId,
|
signResponse.appId,
|
||||||
|
@ -100,8 +104,7 @@ function startSecondFactorU2fSigning(fn, timeout) {
|
||||||
if (response.errorCode) {
|
if (response.errorCode) {
|
||||||
fn(response);
|
fn(response);
|
||||||
} else {
|
} else {
|
||||||
// response['sessionId'] = sessionIds[response.keyHandle];
|
finishU2fAuthentication('/auth/2ndfactor/u2f/sign', response, fn);
|
||||||
finishSecondFactorU2f('/auth/2ndfactor/u2f/sign', response, fn);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
timeout
|
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) {
|
function validateSecondFactorTotp(token, fn) {
|
||||||
$.post('/auth/2ndfactor/totp', {
|
$.post('/auth/2ndfactor/totp', {
|
||||||
token: token,
|
token: token,
|
||||||
|
@ -146,7 +127,6 @@ function validateSecondFactorTotp(token, fn) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function validateFirstFactor(username, password, fn) {
|
function validateFirstFactor(username, password, fn) {
|
||||||
$.post('/auth/1stfactor', {
|
$.post('/auth/1stfactor', {
|
||||||
username: username,
|
username: username,
|
||||||
|
@ -193,21 +173,11 @@ function onSecondFactorTotpFailure() {
|
||||||
$.notify('Wrong TOTP token', 'error');
|
$.notify('Wrong TOTP token', 'error');
|
||||||
}
|
}
|
||||||
|
|
||||||
function onSecondFactorU2fSigningSuccess() {
|
function onU2fAuthenticationSuccess() {
|
||||||
onAuthenticationSuccess();
|
onAuthenticationSuccess();
|
||||||
}
|
}
|
||||||
|
|
||||||
function onSecondFactorU2fSigningFailure(err) {
|
function onU2fAuthenticationFailure(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);
|
|
||||||
$.notify('Problem authenticating with U2F.', 'error');
|
$.notify('Problem authenticating with U2F.', 'error');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -247,26 +217,23 @@ function setupU2fSignButton() {
|
||||||
setupEnterKeypressListener('#u2f', onU2fSignButtonClicked);
|
setupEnterKeypressListener('#u2f', onU2fSignButtonClicked);
|
||||||
}
|
}
|
||||||
|
|
||||||
function setupU2fRegisterButton() {
|
function setupU2fRegistrationButton() {
|
||||||
$('#second-factor #u2f-register-button').on('click', onU2fRegisterButtonClicked);
|
$('#second-factor #u2f-register-button').on('click', onU2fRegistrationButtonClicked);
|
||||||
setupEnterKeypressListener('#u2f', onU2fRegisterButtonClicked);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function enterFirstFactor() {
|
function enterFirstFactor() {
|
||||||
// console.log('entering first factor');
|
|
||||||
showFirstFactorLayout();
|
showFirstFactorLayout();
|
||||||
hideSecondFactorLayout();
|
hideSecondFactorLayout();
|
||||||
setupFirstFactorLoginButton();
|
setupFirstFactorLoginButton();
|
||||||
}
|
}
|
||||||
|
|
||||||
function enterSecondFactor() {
|
function enterSecondFactor() {
|
||||||
// console.log('entering second factor');
|
|
||||||
hideFirstFactorLayout();
|
hideFirstFactorLayout();
|
||||||
showSecondFactorLayout();
|
showSecondFactorLayout();
|
||||||
cleanupFirstFactorLoginButton();
|
cleanupFirstFactorLoginButton();
|
||||||
setupTotpSignButton();
|
setupTotpSignButton();
|
||||||
setupU2fSignButton();
|
setupU2fSignButton();
|
||||||
setupU2fRegisterButton();
|
setupU2fRegistrationButton();
|
||||||
}
|
}
|
||||||
|
|
||||||
$(document).ready(function() {
|
$(document).ready(function() {
|
||||||
|
|
|
@ -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);
|
||||||
|
});
|
||||||
|
|
||||||
|
})();
|
|
@ -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>
|
|
@ -64,12 +64,12 @@ describe('test the server', function() {
|
||||||
it('should fail the first_factor login', function() {
|
it('should fail the first_factor login', function() {
|
||||||
return postPromised(BASE_URL + '/auth/1stfactor', {
|
return postPromised(BASE_URL + '/auth/1stfactor', {
|
||||||
form: {
|
form: {
|
||||||
username: 'admin',
|
username: 'user',
|
||||||
password: 'bad_password'
|
password: 'bad_password'
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.then(function(data) {
|
.then(function(data) {
|
||||||
assert.equal(401, data.statusCode);
|
assert.equal(data.statusCode, 401);
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -82,7 +82,7 @@ describe('test the server', function() {
|
||||||
|
|
||||||
return postPromised(BASE_URL + '/auth/1stfactor', {
|
return postPromised(BASE_URL + '/auth/1stfactor', {
|
||||||
form: {
|
form: {
|
||||||
username: 'admin',
|
username: 'user',
|
||||||
password: 'password',
|
password: 'password',
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
@ -2,24 +2,40 @@
|
||||||
var sinon = require('sinon');
|
var sinon = require('sinon');
|
||||||
var Promise = require('bluebird');
|
var Promise = require('bluebird');
|
||||||
var assert = require('assert');
|
var assert = require('assert');
|
||||||
|
var winston = require('winston');
|
||||||
var first_factor = require('../../../src/lib/routes/first_factor');
|
var first_factor = require('../../../src/lib/routes/first_factor');
|
||||||
|
|
||||||
describe('test the first factor validation route', function() {
|
describe('test the first factor validation route', function() {
|
||||||
var req, res;
|
var req, res;
|
||||||
var ldap_interface_mock;
|
var ldap_interface_mock;
|
||||||
|
var search_res_ok;
|
||||||
|
|
||||||
beforeEach(function() {
|
beforeEach(function() {
|
||||||
var bind_mock = sinon.stub();
|
|
||||||
ldap_interface_mock = {
|
ldap_interface_mock = {
|
||||||
bind: bind_mock
|
bind: sinon.stub(),
|
||||||
|
search: sinon.stub()
|
||||||
}
|
}
|
||||||
var config = {
|
var config = {
|
||||||
ldap_users_dn: 'dc=example,dc=com'
|
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();
|
var app_get = sinon.stub();
|
||||||
app_get.withArgs('ldap client').returns(ldap_interface_mock);
|
app_get.withArgs('ldap client').returns(ldap_interface_mock);
|
||||||
app_get.withArgs('config').returns(ldap_interface_mock);
|
app_get.withArgs('config').returns(ldap_interface_mock);
|
||||||
|
app_get.withArgs('logger').returns(winston);
|
||||||
|
|
||||||
req = {
|
req = {
|
||||||
app: {
|
app: {
|
||||||
get: app_get
|
get: app_get
|
||||||
|
@ -63,6 +79,18 @@ describe('test the first factor validation route', function() {
|
||||||
first_factor(req, res);
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -95,7 +95,7 @@ describe('test u2f routes', function() {
|
||||||
u2f.register(req, res);
|
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) {
|
res.send = sinon.spy(function(data) {
|
||||||
assert.equal(401, res.status.getCall(0).args[0]);
|
assert.equal(401, res.status.getCall(0).args[0]);
|
||||||
done();
|
done();
|
||||||
|
@ -105,6 +105,7 @@ describe('test u2f routes', function() {
|
||||||
u2f_mock.finishRegistration = sinon.stub();
|
u2f_mock.finishRegistration = sinon.stub();
|
||||||
u2f_mock.finishRegistration.returns(Promise.reject('Internal error'));
|
u2f_mock.finishRegistration.returns(Promise.reject('Internal error'));
|
||||||
|
|
||||||
|
req.session.auth_session.register_request = 'abc';
|
||||||
req.app.get.withArgs('u2f').returns(u2f_mock);
|
req.app.get.withArgs('u2f').returns(u2f_mock);
|
||||||
u2f.register(req, res);
|
u2f.register(req, res);
|
||||||
});
|
});
|
||||||
|
|
|
@ -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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
|
@ -17,7 +17,8 @@ describe('test data persistence', function() {
|
||||||
var u2f;
|
var u2f;
|
||||||
var tmpDir;
|
var tmpDir;
|
||||||
var ldap_client = {
|
var ldap_client = {
|
||||||
bind: sinon.stub()
|
bind: sinon.stub(),
|
||||||
|
search: sinon.stub()
|
||||||
};
|
};
|
||||||
var config;
|
var config;
|
||||||
|
|
||||||
|
@ -28,10 +29,23 @@ describe('test data persistence', function() {
|
||||||
u2f.startAuthentication = sinon.stub();
|
u2f.startAuthentication = sinon.stub();
|
||||||
u2f.finishAuthentication = 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',
|
ldap_client.bind.withArgs('cn=test_ok,ou=users,dc=example,dc=com',
|
||||||
'password').yields(undefined);
|
'password').yields(undefined);
|
||||||
ldap_client.bind.withArgs('cn=test_nok,ou=users,dc=example,dc=com',
|
ldap_client.bind.withArgs('cn=test_nok,ou=users,dc=example,dc=com',
|
||||||
'password').yields('error');
|
'password').yields('error');
|
||||||
|
ldap_client.search.yields(undefined, search_res);
|
||||||
|
|
||||||
tmpDir = tmp.dirSync({ unsafeCleanup: true });
|
tmpDir = tmp.dirSync({ unsafeCleanup: true });
|
||||||
config = {
|
config = {
|
||||||
port: PORT,
|
port: PORT,
|
||||||
|
@ -40,7 +54,8 @@ describe('test data persistence', function() {
|
||||||
ldap_users_dn: 'ou=users,dc=example,dc=com',
|
ldap_users_dn: 'ou=users,dc=example,dc=com',
|
||||||
session_secret: 'session_secret',
|
session_secret: 'session_secret',
|
||||||
session_max_age: 50000,
|
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) {
|
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) {
|
function execute_login(jar) {
|
||||||
|
|
|
@ -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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -10,43 +10,80 @@ describe('test ldap validation', function() {
|
||||||
|
|
||||||
beforeEach(function() {
|
beforeEach(function() {
|
||||||
ldap_client = {
|
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 username = 'user';
|
||||||
var password = 'password';
|
var password = 'password';
|
||||||
var ldap_url = 'http://ldap';
|
var ldap_url = 'http://ldap';
|
||||||
var users_dn = 'dc=example,dc=com';
|
var users_dn = 'dc=example,dc=com';
|
||||||
return ldap.validate(ldap_client, username, password, ldap_url, users_dn);
|
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.search.yields(undefined, res_emitter);
|
||||||
ldap_client.bind.yields();
|
|
||||||
return test_validate();
|
|
||||||
});
|
|
||||||
|
|
||||||
// cover an issue with promisify context
|
return ldap.get_email(ldap_client, 'user', 'dc=example,dc=com')
|
||||||
it('should promisify correctly', function() {
|
.then(function(doc) {
|
||||||
function LdapClient() {
|
assert.deepEqual(doc, expected_doc.object);
|
||||||
this.test = 'abc';
|
return Promise.resolve();
|
||||||
}
|
})
|
||||||
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();
|
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
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();
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -15,7 +15,8 @@ describe('test the server', function() {
|
||||||
var _server
|
var _server
|
||||||
var u2f;
|
var u2f;
|
||||||
var ldap_client = {
|
var ldap_client = {
|
||||||
bind: sinon.stub()
|
bind: sinon.stub(),
|
||||||
|
search: sinon.stub()
|
||||||
};
|
};
|
||||||
|
|
||||||
beforeEach(function(done) {
|
beforeEach(function(done) {
|
||||||
|
@ -25,7 +26,11 @@ describe('test the server', function() {
|
||||||
ldap_url: 'ldap://127.0.0.1:389',
|
ldap_url: 'ldap://127.0.0.1:389',
|
||||||
ldap_users_dn: 'ou=users,dc=example,dc=com',
|
ldap_users_dn: 'ou=users,dc=example,dc=com',
|
||||||
session_secret: 'session_secret',
|
session_secret: 'session_secret',
|
||||||
session_max_age: 50000
|
session_max_age: 50000,
|
||||||
|
gmail: {
|
||||||
|
user: 'user@example.com',
|
||||||
|
pass: 'password'
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
u2f = {};
|
u2f = {};
|
||||||
|
@ -34,8 +39,20 @@ describe('test the server', function() {
|
||||||
u2f.startAuthentication = sinon.stub();
|
u2f.startAuthentication = sinon.stub();
|
||||||
u2f.finishAuthentication = 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',
|
ldap_client.bind.withArgs('cn=test_ok,ou=users,dc=example,dc=com',
|
||||||
'password').yields(undefined);
|
'password').yields(undefined);
|
||||||
|
ldap_client.search.yields(undefined, search_res);
|
||||||
ldap_client.bind.withArgs('cn=test_nok,ou=users,dc=example,dc=com',
|
ldap_client.bind.withArgs('cn=test_nok,ou=users,dc=example,dc=com',
|
||||||
'password').yields('error');
|
'password').yields('error');
|
||||||
_server = server.run(config, ldap_client, u2f, function() {
|
_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() {
|
it('should return status code 204 when user is authenticated using u2f', function() {
|
||||||
var sign_request = {};
|
var sign_request = {};
|
||||||
var sign_status = {};
|
var sign_status = {};
|
||||||
|
@ -193,6 +255,5 @@ describe('test the server', function() {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -3,8 +3,15 @@ var UserDataStore = require('../../src/lib/user_data_store');
|
||||||
var DataStore = require('nedb');
|
var DataStore = require('nedb');
|
||||||
var assert = require('assert');
|
var assert = require('assert');
|
||||||
var Promise = require('bluebird');
|
var Promise = require('bluebird');
|
||||||
|
var sinon = require('sinon');
|
||||||
|
var MockDate = require('mockdate');
|
||||||
|
|
||||||
describe('test user data store', function() {
|
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() {
|
it('should save a u2f meta', function() {
|
||||||
var options = {};
|
var options = {};
|
||||||
options.inMemoryOnly = true;
|
options.inMemoryOnly = true;
|
||||||
|
@ -65,4 +72,91 @@ describe('test user data store', function() {
|
||||||
return Promise.resolve();
|
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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue