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
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:

View File

@ -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

View File

@ -3,7 +3,8 @@
<title>Home page</title>
</head>
<body>
You need to <a href="/authentication/login?redirect=/">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>.
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/>
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>
</html>

View File

@ -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 {
}
}
}

View File

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

View File

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

View File

@ -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) {

View File

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

View File

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

View File

@ -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) {

View File

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

View File

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

View File

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

View File

@ -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();

View File

@ -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();

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