mirror of
https://github.com/0rangebananaspy/authelia.git
synced 2024-09-14 22:47:21 +07:00
First commit with tests
This commit is contained in:
commit
d7d743bdfa
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
|
||||
node_modules/
|
||||
|
7
Dockerfile
Normal file
7
Dockerfile
Normal file
|
@ -0,0 +1,7 @@
|
|||
FROM node
|
||||
|
||||
WORKDIR /usr/src
|
||||
|
||||
COPY app /usr/src
|
||||
|
||||
CMD ["node", "app.js"]
|
37
app/index.js
Normal file
37
app/index.js
Normal file
|
@ -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);
|
||||
});
|
||||
|
57
app/lib/authentication.js
Normal file
57
app/lib/authentication.js
Normal file
|
@ -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);
|
||||
}
|
||||
|
29
app/lib/jwt.js
Normal file
29
app/lib/jwt.js
Normal file
|
@ -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;
|
||||
}
|
||||
|
14
app/lib/ldap_checker.js
Normal file
14
app/lib/ldap_checker.js
Normal file
|
@ -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);
|
||||
}
|
29
app/lib/replies.js
Normal file
29
app/lib/replies.js
Normal file
|
@ -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');
|
||||
}
|
||||
|
35
app/lib/routes.js
Normal file
35
app/lib/routes.js
Normal file
|
@ -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('/');
|
||||
}
|
22
app/lib/totp_checker.js
Normal file
22
app/lib/totp_checker.js
Normal file
|
@ -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;
|
||||
}
|
35
app/lib/utils.js
Normal file
35
app/lib/utils.js
Normal file
|
@ -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;
|
||||
}
|
58
app/public_html/login.css
Normal file
58
app/public_html/login.css
Normal file
|
@ -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); }
|
||||
|
118
app/tests/authentication_test.js
Normal file
118
app/tests/authentication_test.js
Normal file
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
33
app/tests/jwt_test.js
Normal file
33
app/tests/jwt_test.js
Normal file
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
35
app/tests/ldap_checker_test.js
Normal file
35
app/tests/ldap_checker_test.js
Normal file
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
52
app/tests/replies_test.js
Normal file
52
app/tests/replies_test.js
Normal file
|
@ -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));
|
||||
});
|
||||
});
|
||||
|
24
app/tests/res_mock.js
Normal file
24
app/tests/res_mock.js
Normal file
|
@ -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
|
||||
};
|
||||
}
|
32
app/tests/totp_checker_test.js
Normal file
32
app/tests/totp_checker_test.js
Normal file
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
17
app/views/login.ejs
Normal file
17
app/views/login.ejs
Normal file
|
@ -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>
|
39
docker-compose.yml
Normal file
39
docker-compose.yml
Normal file
|
@ -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
|
36
package.json
Normal file
36
package.json
Normal file
|
@ -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"
|
||||
}
|
||||
}
|
5
proxy/index.html
Normal file
5
proxy/index.html
Normal file
|
@ -0,0 +1,5 @@
|
|||
<html>
|
||||
<body>
|
||||
Coucou
|
||||
</body>
|
||||
</html>
|
76
proxy/nginx.conf
Normal file
76
proxy/nginx.conf
Normal file
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
5
secret/index.html
Normal file
5
secret/index.html
Normal file
|
@ -0,0 +1,5 @@
|
|||
<html>
|
||||
<body>
|
||||
Coucou
|
||||
</body>
|
||||
</html>
|
32
secret/nginx.conf
Normal file
32
secret/nginx.conf
Normal file
|
@ -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
Block a user