Edit nginx configuration and add redirection during login and logout
parent
e13315eb92
commit
7aacae842d
|
@ -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"]
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
<html>
|
||||
<head>
|
||||
<title>Secret</title>
|
||||
</head>
|
||||
<body>
|
||||
This is a very important secret!
|
||||
</body>
|
||||
</html>
|
|
@ -1,5 +0,0 @@
|
|||
<html>
|
||||
<body>
|
||||
Coucou
|
||||
</body>
|
||||
</html>
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,5 +0,0 @@
|
|||
<html>
|
||||
<body>
|
||||
Coucou
|
||||
</body>
|
||||
</html>
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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));
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -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(){})});
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
})();
|
|
@ -1,17 +1,21 @@
|
|||
<!DOCTYPE html>
|
||||
<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>
|
||||
<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>
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue