Use filesystem data store to save u2f meta info

pull/7/head
Clement Michaud 2017-01-21 20:24:35 +01:00
parent 9670b23a8b
commit 8b4339f8da
12 changed files with 433 additions and 109 deletions

View File

@ -10,4 +10,6 @@ COPY src /usr/src
ENV PORT=80
EXPOSE 80
VOLUME /var/lib/auth-server
CMD ["node", "index.js"]

View File

@ -3,6 +3,7 @@ version: '2'
services:
auth:
volumes:
- ./test:/usr/src/test
- ./src/views:/usr/src/views
- ./src/public_html:/usr/src/public_html

View File

@ -9,6 +9,7 @@ services:
- TOTP_SECRET=GRWGIJS6IRHVEODVNRCXCOBMJ5AGC6ZE
- SESSION_SECRET=unsecure_secret
- SESSION_EXPIRATION_TIME=3600000
- STORE_DIRECTORY=/var/lib/auth-server
depends_on:
- ldap
restart: always

View File

@ -8,7 +8,7 @@
"unit-test": "./node_modules/.bin/mocha --recursive test/unitary",
"int-test": "./node_modules/.bin/mocha --recursive test/integration",
"all-test": "./node_modules/.bin/mocha --recursive test",
"coverage": "./node_modules/.bin/istanbul cover _mocha -- -R spec"
"coverage": "./node_modules/.bin/istanbul cover _mocha -- -R spec --recursive test"
},
"repository": {
"type": "git",
@ -27,6 +27,7 @@
"express": "^4.14.0",
"express-session": "^1.14.2",
"ldapjs": "^1.0.1",
"nedb": "^1.8.0",
"object-path": "^0.11.3",
"speakeasy": "^2.0.0",
"winston": "^2.3.1"
@ -36,6 +37,7 @@
"request": "^2.79.0",
"should": "^11.1.1",
"sinon": "^1.17.6",
"sinon-promise": "^0.1.3"
"sinon-promise": "^0.1.3",
"tmp": "0.0.31"
}
}

View File

