Merge pull request #17 from clems4ever/multi-subdomain

Handle SSO accross subdomains
This commit is contained in:
Clément Michaud 2017-03-16 00:01:30 +01:00 committed by GitHub
commit 0d494055ac
16 changed files with 143 additions and 25 deletions

View File

@ -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 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 quickly. First clone the repo make sure you don't have anything listening on
port 8080 before starting. 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: Then, type the following command to build and deploy the services:
docker-compose build docker-compose build
docker-compose up -d docker-compose up -d
After few seconds the services should be running and you should be able to visit 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 Normally, a self-signed certificate exception should appear, it has to be
accepted before getting to the login page: accepted before getting to the login page:

View File

@ -1,32 +1,41 @@
### Level of verbosity for logs # Level of verbosity for logs
logs_level: info logs_level: info
### Configuration of your LDAP # Configuration of LDAP
ldap: ldap:
url: ldap://ldap url: ldap://ldap
base_dn: ou=users,dc=example,dc=com base_dn: ou=users,dc=example,dc=com
user: cn=admin,dc=example,dc=com user: cn=admin,dc=example,dc=com
password: password 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: session:
secret: unsecure_secret secret: unsecure_secret
expiration: 3600000 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 store_directory: /var/lib/auth-server/store
### Notifications are sent to users when they require a password reset, a u2f # Notifications are sent to users when they require a password reset, a u2f
### registration or a TOTP registration. # registration or a TOTP registration.
### Use only one available configuration: filesystem, gmail # Use only one available configuration: filesystem, gmail
notifier: notifier:
### For testing purpose, notifications can be sent in a file # For testing purpose, notifications can be sent in a file
filesystem: filesystem:
filename: /var/lib/auth-server/notifications/notification.txt 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: # gmail:
# username: user@example.com # username: user@example.com
# password: yourpassword # password: yourpassword

View File

@ -3,7 +3,8 @@
<title>Home page</title> <title>Home page</title>
</head> </head>
<body> <body>
You need to <a href="/authentication/login?redirect=/">log in</a> to access the <a href="/secret.html">secret</a>!<br/><br/> You need to <a href="https://auth.test.local:8080/authentication/login?redirect=https://secret.test.local:8080/">log in</a> to access the <a href="/secret.html">secret</a>!<br/><br/>
You can also log off by visiting the following <a href="/authentication/logout?redirect=/">link</a>. But you can also access it from another <a href="https://secret1.test.local:8080/secret.html">domain</a> or still <a href="https://secret2.test.local:8080/secret.html">another one</a>.<br/><br/>
You can also log off by visiting the following <a href="https://auth.test.local:8080/authentication/logout?redirect=https://secret.test.local:8080/">link</a>.
</body> </body>
</html> </html>

View File

