diff --git a/.gitignore b/.gitignore index 41195164..0f57c731 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,7 @@ node_modules/ +coverage/ + +*.swp + diff --git a/app/index.js b/app/index.js deleted file mode 100644 index 695502f1..00000000 --- a/app/index.js +++ /dev/null @@ -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); -}); - diff --git a/package.json b/package.json index 8dd740e3..1bd63959 100644 --- a/package.json +++ b/package.json @@ -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" diff --git a/src/index.js b/src/index.js new file mode 100644 index 00000000..1e540891 --- /dev/null +++ b/src/index.js @@ -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); + diff --git a/app/lib/authentication.js b/src/lib/authentication.js similarity index 55% rename from app/lib/authentication.js rename to src/lib/authentication.js index 3c276b3a..cf8e66f1 100644 --- a/app/lib/authentication.js +++ b/src/lib/authentication.js @@ -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); } diff --git a/app/lib/jwt.js b/src/lib/jwt.js similarity index 100% rename from app/lib/jwt.js rename to src/lib/jwt.js diff --git a/app/lib/ldap_checker.js b/src/lib/ldap_checker.js similarity index 100% rename from app/lib/ldap_checker.js rename to src/lib/ldap_checker.js diff --git a/app/lib/replies.js b/src/lib/replies.js similarity index 69% rename from app/lib/replies.js rename to src/lib/replies.js index f60199a1..a68d74fa 100644 --- a/app/lib/replies.js +++ b/src/lib/replies.js @@ -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(); } diff --git a/app/lib/routes.js b/src/lib/routes.js similarity index 75% rename from app/lib/routes.js rename to src/lib/routes.js index cadcd116..cd8f2b59 100644 --- a/app/lib/routes.js +++ b/src/lib/routes.js @@ -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,14 +28,12 @@ function serveAuth(req, res) { }); } +function serveAuthPost(req, res) { + authentication.authenticate(req, res); +} + function serveLogin(req, res) { - console.log('METHOD=%s', req.method); - if(req.method == 'POST') { - authentication.authenticate(req, res); - } - else { - res.render('login'); - } + res.render('login'); } function serveLogout(req, res) { diff --git a/src/lib/server.js b/src/lib/server.js new file mode 100644 index 00000000..f73159c9 --- /dev/null +++ b/src/lib/server.js @@ -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); + }); +} diff --git a/app/lib/totp_checker.js b/src/lib/totp_checker.js similarity index 75% rename from app/lib/totp_checker.js rename to src/lib/totp_checker.js index 24c061d9..a68c1235 100644 --- a/app/lib/totp_checker.js +++ b/src/lib/totp_checker.js @@ -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' }); diff --git a/app/lib/utils.js b/src/lib/utils.js similarity index 100% rename from app/lib/utils.js rename to src/lib/utils.js diff --git a/app/public_html/login.css b/src/public_html/login.css similarity index 100% rename from app/public_html/login.css rename to src/public_html/login.css diff --git a/app/views/login.ejs b/src/views/login.ejs similarity index 100% rename from app/views/login.ejs rename to src/views/login.ejs diff --git a/app/tests/authentication_test.js b/test/authentication_test.js similarity index 70% rename from app/tests/authentication_test.js rename to test/authentication_test.js index 7e27b699..00eb6e95 100644 --- a/app/tests/authentication_test.js +++ b/test/authentication_test.js @@ -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(); }); diff --git a/app/tests/jwt_test.js b/test/jwt_test.js similarity index 95% rename from app/tests/jwt_test.js rename to test/jwt_test.js index 45e6b603..30cef7d9 100644 --- a/app/tests/jwt_test.js +++ b/test/jwt_test.js @@ -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); diff --git a/app/tests/ldap_checker_test.js b/test/ldap_checker_test.js similarity index 94% rename from app/tests/ldap_checker_test.js rename to test/ldap_checker_test.js index 7a672fe7..db3b82fe 100644 --- a/app/tests/ldap_checker_test.js +++ b/test/ldap_checker_test.js @@ -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'); diff --git a/app/tests/replies_test.js b/test/replies_test.js similarity index 93% rename from app/tests/replies_test.js rename to test/replies_test.js index 4c88f3fd..c7ea98d1 100644 --- a/app/tests/replies_test.js +++ b/test/replies_test.js @@ -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 })); }); diff --git a/app/tests/res_mock.js b/test/res_mock.js similarity index 100% rename from app/tests/res_mock.js rename to test/res_mock.js diff --git a/test/test_server.js b/test/test_server.js new file mode 100644 index 00000000..7f9f496d --- /dev/null +++ b/test/test_server.js @@ -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(); + } + }); + }); +}); diff --git a/app/tests/totp_checker_test.js b/test/totp_checker_test.js similarity index 94% rename from app/tests/totp_checker_test.js rename to test/totp_checker_test.js index 3a618696..c89b5aaa 100644 --- a/app/tests/totp_checker_test.js +++ b/test/totp_checker_test.js @@ -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);