@ -10,7 +10,8 @@ var config = {
ldap_url: process.env.LDAP_URL || 'ldap://127.0.0.1:389',
ldap_users_dn: process.env.LDAP_USERS_DN,
session_secret: process.env.SESSION_SECRET,
session_max_age: process.env.SESSION_MAX_AGE || 3600000 // in ms
session_max_age: process.env.SESSION_MAX_AGE || 3600000, // in ms
store_directory: process.env.STORE_DIRECTORY
}
var ldap_client = ldap.createClient({

View File

@ -1,8 +1,6 @@
var user_key_container = {};
var denyNotLogged = require('./deny_not_logged');
var u2f = require('./u2f')(user_key_container); // create a u2f handler bound to
// user key container
var u2f = require('./u2f');
module.exports = {
totp: denyNotLogged(require('./totp')),

View File

@ -1,11 +1,9 @@
module.exports = function(user_key_container) {
return {
module.exports = {
register_request: register_request,
register: register(user_key_container),
sign_request: sign_request(user_key_container),
sign: sign(user_key_container),
}
register: register,
sign_request: sign_request,
sign: sign,
}
var objectPath = require('object-path');
@ -26,15 +24,18 @@ function replyWithUnauthorized(res) {
res.send();
}
function extractAppId(req) {
return util.format('https://%s', req.headers.host);
}
function register_request(req, res) {
var u2f = req.app.get('u2f');
var logger = req.app.get('logger');
var app_id = util.format('https://%s', req.headers.host);
var appid = extractAppId(req);
logger.debug('U2F register_request: headers=%s', JSON.stringify(req.headers));
logger.info('U2F register_request: Starting registration');
u2f.startRegistration(app_id, [])
u2f.startRegistration(appid, [])
.then(function(registrationRequest) {
logger.info('U2F register_request: Sending back registration request');
req.session.auth_session.register_request = registrationRequest;
@ -46,15 +47,17 @@ function register_request(req, res) {
});
}
function register(user_key_container) {
return function(req, res) {
function register(req, res) {
if(!objectPath.has(req, 'session.auth_session.register_request')) {
replyWithUnauthorized(res);
return;
}
var user_data_storage = req.app.get('user data store');
var u2f = req.app.get('u2f');
var registrationRequest = req.session.auth_session.register_request;
var userid = req.session.auth_session.userid;
var appid = extractAppId(req);
var logger = req.app.get('logger');
logger.info('U2F register: Finishing registration');
@ -69,7 +72,7 @@ function register(user_key_container) {
publicKey: registrationStatus.publicKey,
certificate: registrationStatus.certificate
}
user_key_container[req.session.auth_session.userid] = meta;
user_data_storage.set_u2f_meta(userid, appid, meta);
res.status(204);
res.send();
}, function(err) {
@ -77,59 +80,83 @@ function register(user_key_container) {
replyWithInternalError(res, 'Unable to complete the registration');
});
}
function retrieveU2fMeta(req, user_data_storage) {
var userid = req.session.auth_session.userid;
var appid = extractAppId(req);
return user_data_storage.get_u2f_meta(userid, appid);
}
function userKeyExists(req, user_key_container) {
return req.session.auth_session.userid in user_key_container;
function startU2fAuthentication(u2f, appid, meta) {
return new Promise(function(resolve, reject) {
u2f.startAuthentication(appid, [meta])
.then(function(authRequest) {
resolve(authRequest);
}, function(err) {
reject(err);
});
});
}
function finishU2fAuthentication(u2f, authRequest, data, meta) {
return new Promise(function(resolve, reject) {
u2f.finishAuthentication(authRequest, data, [meta])
.then(function(authenticationStatus) {
resolve(authenticationStatus);
}, function(err) {
reject(err);
})
});
}
function sign_request(req, res) {
var logger = req.app.get('logger');
var user_data_storage = req.app.get('user data store');
function sign_request(user_key_container) {
return function(req, res) {
if(!userKeyExists(req, user_key_container)) {
retrieveU2fMeta(req, user_data_storage)
.then(function(doc) {
if(!doc) {
replyWithMissingRegistration(res);
return;
}
var logger = req.app.get('logger');
var u2f = req.app.get('u2f');
var key = user_key_container[req.session.auth_session.userid];
var app_id = util.format('https://%s', req.headers.host);
var meta = doc.meta;
var appid = extractAppId(req);
logger.info('U2F sign_request: Start authentication');
u2f.startAuthentication(app_id, [key])
return startU2fAuthentication(u2f, appid, meta);
})
.then(function(authRequest) {
logger.info('U2F sign_request: Store authentication request and reply');
req.session.auth_session.sign_request = authRequest;
res.status(200);
res.json(authRequest);
}, function(err) {
})
.catch(function(err) {
logger.info('U2F sign_request: %s', err);
replyWithUnauthorized(res);
});
}
}
function sign(user_key_container) {
return function(req, res) {
if(!userKeyExists(req, user_key_container)) {
replyWithMissingRegistration(res);
return;
}
function sign(req, res) {
if(!objectPath.has(req, 'session.auth_session.sign_request')) {
replyWithUnauthorized(res);
return;
}
var logger = req.app.get('logger');
var user_data_storage = req.app.get('user data store');
retrieveU2fMeta(req, user_data_storage)
.then(function(doc) {
var appid = extractAppId(req);
var u2f = req.app.get('u2f');
var authRequest = req.session.auth_session.sign_request;
var key = user_key_container[req.session.auth_session.userid];
var meta = doc.meta;
logger.info('U2F sign: Finish authentication');
u2f.finishAuthentication(authRequest, req.body, [key])
return finishU2fAuthentication(u2f, authRequest, req.body, meta);
})
.then(function(authenticationStatus) {
logger.info('U2F sign: Authentication successful');
req.session.auth_session.second_factor = true;
@ -141,4 +168,4 @@ function sign(user_key_container) {
res.send();
});
}
}

View File

@ -11,10 +11,14 @@ var speakeasy = require('speakeasy');
var path = require('path');
var session = require('express-session');
var winston = require('winston');
var DataStore = require('nedb');
var UserDataStore = require('./user_data_store');
function run(config, ldap_client, u2f, fn) {
var view_directory = path.resolve(__dirname, '../views');
var public_html_directory = path.resolve(__dirname, '../public_html');
var datastore_options = {};
datastore_options.directory = config.store_directory;
var app = express();
app.use(express.static(public_html_directory));
@ -41,6 +45,7 @@ function run(config, ldap_client, u2f, fn) {
app.set('ldap client', ldap_client);
app.set('totp engine', speakeasy);
app.set('u2f', u2f);
app.set('user data store', new UserDataStore(DataStore, datastore_options));
app.set('config', config);
app.get ('/login', routes.login);

View File

@ -0,0 +1,39 @@
module.exports = UserDataStore;
var Promise = require('bluebird');
var path = require('path');
function UserDataStore(DataStore, options) {
var datastore_options = {};
if(options.directory)
datastore_options.filename = path.resolve(options.directory, 'u2f_meta');
datastore_options.inMemoryOnly = options.inMemoryOnly || false;
datastore_options.autoload = true;
console.log(datastore_options);
this._u2f_meta_collection = Promise.promisifyAll(new DataStore(datastore_options));
}
UserDataStore.prototype.set_u2f_meta = function(userid, app_id, meta) {
var newDocument = {};
newDocument.userid = userid;
newDocument.appid = app_id;
newDocument.meta = meta;
var filter = {};
filter.userid = userid;
filter.appid = app_id;
return this._u2f_meta_collection.updateAsync(filter, newDocument, { upsert: true });
}
UserDataStore.prototype.get_u2f_meta = function(userid, app_id) {
var filter = {};
filter.userid = userid;
filter.appid = app_id;
return this._u2f_meta_collection.findOneAsync(filter);
}

View File

@ -7,6 +7,7 @@ var winston = require('winston');
describe('test u2f routes', function() {
var req, res;
var user_data_store;
beforeEach(function() {
req = {}
@ -21,8 +22,17 @@ describe('test u2f routes', function() {
req.headers = {};
req.headers.host = 'localhost';
var options = {};
options.inMemoryOnly = true;
user_data_store = {};
user_data_store.set_u2f_meta = sinon.stub().returns(Promise.resolve({}));
user_data_store.get_u2f_meta = sinon.stub().returns(Promise.resolve({}));
req.app.get.withArgs('user data store').returns(user_data_store);
res = {};
res.send = sinon.spy();
res.json = sinon.spy();
res.status = sinon.spy();
})
@ -31,8 +41,6 @@ describe('test u2f routes', function() {
describe('test signing request', test_signing_request);
describe('test signing', test_signing);
function test_registration_request() {
it('should send back the registration request and save it in the session', function(done) {
var expectedRequest = {
@ -49,7 +57,7 @@ describe('test u2f routes', function() {
u2f_mock.startRegistration.returns(Promise.resolve(expectedRequest));
req.app.get.withArgs('u2f').returns(u2f_mock);
u2f(user_key_container).register_request(req, res);
u2f.register_request(req, res);
});
it('should return internal error on registration request', function(done) {
@ -63,21 +71,19 @@ describe('test u2f routes', function() {
u2f_mock.startRegistration.returns(Promise.reject('Internal error'));
req.app.get.withArgs('u2f').returns(u2f_mock);
u2f(user_key_container).register_request(req, res);
u2f.register_request(req, res);
});
}
function test_registration() {
it('should return status code 200', function(done) {
var user_key_container = {};
it('should save u2f meta and return status code 200', function(done) {
var expectedStatus = {
keyHandle: 'keyHandle',
publicKey: 'pbk',
certificate: 'cert'
};
res.send = sinon.spy(function(data) {
assert('user' in user_key_container);
assert.deepEqual(expectedStatus, user_key_container['user']);
assert('user', user_data_store.set_u2f_meta.getCall(0).args[0])
done();
});
var u2f_mock = {};
@ -86,7 +92,7 @@ describe('test u2f routes', function() {
req.session.auth_session.register_request = {};
req.app.get.withArgs('u2f').returns(u2f_mock);
u2f(user_key_container).register(req, res);
u2f.register(req, res);
});
it('should return unauthorized error on registration request', function(done) {
@ -100,7 +106,7 @@ describe('test u2f routes', function() {
u2f_mock.finishRegistration.returns(Promise.reject('Internal error'));
req.app.get.withArgs('u2f').returns(u2f_mock);
u2f(user_key_container).register(req, res);
u2f.register(req, res);
});
it('should return unauthorized error when no auth request has been initiated', function(done) {
@ -114,7 +120,7 @@ describe('test u2f routes', function() {
u2f_mock.finishRegistration.returns(Promise.resolve());
req.app.get.withArgs('u2f').returns(u2f_mock);
u2f(user_key_container).register(req, res);
u2f.register(req, res);
});
}
@ -136,7 +142,7 @@ describe('test u2f routes', function() {
u2f_mock.startAuthentication.returns(Promise.resolve(expectedRequest));
req.app.get.withArgs('u2f').returns(u2f_mock);
u2f(user_key_container).sign_request(req, res);
u2f.sign_request(req, res);
});
it('should return unauthorized error on registration request error', function(done) {
@ -151,7 +157,7 @@ describe('test u2f routes', function() {
u2f_mock.startAuthentication.returns(Promise.reject('Internal error'));
req.app.get.withArgs('u2f').returns(u2f_mock);
u2f(user_key_container).sign_request(req, res);
u2f.sign_request(req, res);
});
it('should send unauthorized error when no registration exists', function(done) {
@ -167,8 +173,13 @@ describe('test u2f routes', function() {
u2f_mock.startAuthentication = sinon.stub();
u2f_mock.startAuthentication.returns(Promise.resolve(expectedRequest));
user_data_store.get_u2f_meta = sinon.stub().returns(Promise.resolve());
req.app.get = sinon.stub();
req.app.get.withArgs('logger').returns(winston);
req.app.get.withArgs('user data store').returns(user_data_store);
req.app.get.withArgs('u2f').returns(u2f_mock);
u2f(user_key_container).sign_request(req, res);
u2f.sign_request(req, res);
});
}
@ -192,7 +203,7 @@ describe('test u2f routes', function() {
req.session.auth_session.sign_request = {};
req.app.get.withArgs('u2f').returns(u2f_mock);
u2f(user_key_container).sign(req, res);
u2f.sign(req, res);
});
it('should return unauthorized error on registration request internal error', function(done) {
@ -209,7 +220,7 @@ describe('test u2f routes', function() {
req.session.auth_session.sign_request = {};
req.app.get.withArgs('u2f').returns(u2f_mock);
u2f(user_key_container).register(req, res);
u2f.register(req, res);
});
it('should return unauthorized error when no sign request has been initiated', function(done) {
@ -223,7 +234,7 @@ describe('test u2f routes', function() {
u2f_mock.finishAuthentication.returns(Promise.resolve());
req.app.get.withArgs('u2f').returns(u2f_mock);
u2f(user_key_container).register(req, res);
u2f.register(req, res);
});
}
});

View File

@ -0,0 +1,169 @@
var server = require('../../src/lib/server');
var request = require('request');
var assert = require('assert');
var speakeasy = require('speakeasy');
var sinon = require('sinon');
var Promise = require('bluebird');
var tmp = require('tmp');
var request = Promise.promisifyAll(request);
var PORT = 8050;
var BASE_URL = 'http://localhost:' + PORT;
describe('test data persistence', function() {
var u2f;
var tmpDir;
var ldap_client = {
bind: sinon.stub()
};
var config;
before(function() {
u2f = {};
u2f.startRegistration = sinon.stub();
u2f.finishRegistration = sinon.stub();
u2f.startAuthentication = sinon.stub();
u2f.finishAuthentication = sinon.stub();
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('error');
tmpDir = tmp.dirSync({ unsafeCleanup: true });
config = {
port: PORT,
totp_secret: 'totp_secret',
ldap_url: 'ldap://127.0.0.1:389',
ldap_users_dn: 'ou=users,dc=example,dc=com',
session_secret: 'session_secret',
session_max_age: 50000,
store_directory: tmpDir.name
};
});
after(function() {
tmpDir.removeCallback();
});
it('should save a u2f meta and reload it after a restart of the server', function() {
var server;
var sign_request = {};
var sign_status = {};
var registration_request = {};
var registration_status = {};
u2f.startRegistration.returns(Promise.resolve(sign_request));
u2f.finishRegistration.returns(Promise.resolve(sign_status));
u2f.startAuthentication.returns(Promise.resolve(registration_request));
u2f.finishAuthentication.returns(Promise.resolve(registration_status));
var j1 = request.jar();
var j2 = request.jar();
return start_server(config, ldap_client, u2f)
.then(function(s) {
server = s;
return execute_login(j1);
})
.then(function(res) {
return execute_first_factor(j1);
})
.then(function() {
return execute_u2f_registration(j1);
})
.then(function() {
return execute_u2f_authentication(j1);
})
.then(function() {
return stop_server(server);
})
.then(function() {
return start_server(config, ldap_client, u2f)
})
.then(function(s) {
server = s;
return execute_login(j2);
})
.then(function() {
return execute_first_factor(j2);
})
.then(function() {
return execute_u2f_authentication(j2);
})
.then(function(res) {
assert.equal(204, res.statusCode);
server.close();
return Promise.resolve();
})
.catch(function(err) {
console.error(err);
return Promise.reject(err);
});
});
function start_server(config, ldap_client, u2f) {
return new Promise(function(resolve, reject) {
var s = server.run(config, ldap_client, u2f);
resolve(s);
});
}
function stop_server(s) {
return new Promise(function(resolve, reject) {
s.close();
resolve();
});
}
function execute_first_factor(jar) {
return request.postAsync({
url: BASE_URL + '/_auth/1stfactor',
jar: jar,
form: {
username: 'test_ok',
password: 'password'
}
});
}
function execute_u2f_registration(jar) {
return request.getAsync({
url: BASE_URL + '/_auth/2ndfactor/u2f/register_request',
jar: jar
})
.then(function(res) {
return request.postAsync({
url: BASE_URL + '/_auth/2ndfactor/u2f/register',
jar: jar,
form: {
s: 'test'
}
});
});
}
function execute_u2f_authentication(jar) {
return request.getAsync({
url: BASE_URL + '/_auth/2ndfactor/u2f/sign_request',
jar: jar
})
.then(function() {
return request.postAsync({
url: BASE_URL + '/_auth/2ndfactor/u2f/sign',
jar: jar,
form: {
s: 'test'
}
});
});
}
function execute_verification(jar) {
return request.getAsync({ url: BASE_URL + '/_verify', jar: jar })
}
function execute_login(jar) {
return request.getAsync({ url: BASE_URL + '/login', jar: jar })
}
});

View File

@ -0,0 +1,68 @@
var UserDataStore = require('../../src/lib/user_data_store');
var DataStore = require('nedb');
var assert = require('assert');
var Promise = require('bluebird');
describe('test user data store', function() {
it('should save a u2f meta', function() {
var options = {};
options.inMemoryOnly = true;
var data_store = new UserDataStore(DataStore, options);
var userid = 'user';
var app_id = 'https://localhost';
var meta = {};
meta.publicKey = 'pbk';
return data_store.set_u2f_meta(userid, app_id, meta)
.then(function(numUpdated) {
assert.equal(1, numUpdated);
return Promise.resolve();
});
});
it('should retrieve no u2f meta', function() {
var options = {};
options.inMemoryOnly = true;
var data_store = new UserDataStore(DataStore, options);
var userid = 'user';
var app_id = 'https://localhost';
var meta = {};
meta.publicKey = 'pbk';
return data_store.get_u2f_meta(userid, app_id)
.then(function(doc) {
assert.equal(undefined, doc);
return Promise.resolve();
});
});
it('should insert and retrieve a u2f meta', function() {
var options = {};
options.inMemoryOnly = true;
var data_store = new UserDataStore(DataStore, options);
var userid = 'user';
var app_id = 'https://localhost';
var meta = {};
meta.publicKey = 'pbk';
return data_store.set_u2f_meta(userid, app_id, meta)
.then(function(numUpdated, data) {
assert.equal(1, numUpdated);
return data_store.get_u2f_meta(userid, app_id)
})
.then(function(doc) {
assert.deepEqual(meta, doc.meta);
assert.deepEqual(userid, doc.userid);
assert.deepEqual(app_id, doc.appid);
assert('_id' in doc);
return Promise.resolve();
});
});
});