First commit with tests
commit
d7d743bdfa
|
@ -0,0 +1,3 @@
|
||||||
|
|
||||||
|
node_modules/
|
||||||
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
FROM node
|
||||||
|
|
||||||
|
WORKDIR /usr/src
|
||||||
|
|
||||||
|
COPY app /usr/src
|
||||||
|
|
||||||
|
CMD ["node", "app.js"]
|
|
@ -0,0 +1,37 @@
|
||||||
|
|
||||||
|
var express = require('express');
|
||||||
|
var bodyParser = require('body-parser');
|
||||||
|
var cookieParser = require('cookie-parser');
|
||||||
|
var routes = require('./lib/routes');
|
||||||
|
var ldap = require('ldapjs');
|
||||||
|
var speakeasy = require('speakeasy');
|
||||||
|
|
||||||
|
var totpSecret = process.env.SECRET;
|
||||||
|
var LDAP_URL = process.env.LDAP_URL || 'ldap://127.0.0.1:389';
|
||||||
|
var USERS_DN = process.env.USERS_DN;
|
||||||
|
var PORT = process.env.PORT || 80
|
||||||
|
var JWT_SECRET = 'this is the secret';
|
||||||
|
var EXPIRATION_TIME = process.env.EXPIRATION_TIME || '1h';
|
||||||
|
|
||||||
|
var ldap_client = ldap.createClient({
|
||||||
|
url: LDAP_URL
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
var app = express();
|
||||||
|
app.use(cookieParser());
|
||||||
|
app.use(express.static(__dirname + '/public_html'));
|
||||||
|
app.use(bodyParser.urlencoded({ extended: false }));
|
||||||
|
|
||||||
|
app.set('view engine', 'ejs');
|
||||||
|
|
||||||
|
app.get ('/login', routes.login);
|
||||||
|
app.post ('/login', routes.login);
|
||||||
|
|
||||||
|
app.get ('/logout', routes.logout);
|
||||||
|
app.get ('/_auth', routes.auth);
|
||||||
|
|
||||||
|
app.listen(PORT, function(err) {
|
||||||
|
console.log('Listening on %d...', PORT);
|
||||||
|
});
|
||||||
|
|
|
@ -0,0 +1,57 @@
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
'authenticate': authenticate,
|
||||||
|
'verify_authentication': verify_authentication
|
||||||
|
}
|
||||||
|
|
||||||
|
var objectPath = require('object-path');
|
||||||
|
var Jwt = require('./jwt');
|
||||||
|
var ldap_checker = require('./ldap_checker');
|
||||||
|
var totp_checker = require('./totp_checker');
|
||||||
|
var replies = require('./replies');
|
||||||
|
var Q = require('q');
|
||||||
|
var utils = require('./utils');
|
||||||
|
|
||||||
|
|
||||||
|
function authenticate(req, res, args) {
|
||||||
|
var defer = Q.defer();
|
||||||
|
var username = req.body.username;
|
||||||
|
var password = req.body.password;
|
||||||
|
var token = req.body.token;
|
||||||
|
console.log('Start authentication');
|
||||||
|
|
||||||
|
if(!username || !password || !token) {
|
||||||
|
replies.authentication_failed(res);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var totp_promise = totp_checker.validate(args.totp_interface, token, args.totp_secret);
|
||||||
|
var credentials_promise = ldap_checker.validate(args.ldap_interface, username, password, args.users_dn);
|
||||||
|
|
||||||
|
Q.all([totp_promise, credentials_promise])
|
||||||
|
.then(function() {
|
||||||
|
var token = args.jwt.sign({ user: username }, args.jwt_expiration_time);
|
||||||
|
res.cookie('access_token', token);
|
||||||
|
res.redirect('/');
|
||||||
|
console.log('Authentication succeeded');
|
||||||
|
defer.resolve();
|
||||||
|
})
|
||||||
|
.fail(function(err1, err2) {
|
||||||
|
res.render('login');
|
||||||
|
console.log('Authentication failed', err1, err2);
|
||||||
|
defer.reject();
|
||||||
|
});
|
||||||
|
return defer.promise;
|
||||||
|
}
|
||||||
|
|
||||||
|
function verify_authentication(req, res, args) {
|
||||||
|
console.log('Verify authentication');
|
||||||
|
|
||||||
|
if(!objectPath.has(req, 'cookies.access_token')) {
|
||||||
|
return utils.reject('No access token provided');
|
||||||
|
}
|
||||||
|
|
||||||
|
var jsonWebToken = req.cookies['access_token'];
|
||||||
|
return args.jwt.verify(jsonWebToken);
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,29 @@
|
||||||
|
|
||||||
|
module.exports = Jwt;
|
||||||
|
|
||||||
|
var jwt = require('jsonwebtoken');
|
||||||
|
var utils = require('./utils');
|
||||||
|
var Q = require('q');
|
||||||
|
|
||||||
|
function Jwt(secret) {
|
||||||
|
var _secret;
|
||||||
|
|
||||||
|
this._secret = secret;
|
||||||
|
}
|
||||||
|
|
||||||
|
Jwt.prototype.sign = function(data, expiration_time) {
|
||||||
|
return jwt.sign(data, this._secret, { expiresIn: expiration_time });
|
||||||
|
}
|
||||||
|
|
||||||
|
Jwt.prototype.verify = function(token) {
|
||||||
|
var defer = Q.defer();
|
||||||
|
try {
|
||||||
|
var decoded = jwt.verify(token, this._secret);
|
||||||
|
defer.resolve(decoded);
|
||||||
|
}
|
||||||
|
catch(err) {
|
||||||
|
defer.reject(err);
|
||||||
|
}
|
||||||
|
return defer.promise;
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,14 @@
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
'validate': validateCredentials
|
||||||
|
}
|
||||||
|
|
||||||
|
var Q = require('q');
|
||||||
|
var util = require('util');
|
||||||
|
var utils = require('./utils');
|
||||||
|
|
||||||
|
function validateCredentials(ldap_client, username, password, users_dn) {
|
||||||
|
var userDN = util.format("cn=%s,%s", username, users_dn);
|
||||||
|
var bind_promised = utils.promisify(ldap_client.bind, ldap_client);
|
||||||
|
return bind_promised(userDN, password);
|
||||||
|
}
|
|
@ -0,0 +1,29 @@
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
'authentication_failed': authentication_failed,
|
||||||
|
'authentication_succeeded': authentication_succeeded,
|
||||||
|
'already_authenticated': already_authenticated
|
||||||
|
}
|
||||||
|
|
||||||
|
function authentication_failed(res) {
|
||||||
|
console.log('Reply: authentication failed');
|
||||||
|
res.status(401)
|
||||||
|
res.send('Authentication failed');
|
||||||
|
}
|
||||||
|
|
||||||
|
function send_success(res, username, msg) {
|
||||||
|
res.status(200);
|
||||||
|
res.set({ 'X-Remote-User': username });
|
||||||
|
res.send(msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
function authentication_succeeded(res, username) {
|
||||||
|
console.log('Reply: authentication succeeded');
|
||||||
|
send_success(res, username, 'Authentication succeeded');
|
||||||
|
}
|
||||||
|
|
||||||
|
function already_authenticated(res, username) {
|
||||||
|
console.log('Reply: already authenticated');
|
||||||
|
send_success(res, username, 'Authentication succeeded');
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,35 @@
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
'auth': serveAuth,
|
||||||
|
'login': serveLogin,
|
||||||
|
'logout': serveLogout
|
||||||
|
}
|
||||||
|
|
||||||
|
var authentication = require('./authentication');
|
||||||
|
var replies = require('./replies');
|
||||||
|
|
||||||
|
function serveAuth(req, res) {
|
||||||
|
authentication.verify(req, res)
|
||||||
|
.then(function(user) {
|
||||||
|
replies.already_authenticated(res, user);
|
||||||
|
})
|
||||||
|
.fail(function(err) {
|
||||||
|
replies.authentication_failed(res);
|
||||||
|
console.error(err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function serveLogin(req, res) {
|
||||||
|
console.log('METHOD=%s', req.method);
|
||||||
|
if(req.method == 'POST') {
|
||||||
|
authentication.authenticate(req, res);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
res.render('login');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function serveLogout(req, res) {
|
||||||
|
res.clearCookie('access_token');
|
||||||
|
res.redirect('/');
|
||||||
|
}
|
|
@ -0,0 +1,22 @@
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
'validate': validate
|
||||||
|
}
|
||||||
|
|
||||||
|
var Q = require('q');
|
||||||
|
|
||||||
|
function validate(speakeasy, token, totp_secret) {
|
||||||
|
var defer = Q.defer();
|
||||||
|
var real_token = speakeasy.totp({
|
||||||
|
secret: totp_secret,
|
||||||
|
encoding: 'base32'
|
||||||
|
});
|
||||||
|
|
||||||
|
if(token == real_token) {
|
||||||
|
defer.resolve();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
defer.reject('Wrong challenge');
|
||||||
|
}
|
||||||
|
return defer.promise;
|
||||||
|
}
|
|
@ -0,0 +1,35 @@
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
'promisify': promisify,
|
||||||
|
'resolve': resolve,
|
||||||
|
'reject': reject
|
||||||
|
}
|
||||||
|
|
||||||
|
var Q = require('q');
|
||||||
|
|
||||||
|
function promisify(fn, context) {
|
||||||
|
return function() {
|
||||||
|
var defer = Q.defer();
|
||||||
|
var args = Array.prototype.slice.call(arguments);
|
||||||
|
args.push(function(err, val) {
|
||||||
|
if (err !== null && err !== undefined) {
|
||||||
|
return defer.reject(err);
|
||||||
|
}
|
||||||
|
return defer.resolve(val);
|
||||||
|
});
|
||||||
|
fn.apply(context || {}, args);
|
||||||
|
return defer.promise;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolve(data) {
|
||||||
|
var defer = Q.defer();
|
||||||
|
defer.resolve(data);
|
||||||
|
return defer.promise;
|
||||||
|
}
|
||||||
|
|
||||||
|
function reject(err) {
|
||||||
|
var defer = Q.defer();
|
||||||
|
defer.reject(err);
|
||||||
|
return defer.promise;
|
||||||
|
}
|
|
@ -0,0 +1,58 @@
|
||||||
|
@import url(http://fonts.googleapis.com/css?family=Open+Sans);
|
||||||
|
.btn { display: inline-block; *display: inline; *zoom: 1; padding: 4px 10px 4px; margin-bottom: 0; font-size: 13px; line-height: 18px; color: #333333; text-align: center;text-shadow: 0 1px 1px rgba(255, 255, 255, 0.75); vertical-align: middle; background-color: #f5f5f5; background-image: -moz-linear-gradient(top, #ffffff, #e6e6e6); background-image: -ms-linear-gradient(top, #ffffff, #e6e6e6); background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#ffffff), to(#e6e6e6)); background-image: -webkit-linear-gradient(top, #ffffff, #e6e6e6); background-image: -o-linear-gradient(top, #ffffff, #e6e6e6); background-image: linear-gradient(top, #ffffff, #e6e6e6); background-repeat: repeat-x; filter: progid:dximagetransform.microsoft.gradient(startColorstr=#ffffff, endColorstr=#e6e6e6, GradientType=0); border-color: #e6e6e6 #e6e6e6 #e6e6e6; border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); border: 1px solid #e6e6e6; -webkit-border-radius: 4px; -moz-border-radius: 4px; border-radius: 4px; -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); -moz-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); cursor: pointer; *margin-left: .3em; }
|
||||||
|
.btn:hover, .btn:active, .btn.active, .btn.disabled, .btn[disabled] { background-color: #e6e6e6; }
|
||||||
|
.btn-large { padding: 9px 14px; font-size: 15px; line-height: normal; -webkit-border-radius: 5px; -moz-border-radius: 5px; border-radius: 5px; }
|
||||||
|
.btn:hover { color: #333333; text-decoration: none; background-color: #e6e6e6; background-position: 0 -15px; -webkit-transition: background-position 0.1s linear; -moz-transition: background-position 0.1s linear; -ms-transition: background-position 0.1s linear; -o-transition: background-position 0.1s linear; transition: background-position 0.1s linear; }
|
||||||
|
.btn-primary, .btn-primary:hover { text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); color: #ffffff; }
|
||||||
|
.btn-primary.active { color: rgba(255, 255, 255, 0.75); }
|
||||||
|
.btn-primary { background-color: #4a77d4; background-image: -moz-linear-gradient(top, #6eb6de, #4a77d4); background-image: -ms-linear-gradient(top, #6eb6de, #4a77d4); background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#6eb6de), to(#4a77d4)); background-image: -webkit-linear-gradient(top, #6eb6de, #4a77d4); background-image: -o-linear-gradient(top, #6eb6de, #4a77d4); background-image: linear-gradient(top, #6eb6de, #4a77d4); background-repeat: repeat-x; filter: progid:dximagetransform.microsoft.gradient(startColorstr=#6eb6de, endColorstr=#4a77d4, GradientType=0); border: 1px solid #3762bc; text-shadow: 1px 1px 1px rgba(0,0,0,0.4); box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.5); }
|
||||||
|
.btn-primary:hover, .btn-primary:active, .btn-primary.active, .btn-primary.disabled, .btn-primary[disabled] { filter: none; background-color: #4a77d4; }
|
||||||
|
.btn-block { width: 100%; display:block; }
|
||||||
|
|
||||||
|
* { -webkit-box-sizing:border-box; -moz-box-sizing:border-box; -ms-box-sizing:border-box; -o-box-sizing:border-box; box-sizing:border-box; }
|
||||||
|
|
||||||
|
html { width: 100%; height:100%; overflow:hidden; }
|
||||||
|
|
||||||
|
body {
|
||||||
|
width: 100%;
|
||||||
|
height:100%;
|
||||||
|
font-family: 'Open Sans', sans-serif;
|
||||||
|
background: #092756;
|
||||||
|
background: -moz-radial-gradient(0% 100%, ellipse cover, rgba(104,128,138,.4) 10%,rgba(138,114,76,0) 40%),-moz-linear-gradient(top, rgba(57,173,219,.25) 0%, rgba(42,60,87,.4) 100%), -moz-linear-gradient(-45deg, #670d10 0%, #092756 100%);
|
||||||
|
background: -webkit-radial-gradient(0% 100%, ellipse cover, rgba(104,128,138,.4) 10%,rgba(138,114,76,0) 40%), -webkit-linear-gradient(top, rgba(57,173,219,.25) 0%,rgba(42,60,87,.4) 100%), -webkit-linear-gradient(-45deg, #670d10 0%,#092756 100%);
|
||||||
|
background: -o-radial-gradient(0% 100%, ellipse cover, rgba(104,128,138,.4) 10%,rgba(138,114,76,0) 40%), -o-linear-gradient(top, rgba(57,173,219,.25) 0%,rgba(42,60,87,.4) 100%), -o-linear-gradient(-45deg, #670d10 0%,#092756 100%);
|
||||||
|
background: -ms-radial-gradient(0% 100%, ellipse cover, rgba(104,128,138,.4) 10%,rgba(138,114,76,0) 40%), -ms-linear-gradient(top, rgba(57,173,219,.25) 0%,rgba(42,60,87,.4) 100%), -ms-linear-gradient(-45deg, #670d10 0%,#092756 100%);
|
||||||
|
background: -webkit-radial-gradient(0% 100%, ellipse cover, rgba(104,128,138,.4) 10%,rgba(138,114,76,0) 40%), linear-gradient(to bottom, rgba(57,173,219,.25) 0%,rgba(42,60,87,.4) 100%), linear-gradient(135deg, #670d10 0%,#092756 100%);
|
||||||
|
filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#3E1D6D', endColorstr='#092756',GradientType=1 );
|
||||||
|
}
|
||||||
|
.login {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
margin: -150px 0 0 -150px;
|
||||||
|
width:300px;
|
||||||
|
height:300px;
|
||||||
|
}
|
||||||
|
.login h1 { color: #fff; text-shadow: 0 0 10px rgba(0,0,0,0.3); letter-spacing:1px; text-align:center; }
|
||||||
|
|
||||||
|
input {
|
||||||
|
width: 100%;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
background: rgba(0,0,0,0.3);
|
||||||
|
border: none;
|
||||||
|
outline: none;
|
||||||
|
padding: 10px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #fff;
|
||||||
|
text-shadow: 1px 1px 1px rgba(0,0,0,0.3);
|
||||||
|
border: 1px solid rgba(0,0,0,0.3);
|
||||||
|
border-radius: 4px;
|
||||||
|
box-shadow: inset 0 -5px 45px rgba(100,100,100,0.2), 0 1px 1px rgba(255,255,255,0.2);
|
||||||
|
-webkit-transition: box-shadow .5s ease;
|
||||||
|
-moz-transition: box-shadow .5s ease;
|
||||||
|
-o-transition: box-shadow .5s ease;
|
||||||
|
-ms-transition: box-shadow .5s ease;
|
||||||
|
transition: box-shadow .5s ease;
|
||||||
|
}
|
||||||
|
input:focus { box-shadow: inset 0 -5px 45px rgba(100,100,100,0.4), 0 1px 1px rgba(255,255,255,0.2); }
|
||||||
|
|
|
@ -0,0 +1,118 @@
|
||||||
|
|
||||||
|
var assert = require('assert');
|
||||||
|
var authentication = require('../lib/authentication');
|
||||||
|
var create_res_mock = require('./res_mock');
|
||||||
|
var sinon = require('sinon');
|
||||||
|
var sinonPromise = require('sinon-promise');
|
||||||
|
sinonPromise(sinon);
|
||||||
|
|
||||||
|
var autoResolving = sinon.promise().resolves();
|
||||||
|
|
||||||
|
function create_req_mock(token) {
|
||||||
|
return {
|
||||||
|
body: {
|
||||||
|
username: 'username',
|
||||||
|
password: 'password',
|
||||||
|
token: token
|
||||||
|
},
|
||||||
|
cookies: {
|
||||||
|
'access_token': 'cookie_token'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function create_mocks() {
|
||||||
|
var totp_token = 'totp_token';
|
||||||
|
var jwt_token = 'jwt_token';
|
||||||
|
|
||||||
|
var res_mock = create_res_mock();
|
||||||
|
var req_mock = create_req_mock(totp_token);
|
||||||
|
var bind_mock = sinon.mock();
|
||||||
|
var totp_mock = sinon.mock();
|
||||||
|
var sign_mock = sinon.mock();
|
||||||
|
var verify_mock = sinon.promise();
|
||||||
|
var jwt = {
|
||||||
|
sign: sign_mock,
|
||||||
|
verify: verify_mock
|
||||||
|
};
|
||||||
|
var ldap_interface_mock = {
|
||||||
|
bind: bind_mock
|
||||||
|
};
|
||||||
|
var totp_interface_mock = {
|
||||||
|
totp: totp_mock
|
||||||
|
};
|
||||||
|
|
||||||
|
bind_mock.yields();
|
||||||
|
totp_mock.returns(totp_token);
|
||||||
|
sign_mock.returns(jwt_token);
|
||||||
|
|
||||||
|
var args = {
|
||||||
|
totp_secret: 'totp_secret',
|
||||||
|
jwt: jwt,
|
||||||
|
jwt_expiration_time: '1h',
|
||||||
|
users_dn: 'dc=example,dc=com',
|
||||||
|
ldap_interface: ldap_interface_mock,
|
||||||
|
totp_interface: totp_interface_mock
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
req: req_mock,
|
||||||
|
res: res_mock,
|
||||||
|
args: args,
|
||||||
|
totp: totp_mock,
|
||||||
|
jwt: jwt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('test jwt', function() {
|
||||||
|
describe('test authentication', function() {
|
||||||
|
it('should authenticate user successfuly', function(done) {
|
||||||
|
var jwt_token = 'jwt_token';
|
||||||
|
var clock = sinon.useFakeTimers();
|
||||||
|
var mocks = create_mocks();
|
||||||
|
authentication.authenticate(mocks.req, mocks.res, mocks.args)
|
||||||
|
.then(function() {
|
||||||
|
clock.restore();
|
||||||
|
assert(mocks.res.cookie.calledWith('access_token', jwt_token));
|
||||||
|
assert(mocks.res.redirect.calledWith('/'));
|
||||||
|
done();
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should fail authentication', function(done) {
|
||||||
|
var clock = sinon.useFakeTimers();
|
||||||
|
var mocks = create_mocks();
|
||||||
|
mocks.totp.returns('wrong token');
|
||||||
|
authentication.authenticate(mocks.req, mocks.res, mocks.args)
|
||||||
|
.fail(function(err) {
|
||||||
|
clock.restore();
|
||||||
|
done();
|
||||||
|
})
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
describe('test verify authentication', function() {
|
||||||
|
it('should be already authenticated', function(done) {
|
||||||
|
var mocks = create_mocks();
|
||||||
|
var data = { user: 'username' };
|
||||||
|
mocks.jwt.verify = sinon.promise().resolves(data);
|
||||||
|
authentication.verify_authentication(mocks.req, mocks.res, mocks.args)
|
||||||
|
.then(function(actual_data) {
|
||||||
|
assert.equal(actual_data, data);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not be already authenticated', function(done) {
|
||||||
|
var mocks = create_mocks();
|
||||||
|
var data = { user: 'username' };
|
||||||
|
mocks.jwt.verify = sinon.promise().rejects('Error with JWT token');
|
||||||
|
return authentication.verify_authentication(mocks.req, mocks.res, mocks.args)
|
||||||
|
.fail(function() {
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
|
@ -0,0 +1,33 @@
|
||||||
|
|
||||||
|
var Jwt = require('../lib/jwt');
|
||||||
|
var sinon = require('sinon');
|
||||||
|
var sinonPromise = require('sinon-promise');
|
||||||
|
sinonPromise(sinon);
|
||||||
|
|
||||||
|
var autoResolving = sinon.promise().resolves();
|
||||||
|
|
||||||
|
describe('test jwt', function() {
|
||||||
|
it('should sign and verify the token', function() {
|
||||||
|
var data = {user: 'user'};
|
||||||
|
var secret = 'secret';
|
||||||
|
var jwt = new Jwt(secret);
|
||||||
|
var token = jwt.sign(data, '1m');
|
||||||
|
return jwt.verify(token);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should verify and fail on wrong token', function() {
|
||||||
|
var jwt = new Jwt('secret');
|
||||||
|
return jwt.verify('wrong token').fail(autoResolving);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should fail after expiry', function(done) {
|
||||||
|
var clock = sinon.useFakeTimers(0);
|
||||||
|
var data = {user: 'user'};
|
||||||
|
var jwt = new Jwt('secret');
|
||||||
|
var token = jwt.sign(data, '1m');
|
||||||
|
clock.tick(1000 * 61); // 61 seconds
|
||||||
|
jwt.verify(token).fail(function() { done(); });
|
||||||
|
clock.restore();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
|
@ -0,0 +1,35 @@
|
||||||
|
|
||||||
|
var ldap_checker = require('../lib/ldap_checker');
|
||||||
|
var sinon = require('sinon');
|
||||||
|
var sinonPromise = require('sinon-promise');
|
||||||
|
|
||||||
|
sinonPromise(sinon);
|
||||||
|
|
||||||
|
var autoResolving = sinon.promise().resolves();
|
||||||
|
|
||||||
|
function test_validate(bind_mock) {
|
||||||
|
var username = 'user';
|
||||||
|
var password = 'password';
|
||||||
|
var ldap_url = 'http://ldap';
|
||||||
|
var users_dn = 'dc=example,dc=com';
|
||||||
|
|
||||||
|
var ldap_client_mock = {
|
||||||
|
bind: bind_mock
|
||||||
|
}
|
||||||
|
|
||||||
|
return ldap_checker.validate(ldap_client_mock, username, password, ldap_url, users_dn);
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('test ldap checker', function() {
|
||||||
|
it('should bind the user if good credentials provided', function() {
|
||||||
|
var bind_mock = sinon.mock().yields();
|
||||||
|
return test_validate(bind_mock);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not bind the user if wrong credentials provided', function() {
|
||||||
|
var bind_mock = sinon.mock().yields('wrong credentials');
|
||||||
|
var promise = test_validate(bind_mock);
|
||||||
|
return promise.fail(autoResolving);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
|
@ -0,0 +1,52 @@
|
||||||
|
|
||||||
|
var replies = require('../lib/replies');
|
||||||
|
var assert = require('assert');
|
||||||
|
var sinon = require('sinon');
|
||||||
|
var sinonPromise = require('sinon-promise');
|
||||||
|
sinonPromise(sinon);
|
||||||
|
|
||||||
|
var autoResolving = sinon.promise().resolves();
|
||||||
|
|
||||||
|
function create_res_mock() {
|
||||||
|
var status_mock = sinon.mock();
|
||||||
|
var send_mock = sinon.mock();
|
||||||
|
var set_mock = sinon.mock();
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: status_mock,
|
||||||
|
send: send_mock,
|
||||||
|
set: set_mock
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('test jwt', function() {
|
||||||
|
it('should authenticate with success', function() {
|
||||||
|
var res_mock = create_res_mock();
|
||||||
|
var username = 'username';
|
||||||
|
|
||||||
|
replies.authentication_succeeded(res_mock, username);
|
||||||
|
|
||||||
|
assert(res_mock.status.calledWith(200));
|
||||||
|
assert(res_mock.set.calledWith({'X-Remote-User': username }));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reply successfully when already authenticated', function() {
|
||||||
|
var res_mock = create_res_mock();
|
||||||
|
var username = 'username';
|
||||||
|
|
||||||
|
replies.already_authenticated(res_mock, username);
|
||||||
|
|
||||||
|
assert(res_mock.status.calledWith(200));
|
||||||
|
assert(res_mock.set.calledWith({'X-Remote-User': username }));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reply with failed authentication', function() {
|
||||||
|
var res_mock = create_res_mock();
|
||||||
|
var username = 'username';
|
||||||
|
|
||||||
|
replies.authentication_failed(res_mock, username);
|
||||||
|
|
||||||
|
assert(res_mock.status.calledWith(401));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
|
@ -0,0 +1,24 @@
|
||||||
|
|
||||||
|
module.exports = create_res_mock;
|
||||||
|
|
||||||
|
var sinon = require('sinon');
|
||||||
|
var sinonPromise = require('sinon-promise');
|
||||||
|
sinonPromise(sinon);
|
||||||
|
|
||||||
|
function create_res_mock() {
|
||||||
|
var status_mock = sinon.mock();
|
||||||
|
var send_mock = sinon.mock();
|
||||||
|
var set_mock = sinon.mock();
|
||||||
|
var cookie_mock = sinon.mock();
|
||||||
|
var render_mock = sinon.mock();
|
||||||
|
var redirect_mock = sinon.mock();
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: status_mock,
|
||||||
|
send: send_mock,
|
||||||
|
set: set_mock,
|
||||||
|
cookie: cookie_mock,
|
||||||
|
render: render_mock,
|
||||||
|
redirect: redirect_mock
|
||||||
|
};
|
||||||
|
}
|
|
@ -0,0 +1,32 @@
|
||||||
|
|
||||||
|
var totp_checker = require('../lib/totp_checker');
|
||||||
|
var sinon = require('sinon');
|
||||||
|
var sinonPromise = require('sinon-promise');
|
||||||
|
sinonPromise(sinon);
|
||||||
|
|
||||||
|
var autoResolving = sinon.promise().resolves();
|
||||||
|
|
||||||
|
describe('test TOTP checker', function() {
|
||||||
|
it('should validate the TOTP token', function() {
|
||||||
|
var totp_secret = 'NBD2ZV64R9UV1O7K';
|
||||||
|
var token = 'token';
|
||||||
|
var totp_mock = sinon.mock();
|
||||||
|
totp_mock.returns('token');
|
||||||
|
var speakeasy_mock = {
|
||||||
|
totp: totp_mock
|
||||||
|
}
|
||||||
|
return totp_checker.validate(speakeasy_mock, token, totp_secret);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not validate a wrong TOTP token', function() {
|
||||||
|
var totp_secret = 'NBD2ZV64R9UV1O7K';
|
||||||
|
var token = 'wrong token';
|
||||||
|
var totp_mock = sinon.mock();
|
||||||
|
totp_mock.returns('token');
|
||||||
|
var speakeasy_mock = {
|
||||||
|
totp: totp_mock
|
||||||
|
}
|
||||||
|
return totp_checker.validate(speakeasy_mock, token, totp_secret).fail(autoResolving);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
|
@ -0,0 +1,17 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<head>
|
||||||
|
<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>
|
||||||
|
</html>
|
|
@ -0,0 +1,39 @@
|
||||||
|
|
||||||
|
version: '2'
|
||||||
|
services:
|
||||||
|
auth-server:
|
||||||
|
build: .
|
||||||
|
environment:
|
||||||
|
- LDAP_URL=ldap://ldap
|
||||||
|
- SECRET=NBD2ZV64R9UV1O7K
|
||||||
|
- USERS_DN=dc=example,dc=com
|
||||||
|
- PORT=80
|
||||||
|
- EXPIRATION_TIME=2m
|
||||||
|
depends_on:
|
||||||
|
- ldap
|
||||||
|
expose:
|
||||||
|
- "80"
|
||||||
|
|
||||||
|
ldap:
|
||||||
|
image: osixia/openldap:1.1.7
|
||||||
|
environment:
|
||||||
|
- LDAP_ORGANISATION=MyCompany
|
||||||
|
- LDAP_DOMAIN=example.com
|
||||||
|
- LDAP_ADMIN_PASSWORD=test
|
||||||
|
expose:
|
||||||
|
- "389"
|
||||||
|
|
||||||
|
nginx:
|
||||||
|
image: nginx:alpine
|
||||||
|
volumes:
|
||||||
|
- ./proxy/nginx.conf:/etc/nginx/nginx.conf
|
||||||
|
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,36 @@
|
||||||
|
{
|
||||||
|
"name": "ldap-totp-nginx-auth",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "",
|
||||||
|
"main": "app/index.js",
|
||||||
|
"scripts": {
|
||||||
|
"test": "echo \"Error: no test specified\" && exit 1"
|
||||||
|
},
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "git+https://github.com/clems4ever/ldap-totp-nginx-auth.git"
|
||||||
|
},
|
||||||
|
"author": "",
|
||||||
|
"license": "ISC",
|
||||||
|
"bugs": {
|
||||||
|
"url": "https://github.com/clems4ever/ldap-totp-nginx-auth/issues"
|
||||||
|
},
|
||||||
|
"homepage": "https://github.com/clems4ever/ldap-totp-nginx-auth#readme",
|
||||||
|
"dependencies": {
|
||||||
|
"body-parser": "^1.15.2",
|
||||||
|
"cookie-parser": "^1.4.3",
|
||||||
|
"ejs": "^2.5.5",
|
||||||
|
"express": "^4.14.0",
|
||||||
|
"jsonwebtoken": "^7.2.1",
|
||||||
|
"ldapjs": "^1.0.1",
|
||||||
|
"object-path": "^0.11.3",
|
||||||
|
"q": "^1.4.1",
|
||||||
|
"speakeasy": "^2.0.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"mocha": "^3.2.0",
|
||||||
|
"should": "^11.1.1",
|
||||||
|
"sinon": "^1.17.6",
|
||||||
|
"sinon-promise": "^0.1.3"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
<html>
|
||||||
|
<body>
|
||||||
|
Coucou
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -0,0 +1,76 @@
|
||||||
|
# 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
<html>
|
||||||
|
<body>
|
||||||
|
Coucou
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -0,0 +1,32 @@
|
||||||
|
# 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;
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue