Create a filesystem notifier for simple getting started

This commit is contained in:
Clement Michaud 2017-01-28 19:59:15 +01:00
parent 7e41c68aa7
commit d29aac78d0
17 changed files with 237 additions and 105 deletions

View File

@ -1,20 +1,33 @@
debug_level: info ### Level of verbosity for logs
logs_level: info
### Configuration of your LDAP
ldap: ldap:
url: ldap://ldap url: ldap://ldap
base_dn: ou=users,dc=example,dc=com base_dn: ou=users,dc=example,dc=com
user: cn=admin,dc=example,dc=com user: cn=admin,dc=example,dc=com
password: password password: password
### Configuration of session cookies
session: session:
secret: unsecure_secret secret: unsecure_secret
expiration: 3600000 expiration: 3600000
store_directory: /var/lib/auth-server ### The directory where the DB files will be saved
store_directory: /var/lib/auth-server/store
### Notifications are sent to users when they require a password reset, a u2f
### registration or a TOTP registration.
### Use only one available configuration: filesystem, gmail
notifier: notifier:
gmail: ### For testing purpose, notifications can be sent in a file
username: user@example.com filesystem:
password: yourpassword filename: /var/lib/auth-server/notifications/notification.txt
### Use your gmail account to send the notifications. You can use an app password.
# gmail:
# username: user@example.com
# password: yourpassword

View File

@ -6,6 +6,7 @@ services:
- ./test:/usr/src/test - ./test:/usr/src/test
- ./src/views:/usr/src/views - ./src/views:/usr/src/views
- ./src/public_html:/usr/src/public_html - ./src/public_html:/usr/src/public_html
- ./notifications:/var/lib/auth-server/notifications
ldap-admin: ldap-admin:
image: osixia/phpldapadmin:0.6.11 image: osixia/phpldapadmin:0.6.11

View File

@ -0,0 +1,3 @@
User: user
Subject: Reset your password
Link: https://localhost:8080/authentication/reset-password?identity_token=CmJ51IdJLEcVr7AbbJPANe0wmJoOcgYzPqgGOngVRIhKq1UbQUoS44FXDEXBcolz

View File

