Move TOTP Validator and Generator to typescript

This commit is contained in:
Clement Michaud 2017-05-20 19:16:57 +02:00
parent 40e02d23bf
commit bf74667726
26 changed files with 161 additions and 194 deletions

View File

@ -8,6 +8,7 @@
}, },
"scripts": { "scripts": {
"test": "./node_modules/.bin/mocha --compilers ts:ts-node/register --recursive test/unitary", "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", "int-test": "./node_modules/.bin/mocha --recursive test/integration",
"coverage": "./node_modules/.bin/istanbul cover _mocha -- -R spec --recursive test", "coverage": "./node_modules/.bin/istanbul cover _mocha -- -R spec --recursive test",
"build-ts": "tsc", "build-ts": "tsc",

View File

@ -10,7 +10,7 @@ interface DatedDocument {
date: Date; date: Date;
} }
export class AuthenticationRegulator { export default class AuthenticationRegulator {
private _user_data_store: any; private _user_data_store: any;
private _lock_time_in_seconds: number; private _lock_time_in_seconds: number;

View File

@ -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;
}

View File

@ -1,10 +1,12 @@
import { UserConfiguration } from "./Configuration"; import { UserConfiguration } from "./Configuration";
import { GlobalDependencies } from "./Dependencies"; import { GlobalDependencies } from "../types/Dependencies";
import { AuthenticationRegulator } from "./AuthenticationRegulator"; import AuthenticationRegulator from "./AuthenticationRegulator";
import UserDataStore from "./UserDataStore"; import UserDataStore from "./UserDataStore";
import ConfigurationAdapter from "./ConfigurationAdapter"; import ConfigurationAdapter from "./ConfigurationAdapter";
import { NotifierFactory } from "./notifiers/NotifierFactory"; import { NotifierFactory } from "./notifiers/NotifierFactory";
import TOTPValidator from "./TOTPValidator";
import TOTPGenerator from "./TOTPGenerator";
import * as Express from "express"; import * as Express from "express";
import * as BodyParser from "body-parser"; import * as BodyParser from "body-parser";
@ -58,10 +60,13 @@ export default class Server {
const notifier = NotifierFactory.build(config.notifier, deps); const notifier = NotifierFactory.build(config.notifier, deps);
const ldap = new Ldap(deps, config.ldap); const ldap = new Ldap(deps, config.ldap);
const accessController = new AccessController(config.access_control, deps.winston); 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("logger", deps.winston);
app.set("ldap", ldap); 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("u2f", deps.u2f);
app.set("user data store", data_store); app.set("user data store", data_store);
app.set("notifier", notifier); app.set("notifier", notifier);

16
src/lib/TOTPGenerator.ts Normal file
View File

@ -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);
}
}

23
src/lib/TOTPValidator.ts Normal file
View File

@ -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<void> {
const real_token = this.speakeasy.totp({
secret: secret,
encoding: TOTP_ENCODING
});
if (token == real_token) return BluebirdPromise.resolve();
return BluebirdPromise.reject("Wrong challenge");
}
}

View File

@ -2,7 +2,7 @@ import * as Promise from "bluebird";
import * as path from "path"; import * as path from "path";
import Nedb = require("nedb"); import Nedb = require("nedb");
import { NedbAsync } from "nedb"; import { NedbAsync } from "nedb";
import { TOTPSecret } from "./TOTPSecret"; import { TOTPSecret } from "../types/TOTPSecret";
// Constants // Constants

View File

@ -4,7 +4,7 @@ import * as fs from "fs";
import * as ejs from "ejs"; import * as ejs from "ejs";
import nodemailer = require("nodemailer"); import nodemailer = require("nodemailer");
import { NodemailerDependencies } from "../Dependencies"; import { NodemailerDependencies } from "../../types/Dependencies";
import { Identity } from "../Identity"; import { Identity } from "../Identity";
import { INotifier } from "../notifiers/INotifier"; import { INotifier } from "../notifiers/INotifier";
import { GmailNotifierConfiguration } from "../Configuration"; import { GmailNotifierConfiguration } from "../Configuration";

View File

@ -1,6 +1,6 @@
import { NotifierConfiguration } from "..//Configuration"; import { NotifierConfiguration } from "..//Configuration";
import { NotifierDependencies } from "../Dependencies"; import { NotifierDependencies } from "../../types/Dependencies";
import { INotifier } from "./INotifier"; import { INotifier } from "./INotifier";
import { GMailNotifier } from "./GMailNotifier"; import { GMailNotifier } from "./GMailNotifier";

View File

