Move files from app to src and tests in root directory + adding more tests

pull/2/head
Clement Michaud 2016-12-17 02:06:40 +01:00
parent d7d743bdfa
commit e13315eb92
21 changed files with 218 additions and 82 deletions

4
.gitignore vendored
View File

@ -1,3 +1,7 @@
node_modules/ node_modules/
coverage/
*.swp

View File

@ -1,37 +0,0 @@
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);
});

View File

@ -2,9 +2,10 @@
"name": "ldap-totp-nginx-auth", "name": "ldap-totp-nginx-auth",
"version": "1.0.0", "version": "1.0.0",
"description": "", "description": "",
"main": "app/index.js", "main": "src/index.js",
"scripts": { "scripts": {
"test": "echo \"Error: no test specified\" && exit 1" "test": "./node_modules/.bin/mocha",
"coverage": "./node_modules/.bin/istanbul cover _mocha -- -R spec"
}, },
"repository": { "repository": {
"type": "git", "type": "git",
@ -29,6 +30,7 @@
}, },
"devDependencies": { "devDependencies": {
"mocha": "^3.2.0", "mocha": "^3.2.0",
"request": "^2.79.0",
"should": "^11.1.1", "should": "^11.1.1",
"sinon": "^1.17.6", "sinon": "^1.17.6",
"sinon-promise": "^0.1.3" "sinon-promise": "^0.1.3"

20
src/index.js 100644
View File

@ -0,0 +1,20 @@
var server = require('./lib/server');
var ldap = require('ldapjs');
var config = {
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,
jwt_secret: process.env.JWT_SECRET,
jwt_expiration_time: process.env.JWT_EXPIRATION_TIME || '1h'
}
var ldap_client = ldap.createClient({
url: config.ldap_url
});
server.run(config, ldap_client);

View File

@ -1,11 +1,10 @@
module.exports = { module.exports = {
'authenticate': authenticate, 'authenticate': authenticate,
'verify_authentication': verify_authentication 'verify': verify_authentication
} }
var objectPath = require('object-path'); var objectPath = require('object-path');
var Jwt = require('./jwt');
var ldap_checker = require('./ldap_checker'); var ldap_checker = require('./ldap_checker');
var totp_checker = require('./totp_checker'); var totp_checker = require('./totp_checker');
var replies = require('./replies'); var replies = require('./replies');
@ -13,38 +12,42 @@ var Q = require('q');
var utils = require('./utils'); var utils = require('./utils');
function authenticate(req, res, args) { function authenticate(req, res) {
var defer = Q.defer(); var defer = Q.defer();
var username = req.body.username; var username = req.body.username;
var password = req.body.password; var password = req.body.password;
var token = req.body.token; var token = req.body.token;
console.log('Start authentication'); console.log('Start authentication of user %s', username);
if(!username || !password || !token) { if(!username || !password || !token) {
replies.authentication_failed(res); replies.authentication_failed(res);
return; return;
} }
var totp_promise = totp_checker.validate(args.totp_interface, token, args.totp_secret); var jwt_engine = req.app.get('jwt engine');
var credentials_promise = ldap_checker.validate(args.ldap_interface, username, password, args.users_dn); var ldap_client = req.app.get('ldap client');
var totp_engine = req.app.get('totp engine');
var config = req.app.get('config');
var totp_promise = totp_checker.validate(totp_engine, token, config.totp_secret);
var credentials_promise = ldap_checker.validate(ldap_client, username, password, config.ldap_users_dn);
Q.all([totp_promise, credentials_promise]) Q.all([totp_promise, credentials_promise])
.then(function() { .then(function() {
var token = args.jwt.sign({ user: username }, args.jwt_expiration_time); var token = jwt_engine.sign({ user: username }, config.jwt_expiration_time);
res.cookie('access_token', token); replies.authentication_succeeded(res, username, token);
res.redirect('/');
console.log('Authentication succeeded'); console.log('Authentication succeeded');
defer.resolve(); defer.resolve();
}) })
.fail(function(err1, err2) { .fail(function(err1, err2) {
res.render('login');
console.log('Authentication failed', err1, err2); console.log('Authentication failed', err1, err2);
replies.authentication_failed(res);
defer.reject(); defer.reject();
}); });
return defer.promise; return defer.promise;
} }
function verify_authentication(req, res, args) { function verify_authentication(req, res) {
console.log('Verify authentication'); console.log('Verify authentication');
if(!objectPath.has(req, 'cookies.access_token')) { if(!objectPath.has(req, 'cookies.access_token')) {
@ -52,6 +55,6 @@ function verify_authentication(req, res, args) {
} }
var jsonWebToken = req.cookies['access_token']; var jsonWebToken = req.cookies['access_token'];
return args.jwt.verify(jsonWebToken); return req.app.get('jwt engine').verify(jsonWebToken);
} }

View File

@ -11,19 +11,17 @@ function authentication_failed(res) {
res.send('Authentication failed'); res.send('Authentication failed');
} }
function send_success(res, username, msg) { function authentication_succeeded(res, username, token) {
console.log('Reply: authentication succeeded');
res.status(200); res.status(200);
res.set({ 'X-Remote-User': username }); res.set({ 'X-Remote-User': username });
res.send(msg); res.send(token);
}
function authentication_succeeded(res, username) {
console.log('Reply: authentication succeeded');
send_success(res, username, 'Authentication succeeded');
} }
function already_authenticated(res, username) { function already_authenticated(res, username) {
console.log('Reply: already authenticated'); console.log('Reply: already authenticated');
send_success(res, username, 'Authentication succeeded'); res.status(204);
res.set({ 'X-Remote-User': username });
res.send();
} }

View File

@ -9,6 +9,15 @@ var authentication = require('./authentication');
var replies = require('./replies'); var replies = require('./replies');
function serveAuth(req, res) { function serveAuth(req, res) {
if(req.method == 'POST') {
serveAuthPost(req, res);
}
else {
serveAuthGet(req, res);
}
}
function serveAuthGet(req, res) {
authentication.verify(req, res) authentication.verify(req, res)
.then(function(user) { .then(function(user) {
replies.already_authenticated(res, user); replies.already_authenticated(res, user);
@ -19,15 +28,13 @@ function serveAuth(req, res) {
}); });
} }
function serveLogin(req, res) { function serveAuthPost(req, res) {
console.log('METHOD=%s', req.method);
if(req.method == 'POST') {
authentication.authenticate(req, res); authentication.authenticate(req, res);
} }
else {
function serveLogin(req, res) {
res.render('login'); res.render('login');
} }
}
function serveLogout(req, res) { function serveLogout(req, res) {
res.clearCookie('access_token'); res.clearCookie('access_token');

37
src/lib/server.js 100644
View File

@ -0,0 +1,37 @@
module.exports = {
run: run
}
var routes = require('./routes');
var Jwt = require('./jwt');
var express = require('express');
var bodyParser = require('body-parser');
var cookieParser = require('cookie-parser');
var speakeasy = require('speakeasy');
function run(config, ldap_client) {
var app = express();
app.set('views', './src/views');
app.use(cookieParser());
app.use(express.static(__dirname + '/public_html'));
app.use(bodyParser.urlencoded({ extended: false }));
app.set('view engine', 'ejs');
app.set('jwt engine', new Jwt(config.jwt_secret));
app.set('ldap client', ldap_client);
app.set('totp engine', speakeasy);
app.set('config', config);
app.get ('/login', routes.login);
app.get ('/logout', routes.logout);
app.get ('/_auth', routes.auth);
app.post ('/_auth', routes.auth);
app.listen(config.port, function(err) {
console.log('Listening on %d...', config.port);
});
}

View File

@ -5,9 +5,9 @@ module.exports = {
var Q = require('q'); var Q = require('q');
function validate(speakeasy, token, totp_secret) { function validate(totp_engine, token, totp_secret) {
var defer = Q.defer(); var defer = Q.defer();
var real_token = speakeasy.totp({ var real_token = totp_engine.totp({
secret: totp_secret, secret: totp_secret,
encoding: 'base32' encoding: 'base32'
}); });

View File

@ -1,6 +1,6 @@
var assert = require('assert'); var assert = require('assert');
var authentication = require('../lib/authentication'); var authentication = require('../src/lib/authentication');
var create_res_mock = require('./res_mock'); var create_res_mock = require('./res_mock');
var sinon = require('sinon'); var sinon = require('sinon');
var sinonPromise = require('sinon-promise'); var sinonPromise = require('sinon-promise');
@ -17,6 +17,9 @@ function create_req_mock(token) {
}, },
cookies: { cookies: {
'access_token': 'cookie_token' 'access_token': 'cookie_token'
},
app: {
get: sinon.stub()
} }
} }
} }
@ -55,6 +58,14 @@ function create_mocks() {
totp_interface: totp_interface_mock totp_interface: totp_interface_mock
} }
req_mock.app.get.withArgs('ldap client').returns(args.ldap_interface);
req_mock.app.get.withArgs('jwt engine').returns(args.jwt);
req_mock.app.get.withArgs('totp engine').returns(args.totp_interface);
req_mock.app.get.withArgs('config').returns({
totp_secret: 'totp_secret',
ldap_users_dn: 'ou=users,dc=example,dc=com'
});
return { return {
req: req_mock, req: req_mock,
res: res_mock, res: res_mock,
@ -70,11 +81,11 @@ describe('test jwt', function() {
var jwt_token = 'jwt_token'; var jwt_token = 'jwt_token';
var clock = sinon.useFakeTimers(); var clock = sinon.useFakeTimers();
var mocks = create_mocks(); var mocks = create_mocks();
authentication.authenticate(mocks.req, mocks.res, mocks.args) authentication.authenticate(mocks.req, mocks.res)
.then(function() { .then(function() {
clock.restore(); clock.restore();
assert(mocks.res.cookie.calledWith('access_token', jwt_token)); assert(mocks.res.status.calledWith(200));
assert(mocks.res.redirect.calledWith('/')); assert(mocks.res.send.calledWith(jwt_token));
done(); done();
}) })
}); });
@ -83,7 +94,7 @@ describe('test jwt', function() {
var clock = sinon.useFakeTimers(); var clock = sinon.useFakeTimers();
var mocks = create_mocks(); var mocks = create_mocks();
mocks.totp.returns('wrong token'); mocks.totp.returns('wrong token');
authentication.authenticate(mocks.req, mocks.res, mocks.args) authentication.authenticate(mocks.req, mocks.res)
.fail(function(err) { .fail(function(err) {
clock.restore(); clock.restore();
done(); done();
@ -96,8 +107,11 @@ describe('test jwt', function() {
it('should be already authenticated', function(done) { it('should be already authenticated', function(done) {
var mocks = create_mocks(); var mocks = create_mocks();
var data = { user: 'username' }; var data = { user: 'username' };
mocks.jwt.verify = sinon.promise().resolves(data); mocks.req.app.get.withArgs('jwt engine').returns({
authentication.verify_authentication(mocks.req, mocks.res, mocks.args) verify: sinon.promise().resolves(data)
});
authentication.verify(mocks.req, mocks.res)
.then(function(actual_data) { .then(function(actual_data) {
assert.equal(actual_data, data); assert.equal(actual_data, data);
done(); done();
@ -107,8 +121,10 @@ describe('test jwt', function() {
it('should not be already authenticated', function(done) { it('should not be already authenticated', function(done) {
var mocks = create_mocks(); var mocks = create_mocks();
var data = { user: 'username' }; var data = { user: 'username' };
mocks.jwt.verify = sinon.promise().rejects('Error with JWT token'); mocks.req.app.get.withArgs('jwt engine').returns({
return authentication.verify_authentication(mocks.req, mocks.res, mocks.args) verify: sinon.promise().rejects('Error with JWT token')
});
return authentication.verify(mocks.req, mocks.res, mocks.args)
.fail(function() { .fail(function() {
done(); done();
}); });

View File

@ -1,5 +1,5 @@
var Jwt = require('../lib/jwt'); var Jwt = require('../src/lib/jwt');
var sinon = require('sinon'); var sinon = require('sinon');
var sinonPromise = require('sinon-promise'); var sinonPromise = require('sinon-promise');
sinonPromise(sinon); sinonPromise(sinon);

View File

@ -1,5 +1,5 @@
var ldap_checker = require('../lib/ldap_checker'); var ldap_checker = require('../src/lib/ldap_checker');
var sinon = require('sinon'); var sinon = require('sinon');
var sinonPromise = require('sinon-promise'); var sinonPromise = require('sinon-promise');

View File

@ -1,5 +1,5 @@
var replies = require('../lib/replies'); var replies = require('../src/lib/replies');
var assert = require('assert'); var assert = require('assert');
var sinon = require('sinon'); var sinon = require('sinon');
var sinonPromise = require('sinon-promise'); var sinonPromise = require('sinon-promise');
@ -36,7 +36,7 @@ describe('test jwt', function() {
replies.already_authenticated(res_mock, username); replies.already_authenticated(res_mock, username);
assert(res_mock.status.calledWith(200)); assert(res_mock.status.calledWith(204));
assert(res_mock.set.calledWith({'X-Remote-User': username })); assert(res_mock.set.calledWith({'X-Remote-User': username }));
}); });

View File

@ -0,0 +1,86 @@
var request = require('request');
var assert = require('assert');
var server = require('../src/lib/server');
var Jwt = require('../src/lib/jwt');
var speakeasy = require('speakeasy');
var sinon = require('sinon');
describe('test the server', function() {
var jwt = new Jwt('jwt_secret');
var ldap_client = {
bind: sinon.mock()
};
before(function() {
var config = {
port: 8080,
totp_secret: 'totp_secret',
ldap_url: 'ldap://127.0.0.1:389',
ldap_users_dn: 'ou=users,dc=example,dc=com',
jwt_secret: 'jwt_secret',
jwt_expiration_time: '1h'
};
// 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');
server.run(config, ldap_client);
});
it('should serve the login page', function(done) {
request.get('http://localhost:8080/login')
.on('response', function(response) {
assert.equal(response.statusCode, 200);
done();
})
});
it('should return status code 401 when user is not authenticated', function(done) {
request.get('http://localhost:8080/_auth')
.on('response', function(response) {
assert.equal(response.statusCode, 401);
done();
})
});
it('should return status code 204 when user is authenticated', function(done) {
var j = request.jar();
var r = request.defaults({jar: j});
var token = jwt.sign({ user: 'test' }, '1h');
var cookie = r.cookie('access_token=' + token);
j.setCookie(cookie, 'http://localhost:8080/_auth');
r.get('http://localhost:8080/_auth')
.on('response', function(response) {
assert.equal(response.statusCode, 204);
done();
})
});
it('should return the JWT token when authentication is successful', function(done) {
var clock = sinon.useFakeTimers();
var real_token = speakeasy.totp({
secret: 'totp_secret',
encoding: 'base32'
});
var expectedJwt = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoidGVzdF9vayIsImlhdCI6MCwiZXhwIjozNjAwfQ.ihvaljGjO5h3iSO_h3PkNNSCYeePyB8Hr5lfVZZYyrQ';
request.post('http://localhost:8080/_auth', {
form: {
username: 'test_ok',
password: 'password',
token: real_token
}
},
function (error, response, body) {
if (!error && response.statusCode == 200) {
assert.equal(body, expectedJwt);
clock.restore();
done();
}
});
});
});

View File

@ -1,5 +1,5 @@
var totp_checker = require('../lib/totp_checker'); var totp_checker = require('../src/lib/totp_checker');
var sinon = require('sinon'); var sinon = require('sinon');
var sinonPromise = require('sinon-promise'); var sinonPromise = require('sinon-promise');
sinonPromise(sinon); sinonPromise(sinon);