@ -23,13 +23,12 @@ var config = {
session_secret: yaml_config.session.secret, session_secret: yaml_config.session.secret,
session_max_age: yaml_config.session.expiration || 3600000, // in ms session_max_age: yaml_config.session.expiration || 3600000, // in ms
store_directory: yaml_config.store_directory, store_directory: yaml_config.store_directory,
debug_level: yaml_config.debug_level, logs_level: yaml_config.logs_level,
gmail: { notifier: yaml_config.notifier,
user: yaml_config.notifier.gmail.username,
pass: yaml_config.notifier.gmail.password
}
} }
console.log(config);
var ldap_client = ldap.createClient({ var ldap_client = ldap.createClient({
url: config.ldap_url, url: config.ldap_url,
reconnect: true reconnect: true

View File

@ -1,25 +0,0 @@
module.exports = EmailSender;
var Promise = require('bluebird');
function EmailSender(nodemailer, options) {
var transporter = nodemailer.createTransport({
service: 'gmail',
auth: {
user: options.gmail.user,
pass: options.gmail.pass
}
});
this.transporter = Promise.promisifyAll(transporter);
}
EmailSender.prototype.send = function(to, subject, html) {
var mailOptions = {};
mailOptions.from = 'auth-server@open-intent.io';
mailOptions.to = to;
mailOptions.subject = subject;
mailOptions.html = html;
return this.transporter.sendMailAsync(mailOptions);
}

View File

@ -14,13 +14,12 @@ var email_template = fs.readFileSync(filePath, 'utf8');
// IdentityCheck class // IdentityCheck class
function IdentityCheck(user_data_store, email_sender, logger) { function IdentityCheck(user_data_store, logger) {
this._user_data_store = user_data_store; this._user_data_store = user_data_store;
this._email_sender = email_sender;
this._logger = logger; this._logger = logger;
} }
IdentityCheck.prototype.issue_token = function(userid, email, content, logger) { IdentityCheck.prototype.issue_token = function(userid, content, logger) {
var five_minutes = 4 * 60 * 1000; var five_minutes = 4 * 60 * 1000;
var token = randomstring.generate({ length: 64 }); var token = randomstring.generate({ length: 64 });
var that = this; var that = this;
@ -61,7 +60,7 @@ function identity_check_get(endpoint, icheck_interface) {
var email_sender = req.app.get('email sender'); var email_sender = req.app.get('email sender');
var user_data_store = req.app.get('user data store'); var user_data_store = req.app.get('user data store');
var identity_check = new IdentityCheck(user_data_store, email_sender, logger); var identity_check = new IdentityCheck(user_data_store, logger);
identity_check.consume_token(identity_token, logger) identity_check.consume_token(identity_token, logger)
.then(function(content) { .then(function(content) {
@ -90,15 +89,16 @@ function identity_check_get(endpoint, icheck_interface) {
function identity_check_post(endpoint, icheck_interface) { function identity_check_post(endpoint, icheck_interface) {
return function(req, res) { return function(req, res) {
var logger = req.app.get('logger'); var logger = req.app.get('logger');
var email_sender = req.app.get('email sender'); var notifier = req.app.get('notifier');
var user_data_store = req.app.get('user data store'); var user_data_store = req.app.get('user data store');
var identity_check = new IdentityCheck(user_data_store, email_sender, logger); var identity_check = new IdentityCheck(user_data_store, logger);
var userid, email_address; var identity;
icheck_interface.pre_check_callback(req) icheck_interface.pre_check_callback(req)
.then(function(identity) { .then(function(id) {
email_address = objectPath.get(identity, 'email'); identity = id;
userid = objectPath.get(identity, 'userid'); var email_address = objectPath.get(identity, 'email');
var userid = objectPath.get(identity, 'userid');
if(!(email_address && userid)) { if(!(email_address && userid)) {
throw new exceptions.IdentityError('Missing user id or email address'); throw new exceptions.IdentityError('Missing user id or email address');
@ -111,19 +111,9 @@ function identity_check_post(endpoint, icheck_interface) {
.then(function(token) { .then(function(token) {
var original_url = util.format('https://%s%s', req.headers.host, req.headers['x-original-uri']); var original_url = util.format('https://%s%s', req.headers.host, req.headers['x-original-uri']);
var link_url = util.format('%s?identity_token=%s', original_url, token); var link_url = util.format('%s?identity_token=%s', original_url, token);
var email = {};
var d = {}; logger.info('POST identity_check: notify to %s', identity.userid);
d.url = link_url; return notifier.notify(identity, icheck_interface.email_subject, link_url);
d.button_title = 'Continue';
d.title = icheck_interface.email_subject;
email.to = email_address;
email.subject = icheck_interface.email_subject;
email.content = ejs.render(email_template, d);
logger.info('POST identity_check: send email to %s', email.to);
return email_sender.send(email.to, email.subject, email.content);
}) })
.then(function() { .then(function() {
res.status(204); res.status(204);

24
src/lib/notifier.js Normal file
View File

@ -0,0 +1,24 @@
module.exports = Notifier;
var GmailNotifier = require('./notifiers/gmail.js');
var FSNotifier = require('./notifiers/filesystem.js');
function notifier_factory(options, deps) {
if('gmail' in options) {
return new GmailNotifier(options.gmail, deps);
}
else if('filesystem' in options) {
return new FSNotifier(options.filesystem);
}
}
function Notifier(options, deps) {
this._notifier = notifier_factory(options, deps);
}
Notifier.prototype.notify = function(identity, subject, link) {
return this._notifier.notify(identity, subject, link);
}

View File

@ -0,0 +1,16 @@
module.exports = FSNotifier;
var Promise = require('bluebird');
var fs = Promise.promisifyAll(require('fs'));
var util = require('util');
function FSNotifier(options) {
this._filename = options.filename;
}
FSNotifier.prototype.notify = function(identity, subject, link) {
var content = util.format('User: %s\nSubject: %s\nLink: %s', identity.userid,
subject, link);
return fs.writeFileAsync(this._filename, content);
}

View File

@ -0,0 +1,33 @@
module.exports = GmailNotifier;
var Promise = require('bluebird');
var fs = require('fs');
var ejs = require('ejs');
var email_template = fs.readFileSync(__dirname + '/../../resources/email-template.ejs', 'UTF-8');
function GmailNotifier(options, deps) {
var transporter = deps.nodemailer.createTransport({
service: 'gmail',
auth: {
user: options.username,
pass: options.password
}
});
this.transporter = Promise.promisifyAll(transporter);
}
GmailNotifier.prototype.notify = function(identity, subject, link) {
var d = {};
d.url = link;
d.button_title = 'Continue';
d.title = subject;
var mailOptions = {};
mailOptions.from = 'auth-server@open-intent.io';
mailOptions.to = identity.email;
mailOptions.subject = subject;
mailOptions.html = ejs.render(email_template, d);
return this.transporter.sendMailAsync(mailOptions);
}

View File

@ -12,7 +12,7 @@ var path = require('path');
var session = require('express-session'); var session = require('express-session');
var winston = require('winston'); var winston = require('winston');
var UserDataStore = require('./user_data_store'); var UserDataStore = require('./user_data_store');
var EmailSender = require('./email_sender'); var Notifier = require('./notifier');
var AuthenticationRegulator = require('./authentication_regulator'); var AuthenticationRegulator = require('./authentication_regulator');
var identity_check = require('./identity_check'); var identity_check = require('./identity_check');
@ -24,9 +24,6 @@ function run(config, ldap_client, deps, fn) {
if(config.store_in_memory) if(config.store_in_memory)
datastore_options.inMemory = true; datastore_options.inMemory = true;
var email_options = {};
email_options.gmail = config.gmail;
var app = express(); var app = express();
app.use(express.static(public_html_directory)); app.use(express.static(public_html_directory));
app.use(bodyParser.urlencoded({ extended: false })); app.use(bodyParser.urlencoded({ extended: false }));
@ -46,12 +43,13 @@ function run(config, ldap_client, deps, fn) {
app.set('views', view_directory); app.set('views', view_directory);
app.set('view engine', 'ejs'); app.set('view engine', 'ejs');
winston.level = config.debug_level || 'info'; // by default the level of logs is info
winston.level = config.logs_level || 'info';
var five_minutes = 5 * 60; var five_minutes = 5 * 60;
var data_store = new UserDataStore(deps.nedb, datastore_options); var data_store = new UserDataStore(deps.nedb, datastore_options);
var regulator = new AuthenticationRegulator(data_store, five_minutes); var regulator = new AuthenticationRegulator(data_store, five_minutes);
var notifier = new EmailSender(deps.nodemailer, email_options); var notifier = new Notifier(config.notifier, deps);
app.set('logger', winston); app.set('logger', winston);
app.set('ldap', deps.ldap); app.set('ldap', deps.ldap);
@ -59,7 +57,7 @@ function run(config, ldap_client, deps, fn) {
app.set('totp engine', speakeasy); app.set('totp engine', speakeasy);
app.set('u2f', deps.u2f); app.set('u2f', deps.u2f);
app.set('user data store', data_store); app.set('user data store', data_store);
app.set('email sender', notifier); app.set('notifier', notifier);
app.set('authentication regulator', regulator); app.set('authentication regulator', regulator);
app.set('config', config); app.set('config', config);
@ -88,9 +86,11 @@ function run(config, ldap_client, deps, fn) {
app.post (base_endpoint + '/1stfactor', routes.first_factor); app.post (base_endpoint + '/1stfactor', routes.first_factor);
app.post (base_endpoint + '/2ndfactor/totp', routes.second_factor.totp); app.post (base_endpoint + '/2ndfactor/totp', routes.second_factor.totp);
// U2F registration
app.get (base_endpoint + '/2ndfactor/u2f/register_request', routes.second_factor.u2f.register_request); app.get (base_endpoint + '/2ndfactor/u2f/register_request', routes.second_factor.u2f.register_request);
app.post (base_endpoint + '/2ndfactor/u2f/register', routes.second_factor.u2f.register); app.post (base_endpoint + '/2ndfactor/u2f/register', routes.second_factor.u2f.register);
// U2F authentication
app.get (base_endpoint + '/2ndfactor/u2f/sign_request', routes.second_factor.u2f.sign_request); app.get (base_endpoint + '/2ndfactor/u2f/sign_request', routes.second_factor.u2f.sign_request);
app.post (base_endpoint + '/2ndfactor/u2f/sign', routes.second_factor.u2f.sign); app.post (base_endpoint + '/2ndfactor/u2f/sign', routes.second_factor.u2f.sign);

View File

@ -0,0 +1,37 @@
var sinon = require('sinon');
var assert = require('assert');
var FSNotifier = require('../../../src/lib/notifiers/filesystem');
var tmp = require('tmp');
var fs = require('fs');
describe('test FS notifier', function() {
var tmpDir;
before(function() {
tmpDir = tmp.dirSync({ unsafeCleanup: true });
});
after(function() {
tmpDir.removeCallback();
});
it('should write the notification in a file', function() {
var options = {};
options.filename = tmpDir.name + '/notification';
var sender = new FSNotifier(options);
var subject = 'subject';
var identity = {};
identity.userid = 'user';
identity.email = 'user@example.com';
var url = 'http://test.com';
return sender.notify(identity, subject, url)
.then(function() {
var content = fs.readFileSync(options.filename, 'UTF-8');
assert(content.length > 0);
return Promise.resolve();
});
});
});

View File

@ -0,0 +1,36 @@
var sinon = require('sinon');
var assert = require('assert');
var GmailNotifier = require('../../../src/lib/notifiers/gmail');
describe('test gmail notifier', function() {
it('should send an email', function() {
var nodemailer = {};
var transporter = {};
nodemailer.createTransport = sinon.stub().returns(transporter);
transporter.sendMail = sinon.stub().yields();
var options = {};
options.username = 'user_gmail';
options.password = 'pass_gmail';
var deps = {};
deps.nodemailer = nodemailer;
var sender = new GmailNotifier(options, deps);
var subject = 'subject';
var identity = {};
identity.userid = 'user';
identity.email = 'user@example.com';
var url = 'http://test.com';
return sender.notify(identity, subject, url)
.then(function() {
assert.equal(nodemailer.createTransport.getCall(0).args[0].auth.user, 'user_gmail');
assert.equal(nodemailer.createTransport.getCall(0).args[0].auth.pass, 'pass_gmail');
assert.equal(transporter.sendMail.getCall(0).args[0].to, 'user@example.com');
assert.equal(transporter.sendMail.getCall(0).args[0].subject, 'subject');
return Promise.resolve();
});
});
});

View File

@ -0,0 +1,35 @@
var sinon = require('sinon');
var Promise = require('bluebird');
var assert = require('assert');
var Notifier = require('../../../src/lib/notifier');
var GmailNotifier = require('../../../src/lib/notifiers/gmail');
var FSNotifier = require('../../../src/lib/notifiers/filesystem');
describe('test notifier', function() {
it('should build a Gmail Notifier', function() {
var deps = {};
deps.nodemailer = {};
deps.nodemailer.createTransport = sinon.stub().returns({});
var options = {};
options.gmail = {};
options.gmail.user = 'abc';
options.gmail.pass = 'abcd';
var notifier = new Notifier(options, deps);
assert(notifier._notifier instanceof GmailNotifier);
});
it('should build a FS Notifier', function() {
var deps = {};
var options = {};
options.filesystem = {};
options.filesystem.filename = 'abc';
var notifier = new Notifier(options, deps);
assert(notifier._notifier instanceof FSNotifier);
});
});

View File

@ -57,7 +57,7 @@ describe('test data persistence', function() {
session_secret: 'session_secret', session_secret: 'session_secret',
session_max_age: 50000, session_max_age: 50000,
store_directory: tmpDir.name, store_directory: tmpDir.name,
gmail: { user: 'user@example.com', pass: 'password' } notifier: { gmail: { user: 'user@example.com', pass: 'password' } }
}; };
}); });

View File

@ -1,31 +0,0 @@
var sinon = require('sinon');
var assert = require('assert');
var EmailSender = require('../../src/lib/email_sender');
describe('test email sender', function() {
it('should send an email', function() {
var nodemailer = {};
var transporter = {};
nodemailer.createTransport = sinon.stub().returns(transporter);
transporter.sendMail = sinon.stub().yields();
var options = {};
options.gmail = {};
options.gmail.user = 'test@gmail.com';
options.gmail.pass = 'test@gmail.com';
var sender = new EmailSender(nodemailer, options);
var to = 'example@gmail.com';
var subject = 'subject';
var content = 'content';
return sender.send(to, subject, content)
.then(function() {
assert.equal(to, transporter.sendMail.getCall(0).args[0].to);
assert.equal(subject, transporter.sendMail.getCall(0).args[0].subject);
assert.equal(content, transporter.sendMail.getCall(0).args[0].html);
return Promise.resolve();
});
});
});

View File

@ -9,7 +9,7 @@ var Promise = require('bluebird');
describe('test identity check process', function() { describe('test identity check process', function() {
var req, res, app, icheck_interface; var req, res, app, icheck_interface;
var user_data_store; var user_data_store;
var email_sender; var notifier;
beforeEach(function() { beforeEach(function() {
req = {}; req = {};
@ -25,9 +25,8 @@ describe('test identity check process', function() {
user_data_store.consume_identity_check_token = sinon.stub(); user_data_store.consume_identity_check_token = sinon.stub();
user_data_store.consume_identity_check_token.returns(Promise.resolve({ userid: 'user' })); user_data_store.consume_identity_check_token.returns(Promise.resolve({ userid: 'user' }));
email_sender = {}; notifier = {};
email_sender.send = sinon.stub(); notifier.notify = sinon.stub().returns(Promise.resolve());
email_sender.send = sinon.stub().returns(Promise.resolve());
req.headers = {}; req.headers = {};
req.session = {}; req.session = {};
@ -38,7 +37,7 @@ describe('test identity check process', function() {
req.app.get = sinon.stub(); req.app.get = sinon.stub();
req.app.get.withArgs('logger').returns(winston); req.app.get.withArgs('logger').returns(winston);
req.app.get.withArgs('user data store').returns(user_data_store); req.app.get.withArgs('user data store').returns(user_data_store);
req.app.get.withArgs('email sender').returns(email_sender); req.app.get.withArgs('notifier').returns(notifier);
res.status = sinon.spy(); res.status = sinon.spy();
res.send = sinon.spy(); res.send = sinon.spy();
@ -126,7 +125,7 @@ describe('test identity check process', function() {
res.send = sinon.spy(function() { res.send = sinon.spy(function() {
assert.equal(res.status.getCall(0).args[0], 204); assert.equal(res.status.getCall(0).args[0], 204);
assert(email_sender.send.calledOnce); assert(notifier.notify.calledOnce);
assert(user_data_store.issue_identity_check_token.calledOnce); assert(user_data_store.issue_identity_check_token.calledOnce);
assert.equal(user_data_store.issue_identity_check_token.getCall(0).args[0], 'user'); assert.equal(user_data_store.issue_identity_check_token.getCall(0).args[0], 'user');
assert.equal(user_data_store.issue_identity_check_token.getCall(0).args[3], 240000); assert.equal(user_data_store.issue_identity_check_token.getCall(0).args[3], 240000);

View File

@ -38,9 +38,11 @@ describe('test the server', function() {
session_secret: 'session_secret', session_secret: 'session_secret',
session_max_age: 50000, session_max_age: 50000,
store_in_memory: true, store_in_memory: true,
gmail: { notifier: {
user: 'user@example.com', gmail: {
pass: 'password' user: 'user@example.com',
pass: 'password'
}
} }
}; };