@ -1,7 +1,6 @@
module.exports = totp_fn; module.exports = totp_fn;
var totp = require('../totp');
var objectPath = require('object-path'); var objectPath = require('object-path');
var exceptions = require('../../../src/lib/exceptions'); var exceptions = require('../../../src/lib/exceptions');
@ -20,14 +19,14 @@ function totp_fn(req, res) {
} }
var token = req.body.token; 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'); var data_store = req.app.get('user data store');
logger.debug('POST 2ndfactor totp: Fetching secret for user %s', userid); logger.debug('POST 2ndfactor totp: Fetching secret for user %s', userid);
data_store.get_totp_secret(userid) data_store.get_totp_secret(userid)
.then(function(doc) { .then(function(doc) {
logger.debug('POST 2ndfactor totp: TOTP secret is %s', JSON.stringify(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() { .then(function() {
logger.debug('POST 2ndfactor totp: TOTP validation succeeded'); logger.debug('POST 2ndfactor totp: TOTP validation succeeded');

View File

@ -47,8 +47,8 @@ function post(req, res) {
} }
var user_data_store = req.app.get('user data store'); var user_data_store = req.app.get('user data store');
var totp = req.app.get('totp engine'); var totpGenerator = req.app.get('totp generator');
var secret = totp.generateSecret(); var secret = totpGenerator.generate();
logger.debug('POST new-totp-secret: save the TOTP secret in DB'); logger.debug('POST new-totp-secret: save the TOTP secret in DB');
user_data_store.set_totp_secret(userid, secret) user_data_store.set_totp_secret(userid, secret)

View File

@ -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');
}
});
}

27
src/types/Dependencies.ts Normal file
View File

@ -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;
}

View File

@ -1,5 +1,5 @@
import { AuthenticationRegulator } from "../../src/lib/AuthenticationRegulator"; import AuthenticationRegulator from "../../src/lib/AuthenticationRegulator";
import UserDataStore from "../../src/lib/UserDataStore"; import UserDataStore from "../../src/lib/UserDataStore";
import * as MockDate from "mockdate"; import * as MockDate from "mockdate";

View File

@ -2,11 +2,11 @@
import Server from "../../src/lib/Server"; import Server from "../../src/lib/Server";
import Ldap = require("../../src/lib/ldap"); import Ldap = require("../../src/lib/ldap");
import * as Promise from "bluebird"; import Promise = require("bluebird");
import * as speakeasy from "speakeasy"; import speakeasy = require("speakeasy");
import * as request from "request"; import request = require("request");
import * as nedb from "nedb"; import nedb = require("nedb");
import { TOTPSecret } from "../../src/lib/TOTPSecret"; import { TOTPSecret } from "../../src/types/TOTPSecret";
const requestp = Promise.promisifyAll(request) as request.RequestAsync; const requestp = Promise.promisifyAll(request) as request.RequestAsync;
@ -29,7 +29,6 @@ describe("test the server", function () {
beforeEach(function () { beforeEach(function () {
const config = { const config = {
port: PORT, port: PORT,
totp_secret: "totp_secret",
ldap: { ldap: {
url: "ldap://127.0.0.1:389", url: "ldap://127.0.0.1:389",
base_dn: "ou=users,dc=example,dc=com", base_dn: "ou=users,dc=example,dc=com",

View File

@ -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();
});
});
});

View File

@ -4,7 +4,7 @@ import * as request from "request";
import Server from "../../src/lib/Server"; import Server from "../../src/lib/Server";
import { UserConfiguration } from "../../src/lib/Configuration"; import { UserConfiguration } from "../../src/lib/Configuration";
import { GlobalDependencies } from "../../src/lib/Dependencies"; import { GlobalDependencies } from "../../src/types/Dependencies";
import * as tmp from "tmp"; import * as tmp from "tmp";

View File

@ -1,6 +1,5 @@
import sinon = require("sinon"); import sinon = require("sinon");
import { Nodemailer } from "../../../src/lib/Dependencies";
export = { export = {
createTransport: sinon.stub() createTransport: sinon.stub()

View File

@ -0,0 +1,7 @@
import sinon = require("sinon");
export = {
totp: sinon.stub(),
generateSecret: sinon.stub()
};

View File

@ -9,7 +9,7 @@ import { NotifierFactory } from "../../../src/lib/notifiers/NotifierFactory";
import { GMailNotifier } from "../../../src/lib/notifiers/GMailNotifier"; import { GMailNotifier } from "../../../src/lib/notifiers/GMailNotifier";
import { FileSystemNotifier } from "../../../src/lib/notifiers/FileSystemNotifier"; import { FileSystemNotifier } from "../../../src/lib/notifiers/FileSystemNotifier";
import { NotifierDependencies } from "../../../src/lib/Dependencies"; import { NotifierDependencies } from "../../../src/types/Dependencies";
describe("test notifier", function() { describe("test notifier", function() {

View File

@ -7,7 +7,7 @@ var winston = require('winston');
describe('test totp route', function() { describe('test totp route', function() {
var req, res; var req, res;
var totp_engine; var totpValidator;
var user_data_store; var user_data_store;
beforeEach(function() { beforeEach(function() {
@ -33,8 +33,8 @@ describe('test totp route', function() {
}; };
var config = { totp_secret: 'secret' }; var config = { totp_secret: 'secret' };
totp_engine = { totpValidator = {
totp: sinon.stub() validate: sinon.stub()
} }
user_data_store = {}; user_data_store = {};
@ -47,14 +47,14 @@ describe('test totp route', function() {
user_data_store.get_totp_secret.returns(Promise.resolve(doc)); user_data_store.get_totp_secret.returns(Promise.resolve(doc));
app_get.withArgs('logger').returns(winston); 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('config').returns(config);
app_get.withArgs('user data store').returns(user_data_store); app_get.withArgs('user data store').returns(user_data_store);
}); });
it('should send status code 204 when totp is valid', function(done) { 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() { res.send = sinon.spy(function() {
// Second factor passed // Second factor passed
assert.equal(true, req.session.auth_session.second_factor) 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) { 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() { res.send = sinon.spy(function() {
assert.equal(false, req.session.auth_session.second_factor) assert.equal(false, req.session.auth_session.second_factor)
assert.equal(401, res.status.getCall(0).args[0]); 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) { 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() { res.send = sinon.spy(function() {
assert.equal(403, res.status.getCall(0).args[0]); assert.equal(403, res.status.getCall(0).args[0]);
done(); done();

View File

@ -81,7 +81,9 @@ describe('test totp register', function() {
function test_post_secret() { function test_post_secret() {
it('should send the secret in json format', function(done) { 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 = {};
req.session.auth_session.identity_check.userid = 'user'; req.session.auth_session.identity_check.userid = 'user';
req.session.auth_session.identity_check.challenge = 'totp-register'; 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) { 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 = {};
req.session.auth_session.identity_check.userid = 'user'; req.session.auth_session.identity_check.userid = 'user';
req.session.auth_session.identity_check.challenge = 'totp-register'; 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) { 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 = {};
req.session.auth_session.identity_check.userid = 'user'; req.session.auth_session.identity_check.userid = 'user';
req.session.auth_session.identity_check.challenge = 'totp-register'; req.session.auth_session.identity_check.challenge = 'totp-register';

View File

@ -1,30 +1,32 @@
import * as assert from "assert"; import assert = require("assert");
import * as sinon from "sinon"; import sinon = require ("sinon");
import nedb = require("nedb"); import nedb = require("nedb");
import * as express from "express"; import express = require("express");
import * as winston from "winston"; import winston = require("winston");
import * as speakeasy from "speakeasy"; import speakeasy = require("speakeasy");
import * as u2f from "authdog"; import u2f = require("authdog");
import nodemailer = require("nodemailer");
import session = require("express-session");
import { AppConfiguration, UserConfiguration } from "../../src/lib/Configuration"; 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"; import Server from "../../src/lib/Server";
describe("test server configuration", function () { describe("test server configuration", function () {
let deps: GlobalDependencies; let deps: GlobalDependencies;
let sessionMock: sinon.SinonSpy;
before(function () { before(function () {
const transporter = { const transporter = {
sendMail: sinon.stub().yields() sendMail: sinon.stub().yields()
}; };
const nodemailer: Nodemailer = { const createTransport = sinon.stub(nodemailer, "createTransport");
createTransport: sinon.spy(function () { createTransport.returns(transporter);
return transporter;
}) sessionMock = sinon.spy(session);
};
deps = { deps = {
nodemailer: nodemailer, nodemailer: nodemailer,
@ -37,9 +39,7 @@ describe("test server configuration", function () {
return { on: sinon.spy() }; return { on: sinon.spy() };
}) })
}, },
session: sinon.spy(function () { session: sessionMock as any
return function (req: express.Request, res: express.Response, next: express.NextFunction) { next(); };
})
}; };
}); });
@ -66,7 +66,7 @@ describe("test server configuration", function () {
const server = new Server(); const server = new Server();
server.start(config, deps); server.start(config, deps);
assert(deps.session.calledOnce); assert(sessionMock.calledOnce);
assert.equal(deps.session.getCall(0).args[0].cookie.domain, "example.com"); assert.equal(sessionMock.getCall(0).args[0].cookie.domain, "example.com");
}); });
}); });

View File

@ -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");
});
});

View File

@ -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();
});
});
});