From 606ddc7308c6400e5c17975c7214bd83a1ea39c7 Mon Sep 17 00:00:00 2001 From: Clement Michaud Date: Wed, 15 Mar 2017 23:07:57 +0100 Subject: [PATCH 1/3] Handle SSO over multiple subdomains --- config.template.yml | 27 ++++++++++++------- example/nginx_conf/index.html | 5 ++-- example/nginx_conf/nginx.conf | 31 +++++++++++++++++++--- src/index.js | 3 +++ src/lib/routes/u2f.js | 2 +- src/lib/routes/u2f_register.js | 2 +- src/lib/server.js | 6 ++--- test/unitary/test_data_persistence.js | 2 ++ test/unitary/test_server.js | 2 ++ test/unitary/test_server_config.js | 37 +++++++++++++++++++++++++++ 10 files changed, 97 insertions(+), 20 deletions(-) create mode 100644 test/unitary/test_server_config.js diff --git a/config.template.yml b/config.template.yml index d514ba31..bba35f99 100644 --- a/config.template.yml +++ b/config.template.yml @@ -1,32 +1,41 @@ -### Level of verbosity for logs +# Level of verbosity for logs logs_level: info -### Configuration of your LDAP +# Configuration of LDAP ldap: url: ldap://ldap base_dn: ou=users,dc=example,dc=com user: cn=admin,dc=example,dc=com password: password -### Configuration of session cookies + +# Configuration of session cookies +# +# _secret_ the secret to encrypt session cookies +# _expiration_ the time before cookies expire +# _domain_ the domain to protect. +# Note: the authenticator must also be in that domain. If empty, the cookie +# is restricted to the subdomain of the issuer. session: secret: unsecure_secret expiration: 3600000 + domain: example.com -### The directory where the DB files will be saved + +# The directory where the DB files will be saved store_directory: /var/lib/auth-server/store -### Notifications are sent to users when they require a password reset, a u2f -### registration or a TOTP registration. -### Use only one available configuration: filesystem, gmail +# Notifications are sent to users when they require a password reset, a u2f +# registration or a TOTP registration. +# Use only one available configuration: filesystem, gmail notifier: - ### For testing purpose, notifications can be sent in a file + # For testing purpose, notifications can be sent in a file filesystem: filename: /var/lib/auth-server/notifications/notification.txt - ### Use your gmail account to send the notifications. You can use an app password. + # Use your gmail account to send the notifications. You can use an app password. # gmail: # username: user@example.com # password: yourpassword diff --git a/example/nginx_conf/index.html b/example/nginx_conf/index.html index 94ff081e..c59b1e20 100644 --- a/example/nginx_conf/index.html +++ b/example/nginx_conf/index.html @@ -3,7 +3,8 @@ Home page - You need to log in to access the secret!

- You can also log off by visiting the following link. + You need to log in to access the secret!

+ But you can also access it from another domain or still another one.

