From d29aac78d05b0828bf85dae9b293873cc6ef5126 Mon Sep 17 00:00:00 2001 From: Clement Michaud Date: Sat, 28 Jan 2017 19:59:15 +0100 Subject: [PATCH] Create a filesystem notifier for simple getting started --- config.template.yml | 23 +++++++++++---- docker-compose.dev.yml | 1 + notifications/notification.txt | 3 ++ src/index.js | 9 +++--- src/lib/email_sender.js | 25 ----------------- src/lib/identity_check.js | 34 ++++++++--------------- src/lib/notifier.js | 24 ++++++++++++++++ src/lib/notifiers/filesystem.js | 16 +++++++++++ src/lib/notifiers/gmail.js | 33 ++++++++++++++++++++++ src/lib/server.js | 14 +++++----- test/unitary/notifiers/test_fs.js | 37 +++++++++++++++++++++++++ test/unitary/notifiers/test_gmail.js | 36 ++++++++++++++++++++++++ test/unitary/notifiers/test_notifier.js | 35 +++++++++++++++++++++++ test/unitary/test_data_persistence.js | 2 +- test/unitary/test_email_sender.js | 31 --------------------- test/unitary/test_identity_check.js | 11 ++++---- test/unitary/test_server.js | 8 ++++-- 17 files changed, 237 insertions(+), 105 deletions(-) create mode 100644 notifications/notification.txt delete mode 100644 src/lib/email_sender.js create mode 100644 src/lib/notifier.js create mode 100644 src/lib/notifiers/filesystem.js create mode 100644 src/lib/notifiers/gmail.js create mode 100644 test/unitary/notifiers/test_fs.js create mode 100644 test/unitary/notifiers/test_gmail.js create mode 100644 test/unitary/notifiers/test_notifier.js delete mode 100644 test/unitary/test_email_sender.js diff --git a/config.template.yml b/config.template.yml index 8c58243a..d514ba31 100644 --- a/config.template.yml +++ b/config.template.yml @@ -1,20 +1,33 @@ -debug_level: info +### Level of verbosity for logs +logs_level: info +### Configuration of your LDAP ldap: url: ldap://ldap base_dn: ou=users,dc=example,dc=com user: cn=admin,dc=example,dc=com password: password +### Configuration of session cookies session: secret: unsecure_secret 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: - gmail: - username: user@example.com - password: yourpassword + ### For testing purpose, notifications can be sent in a file + filesystem: + 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 diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index 2352c93f..980ec7c7 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -6,6 +6,7 @@ services: - ./test:/usr/src/test - ./src/views:/usr/src/views - ./src/public_html:/usr/src/public_html + - ./notifications:/var/lib/auth-server/notifications ldap-admin: image: osixia/phpldapadmin:0.6.11 diff --git a/notifications/notification.txt b/notifications/notification.txt new file mode 100644 index 00000000..19ff6836 --- /dev/null +++ b/notifications/notification.txt @@ -0,0 +1,3 @@ +User: user +Subject: Reset your password +Link: https://localhost:8080/authentication/reset-password?identity_token=CmJ51IdJLEcVr7AbbJPANe0wmJoOcgYzPqgGOngVRIhKq1UbQUoS44FXDEXBcolz \ No newline at end of file diff --git a/src/index.js b/src/index.js index 8755e490..4717ff2e 100644 --- a/src/index.js +++ b/src/index.js @@ -23,13 +23,12 @@ var config = { session_secret: yaml_config.session.secret, session_max_age: yaml_config.session.expiration || 3600000, // in ms store_directory: yaml_config.store_directory, - debug_level: yaml_config.debug_level, - gmail: { - user: yaml_config.notifier.gmail.username, - pass: yaml_config.notifier.gmail.password - } + logs_level: yaml_config.logs_level, + notifier: yaml_config.notifier, } +console.log(config); + var ldap_client = ldap.createClient({ url: config.ldap_url, reconnect: true diff --git a/src/lib/email_sender.js b/src/lib/email_sender.js deleted file mode 100644 index 8f25e841..00000000 --- a/src/lib/email_sender.js +++ /dev/null @@ -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); -} - diff --git a/src/lib/identity_check.js b/src/lib/identity_check.js index bd1550d2..1b37c28b 100644 --- a/src/lib/identity_check.js +++ b/src/lib/identity_check.js @@ -14,13 +14,12 @@ var email_template = fs.readFileSync(filePath, 'utf8'); // IdentityCheck class -function IdentityCheck(user_data_store, email_sender, logger) { +function IdentityCheck(user_data_store, logger) { this._user_data_store = user_data_store; - this._email_sender = email_sender; 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 token = randomstring.generate({ length: 64 }); var that = this; @@ -61,7 +60,7 @@ function identity_check_get(endpoint, icheck_interface) { var email_sender = req.app.get('email sender'); 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) .then(function(content) { @@ -90,15 +89,16 @@ function identity_check_get(endpoint, icheck_interface) { function identity_check_post(endpoint, icheck_interface) { return function(req, res) { 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 identity_check = new IdentityCheck(user_data_store, email_sender, logger); - var userid, email_address; + var identity_check = new IdentityCheck(user_data_store, logger); + var identity; icheck_interface.pre_check_callback(req) - .then(function(identity) { - email_address = objectPath.get(identity, 'email'); - userid = objectPath.get(identity, 'userid'); + .then(function(id) { + identity = id; + var email_address = objectPath.get(identity, 'email'); + var userid = objectPath.get(identity, 'userid'); if(!(email_address && userid)) { throw new exceptions.IdentityError('Missing user id or email address'); @@ -111,19 +111,9 @@ function identity_check_post(endpoint, icheck_interface) { .then(function(token) { 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 email = {}; - var d = {}; - d.url = 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); + logger.info('POST identity_check: notify to %s', identity.userid); + return notifier.notify(identity, icheck_interface.email_subject, link_url); }) .then(function() { res.status(204); diff --git a/src/lib/notifier.js b/src/lib/notifier.js new file mode 100644 index 00000000..84ba6060 --- /dev/null +++ b/src/lib/notifier.js @@ -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); +} + + diff --git a/src/lib/notifiers/filesystem.js b/src/lib/notifiers/filesystem.js new file mode 100644 index 00000000..a4a295ce --- /dev/null +++ b/src/lib/notifiers/filesystem.js @@ -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); +} + diff --git a/src/lib/notifiers/gmail.js b/src/lib/notifiers/gmail.js new file mode 100644 index 00000000..5007d858 --- /dev/null +++ b/src/lib/notifiers/gmail.js @@ -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); +} + diff --git a/src/lib/server.js b/src/lib/server.js index 84e8187a..399ab144 100644 --- a/src/lib/server.js +++ b/src/lib/server.js @@ -12,7 +12,7 @@ var path = require('path'); var session = require('express-session'); var winston = require('winston'); var UserDataStore = require('./user_data_store'); -var EmailSender = require('./email_sender'); +var Notifier = require('./notifier'); var AuthenticationRegulator = require('./authentication_regulator'); var identity_check = require('./identity_check'); @@ -24,9 +24,6 @@ function run(config, ldap_client, deps, fn) { if(config.store_in_memory) datastore_options.inMemory = true; - var email_options = {}; - email_options.gmail = config.gmail; - var app = express(); app.use(express.static(public_html_directory)); app.use(bodyParser.urlencoded({ extended: false })); @@ -46,12 +43,13 @@ function run(config, ldap_client, deps, fn) { app.set('views', view_directory); 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 data_store = new UserDataStore(deps.nedb, datastore_options); 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('ldap', deps.ldap); @@ -59,7 +57,7 @@ function run(config, ldap_client, deps, fn) { app.set('totp engine', speakeasy); app.set('u2f', deps.u2f); app.set('user data store', data_store); - app.set('email sender', notifier); + app.set('notifier', notifier); app.set('authentication regulator', regulator); 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 + '/2ndfactor/totp', routes.second_factor.totp); + // U2F registration 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); + // U2F authentication 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); diff --git a/test/unitary/notifiers/test_fs.js b/test/unitary/notifiers/test_fs.js new file mode 100644 index 00000000..60b16b8e --- /dev/null +++ b/test/unitary/notifiers/test_fs.js @@ -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(); + }); + }); +}); diff --git a/test/unitary/notifiers/test_gmail.js b/test/unitary/notifiers/test_gmail.js new file mode 100644 index 00000000..bc65967a --- /dev/null +++ b/test/unitary/notifiers/test_gmail.js @@ -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(); + }); + }); +}); diff --git a/test/unitary/notifiers/test_notifier.js b/test/unitary/notifiers/test_notifier.js new file mode 100644 index 00000000..efa4413d --- /dev/null +++ b/test/unitary/notifiers/test_notifier.js @@ -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); + }); +}); diff --git a/test/unitary/test_data_persistence.js b/test/unitary/test_data_persistence.js index 072f9c36..20264104 100644 --- a/test/unitary/test_data_persistence.js +++ b/test/unitary/test_data_persistence.js @@ -57,7 +57,7 @@ describe('test data persistence', function() { session_secret: 'session_secret', session_max_age: 50000, store_directory: tmpDir.name, - gmail: { user: 'user@example.com', pass: 'password' } + notifier: { gmail: { user: 'user@example.com', pass: 'password' } } }; }); diff --git a/test/unitary/test_email_sender.js b/test/unitary/test_email_sender.js deleted file mode 100644 index 9328d674..00000000 --- a/test/unitary/test_email_sender.js +++ /dev/null @@ -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(); - }); - }); -}); diff --git a/test/unitary/test_identity_check.js b/test/unitary/test_identity_check.js index 7100db1e..52e890dd 100644 --- a/test/unitary/test_identity_check.js +++ b/test/unitary/test_identity_check.js @@ -9,7 +9,7 @@ var Promise = require('bluebird'); describe('test identity check process', function() { var req, res, app, icheck_interface; var user_data_store; - var email_sender; + var notifier; beforeEach(function() { 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.returns(Promise.resolve({ userid: 'user' })); - email_sender = {}; - email_sender.send = sinon.stub(); - email_sender.send = sinon.stub().returns(Promise.resolve()); + notifier = {}; + notifier.notify = sinon.stub().returns(Promise.resolve()); req.headers = {}; req.session = {}; @@ -38,7 +37,7 @@ describe('test identity check process', function() { 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('email sender').returns(email_sender); + req.app.get.withArgs('notifier').returns(notifier); res.status = sinon.spy(); res.send = sinon.spy(); @@ -126,7 +125,7 @@ describe('test identity check process', function() { res.send = sinon.spy(function() { 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.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); diff --git a/test/unitary/test_server.js b/test/unitary/test_server.js index 502c2930..5e2160d5 100644 --- a/test/unitary/test_server.js +++ b/test/unitary/test_server.js @@ -38,9 +38,11 @@ describe('test the server', function() { session_secret: 'session_secret', session_max_age: 50000, store_in_memory: true, - gmail: { - user: 'user@example.com', - pass: 'password' + notifier: { + gmail: { + user: 'user@example.com', + pass: 'password' + } } };