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
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: .
environment:
- LDAP_URL=ldap://ldap
- SECRET=NBD2ZV64R9UV1O7K
- USERS_DN=dc=example,dc=com
- LDAP_USERS_DN=dc=example,dc=com
- TOTP_SECRET=NBD2ZV64R7UV1O7K
- JWT_SECRET=unsecure_secret
- JWT_EXPIRATION_TIME=1h
- PORT=80
- EXPIRATION_TIME=2m
depends_on:
- ldap
expose:
@ -26,14 +27,10 @@ services:
nginx:
image: nginx:alpine
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:
- auth-server
ports:
- "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 config = {
port: process.env.PORT || 8080
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,

View File

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

View File

@ -33,10 +33,14 @@ function serveAuthPost(req, res) {
}
function serveLogin(req, res) {
console.log(req.headers);
res.render('login');
}
function serveLogout(req, res) {
var redirect_param = req.query.redirect;
var redirect_url = redirect_param || '/';
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 cookieParser = require('cookie-parser');
var speakeasy = require('speakeasy');
var path = require('path');
function run(config, ldap_client) {
var view_directory = path.resolve(__dirname, '../views');
var public_html_directory = path.resolve(__dirname, '../public_html');
var app = express();
app.set('views', './src/views');
app.use(cookieParser());
app.use(express.static(__dirname + '/public_html'));
app.use(express.static(public_html_directory));
app.use(bodyParser.urlencoded({ extended: false }));
app.set('views', view_directory);
app.set('view engine', 'ejs');
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); }
#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>
<head>
<head>
<title>Login Portal</title>
<link rel="stylesheet" type="text/css" href="login.css">
</head>
<body>
<div class="login">
<h1>Login</h1>
<form method="POST">
<input type="text" name="username" placeholder="Username" required="required" />
<input type="password" name="password" placeholder="Password" required="required" />
<input type="text" name="token" placeholder="Verification token" required="required" />
<button type="submit" class="btn btn-primary btn-block btn-large">Enter</button>
</form>
<br>
</div>
</body>
</head>
<body>
<div class="login">
<h1>Login Portal</h1>
<div id="login-form">
<input type="text" name="username" id="username" placeholder="Username" required="required" />
<input type="password" name="password" id="password" placeholder="Password" required="required" />
<input type="text" name="token" id="token" placeholder="Verification token" required="required" />
<button type="button" id="login-button" class="btn btn-primary btn-block btn-large">Enter</button>
</div>
<div id="information" style="display: none;"></div>
</div>
</body>
<script src="jquery.min.js"></script>
<script src="js.cookie.min.js"></script>
<script src="login.js"></script>
</html>

View File

@ -9,7 +9,7 @@ var sinon = require('sinon');
describe('test the server', function() {
var jwt = new Jwt('jwt_secret');
var ldap_client = {
bind: sinon.mock()
bind: sinon.stub()
};
before(function() {
@ -25,11 +25,30 @@ describe('test the server', function() {
// ldap_client.bind.yields(undefined);
ldap_client.bind.withArgs('cn=test_ok,ou=users,dc=example,dc=com',
'password').yields(undefined);
// ldap_client.bind.withArgs('cn=test_nok,ou=users,dc=example,dc=com',
// 'password').yields(undefined, 'error');
ldap_client.bind.withArgs('cn=test_nok,ou=users,dc=example,dc=com',
'password').yields('error');
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) {
request.get('http://localhost:8080/login')
.on('response', function(response) {
@ -37,7 +56,19 @@ describe('test the server', function() {
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) {
request.get('http://localhost:8080/_auth')
.on('response', function(response) {
@ -59,7 +90,9 @@ describe('test the server', function() {
done();
})
});
}
function test_post_auth() {
it('should return the JWT token when authentication is successful', function(done) {
var clock = sinon.useFakeTimers();
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();
}
});
});
}