Edit nginx configuration and add redirection during login and logout

This commit is contained in:
Clement Michaud 2016-12-17 19:36:41 +01:00
parent e13315eb92
commit 7aacae842d
19 changed files with 319 additions and 153 deletions

View File

@ -2,6 +2,10 @@ FROM node
WORKDIR /usr/src WORKDIR /usr/src
COPY app /usr/src COPY package.json /usr/src/package.json
RUN npm install
CMD ["node", "app.js"] COPY src /usr/src
CMD ["node", "index.js"]

View File

@ -5,10 +5,11 @@ services:
build: . build: .
environment: environment:
- LDAP_URL=ldap://ldap - LDAP_URL=ldap://ldap
- SECRET=NBD2ZV64R9UV1O7K - LDAP_USERS_DN=dc=example,dc=com
- USERS_DN=dc=example,dc=com - TOTP_SECRET=NBD2ZV64R7UV1O7K
- JWT_SECRET=unsecure_secret
- JWT_EXPIRATION_TIME=1h
- PORT=80 - PORT=80
- EXPIRATION_TIME=2m
depends_on: depends_on:
- ldap - ldap
expose: expose:
@ -26,14 +27,10 @@ services:
nginx: nginx:
image: nginx:alpine image: nginx:alpine
volumes: volumes:
- ./proxy/nginx.conf:/etc/nginx/nginx.conf - ./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
depends_on: depends_on:
- auth-server - auth-server
ports: ports:
- "8085:80" - "8085:80"
secret:
image: nginx:alpine
volumes:
- ./secret/nginx.conf:/etc/nginx/nginx.conf
- ./secret/index.html:/usr/share/nginx/html/index.html

9
nginx_conf/index.html Normal file
View File

@ -0,0 +1,9 @@
<html>
<head>
<title>Home page</title>
</head>
<body>
You need to <a href="/auth/login?redirect=http://localhost:8085/">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="/auth/logout?redirect=http://localhost:8085/">link</a>.
</body>
</html>

73
nginx_conf/nginx.conf Normal file
View File

@ -0,0 +1,73 @@
# nginx-sso - example nginx config
#
# (c) 2015 by Johannes Gilger <heipei@hackvalue.de>
#
# This is an example config for using nginx with the nginx-sso cookie system.
# For simplicity, this config sets up two fictional vhosts that you can use to
# test against both components of the nginx-sso system: ssoauth & ssologin.
# In a real deployment, these vhosts would be separate hosts.
#user nobody;
worker_processes 1;
#error_log logs/error.log;
#error_log logs/error.log notice;
#error_log logs/error.log info;
#pid logs/nginx.pid;
events {
worker_connections 1024;
}
http {
server {
listen 80;
root /usr/share/nginx/html;
error_page 401 = @error401;
location @error401 {
return 302 http://localhost:8085/auth/login?redirect=$request_uri;
}
location = /check-auth {
internal;
# proxy_pass_request_body off;
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-server/_auth;
}
location /auth/ {
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-server/;
}
location = /secret.html {
auth_request /check-auth;
auth_request_set $user $upstream_http_x_remote_user;
proxy_set_header X-Forwarded-User $user;
auth_request_set $groups $upstream_http_remote_groups;
proxy_set_header Remote-Groups $groups;
auth_request_set $expiry $upstream_http_remote_expiry;
proxy_set_header Remote-Expiry $expiry;
}
# Block everything but POST on _auth
location = /_auth {
if ($request_method != POST) {
return 403;
}
proxy_pass http://auth-server/_auth;
}
}
}

8
nginx_conf/secret.html Normal file
View File

@ -0,0 +1,8 @@
<html>
<head>
<title>Secret</title>
</head>
<body>
This is a very important secret!
</body>
</html>

View File

@ -1,5 +0,0 @@
<html>
<body>
Coucou
</body>
</html>

View File