+ You can also log off by visiting the following link. diff --git a/example/nginx_conf/nginx.conf b/example/nginx_conf/nginx.conf index 4919f43a..5ffafad6 100644 --- a/example/nginx_conf/nginx.conf +++ b/example/nginx_conf/nginx.conf @@ -24,9 +24,7 @@ events { http { server { listen 443 ssl; - root /usr/share/nginx/html; - - server_name 127.0.0.1 localhost; + server_name auth.test.local localhost; ssl on; ssl_certificate /etc/ssl/server.crt; @@ -34,7 +32,7 @@ http { error_page 401 = @error401; location @error401 { - return 302 https://localhost:8080/authentication/login?redirect=$request_uri; + return 302 https://auth.test.local:8080/authentication/login?redirect=$scheme://$http_host$request_uri; } location /authentication/ { @@ -56,6 +54,30 @@ http { location /authentication/css/ { proxy_pass http://auth/css/; } + } + + server { + listen 443 ssl; + root /usr/share/nginx/html; + + server_name secret1.test.local secret2.test.local secret.test.local localhost; + + ssl on; + ssl_certificate /etc/ssl/server.crt; + ssl_certificate_key /etc/ssl/server.key; + + error_page 401 = @error401; + location @error401 { + return 302 https://auth.test.local:8080/authentication/login?redirect=$scheme://$http_host$request_uri; + } + + location /authentication/verify { + proxy_set_header X-Original-URI $request_uri; + proxy_set_header Host $http_host; + proxy_set_header X-Real-IP $remote_addr; + + proxy_pass http://auth/authentication/verify; + } location = /secret.html { auth_request /authentication/verify; @@ -69,3 +91,4 @@ http { } } } + diff --git a/src/index.js b/src/index.js index cb452f66..2a0a00a1 100755 --- a/src/index.js +++ b/src/index.js @@ -9,6 +9,7 @@ var u2f = require('authdog'); var nodemailer = require('nodemailer'); var nedb = require('nedb'); var YAML = require('yamljs'); +var session = require('express-session'); var config_path = process.argv[2]; if(!config_path) { @@ -27,6 +28,7 @@ var config = { ldap_users_dn: yaml_config.ldap.base_dn, ldap_user: yaml_config.ldap.user, ldap_password: yaml_config.ldap.password, + session_domain: yaml_config.session.domain, session_secret: yaml_config.session.secret, session_max_age: yaml_config.session.expiration || 3600000, // in ms store_directory: yaml_config.store_directory, @@ -48,5 +50,6 @@ deps.u2f = u2f; deps.nedb = nedb; deps.nodemailer = nodemailer; deps.ldap = ldap; +deps.session = session; server.run(config, ldap_client, deps); diff --git a/src/lib/routes/u2f.js b/src/lib/routes/u2f.js index 00e8d929..604159e2 100644 --- a/src/lib/routes/u2f.js +++ b/src/lib/routes/u2f.js @@ -35,7 +35,7 @@ function sign_request(req, res) { var u2f = req.app.get('u2f'); var meta = doc.meta; var appid = u2f_common.extract_app_id(req); - logger.info('U2F sign_request: Start authentication'); + logger.info('U2F sign_request: Start authentication to app %s', appid); return u2f.startAuthentication(appid, [meta]) }) .then(function(authRequest) { diff --git a/src/lib/routes/u2f_register.js b/src/lib/routes/u2f_register.js index 6ba17ede..5161d965 100644 --- a/src/lib/routes/u2f_register.js +++ b/src/lib/routes/u2f_register.js @@ -25,7 +25,7 @@ function register_request(req, res) { 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'); + logger.info('U2F register_request: Starting registration of app %s', appid); u2f.startRegistration(appid, []) .then(function(registrationRequest) { logger.info('U2F register_request: Sending back registration request'); diff --git a/src/lib/server.js b/src/lib/server.js index 4dbfcd7b..a828d670 100644 --- a/src/lib/server.js +++ b/src/lib/server.js @@ -7,7 +7,6 @@ var express = require('express'); var bodyParser = require('body-parser'); var speakeasy = require('speakeasy'); var path = require('path'); -var session = require('express-session'); var winston = require('winston'); var UserDataStore = require('./user_data_store'); var Notifier = require('./notifier'); @@ -28,13 +27,14 @@ function run(config, ldap_client, deps, fn) { app.use(bodyParser.json()); app.set('trust proxy', 1); // trust first proxy - app.use(session({ + app.use(deps.session({ secret: config.session_secret, resave: false, saveUninitialized: true, cookie: { secure: false, - maxAge: config.session_max_age + maxAge: config.session_max_age, + domain: config.session_domain }, })); diff --git a/test/unitary/test_data_persistence.js b/test/unitary/test_data_persistence.js index 20264104..32effd2c 100644 --- a/test/unitary/test_data_persistence.js +++ b/test/unitary/test_data_persistence.js @@ -8,6 +8,7 @@ var speakeasy = require('speakeasy'); var sinon = require('sinon'); var tmp = require('tmp'); var nedb = require('nedb'); +var session = require('express-session'); var PORT = 8050; var BASE_URL = 'http://localhost:' + PORT; @@ -88,6 +89,7 @@ describe('test data persistence', function() { deps.u2f = u2f; deps.nedb = nedb; deps.nodemailer = nodemailer; + deps.session = session; var j1 = request.jar(); var j2 = request.jar(); diff --git a/test/unitary/test_server.js b/test/unitary/test_server.js index 5e2160d5..8f6632e4 100644 --- a/test/unitary/test_server.js +++ b/test/unitary/test_server.js @@ -7,6 +7,7 @@ var assert = require('assert'); var speakeasy = require('speakeasy'); var sinon = require('sinon'); var MockDate = require('mockdate'); +var session = require('express-session'); var PORT = 8090; var BASE_URL = 'http://localhost:' + PORT; @@ -89,6 +90,7 @@ describe('test the server', function() { deps.nedb = nedb; deps.nodemailer = nodemailer; deps.ldap = ldap; + deps.session = session; _server = server.run(config, ldap_client, deps, function() { done(); diff --git a/test/unitary/test_server_config.js b/test/unitary/test_server_config.js new file mode 100644 index 00000000..1e3d74b9 --- /dev/null +++ b/test/unitary/test_server_config.js @@ -0,0 +1,37 @@ + +var sinon = require('sinon'); +var server = require('../../src/lib/server'); +var assert = require('assert'); + +describe('test server configuration', function() { + it('should set cookie scope to domain set in the config', function() { + var config = {}; + config.session_domain = 'example.com'; + config.notifier = { + gmail: { + user: 'user@example.com', + pass: 'password' + } + } + + transporter = {}; + transporter.sendMail = sinon.stub().yields(); + + var nodemailer = {}; + nodemailer.createTransport = sinon.spy(function() { + return transporter; +  }); + + var deps = {}; + deps.nedb = require('nedb'); + deps.nodemailer = nodemailer; + deps.session = sinon.spy(function() { + return function(req, res, next) { next(); }; + }); + + server.run(config, undefined, deps); + + assert(deps.session.calledOnce); + assert.equal(deps.session.getCall(0).args[0].cookie.domain, 'example.com'); + }); +}); From 0eb5379a452b7bb0ded3960ee750050fd65040b5 Mon Sep 17 00:00:00 2001 From: Clement Michaud Date: Wed, 15 Mar 2017 23:47:59 +0100 Subject: [PATCH 2/3] Handle redirection after registration either with U2F or TOTP --- src/lib/identity_check.js | 4 ++++ src/public_html/js/login.js | 20 ++++++++++++++++++-- src/public_html/js/totp-register.js | 14 ++++++++++++++ src/public_html/js/u2f-register.js | 2 +- src/views/totp-register.ejs | 2 +- 5 files changed, 38 insertions(+), 4 deletions(-) diff --git a/src/lib/identity_check.js b/src/lib/identity_check.js index 1b37c28b..bef35534 100644 --- a/src/lib/identity_check.js +++ b/src/lib/identity_check.js @@ -109,8 +109,12 @@ function identity_check_post(endpoint, icheck_interface) { throw new exceptions.AccessDeniedError(); }) .then(function(token) { + var redirect_url = objectPath.get(req, 'body.redirect'); var original_url = util.format('https://%s%s', req.headers.host, req.headers['x-original-uri']); var link_url = util.format('%s?identity_token=%s', original_url, token); + if(redirect_url) { + link_url = util.format('%s&redirect=%s', link_url, redirect_url); + } logger.info('POST identity_check: notify to %s', identity.userid); return notifier.notify(identity, icheck_interface.email_subject, link_url); diff --git a/src/public_html/js/login.js b/src/public_html/js/login.js index 6fa227f8..5da20d41 100644 --- a/src/public_html/js/login.js +++ b/src/public_html/js/login.js @@ -3,6 +3,11 @@ params={}; location.search.replace(/[?&]+([^=&]+)=([^&]*)/gi,function(s,k,v){params[k]=v}); +function get_redirect_param() { + if('redirect' in params) + return params['redirect']; + return; +} function setupEnterKeypressListener(filter, fn) { $(filter).on('keydown', 'input', function (e) { @@ -49,7 +54,12 @@ function onTotpSignButtonClicked() { function onTotpRegisterButtonClicked() { $.ajax({ type: 'POST', - url: '/authentication/totp-register' + url: '/authentication/totp-register', + data: JSON.stringify({ + redirect: get_redirect_param() + }), + contentType: 'application/json', + dataType: 'json', }) .done(function(data) { $.notify('An email has been sent to your email address', 'info'); @@ -82,7 +92,12 @@ function onU2fRegistrationButtonClicked() { function askForU2fRegistration(fn) { $.ajax({ type: 'POST', - url: '/authentication/u2f-register' + url: '/authentication/u2f-register', + data: JSON.stringify({ + redirect: get_redirect_param() + }), + contentType: 'application/json', + dataType: 'json', }) .done(function(data) { fn(undefined, data); @@ -158,6 +173,7 @@ function validateFirstFactor(username, password, fn) { }); } + function redirect() { var redirect_uri = '/'; if('redirect' in params) { diff --git a/src/public_html/js/totp-register.js b/src/public_html/js/totp-register.js index e4f4b7eb..51a0c768 100644 --- a/src/public_html/js/totp-register.js +++ b/src/public_html/js/totp-register.js @@ -1,5 +1,8 @@ (function() { +params={}; +location.search.replace(/[?&]+([^=&]+)=([^&]*)/gi,function(s,k,v){params[k]=v}); + function generateSecret(fn) { $.ajax({ type: 'POST', @@ -22,7 +25,18 @@ function onSecretGenerated(err, secret) { $("#secret").text(secret.base32); } +function redirect() { + var redirect_uri = '/authentication/login'; + if('redirect' in params) { + redirect_uri = params['redirect']; + } + window.location.replace(redirect_uri); +} + $(document).ready(function() { generateSecret(onSecretGenerated); + $('#login-button').on('click', function() { + redirect(); + }); }); })(); diff --git a/src/public_html/js/u2f-register.js b/src/public_html/js/u2f-register.js index 619e6ad6..53614bf6 100644 --- a/src/public_html/js/u2f-register.js +++ b/src/public_html/js/u2f-register.js @@ -39,7 +39,7 @@ function startRegister(fn, timeout) { } function redirect() { - var redirect_uri = '/'; + var redirect_uri = '/authentication/login'; if('redirect' in params) { redirect_uri = params['redirect']; } diff --git a/src/views/totp-register.ejs b/src/views/totp-register.ejs index 2652459e..03f06763 100644 --- a/src/views/totp-register.ejs +++ b/src/views/totp-register.ejs @@ -9,7 +9,7 @@

Insert your secret in Google Authenticator

-

Login

+

Login

From d91ffb0b06649fd2f605e207979fafb7b7596b1d Mon Sep 17 00:00:00 2001 From: Clement Michaud Date: Wed, 15 Mar 2017 23:51:29 +0100 Subject: [PATCH 3/3] Ask the user to add lines to /etc/hosts in the getting started section of the README --- README.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 8b4c362f..43a65e8d 100644 --- a/README.md +++ b/README.md @@ -35,13 +35,20 @@ Otherwise here are the available steps to deploy on your machine. The provided example is docker-based so that you can deploy and test it very quickly. First clone the repo make sure you don't have anything listening on port 8080 before starting. +Add the following lines to your /etc/hosts to simulate multiple subdomains + + 127.0.0.1 secret.test.local + 127.0.0.1 secret1.test.local + 127.0.0.1 secret2.test.local + 127.0.0.1 auth.test.local + Then, type the following command to build and deploy the services: docker-compose build docker-compose up -d After few seconds the services should be running and you should be able to visit -[https://localhost:8080/](https://localhost:8080/). +[https://localhost:8080/](https://secret.test.local:8080/). Normally, a self-signed certificate exception should appear, it has to be accepted before getting to the login page: