diff --git a/.gitignore b/.gitignore index 924d1e3ac..7a4b1e5aa 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,7 @@ coverage/ *.swp *.sh + +config.yml + +npm-debug.log diff --git a/Dockerfile b/Dockerfile index 72af1bb0c..1bf24d1da 100644 --- a/Dockerfile +++ b/Dockerfile @@ -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"] diff --git a/config.template.yml b/config.template.yml new file mode 100644 index 000000000..bef6d1089 --- /dev/null +++ b/config.template.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 + diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index 00074863b..2352c93fc 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -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 diff --git a/docker-compose.yml b/docker-compose.yml index 1c18c8329..8f62215fd 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -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: diff --git a/example/ldap/base.ldif b/example/ldap/base.ldif new file mode 100644 index 000000000..ba1f727d9 --- /dev/null +++ b/example/ldap/base.ldif @@ -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= + diff --git a/nginx_conf/index.html b/example/nginx_conf/index.html similarity index 100% rename from nginx_conf/index.html rename to example/nginx_conf/index.html diff --git a/nginx_conf/nginx.conf b/example/nginx_conf/nginx.conf similarity index 100% rename from nginx_conf/nginx.conf rename to example/nginx_conf/nginx.conf diff --git a/nginx_conf/secret.html b/example/nginx_conf/secret.html similarity index 100% rename from nginx_conf/secret.html rename to example/nginx_conf/secret.html diff --git a/nginx_conf/ssl/server.crt b/example/nginx_conf/ssl/server.crt similarity index 100% rename from nginx_conf/ssl/server.crt rename to example/nginx_conf/ssl/server.crt diff --git a/nginx_conf/ssl/server.csr b/example/nginx_conf/ssl/server.csr similarity index 100% rename from nginx_conf/ssl/server.csr rename to example/nginx_conf/ssl/server.csr diff --git a/nginx_conf/ssl/server.key b/example/nginx_conf/ssl/server.key similarity index 100% rename from nginx_conf/ssl/server.key rename to example/nginx_conf/ssl/server.key diff --git a/package.json b/package.json index ac5bb0573..bb9763798 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/index.js b/src/index.js index 9ba2c02fa..4e9a10385 100644 --- a/src/index.js +++ b/src/index.js @@ -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({ diff --git a/src/lib/email_sender.js b/src/lib/email_sender.js new file mode 100644 index 000000000..8f25e841c --- /dev/null +++ b/src/lib/email_sender.js @@ -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); +} + diff --git a/src/lib/exceptions.js b/src/lib/exceptions.js new file mode 100644 index 000000000..7b7a1d98d --- /dev/null +++ b/src/lib/exceptions.js @@ -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); diff --git a/src/lib/ldap.js b/src/lib/ldap.js index c2e36156b..b267cfe98 100644 --- a/src/lib/ldap.js +++ b/src/lib/ldap.js @@ -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)); + }); + }); } diff --git a/src/lib/routes.js b/src/lib/routes.js index b9e46e202..b74de5db0 100644 --- a/src/lib/routes.js +++ b/src/lib/routes.js @@ -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'); } diff --git a/src/lib/routes/first_factor.js b/src/lib/routes/first_factor.js index 4588a9749..1551be6cd 100644 --- a/src/lib/routes/first_factor.js +++ b/src/lib/routes/first_factor.js @@ -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); }); } diff --git a/src/lib/routes/second_factor.js b/src/lib/routes/second_factor.js index 1c5f72e4e..f57149dd0 100644 --- a/src/lib/routes/second_factor.js +++ b/src/lib/routes/second_factor.js @@ -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), } diff --git a/src/lib/routes/u2f.js b/src/lib/routes/u2f.js index 0f504fa25..33f423d73 100644 --- a/src/lib/routes/u2f.js +++ b/src/lib/routes/u2f.js @@ -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; diff --git a/src/lib/routes/u2f_common.js b/src/lib/routes/u2f_common.js new file mode 100644 index 000000000..4b7e46020 --- /dev/null +++ b/src/lib/routes/u2f_common.js @@ -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(); +} diff --git a/src/lib/routes/u2f_register.js b/src/lib/routes/u2f_register.js new file mode 100644 index 000000000..50785232a --- /dev/null +++ b/src/lib/routes/u2f_register.js @@ -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); + }); +} + diff --git a/src/lib/routes/u2f_register_handler.js b/src/lib/routes/u2f_register_handler.js new file mode 100644 index 000000000..3e99c8fa6 --- /dev/null +++ b/src/lib/routes/u2f_register_handler.js @@ -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('Register', 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(); + }); +} diff --git a/src/lib/server.js b/src/lib/server.js index ee02d70e4..fe2394fc6 100644 --- a/src/lib/server.js +++ b/src/lib/server.js @@ -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); diff --git a/src/lib/user_data_store.js b/src/lib/user_data_store.js index 5c7b363fe..b44e00af0 100644 --- a/src/lib/user_data_store.js +++ b/src/lib/user_data_store.js @@ -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(); + }); +} diff --git a/src/public_html/img/pendrive.png b/src/public_html/img/pendrive.png new file mode 100644 index 000000000..fa49178c3 Binary files /dev/null and b/src/public_html/img/pendrive.png differ diff --git a/src/public_html/login.js b/src/public_html/login.js index 55df039dc..7c0e22cd2 100644 --- a/src/public_html/login.js +++ b/src/public_html/login.js @@ -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() { diff --git a/src/public_html/u2f-register.js b/src/public_html/u2f-register.js new file mode 100644 index 000000000..56b6721a9 --- /dev/null +++ b/src/public_html/u2f-register.js @@ -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); +}); + +})(); diff --git a/src/views/u2f_register.ejs b/src/views/u2f_register.ejs new file mode 100644 index 000000000..0bf8b353d --- /dev/null +++ b/src/views/u2f_register.ejs @@ -0,0 +1,17 @@ + +
+