@ -24,9 +24,7 @@ events {
http { http {
server { server {
listen 443 ssl; listen 443 ssl;
root /usr/share/nginx/html; server_name auth.test.local localhost;
server_name 127.0.0.1 localhost;
ssl on; ssl on;
ssl_certificate /etc/ssl/server.crt; ssl_certificate /etc/ssl/server.crt;
@ -34,7 +32,7 @@ http {
error_page 401 = @error401; error_page 401 = @error401;
location @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/ { location /authentication/ {
@ -56,6 +54,30 @@ http {
location /authentication/css/ { location /authentication/css/ {
proxy_pass http://auth/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 { location = /secret.html {
auth_request /authentication/verify; auth_request /authentication/verify;
@ -69,3 +91,4 @@ http {
} }
} }
} }

View File

@ -9,6 +9,7 @@ var u2f = require('authdog');
var nodemailer = require('nodemailer'); var nodemailer = require('nodemailer');
var nedb = require('nedb'); var nedb = require('nedb');
var YAML = require('yamljs'); var YAML = require('yamljs');
var session = require('express-session');
var config_path = process.argv[2]; var config_path = process.argv[2];
if(!config_path) { if(!config_path) {
@ -27,6 +28,7 @@ var config = {
ldap_users_dn: yaml_config.ldap.base_dn, ldap_users_dn: yaml_config.ldap.base_dn,
ldap_user: yaml_config.ldap.user, ldap_user: yaml_config.ldap.user,
ldap_password: yaml_config.ldap.password, ldap_password: yaml_config.ldap.password,
session_domain: yaml_config.session.domain,
session_secret: yaml_config.session.secret, session_secret: yaml_config.session.secret,
session_max_age: yaml_config.session.expiration || 3600000, // in ms session_max_age: yaml_config.session.expiration || 3600000, // in ms
store_directory: yaml_config.store_directory, store_directory: yaml_config.store_directory,
@ -48,5 +50,6 @@ deps.u2f = u2f;
deps.nedb = nedb; deps.nedb = nedb;
deps.nodemailer = nodemailer; deps.nodemailer = nodemailer;
deps.ldap = ldap; deps.ldap = ldap;
deps.session = session;
server.run(config, ldap_client, deps); server.run(config, ldap_client, deps);

View File

@ -109,8 +109,12 @@ function identity_check_post(endpoint, icheck_interface) {
throw new exceptions.AccessDeniedError(); throw new exceptions.AccessDeniedError();
}) })
.then(function(token) { .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 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); 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); logger.info('POST identity_check: notify to %s', identity.userid);
return notifier.notify(identity, icheck_interface.email_subject, link_url); return notifier.notify(identity, icheck_interface.email_subject, link_url);

View File

@ -35,7 +35,7 @@ function sign_request(req, res) {
var u2f = req.app.get('u2f'); var u2f = req.app.get('u2f');
var meta = doc.meta; var meta = doc.meta;
var appid = u2f_common.extract_app_id(req); 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]) return u2f.startAuthentication(appid, [meta])
}) })
.then(function(authRequest) { .then(function(authRequest) {

View File

@ -25,7 +25,7 @@ function register_request(req, res) {
var appid = u2f_common.extract_app_id(req); var appid = u2f_common.extract_app_id(req);
logger.debug('U2F register_request: headers=%s', JSON.stringify(req.headers)); 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, []) u2f.startRegistration(appid, [])
.then(function(registrationRequest) { .then(function(registrationRequest) {
logger.info('U2F register_request: Sending back registration request'); logger.info('U2F register_request: Sending back registration request');

View File

@ -7,7 +7,6 @@ var express = require('express');
var bodyParser = require('body-parser'); var bodyParser = require('body-parser');
var speakeasy = require('speakeasy'); var speakeasy = require('speakeasy');
var path = require('path'); var path = require('path');
var session = require('express-session');
var winston = require('winston'); var winston = require('winston');
var UserDataStore = require('./user_data_store'); var UserDataStore = require('./user_data_store');
var Notifier = require('./notifier'); var Notifier = require('./notifier');
@ -28,13 +27,14 @@ function run(config, ldap_client, deps, fn) {
app.use(bodyParser.json()); app.use(bodyParser.json());
app.set('trust proxy', 1); // trust first proxy app.set('trust proxy', 1); // trust first proxy
app.use(session({ app.use(deps.session({
secret: config.session_secret, secret: config.session_secret,
resave: false, resave: false,
saveUninitialized: true, saveUninitialized: true,
cookie: { cookie: {
secure: false, secure: false,
maxAge: config.session_max_age maxAge: config.session_max_age,
domain: config.session_domain
}, },
})); }));

View File

@ -3,6 +3,11 @@
params={}; params={};
location.search.replace(/[?&]+([^=&]+)=([^&]*)/gi,function(s,k,v){params[k]=v}); 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) { function setupEnterKeypressListener(filter, fn) {
$(filter).on('keydown', 'input', function (e) { $(filter).on('keydown', 'input', function (e) {
@ -49,7 +54,12 @@ function onTotpSignButtonClicked() {
function onTotpRegisterButtonClicked() { function onTotpRegisterButtonClicked() {
$.ajax({ $.ajax({
type: 'POST', 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) { .done(function(data) {
$.notify('An email has been sent to your email address', 'info'); $.notify('An email has been sent to your email address', 'info');
@ -82,7 +92,12 @@ function onU2fRegistrationButtonClicked() {
function askForU2fRegistration(fn) { function askForU2fRegistration(fn) {
$.ajax({ $.ajax({
type: 'POST', 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) { .done(function(data) {
fn(undefined, data); fn(undefined, data);
@ -158,6 +173,7 @@ function validateFirstFactor(username, password, fn) {
}); });
} }
function redirect() { function redirect() {
var redirect_uri = '/'; var redirect_uri = '/';
if('redirect' in params) { if('redirect' in params) {

View File

@ -1,5 +1,8 @@
(function() { (function() {
params={};
location.search.replace(/[?&]+([^=&]+)=([^&]*)/gi,function(s,k,v){params[k]=v});
function generateSecret(fn) { function generateSecret(fn) {
$.ajax({ $.ajax({
type: 'POST', type: 'POST',
@ -22,7 +25,18 @@ function onSecretGenerated(err, secret) {
$("#secret").text(secret.base32); $("#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() { $(document).ready(function() {
generateSecret(onSecretGenerated); generateSecret(onSecretGenerated);
$('#login-button').on('click', function() {
redirect();
});
}); });
})(); })();

View File

@ -39,7 +39,7 @@ function startRegister(fn, timeout) {
} }
function redirect() { function redirect() {
var redirect_uri = '/'; var redirect_uri = '/authentication/login';
if('redirect' in params) { if('redirect' in params) {
redirect_uri = params['redirect']; redirect_uri = params['redirect'];
} }

View File

@ -9,7 +9,7 @@
<p>Insert your secret in Google Authenticator</p> <p>Insert your secret in Google Authenticator</p>
<p id="secret"></p> <p id="secret"></p>
<div id="qrcode"></div> <div id="qrcode"></div>
<p><a href="/authentication/login">Login</a></p> <p><a href="#" id="login-button">Login</a></p>
</div> </div>
</body> </body>

View File

@ -8,6 +8,7 @@ var speakeasy = require('speakeasy');
var sinon = require('sinon'); var sinon = require('sinon');
var tmp = require('tmp'); var tmp = require('tmp');
var nedb = require('nedb'); var nedb = require('nedb');
var session = require('express-session');
var PORT = 8050; var PORT = 8050;
var BASE_URL = 'http://localhost:' + PORT; var BASE_URL = 'http://localhost:' + PORT;
@ -88,6 +89,7 @@ describe('test data persistence', function() {
deps.u2f = u2f; deps.u2f = u2f;
deps.nedb = nedb; deps.nedb = nedb;
deps.nodemailer = nodemailer; deps.nodemailer = nodemailer;
deps.session = session;
var j1 = request.jar(); var j1 = request.jar();
var j2 = request.jar(); var j2 = request.jar();

View File

@ -7,6 +7,7 @@ var assert = require('assert');
var speakeasy = require('speakeasy'); var speakeasy = require('speakeasy');
var sinon = require('sinon'); var sinon = require('sinon');
var MockDate = require('mockdate'); var MockDate = require('mockdate');
var session = require('express-session');
var PORT = 8090; var PORT = 8090;
var BASE_URL = 'http://localhost:' + PORT; var BASE_URL = 'http://localhost:' + PORT;
@ -89,6 +90,7 @@ describe('test the server', function() {
deps.nedb = nedb; deps.nedb = nedb;
deps.nodemailer = nodemailer; deps.nodemailer = nodemailer;
deps.ldap = ldap; deps.ldap = ldap;
deps.session = session;
_server = server.run(config, ldap_client, deps, function() { _server = server.run(config, ldap_client, deps, function() {
done(); done();

View File

@ -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');
});
});