@ -1,76 +0,0 @@
# nginx-sso - example nginx config
#
# (c) 2015 by Johannes Gilger <heipei@hackvalue.de>
#
# This is an example config for using nginx with the nginx-sso cookie system.
# For simplicity, this config sets up two fictional vhosts that you can use to
# test against both components of the nginx-sso system: ssoauth & ssologin.
# In a real deployment, these vhosts would be separate hosts.
#user nobody;
worker_processes 1;
#error_log logs/error.log;
#error_log logs/error.log notice;
#error_log logs/error.log info;
#pid logs/nginx.pid;
events {
worker_connections 1024;
}
http {
# This is the vserver for the service that you want to protect.
server {
listen 80;
error_page 401 = @error401;
location @error401 {
return 302 http://127.0.0.1:8085/login;
}
location = /_auth {
internal;
proxy_pass http://auth-server/_auth;
proxy_pass_request_body off;
proxy_set_header Content-Length "";
proxy_set_header X-Original-URI $request_uri;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
}
location /secret/ {
auth_request /_auth;
auth_request_set $user $upstream_http_x_remote_user;
proxy_set_header X-Forwarded-User $user;
# auth_request_set $groups $upstream_http_remote_groups;
# proxy_set_header Remote-Groups $groups;
# auth_request_set $expiry $upstream_http_remote_expiry;
# proxy_set_header Remote-Expiry $expiry;
rewrite ^/secret/(.*)$ /$1 break;
proxy_pass http://secret;
}
location /login {
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-server/login;
}
location /logout {
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-server/logout;
}
}
}

View File

@ -1,5 +0,0 @@
<html>
<body>
Coucou
</body>
</html>

View File

@ -1,32 +0,0 @@
# nginx-sso - example nginx config
#
# (c) 2015 by Johannes Gilger <heipei@hackvalue.de>
#
# This is an example config for using nginx with the nginx-sso cookie system.
# For simplicity, this config sets up two fictional vhosts that you can use to
# test against both components of the nginx-sso system: ssoauth & ssologin.
# In a real deployment, these vhosts would be separate hosts.
#user nobody;
worker_processes 1;
#error_log logs/error.log;
#error_log logs/error.log notice;
#error_log logs/error.log info;
#pid logs/nginx.pid;
events {
worker_connections 1024;
}
http {
# This is the vserver for the service that you want to protect.
server {
listen 80;
root /usr/share/nginx/html;
}
}

View File

