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/
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",
"version": "1.0.0",
"description": "",
"main": "app/index.js",
"main": "src/index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
"test": "./node_modules/.bin/mocha",
"coverage": "./node_modules/.bin/istanbul cover _mocha -- -R spec"
},
"repository": {
"type": "git",
@ -29,6 +30,7 @@
},
"devDependencies": {
"mocha": "^3.2.0",
"request": "^2.79.0",
"should": "^11.1.1",
"sinon": "^1.17.6",
"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 = {
'authenticate': authenticate,
'verify_authentication': verify_authentication
'verify': 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');
@ -13,38 +12,42 @@ var Q = require('q');
var utils = require('./utils');
function authenticate(req, res, args) {
function authenticate(req, res) {
var defer = Q.defer();
var username = req.body.username;
var password = req.body.password;
var token = req.body.token;
console.log('Start authentication');
console.log('Start authentication of user %s', username);
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);
var jwt_engine = req.app.get('jwt engine');
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])
.then(function() {
var token = args.jwt.sign({ user: username }, args.jwt_expiration_time);
res.cookie('access_token', token);
res.redirect('/');
var token = jwt_engine.sign({ user: username }, config.jwt_expiration_time);
replies.authentication_succeeded(res, username, token);
console.log('Authentication succeeded');
defer.resolve();
})
.fail(function(err1, err2) {
res.render('login');
console.log('Authentication failed', err1, err2);
replies.authentication_failed(res);
defer.reject();
});
return defer.promise;
}
function verify_authentication(req, res, args) {
function verify_authentication(req, res) {
console.log('Verify authentication');
if(!objectPath.has(req, 'cookies.access_token')) {
@ -52,6 +55,6 @@ function verify_authentication(req, res, args) {
}
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');
}
function send_success(res, username, msg) {
function authentication_succeeded(res, username, token) {
console.log('Reply: authentication succeeded');
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');
res.send(token);
}
function already_authenticated(res, username) {
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');
function serveAuth(req, res) {
if(req.method == 'POST') {
serveAuthPost(req, res);
}
else {
serveAuthGet(req, res);
}
}
function serveAuthGet(req, res) {
authentication.verify(req, res)
.then(function(user) {
replies.already_authenticated(res, user);
@ -19,15 +28,13 @@ function serveAuth(req, res) {
});
}
function serveLogin(req, res) {
console.log('METHOD=%s', req.method);
if(req.method == 'POST') {
function serveAuthPost(req, res) {
authentication.authenticate(req, res);
}
else {
function serveLogin(req, res) {
res.render('login');
}
}
function serveLogout(req, res) {
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');
function validate(speakeasy, token, totp_secret) {
function validate(totp_engine, token, totp_secret) {
var defer = Q.defer();
var real_token = speakeasy.totp({
var real_token = totp_engine.totp({
secret: totp_secret,
encoding: 'base32'
});

View File

@ -1,6 +1,6 @@
var assert = require('assert');
var authentication = require('../lib/authentication');
var authentication = require('../src/lib/authentication');
var create_res_mock = require('./res_mock');
var sinon = require('sinon');
var sinonPromise = require('sinon-promise');
@ -17,6 +17,9 @@ function create_req_mock(token) {
},
cookies: {
'access_token': 'cookie_token'
},
app: {
get: sinon.stub()
}
}
}
@ -55,6 +58,14 @@ function create_mocks() {
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 {
req: req_mock,
res: res_mock,
@ -70,11 +81,11 @@ describe('test jwt', function() {
var jwt_token = 'jwt_token';
var clock = sinon.useFakeTimers();
var mocks = create_mocks();
authentication.authenticate(mocks.req, mocks.res, mocks.args)
authentication.authenticate(mocks.req, mocks.res)
.then(function() {
clock.restore();
assert(mocks.res.cookie.calledWith('access_token', jwt_token));
assert(mocks.res.redirect.calledWith('/'));
assert(mocks.res.status.calledWith(200));
assert(mocks.res.send.calledWith(jwt_token));
done();
})
});
@ -83,7 +94,7 @@ describe('test jwt', function() {
var clock = sinon.useFakeTimers();
var mocks = create_mocks();
mocks.totp.returns('wrong token');
authentication.authenticate(mocks.req, mocks.res, mocks.args)
authentication.authenticate(mocks.req, mocks.res)
.fail(function(err) {
clock.restore();
done();
@ -96,8 +107,11 @@ describe('test jwt', 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)
mocks.req.app.get.withArgs('jwt engine').returns({
verify: sinon.promise().resolves(data)
});
authentication.verify(mocks.req, mocks.res)
.then(function(actual_data) {
assert.equal(actual_data, data);
done();
@ -107,8 +121,10 @@ describe('test jwt', function() {
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)
mocks.req.app.get.withArgs('jwt engine').returns({
verify: sinon.promise().rejects('Error with JWT token')
});
return authentication.verify(mocks.req, mocks.res, mocks.args)
.fail(function() {
done();
});

View File

@ -1,5 +1,5 @@
var Jwt = require('../lib/jwt');
var Jwt = require('../src/lib/jwt');
var sinon = require('sinon');
var sinonPromise = require('sinon-promise');
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 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 sinon = require('sinon');
var sinonPromise = require('sinon-promise');
@ -36,7 +36,7 @@ describe('test jwt', function() {
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 }));
});

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 sinonPromise = require('sinon-promise');
sinonPromise(sinon);