diff --git a/package.json b/package.json index 09cc9f23..f5dc2941 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ }, "scripts": { "test": "./node_modules/.bin/mocha --compilers ts:ts-node/register --recursive test/unitary", + "test-dbg": "./node_modules/.bin/mocha --debug-brk --compilers ts:ts-node/register --recursive test/unitary", "int-test": "./node_modules/.bin/mocha --recursive test/integration", "coverage": "./node_modules/.bin/istanbul cover _mocha -- -R spec --recursive test", "build-ts": "tsc", diff --git a/src/lib/AuthenticationRegulator.ts b/src/lib/AuthenticationRegulator.ts index 16bd4340..e55d860a 100644 --- a/src/lib/AuthenticationRegulator.ts +++ b/src/lib/AuthenticationRegulator.ts @@ -10,7 +10,7 @@ interface DatedDocument { date: Date; } -export class AuthenticationRegulator { +export default class AuthenticationRegulator { private _user_data_store: any; private _lock_time_in_seconds: number; diff --git a/src/lib/Dependencies.ts b/src/lib/Dependencies.ts deleted file mode 100644 index 0cfa344f..00000000 --- a/src/lib/Dependencies.ts +++ /dev/null @@ -1,22 +0,0 @@ -import * as winston from "winston"; -import nodemailer = require("nodemailer"); - -export interface Nodemailer { - createTransport: (options?: any, defaults?: Object) => nodemailer.Transporter; -} - -export interface GlobalDependencies { - u2f: object; - nodemailer: Nodemailer; - ldapjs: object; - session: any; - winston: winston.Winston; - speakeasy: object; - nedb: any; -} - -export type NodemailerDependencies = Nodemailer; - -export interface NotifierDependencies { - nodemailer: Nodemailer; -} \ No newline at end of file diff --git a/src/lib/Server.ts b/src/lib/Server.ts index d10e2bbe..34d38056 100644 --- a/src/lib/Server.ts +++ b/src/lib/Server.ts @@ -1,10 +1,12 @@ import { UserConfiguration } from "./Configuration"; -import { GlobalDependencies } from "./Dependencies"; -import { AuthenticationRegulator } from "./AuthenticationRegulator"; +import { GlobalDependencies } from "../types/Dependencies"; +import AuthenticationRegulator from "./AuthenticationRegulator"; import UserDataStore from "./UserDataStore"; import ConfigurationAdapter from "./ConfigurationAdapter"; import { NotifierFactory } from "./notifiers/NotifierFactory"; +import TOTPValidator from "./TOTPValidator"; +import TOTPGenerator from "./TOTPGenerator"; import * as Express from "express"; import * as BodyParser from "body-parser"; @@ -58,10 +60,13 @@ export default class Server { const notifier = NotifierFactory.build(config.notifier, deps); const ldap = new Ldap(deps, config.ldap); const accessController = new AccessController(config.access_control, deps.winston); + const totpValidator = new TOTPValidator(deps.speakeasy); + const totpGenerator = new TOTPGenerator(deps.speakeasy); app.set("logger", deps.winston); app.set("ldap", ldap); - app.set("totp engine", deps.speakeasy); + app.set("totp validator", totpValidator); + app.set("totp generator", totpGenerator); app.set("u2f", deps.u2f); app.set("user data store", data_store); app.set("notifier", notifier); diff --git a/src/lib/TOTPGenerator.ts b/src/lib/TOTPGenerator.ts new file mode 100644 index 00000000..2b1555b6 --- /dev/null +++ b/src/lib/TOTPGenerator.ts @@ -0,0 +1,16 @@ + +import * as speakeasy from "speakeasy"; +import { Speakeasy } from "../types/Dependencies"; +import BluebirdPromise = require("bluebird"); + +export default class TOTPGenerator { + private speakeasy: Speakeasy; + + constructor(speakeasy: Speakeasy) { + this.speakeasy = speakeasy; + } + + generate(options: speakeasy.GenerateOptions): speakeasy.Key { + return this.speakeasy.generateSecret(options); + } +} \ No newline at end of file diff --git a/src/lib/TOTPValidator.ts b/src/lib/TOTPValidator.ts new file mode 100644 index 00000000..2b009884 --- /dev/null +++ b/src/lib/TOTPValidator.ts @@ -0,0 +1,23 @@ + +import { Speakeasy } from "../types/Dependencies"; +import BluebirdPromise = require("bluebird"); + +const TOTP_ENCODING = "base32"; + +export default class TOTPValidator { + private speakeasy: Speakeasy; + + constructor(speakeasy: Speakeasy) { + this.speakeasy = speakeasy; + } + + validate(token: string, secret: string): BluebirdPromise { + const real_token = this.speakeasy.totp({ + secret: secret, + encoding: TOTP_ENCODING + }); + + if (token == real_token) return BluebirdPromise.resolve(); + return BluebirdPromise.reject("Wrong challenge"); + } +} \ No newline at end of file diff --git a/src/lib/UserDataStore.ts b/src/lib/UserDataStore.ts index 4aa6a05e..e3cda5ad 100644 --- a/src/lib/UserDataStore.ts +++ b/src/lib/UserDataStore.ts @@ -2,7 +2,7 @@ import * as Promise from "bluebird"; import * as path from "path"; import Nedb = require("nedb"); import { NedbAsync } from "nedb"; -import { TOTPSecret } from "./TOTPSecret"; +import { TOTPSecret } from "../types/TOTPSecret"; // Constants diff --git a/src/lib/notifiers/GMailNotifier.ts b/src/lib/notifiers/GMailNotifier.ts index 030cd85d..949ecf0c 100644 --- a/src/lib/notifiers/GMailNotifier.ts +++ b/src/lib/notifiers/GMailNotifier.ts @@ -4,7 +4,7 @@ import * as fs from "fs"; import * as ejs from "ejs"; import nodemailer = require("nodemailer"); -import { NodemailerDependencies } from "../Dependencies"; +import { NodemailerDependencies } from "../../types/Dependencies"; import { Identity } from "../Identity"; import { INotifier } from "../notifiers/INotifier"; import { GmailNotifierConfiguration } from "../Configuration"; diff --git a/src/lib/notifiers/NotifierFactory.ts b/src/lib/notifiers/NotifierFactory.ts index fe166c72..96f631f6 100644 --- a/src/lib/notifiers/NotifierFactory.ts +++ b/src/lib/notifiers/NotifierFactory.ts @@ -1,6 +1,6 @@ import { NotifierConfiguration } from "..//Configuration"; -import { NotifierDependencies } from "../Dependencies"; +import { NotifierDependencies } from "../../types/Dependencies"; import { INotifier } from "./INotifier"; import { GMailNotifier } from "./GMailNotifier"; diff --git a/src/lib/routes/totp.js b/src/lib/routes/totp.js index b5a00e23..9fdfb93a 100644 --- a/src/lib/routes/totp.js +++ b/src/lib/routes/totp.js @@ -1,7 +1,6 @@ module.exports = totp_fn; -var totp = require('../totp'); var objectPath = require('object-path'); var exceptions = require('../../../src/lib/exceptions'); @@ -20,14 +19,14 @@ function totp_fn(req, res) { } var token = req.body.token; - var totp_engine = req.app.get('totp engine'); + var totpValidator = req.app.get('totp validator'); var data_store = req.app.get('user data store'); logger.debug('POST 2ndfactor totp: Fetching secret for user %s', userid); data_store.get_totp_secret(userid) .then(function(doc) { logger.debug('POST 2ndfactor totp: TOTP secret is %s', JSON.stringify(doc)); - return totp.validate(totp_engine, token, doc.secret.base32) + return totpValidator.validate(token, doc.secret.base32); }) .then(function() { logger.debug('POST 2ndfactor totp: TOTP validation succeeded'); diff --git a/src/lib/routes/totp_register.js b/src/lib/routes/totp_register.js index 4ca2094f..37c7e7ce 100644 --- a/src/lib/routes/totp_register.js +++ b/src/lib/routes/totp_register.js @@ -47,8 +47,8 @@ function post(req, res) { } var user_data_store = req.app.get('user data store'); - var totp = req.app.get('totp engine'); - var secret = totp.generateSecret(); + var totpGenerator = req.app.get('totp generator'); + var secret = totpGenerator.generate(); logger.debug('POST new-totp-secret: save the TOTP secret in DB'); user_data_store.set_totp_secret(userid, secret) diff --git a/src/lib/totp.js b/src/lib/totp.js deleted file mode 100644 index 7d3c194e..00000000 --- a/src/lib/totp.js +++ /dev/null @@ -1,22 +0,0 @@ - -module.exports = { - 'validate': validate -} - -var Promise = require('bluebird'); - -function validate(totp_engine, token, totp_secret) { - return new Promise(function(resolve, reject) { - var real_token = totp_engine.totp({ - secret: totp_secret, - encoding: 'base32' - }); - - if(token == real_token) { - resolve(); - } - else { - reject('Wrong challenge'); - } - }); -} diff --git a/src/types/Dependencies.ts b/src/types/Dependencies.ts new file mode 100644 index 00000000..9d7de366 --- /dev/null +++ b/src/types/Dependencies.ts @@ -0,0 +1,27 @@ +import * as winston from "winston"; +import * as speakeasy from "speakeasy"; +import nodemailer = require("nodemailer"); +import session = require("express-session"); +import nedb = require("nedb"); + +export type Nodemailer = typeof nodemailer; +export type Speakeasy = typeof speakeasy; +export type Winston = typeof winston; +export type Session = typeof session; +export type Nedb = typeof nedb; + +export interface GlobalDependencies { + u2f: object; + nodemailer: Nodemailer; + ldapjs: object; + session: Session; + winston: Winston; + speakeasy: Speakeasy; + nedb: Nedb; +} + +export type NodemailerDependencies = Nodemailer; + +export interface NotifierDependencies { + nodemailer: Nodemailer; +} \ No newline at end of file diff --git a/src/lib/TOTPSecret.ts b/src/types/TOTPSecret.ts similarity index 100% rename from src/lib/TOTPSecret.ts rename to src/types/TOTPSecret.ts diff --git a/test/unitary/AuthenticationRegulator.test.ts b/test/unitary/AuthenticationRegulator.test.ts index 3ee9e1c2..50a739d2 100644 --- a/test/unitary/AuthenticationRegulator.test.ts +++ b/test/unitary/AuthenticationRegulator.test.ts @@ -1,5 +1,5 @@ -import { AuthenticationRegulator } from "../../src/lib/AuthenticationRegulator"; +import AuthenticationRegulator from "../../src/lib/AuthenticationRegulator"; import UserDataStore from "../../src/lib/UserDataStore"; import * as MockDate from "mockdate"; diff --git a/test/unitary/Server.test.ts b/test/unitary/Server.test.ts index bc930f3b..ed698aad 100644 --- a/test/unitary/Server.test.ts +++ b/test/unitary/Server.test.ts @@ -2,11 +2,11 @@ import Server from "../../src/lib/Server"; import Ldap = require("../../src/lib/ldap"); -import * as Promise from "bluebird"; -import * as speakeasy from "speakeasy"; -import * as request from "request"; -import * as nedb from "nedb"; -import { TOTPSecret } from "../../src/lib/TOTPSecret"; +import Promise = require("bluebird"); +import speakeasy = require("speakeasy"); +import request = require("request"); +import nedb = require("nedb"); +import { TOTPSecret } from "../../src/types/TOTPSecret"; const requestp = Promise.promisifyAll(request) as request.RequestAsync; @@ -29,7 +29,6 @@ describe("test the server", function () { beforeEach(function () { const config = { port: PORT, - totp_secret: "totp_secret", ldap: { url: "ldap://127.0.0.1:389", base_dn: "ou=users,dc=example,dc=com", diff --git a/test/unitary/TOTPValidator.ts b/test/unitary/TOTPValidator.ts new file mode 100644 index 00000000..e2d06f67 --- /dev/null +++ b/test/unitary/TOTPValidator.ts @@ -0,0 +1,30 @@ + +import TOTPValidator from "../../src/lib/TOTPValidator"; +import sinon = require("sinon"); +import Promise = require("bluebird"); +import SpeakeasyMock = require("./mocks/speakeasy"); + +describe("test TOTP validation", function() { + let totpValidator: TOTPValidator; + + beforeEach(() => { + SpeakeasyMock.totp.returns("token"); + totpValidator = new TOTPValidator(SpeakeasyMock as any); + }); + + it("should validate the TOTP token", function() { + const totp_secret = "NBD2ZV64R9UV1O7K"; + const token = "token"; + return totpValidator.validate(token, totp_secret); + }); + + it("should not validate a wrong TOTP token", function() { + const totp_secret = "NBD2ZV64R9UV1O7K"; + const token = "wrong token"; + return totpValidator.validate(token, totp_secret) + .catch(function() { + return Promise.resolve(); + }); + }); +}); + diff --git a/test/unitary/data_persistence.test.ts b/test/unitary/data_persistence.test.ts index 494bb0a7..1e721872 100644 --- a/test/unitary/data_persistence.test.ts +++ b/test/unitary/data_persistence.test.ts @@ -4,7 +4,7 @@ import * as request from "request"; import Server from "../../src/lib/Server"; import { UserConfiguration } from "../../src/lib/Configuration"; -import { GlobalDependencies } from "../../src/lib/Dependencies"; +import { GlobalDependencies } from "../../src/types/Dependencies"; import * as tmp from "tmp"; diff --git a/test/unitary/mocks/nodemailer.ts b/test/unitary/mocks/nodemailer.ts index 1e42d13e..09d3564d 100644 --- a/test/unitary/mocks/nodemailer.ts +++ b/test/unitary/mocks/nodemailer.ts @@ -1,6 +1,5 @@ import sinon = require("sinon"); -import { Nodemailer } from "../../../src/lib/Dependencies"; export = { createTransport: sinon.stub() diff --git a/test/unitary/mocks/speakeasy.ts b/test/unitary/mocks/speakeasy.ts new file mode 100644 index 00000000..023614dc --- /dev/null +++ b/test/unitary/mocks/speakeasy.ts @@ -0,0 +1,7 @@ + +import sinon = require("sinon"); + +export = { + totp: sinon.stub(), + generateSecret: sinon.stub() +}; diff --git a/test/unitary/notifiers/NotifierFactory.test.ts b/test/unitary/notifiers/NotifierFactory.test.ts index 25600b0c..b0233d70 100644 --- a/test/unitary/notifiers/NotifierFactory.test.ts +++ b/test/unitary/notifiers/NotifierFactory.test.ts @@ -9,7 +9,7 @@ import { NotifierFactory } from "../../../src/lib/notifiers/NotifierFactory"; import { GMailNotifier } from "../../../src/lib/notifiers/GMailNotifier"; import { FileSystemNotifier } from "../../../src/lib/notifiers/FileSystemNotifier"; -import { NotifierDependencies } from "../../../src/lib/Dependencies"; +import { NotifierDependencies } from "../../../src/types/Dependencies"; describe("test notifier", function() { diff --git a/test/unitary/routes/test_totp.js b/test/unitary/routes/test_totp.js index 1fd0fc4d..18e161df 100644 --- a/test/unitary/routes/test_totp.js +++ b/test/unitary/routes/test_totp.js @@ -7,7 +7,7 @@ var winston = require('winston'); describe('test totp route', function() { var req, res; - var totp_engine; + var totpValidator; var user_data_store; beforeEach(function() { @@ -33,8 +33,8 @@ describe('test totp route', function() { }; var config = { totp_secret: 'secret' }; - totp_engine = { - totp: sinon.stub() + totpValidator = { + validate: sinon.stub() } user_data_store = {}; @@ -47,14 +47,14 @@ describe('test totp route', function() { user_data_store.get_totp_secret.returns(Promise.resolve(doc)); app_get.withArgs('logger').returns(winston); - app_get.withArgs('totp engine').returns(totp_engine); + app_get.withArgs('totp validator').returns(totpValidator); app_get.withArgs('config').returns(config); app_get.withArgs('user data store').returns(user_data_store); }); it('should send status code 204 when totp is valid', function(done) { - totp_engine.totp.returns('abc'); + totpValidator.validate.returns(Promise.resolve("ok")); res.send = sinon.spy(function() { // Second factor passed assert.equal(true, req.session.auth_session.second_factor) @@ -65,7 +65,7 @@ describe('test totp route', function() { }); it('should send status code 401 when totp is not valid', function(done) { - totp_engine.totp.returns('bad_token'); + totpValidator.validate.returns(Promise.reject('bad_token')); res.send = sinon.spy(function() { assert.equal(false, req.session.auth_session.second_factor) assert.equal(401, res.status.getCall(0).args[0]); @@ -75,7 +75,7 @@ describe('test totp route', function() { }); it('should send status code 401 when session has not been initiated', function(done) { - totp_engine.totp.returns('abc'); + totpValidator.validate.returns(Promise.resolve('abc')); res.send = sinon.spy(function() { assert.equal(403, res.status.getCall(0).args[0]); done(); diff --git a/test/unitary/routes/test_totp_register.js b/test/unitary/routes/test_totp_register.js index 784449df..6ef33c69 100644 --- a/test/unitary/routes/test_totp_register.js +++ b/test/unitary/routes/test_totp_register.js @@ -81,7 +81,9 @@ describe('test totp register', function() { function test_post_secret() { it('should send the secret in json format', function(done) { - req.app.get.withArgs('totp engine').returns(require('speakeasy')); + req.app.get.withArgs('totp generator').returns({ + generate: sinon.stub().returns({ otpauth_url: "abc"}) + }); req.session.auth_session.identity_check = {}; req.session.auth_session.identity_check.userid = 'user'; req.session.auth_session.identity_check.challenge = 'totp-register'; @@ -92,7 +94,9 @@ describe('test totp register', function() { }); it('should clear the session for reauthentication', function(done) { - req.app.get.withArgs('totp engine').returns(require('speakeasy')); + req.app.get.withArgs('totp generator').returns({ + generate: sinon.stub().returns({ otpauth_url: "abc"}) + }); req.session.auth_session.identity_check = {}; req.session.auth_session.identity_check.userid = 'user'; req.session.auth_session.identity_check.challenge = 'totp-register'; @@ -114,7 +118,9 @@ describe('test totp register', function() { }); it('should return 500 if db throws', function(done) { - req.app.get.withArgs('totp engine').returns(require('speakeasy')); + req.app.get.withArgs('totp generator').returns({ + generate: sinon.stub().returns({ otpauth_url: "abc" }) + }); req.session.auth_session.identity_check = {}; req.session.auth_session.identity_check.userid = 'user'; req.session.auth_session.identity_check.challenge = 'totp-register'; diff --git a/test/unitary/server_config.test.ts b/test/unitary/server_config.test.ts index c03072ce..350d2d66 100644 --- a/test/unitary/server_config.test.ts +++ b/test/unitary/server_config.test.ts @@ -1,30 +1,32 @@ -import * as assert from "assert"; -import * as sinon from "sinon"; +import assert = require("assert"); +import sinon = require ("sinon"); import nedb = require("nedb"); -import * as express from "express"; -import * as winston from "winston"; -import * as speakeasy from "speakeasy"; -import * as u2f from "authdog"; +import express = require("express"); +import winston = require("winston"); +import speakeasy = require("speakeasy"); +import u2f = require("authdog"); +import nodemailer = require("nodemailer"); +import session = require("express-session"); import { AppConfiguration, UserConfiguration } from "../../src/lib/Configuration"; -import { GlobalDependencies, Nodemailer } from "../../src/lib/Dependencies"; +import { GlobalDependencies, Nodemailer } from "../../src/types/Dependencies"; import Server from "../../src/lib/Server"; describe("test server configuration", function () { let deps: GlobalDependencies; + let sessionMock: sinon.SinonSpy; before(function () { const transporter = { sendMail: sinon.stub().yields() }; - const nodemailer: Nodemailer = { - createTransport: sinon.spy(function () { - return transporter; - }) - }; + const createTransport = sinon.stub(nodemailer, "createTransport"); + createTransport.returns(transporter); + + sessionMock = sinon.spy(session); deps = { nodemailer: nodemailer, @@ -37,9 +39,7 @@ describe("test server configuration", function () { return { on: sinon.spy() }; }) }, - session: sinon.spy(function () { - return function (req: express.Request, res: express.Response, next: express.NextFunction) { next(); }; - }) + session: sessionMock as any }; }); @@ -66,7 +66,7 @@ describe("test server configuration", function () { const server = new Server(); server.start(config, deps); - assert(deps.session.calledOnce); - assert.equal(deps.session.getCall(0).args[0].cookie.domain, "example.com"); + assert(sessionMock.calledOnce); + assert.equal(sessionMock.getCall(0).args[0].cookie.domain, "example.com"); }); }); diff --git a/test/unitary/test_server_config.ts b/test/unitary/test_server_config.ts deleted file mode 100644 index 2bc02178..00000000 --- a/test/unitary/test_server_config.ts +++ /dev/null @@ -1,69 +0,0 @@ - -import Server from "../../src/lib/Server"; - -import { UserConfiguration } from "../../src/lib/Configuration"; -import { GlobalDependencies } from "../../src/lib/Dependencies"; -import * as express from "express"; - -const sinon = require("sinon"); -const assert = require("assert"); - -describe("test server configuration", function () { - let deps: GlobalDependencies; - - before(function () { - const transporter = { - sendMail: sinon.stub().yields() - }; - - const nodemailer = { - createTransport: sinon.spy(function () { - return transporter; - }) - }; - - deps = { - nodemailer: nodemailer, - speakeasy: sinon.spy(), - u2f: sinon.spy(), - nedb: require("nedb"), - winston: sinon.spy(), - ldapjs: { - createClient: sinon.spy(function () { - return { on: sinon.spy() }; - }) - }, - session: sinon.spy(function () { - return function (req: express.Request, res: express.Response, next: express.NextFunction) { next(); }; - }) - }; - }); - - - it("should set cookie scope to domain set in the config", function () { - const config = { - notifier: { - gmail: { - username: "user@example.com", - password: "password" - } - }, - session: { - domain: "example.com", - secret: "secret" - }, - ldap: { - url: "http://ldap", - base_dn: "cn=test,dc=example,dc=com", - user: "user", - password: "password" - } - }; - - const server = new Server(); - server.start(config, deps); - - assert(deps.session.calledOnce); - assert.equal(deps.session.getCall(0).args[0].cookie.domain, "example.com"); - }); -}); diff --git a/test/unitary/totp.test.ts b/test/unitary/totp.test.ts deleted file mode 100644 index f797587d..00000000 --- a/test/unitary/totp.test.ts +++ /dev/null @@ -1,32 +0,0 @@ - -const totp = require("../../src/lib/totp"); -const sinon = require("sinon"); -import Promise = require("bluebird"); - -describe("test TOTP validation", function() { - it("should validate the TOTP token", function() { - const totp_secret = "NBD2ZV64R9UV1O7K"; - const token = "token"; - const totp_mock = sinon.mock(); - totp_mock.returns("token"); - const speakeasy_mock = { - totp: totp_mock - }; - return totp.validate(speakeasy_mock, token, totp_secret); - }); - - it("should not validate a wrong TOTP token", function() { - const totp_secret = "NBD2ZV64R9UV1O7K"; - const token = "wrong token"; - const totp_mock = sinon.mock(); - totp_mock.returns("token"); - const speakeasy_mock = { - totp: totp_mock - }; - return totp.validate(speakeasy_mock, token, totp_secret) - .catch(function() { - return Promise.resolve(); - }); - }); -}); -