@ -4,7 +4,7 @@ var server = require('./lib/server');
var ldap = require('ldapjs'); var ldap = require('ldapjs');
var config = { var config = {
port: process.env.PORT || 8080 port: process.env.PORT || 8080,
totp_secret: process.env.TOTP_SECRET, totp_secret: process.env.TOTP_SECRET,
ldap_url: process.env.LDAP_URL || 'ldap://127.0.0.1:389', ldap_url: process.env.LDAP_URL || 'ldap://127.0.0.1:389',
ldap_users_dn: process.env.LDAP_USERS_DN, ldap_users_dn: process.env.LDAP_USERS_DN,

View File

@ -49,6 +49,7 @@ function authenticate(req, res) {
function verify_authentication(req, res) { function verify_authentication(req, res) {
console.log('Verify authentication'); console.log('Verify authentication');
console.log(req.cookies);
if(!objectPath.has(req, 'cookies.access_token')) { if(!objectPath.has(req, 'cookies.access_token')) {
return utils.reject('No access token provided'); return utils.reject('No access token provided');

View File

@ -33,10 +33,14 @@ function serveAuthPost(req, res) {
} }
function serveLogin(req, res) { function serveLogin(req, res) {
console.log(req.headers);
res.render('login'); res.render('login');
} }
function serveLogout(req, res) { function serveLogout(req, res) {
var redirect_param = req.query.redirect;
var redirect_url = redirect_param || '/';
res.clearCookie('access_token'); res.clearCookie('access_token');
res.redirect('/'); res.redirect(redirect_url);
} }

View File

@ -10,14 +10,18 @@ var express = require('express');
var bodyParser = require('body-parser'); var bodyParser = require('body-parser');
var cookieParser = require('cookie-parser'); var cookieParser = require('cookie-parser');
var speakeasy = require('speakeasy'); var speakeasy = require('speakeasy');
var path = require('path');
function run(config, ldap_client) { function run(config, ldap_client) {
var view_directory = path.resolve(__dirname, '../views');
var public_html_directory = path.resolve(__dirname, '../public_html');
var app = express(); var app = express();
app.set('views', './src/views');
app.use(cookieParser()); app.use(cookieParser());
app.use(express.static(__dirname + '/public_html')); app.use(express.static(public_html_directory));
app.use(bodyParser.urlencoded({ extended: false })); app.use(bodyParser.urlencoded({ extended: false }));
app.set('views', view_directory);
app.set('view engine', 'ejs'); app.set('view engine', 'ejs');
app.set('jwt engine', new Jwt(config.jwt_secret)); app.set('jwt engine', new Jwt(config.jwt_secret));

4
src/public_html/jquery.min.js vendored Normal file

File diff suppressed because one or more lines are too long

2
src/public_html/js.cookie.min.js vendored Normal file
View File

@ -0,0 +1,2 @@
/*! js-cookie v2.1.3 | MIT */
!function(a){var b=!1;if("function"==typeof define&&define.amd&&(define(a),b=!0),"object"==typeof exports&&(module.exports=a(),b=!0),!b){var c=window.Cookies,d=window.Cookies=a();d.noConflict=function(){return window.Cookies=c,d}}}(function(){function a(){for(var a=0,b={};a<arguments.length;a++){var c=arguments[a];for(var d in c)b[d]=c[d]}return b}function b(c){function d(b,e,f){var g;if("undefined"!=typeof document){if(arguments.length>1){if(f=a({path:"/"},d.defaults,f),"number"==typeof f.expires){var h=new Date;h.setMilliseconds(h.getMilliseconds()+864e5*f.expires),f.expires=h}try{g=JSON.stringify(e),/^[\{\[]/.test(g)&&(e=g)}catch(i){}return e=c.write?c.write(e,b):encodeURIComponent(e+"").replace(/%(23|24|26|2B|3A|3C|3E|3D|2F|3F|40|5B|5D|5E|60|7B|7D|7C)/g,decodeURIComponent),b=encodeURIComponent(b+""),b=b.replace(/%(23|24|26|2B|5E|60|7C)/g,decodeURIComponent),b=b.replace(/[\(\)]/g,escape),document.cookie=b+"="+e+(f.expires?"; expires="+f.expires.toUTCString():"")+(f.path?"; path="+f.path:"")+(f.domain?"; domain="+f.domain:"")+(f.secure?"; secure":"")}b||(g={});for(var j=document.cookie?document.cookie.split("; "):[],k=/(%[0-9A-Z]{2})+/g,l=0;l<j.length;l++){var m=j[l].split("="),n=m.slice(1).join("=");'"'===n.charAt(0)&&(n=n.slice(1,-1));try{var o=m[0].replace(k,decodeURIComponent);if(n=c.read?c.read(n,o):c(n,o)||n.replace(k,decodeURIComponent),this.json)try{n=JSON.parse(n)}catch(i){}if(b===o){g=n;break}b||(g[o]=n)}catch(i){}}return g}}return d.set=d,d.get=function(a){return d.call(d,a)},d.getJSON=function(){return d.apply({json:!0},[].slice.call(arguments))},d.defaults={},d.remove=function(b,c){d(b,"",a(c,{expires:-1}))},d.withConverter=b,d}return b(function(){})});

View File

@ -56,3 +56,18 @@ input {
} }
input:focus { box-shadow: inset 0 -5px 45px rgba(100,100,100,0.4), 0 1px 1px rgba(255,255,255,0.2); } input:focus { box-shadow: inset 0 -5px 45px rgba(100,100,100,0.4), 0 1px 1px rgba(255,255,255,0.2); }
#information {
border: 1px solid black;
padding: 10px 20px;
margin-top: 25px;
font-size: 0.8em;
border-radius: 4px;
}
#information.failure {
background-color: rgb(255, 124, 124);
}
#information.success {
background-color: rgb(43, 188, 99);
}

104
src/public_html/login.js Normal file
View File

@ -0,0 +1,104 @@
(function() {
params={};
location.search.replace(/[?&]+([^=&]+)=([^&]*)/gi,function(s,k,v){params[k]=v});
console.log(params);
$(document).ready(function() {
$('#login-button').on('click', onLoginButtonClicked);
setupEnterKeypressListener();
$('#information').hide();
});
function setupEnterKeypressListener() {
$('#login-form').on('keydown', 'input', function (e) {
var key = e.which;
switch (key) {
case 13: // enter key code
onLoginButtonClicked();
break;
default:
break;
}
});
}
function onLoginButtonClicked() {
var username = $('#username').val();
var password = $('#password').val();
var token = $('#token').val();
authenticate(username, password, token, function(err, access_token) {
if(err) {
onAuthenticationFailure();
return;
}
onAuthenticationSuccess(access_token);
});
}
function authenticate(username, password, token, fn) {
$.post('/_auth', {
username: username,
password: password,
token: token
})
.done(function(access_token) {
fn(undefined, access_token);
})
.fail(function(err) {
fn(err);
});
}
function displayInformationMessage(msg, type, time, fn) {
if(type == 'success') {
$('#information').addClass("success");
}
else if(type == 'failure') {
$('#information').addClass("failure");
}
$('#information').text(msg);
$('#information').show("fast");
setTimeout(function() {
$('#information').hide("fast");
$('#information').removeClass("success");
$('#information').removeClass("failure");
if(fn) fn();
},time);
}
function redirect() {
var redirect_uri = '/';
if('redirect' in params) {
redirect_uri = params['redirect'];
}
window.location.replace(redirect_uri);
}
function onAuthenticationSuccess(access_token) {
Cookies.set('access_token', access_token, { path: '/' });
$('#username').val('');
$('#password').val('');
$('#token').val('');
redirect();
// displayInformationMessage('Authentication success, You will be redirected' +
// 'in few seconds.', 'success', 3000, function() {
// });
}
function onAuthenticationFailure() {
$('#password').val('');
$('#token').val('');
displayInformationMessage('Authentication failed, please try again.', 'failure', 3000);
}
})();

View File

@ -1,17 +1,21 @@
<!DOCTYPE html> <!DOCTYPE html>
<head> <head>
<title>Login Portal</title>
<link rel="stylesheet" type="text/css" href="login.css"> <link rel="stylesheet" type="text/css" href="login.css">
</head> </head>
<body> <body>
<div class="login"> <div class="login">
<h1>Login</h1> <h1>Login Portal</h1>
<form method="POST"> <div id="login-form">
<input type="text" name="username" placeholder="Username" required="required" /> <input type="text" name="username" id="username" placeholder="Username" required="required" />
<input type="password" name="password" placeholder="Password" required="required" /> <input type="password" name="password" id="password" placeholder="Password" required="required" />
<input type="text" name="token" placeholder="Verification token" required="required" /> <input type="text" name="token" id="token" placeholder="Verification token" required="required" />
<button type="submit" class="btn btn-primary btn-block btn-large">Enter</button> <button type="button" id="login-button" class="btn btn-primary btn-block btn-large">Enter</button>
</form> </div>
<br> <div id="information" style="display: none;"></div>
</div> </div>
</body> </body>
<script src="jquery.min.js"></script>
<script src="js.cookie.min.js"></script>
<script src="login.js"></script>
</html> </html>

View File

@ -9,7 +9,7 @@ var sinon = require('sinon');
describe('test the server', function() { describe('test the server', function() {
var jwt = new Jwt('jwt_secret'); var jwt = new Jwt('jwt_secret');
var ldap_client = { var ldap_client = {
bind: sinon.mock() bind: sinon.stub()
}; };
before(function() { before(function() {
@ -25,11 +25,30 @@ describe('test the server', function() {
// ldap_client.bind.yields(undefined); // ldap_client.bind.yields(undefined);
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(undefined, 'error'); 'password').yields('error');
server.run(config, ldap_client); server.run(config, ldap_client);
}); });
describe('test GET /login', function() {
test_login()
});
describe('test GET /logout', function() {
test_logout()
});
describe('test GET /_auth', function() {
test_get_auth(jwt);
});
describe('test POST /_auth', function() {
test_post_auth(jwt);
});
});
function test_login() {
it('should serve the login page', function(done) { it('should serve the login page', function(done) {
request.get('http://localhost:8080/login') request.get('http://localhost:8080/login')
.on('response', function(response) { .on('response', function(response) {
@ -37,7 +56,19 @@ describe('test the server', function() {
done(); done();
}) })
}); });
}
function test_logout() {
it('should logout and redirect to /', function(done) {
request.get('http://localhost:8080/logout')
.on('response', function(response) {
assert.equal(response.req.path, '/');
done();
})
});
}
function test_get_auth(jwt) {
it('should return status code 401 when user is not authenticated', function(done) { it('should return status code 401 when user is not authenticated', function(done) {
request.get('http://localhost:8080/_auth') request.get('http://localhost:8080/_auth')
.on('response', function(response) { .on('response', function(response) {
@ -59,7 +90,9 @@ describe('test the server', function() {
done(); done();
}) })
}); });
}
function test_post_auth() {
it('should return the JWT token when authentication is successful', function(done) { it('should return the JWT token when authentication is successful', function(done) {
var clock = sinon.useFakeTimers(); var clock = sinon.useFakeTimers();
var real_token = speakeasy.totp({ var real_token = speakeasy.totp({
@ -83,4 +116,26 @@ describe('test the server', function() {
} }
}); });
}); });
});
it('should return invalid authentication status code', function(done) {
var clock = sinon.useFakeTimers();
var real_token = speakeasy.totp({
secret: 'totp_secret',
encoding: 'base32'
});
var data = {
form: {
username: 'test_nok',
password: 'password',
token: real_token
}
}
request.post('http://localhost:8080/_auth', data, function (error, response, body) {
if(response.statusCode == 401) {
clock.restore();
done();
}
});
});
}