From 4356cfe7c10a26a8b7b4f011d34009826613b6f4 Mon Sep 17 00:00:00 2001 From: Clement Michaud Date: Sat, 13 May 2017 18:12:26 +0200 Subject: [PATCH 01/16] First step to typescript transformation --- .gitignore | 10 +++ Gruntfile.js | 49 +++++++++++++++ package.json | 27 +++++++- src/index.js | 36 ----------- src/index.ts | 29 +++++++++ src/lib/config_adapter.js | 17 ------ src/lib/config_adapter.ts | 38 ++++++++++++ src/lib/identity_check.js | 6 +- src/lib/routes/totp.js | 4 +- src/lib/server.js | 73 ---------------------- src/lib/server.ts | 70 +++++++++++++++++++++ src/lib/utils.js | 35 ----------- src/types/authelia.d.ts | 62 +++++++++++++++++++ test/unitary/config_adapter.test.ts | 88 +++++++++++++++++++++++++++ test/unitary/test_config_adapter.js | 76 ----------------------- test/unitary/test_data_persistence.js | 2 - test/unitary/test_server_config.js | 1 + tsconfig.json | 21 +++++++ tslint.json | 60 ++++++++++++++++++ 19 files changed, 457 insertions(+), 247 deletions(-) create mode 100644 Gruntfile.js delete mode 100755 src/index.js create mode 100755 src/index.ts delete mode 100644 src/lib/config_adapter.js create mode 100644 src/lib/config_adapter.ts delete mode 100644 src/lib/server.js create mode 100644 src/lib/server.ts delete mode 100644 src/lib/utils.js create mode 100644 src/types/authelia.d.ts create mode 100644 test/unitary/config_adapter.test.ts delete mode 100644 test/unitary/test_config_adapter.js create mode 100644 tsconfig.json create mode 100644 tslint.json diff --git a/.gitignore b/.gitignore index 50b2b0a4..ca068bf0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,13 @@ +# NodeJs modules node_modules/ +# Coverage reports coverage/ +src/.baseDir.ts +.vscode/ + *.swp *.sh @@ -11,6 +16,11 @@ config.yml npm-debug.log +# Directory used by example notifications/ +# VSCode user configuration .vscode/ + +# Generated by TypeScript compiler +dist/ diff --git a/Gruntfile.js b/Gruntfile.js new file mode 100644 index 00000000..f5a6b658 --- /dev/null +++ b/Gruntfile.js @@ -0,0 +1,49 @@ +module.exports = function(grunt) { + grunt.initConfig({ + run: { + options: {}, + "build-ts": { + cmd: "npm", + args: ['run', 'build-ts'] + }, + "tslint": { + cmd: "npm", + args: ['run', 'tslint'] + }, + "test": { + cmd: "npm", + args: ['run', 'test'] + } + }, + copy: { + resources: { + expand: true, + cwd: 'src/resources/', + src: '**', + dest: 'dist/src/resources/' + }, + views: { + expand: true, + cwd: 'src/views/', + src: '**', + dest: 'dist/src/views/' + }, + public_html: { + expand: true, + cwd: 'src/public_html/', + src: '**', + dest: 'dist/src/public_html/' + } + } + }); + + grunt.loadNpmTasks('grunt-run'); + grunt.loadNpmTasks('grunt-contrib-copy'); + + grunt.registerTask('default', ['build']); + + grunt.registerTask('res', ['copy:resources', 'copy:views', 'copy:public_html']); + grunt.registerTask('build', ['run:tslint', 'run:build-ts', 'res']); + + grunt.registerTask('test', ['run:test']); +}; diff --git a/package.json b/package.json index 563d58a8..8d52213a 100644 --- a/package.json +++ b/package.json @@ -7,11 +7,15 @@ "authelia": "src/index.js" }, "scripts": { - "test": "./node_modules/.bin/mocha --recursive test/unitary", + "test": "./node_modules/.bin/mocha -r ts-node/register --recursive test/unitary", "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 --recursive test" + "coverage": "./node_modules/.bin/istanbul cover _mocha -- -R spec --recursive test", + "build-ts": "tsc", + "watch-ts": "tsc -w", + "tslint": "tslint -c tslint.json -p tsconfig.json", + "serve": "node dist/src/index.js" }, "repository": { "type": "git", @@ -43,12 +47,29 @@ "yamljs": "^0.2.8" }, "devDependencies": { + "@types/assert": "0.0.31", + "@types/bluebird": "^3.5.3", + "@types/express": "^4.0.35", + "@types/express-session": "0.0.32", + "@types/ldapjs": "^1.0.0", + "@types/mocha": "^2.2.41", + "@types/nedb": "^1.8.3", + "@types/nodemailer": "^1.3.32", + "@types/object-path": "^0.9.28", + "@types/sinon": "^2.2.1", + "@types/winston": "^2.3.2", + "@types/yamljs": "^0.2.30", + "grunt": "^1.0.1", + "grunt-contrib-copy": "^1.0.0", + "grunt-run": "^0.6.0", "mocha": "^3.2.0", "mockdate": "^2.0.1", "request": "^2.79.0", "should": "^11.1.1", "sinon": "^1.17.6", "sinon-promise": "^0.1.3", - "tmp": "0.0.31" + "tmp": "0.0.31", + "ts-node": "^3.0.4", + "tslint": "^5.2.0" } } diff --git a/src/index.js b/src/index.js deleted file mode 100755 index e593e48b..00000000 --- a/src/index.js +++ /dev/null @@ -1,36 +0,0 @@ -#! /usr/bin/env node - -process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0"; - -var server = require('./lib/server'); - -var ldapjs = require('ldapjs'); -var u2f = require('authdog'); -var nodemailer = require('nodemailer'); -var nedb = require('nedb'); -var YAML = require('yamljs'); -var session = require('express-session'); -var winston = require('winston'); -var speakeasy = require('speakeasy'); - -var config_path = process.argv[2]; -if(!config_path) { - console.log('No config file has been provided.'); - console.log('Usage: authelia '); - process.exit(0); -} - -console.log('Parse configuration file: %s', config_path); - -var yaml_config = YAML.load(config_path); - -var deps = {}; -deps.u2f = u2f; -deps.nedb = nedb; -deps.nodemailer = nodemailer; -deps.ldapjs = ldapjs; -deps.session = session; -deps.winston = winston; -deps.speakeasy = speakeasy; - -server.run(yaml_config, deps); diff --git a/src/index.ts b/src/index.ts new file mode 100755 index 00000000..2de121e9 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,29 @@ +#! /usr/bin/env node + +process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0"; + +import * as server from "./lib/server"; +const YAML = require("yamljs"); + +const config_path = process.argv[2]; +if (!config_path) { + console.log("No config file has been provided."); + console.log("Usage: authelia "); + process.exit(0); +} + +console.log("Parse configuration file: %s", config_path); + +const yaml_config = YAML.load(config_path); + +const deps = { + u2f: require("authdog"), + nodemailer: require("nodemailer"), + ldapjs: require("ldapjs"), + session: require("express-session"), + winston: require("winston"), + speakeasy: require("speakeasy"), + nedb: require("nedb") +}; + +server.run(yaml_config, deps); diff --git a/src/lib/config_adapter.js b/src/lib/config_adapter.js deleted file mode 100644 index e91547f2..00000000 --- a/src/lib/config_adapter.js +++ /dev/null @@ -1,17 +0,0 @@ - -var objectPath = require('object-path'); - -module.exports = function(yaml_config) { - return { - port: objectPath.get(yaml_config, 'port', 8080), - ldap: objectPath.get(yaml_config, 'ldap', 'ldap://127.0.0.1:389'), - session_domain: objectPath.get(yaml_config, 'session.domain'), - session_secret: objectPath.get(yaml_config, 'session.secret'), - session_max_age: objectPath.get(yaml_config, 'session.expiration', 3600000), // in ms - store_directory: objectPath.get(yaml_config, 'store_directory'), - logs_level: objectPath.get(yaml_config, 'logs_level'), - notifier: objectPath.get(yaml_config, 'notifier'), - access_control: objectPath.get(yaml_config, 'access_control') - } -}; - diff --git a/src/lib/config_adapter.ts b/src/lib/config_adapter.ts new file mode 100644 index 00000000..6c9721e3 --- /dev/null +++ b/src/lib/config_adapter.ts @@ -0,0 +1,38 @@ + +import * as ObjectPath from "object-path"; +import { authelia } from "../types/authelia"; + + +function get_optional(config: object, path: string, default_value: T): T { + let entry = default_value; + if (ObjectPath.has(config, path)) { + entry = ObjectPath.get(config, path); + } + return entry; +} + +function ensure_key_existence(config: object, path: string): void { + if (!ObjectPath.has(config, path)) { + throw new Error(`Configuration error: key '${path}' is missing in configuration file`); + } +} + +export = function(yaml_config: object): authelia.Configuration { + ensure_key_existence(yaml_config, "ldap"); + ensure_key_existence(yaml_config, "session.secret"); + + const port = ObjectPath.get(yaml_config, "port", 8080); + + return { + port: port, + ldap: ObjectPath.get(yaml_config, "ldap"), + session_domain: ObjectPath.get(yaml_config, "session.domain"), + session_secret: ObjectPath.get(yaml_config, "session.secret"), + session_max_age: get_optional(yaml_config, "session.expiration", 3600000), // in ms + store_directory: get_optional(yaml_config, "store_directory", undefined), + logs_level: get_optional(yaml_config, "logs_level", "info"), + notifier: ObjectPath.get(yaml_config, "notifier"), + access_control: ObjectPath.get(yaml_config, "access_control") + }; +}; + diff --git a/src/lib/identity_check.js b/src/lib/identity_check.js index bef35534..0b03509a 100644 --- a/src/lib/identity_check.js +++ b/src/lib/identity_check.js @@ -106,7 +106,7 @@ function identity_check_post(endpoint, icheck_interface) { return identity_check.issue_token(userid, undefined, logger); }, function(err) { - throw new exceptions.AccessDeniedError(); + throw new exceptions.AccessDeniedError(err); }) .then(function(token) { var redirect_url = objectPath.get(req, 'body.redirect'); @@ -124,12 +124,12 @@ function identity_check_post(endpoint, icheck_interface) { res.send(); }) .catch(exceptions.IdentityError, function(err) { - logger.error('POST identity_check: IdentityError %s', err); + logger.error('POST identity_check: %s', err); res.status(400); res.send(); }) .catch(exceptions.AccessDeniedError, function(err) { - logger.error('POST identity_check: AccessDeniedError %s', err); + logger.error('POST identity_check: %s', err); res.status(403); res.send(); }) diff --git a/src/lib/routes/totp.js b/src/lib/routes/totp.js index 6621ba42..b5a00e23 100644 --- a/src/lib/routes/totp.js +++ b/src/lib/routes/totp.js @@ -1,5 +1,5 @@ -module.exports = totp; +module.exports = totp_fn; var totp = require('../totp'); var objectPath = require('object-path'); @@ -7,7 +7,7 @@ var exceptions = require('../../../src/lib/exceptions'); var UNAUTHORIZED_MESSAGE = 'Unauthorized access'; -function totp(req, res) { +function totp_fn(req, res) { var logger = req.app.get('logger'); var userid = objectPath.get(req, 'session.auth_session.userid'); logger.info('POST 2ndfactor totp: Initiate TOTP validation for user %s', userid); diff --git a/src/lib/server.js b/src/lib/server.js deleted file mode 100644 index f4c1ec83..00000000 --- a/src/lib/server.js +++ /dev/null @@ -1,73 +0,0 @@ - -module.exports = { - run: run -} - -var express = require('express'); -var bodyParser = require('body-parser'); -var path = require('path'); -var UserDataStore = require('./user_data_store'); -var Notifier = require('./notifier'); -var AuthenticationRegulator = require('./authentication_regulator'); -var setup_endpoints = require('./setup_endpoints'); -var config_adapter = require('./config_adapter'); -var Ldap = require('./ldap'); -var AccessControl = require('./access_control'); - -function run(yaml_config, deps, fn) { - var config = config_adapter(yaml_config); - - 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; - if(config.store_in_memory) - datastore_options.inMemory = true; - - var app = express(); - app.use(express.static(public_html_directory)); - app.use(bodyParser.urlencoded({ extended: false })); - app.use(bodyParser.json()); - app.set('trust proxy', 1); // trust first proxy - - app.use(deps.session({ - secret: config.session_secret, - resave: false, - saveUninitialized: true, - cookie: { - secure: false, - maxAge: config.session_max_age, - domain: config.session_domain - }, - })); - - app.set('views', view_directory); - app.set('view engine', 'ejs'); - - // by default the level of logs is info - deps.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 Notifier(config.notifier, deps); - var ldap = new Ldap(deps, config.ldap); - var access_control = AccessControl(deps.winston, config.access_control); - - app.set('logger', deps.winston); - app.set('ldap', ldap); - app.set('totp engine', deps.speakeasy); - app.set('u2f', deps.u2f); - app.set('user data store', data_store); - app.set('notifier', notifier); - app.set('authentication regulator', regulator); - app.set('config', config); - app.set('access control', access_control); - - setup_endpoints(app); - - return app.listen(config.port, function(err) { - console.log('Listening on %d...', config.port); - if(fn) fn(); - }); -} diff --git a/src/lib/server.ts b/src/lib/server.ts new file mode 100644 index 00000000..279c3fbd --- /dev/null +++ b/src/lib/server.ts @@ -0,0 +1,70 @@ + +import { authelia } from "../types/authelia"; +import * as Express from "express"; +import * as BodyParser from "body-parser"; +import * as Path from "path"; + +const UserDataStore = require("./user_data_store"); +const Notifier = require("./notifier"); +const AuthenticationRegulator = require("./authentication_regulator"); +const setup_endpoints = require("./setup_endpoints"); +const config_adapter = require("./config_adapter"); +const Ldap = require("./ldap"); +const AccessControl = require("./access_control"); + +export function run(yaml_configuration: authelia.Configuration, deps: authelia.GlobalDependencies, fn?: () => undefined) { + const config = config_adapter(yaml_configuration); + + const view_directory = Path.resolve(__dirname, "../views"); + const public_html_directory = Path.resolve(__dirname, "../public_html"); + const datastore_options = { + directory: config.store_directory, + inMemory: config.store_in_memory + }; + + const app = Express(); + app.use(Express.static(public_html_directory)); + app.use(BodyParser.urlencoded({ extended: false })); + app.use(BodyParser.json()); + app.set("trust proxy", 1); // trust first proxy + + app.use(deps.session({ + secret: config.session_secret, + resave: false, + saveUninitialized: true, + cookie: { + secure: false, + maxAge: config.session_max_age, + domain: config.session_domain + }, + })); + + app.set("views", view_directory); + app.set("view engine", "ejs"); + + // by default the level of logs is info + deps.winston.level = config.logs_level || "info"; + + const five_minutes = 5 * 60; + const data_store = new UserDataStore(deps.nedb, datastore_options); + const regulator = new AuthenticationRegulator(data_store, five_minutes); + const notifier = new Notifier(config.notifier, deps); + const ldap = new Ldap(deps, config.ldap); + const access_control = AccessControl(deps.winston, config.access_control); + + app.set("logger", deps.winston); + app.set("ldap", ldap); + app.set("totp engine", deps.speakeasy); + app.set("u2f", deps.u2f); + app.set("user data store", data_store); + app.set("notifier", notifier); + app.set("authentication regulator", regulator); + app.set("config", config); + app.set("access control", access_control); + setup_endpoints(app); + + return app.listen(config.port, function(err: string) { + console.log("Listening on %d...", config.port); + if (fn) fn(); + }); +} diff --git a/src/lib/utils.js b/src/lib/utils.js deleted file mode 100644 index 9b3845f4..00000000 --- a/src/lib/utils.js +++ /dev/null @@ -1,35 +0,0 @@ - -module.exports = { - 'promisify': promisify, - 'resolve': resolve, - 'reject': reject -} - -var Q = require('q'); - -function promisify(fn, context) { - return function() { - var defer = Q.defer(); - var args = Array.prototype.slice.call(arguments); - args.push(function(err, val) { - if (err !== null && err !== undefined) { - return defer.reject(err); - } - return defer.resolve(val); - }); - fn.apply(context || {}, args); - return defer.promise; - }; -} - -function resolve(data) { - var defer = Q.defer(); - defer.resolve(data); - return defer.promise; -} - -function reject(err) { - var defer = Q.defer(); - defer.reject(err); - return defer.promise; -} diff --git a/src/types/authelia.d.ts b/src/types/authelia.d.ts new file mode 100644 index 00000000..9b222356 --- /dev/null +++ b/src/types/authelia.d.ts @@ -0,0 +1,62 @@ + +import * as winston from "winston"; +import * as nedb from "nedb"; + +declare namespace authelia { + + interface LdapConfiguration { + url: string; + base_dn: string; + additional_user_dn?: string; + user_name_attribute?: string; // cn by default + additional_group_dn?: string; + group_name_attribute?: string; // cn by default + user: string; // admin username + password: string; // admin password + } + + type UserName = string; + type GroupName = string; + type DomainPattern = string; + + type ACLDefaultRules = Array; + type ACLGroupsRules = Map; + type ACLUsersRules = Map; + + export interface ACLConfiguration { + default: ACLDefaultRules; + groups: ACLGroupsRules; + users: ACLUsersRules; + } + + interface SessionCookieConfiguration { + secret: string; + expiration: number; + domain: string + } + + type NotifierType = string; + export type NotifiersConfiguration = Map; + + export interface Configuration { + port: number; + logs_level: string; + ldap: LdapConfiguration | {}; + session_domain?: string; + session_secret: string; + session_max_age: number; + store_directory?: string; + notifier: NotifiersConfiguration; + access_control: ACLConfiguration; + } + + export interface GlobalDependencies { + u2f: object; + nodemailer: any; + ldapjs: object; + session: any; + winston: winston.Winston; + speakeasy: object; + nedb: object; + } +} \ No newline at end of file diff --git a/test/unitary/config_adapter.test.ts b/test/unitary/config_adapter.test.ts new file mode 100644 index 00000000..3db5cd58 --- /dev/null +++ b/test/unitary/config_adapter.test.ts @@ -0,0 +1,88 @@ +import * as mocha from "mocha"; +import * as Assert from "assert"; + +const config_adapter = require("../../src/lib/config_adapter"); + +describe("test config adapter", function() { + function build_yaml_config(): any { + const yaml_config = { + port: 8080, + ldap: {}, + session: { + domain: "example.com", + secret: "secret", + max_age: 40000 + }, + store_directory: "/mydirectory", + logs_level: "debug" + }; + return yaml_config; + } + + it("should read the port from the yaml file", function() { + const yaml_config = build_yaml_config(); + yaml_config.port = 7070; + const config = config_adapter(yaml_config); + Assert.equal(config.port, 7070); + }); + + it("should default the port to 8080 if not provided", function() { + const yaml_config = build_yaml_config(); + delete yaml_config.port; + const config = config_adapter(yaml_config); + Assert.equal(config.port, 8080); + }); + + it("should get the ldap attributes", function() { + const yaml_config = build_yaml_config(); + yaml_config.ldap = { + url: "http://ldap", + user_search_base: "ou=groups,dc=example,dc=com", + user_search_filter: "uid", + user: "admin", + password: "pass" + }; + + const config = config_adapter(yaml_config); + + Assert.equal(config.ldap.url, "http://ldap"); + Assert.equal(config.ldap.user_search_base, "ou=groups,dc=example,dc=com"); + Assert.equal(config.ldap.user_search_filter, "uid"); + Assert.equal(config.ldap.user, "admin"); + Assert.equal(config.ldap.password, "pass"); + }); + + it("should get the session attributes", function() { + const yaml_config = build_yaml_config(); + yaml_config.session = { + domain: "example.com", + secret: "secret", + expiration: 3600 + }; + const config = config_adapter(yaml_config); + Assert.equal(config.session_domain, "example.com"); + Assert.equal(config.session_secret, "secret"); + Assert.equal(config.session_max_age, 3600); + }); + + it("should get the log level", function() { + const yaml_config = build_yaml_config(); + yaml_config.logs_level = "debug"; + const config = config_adapter(yaml_config); + Assert.equal(config.logs_level, "debug"); + }); + + it("should get the notifier config", function() { + const yaml_config = build_yaml_config(); + yaml_config.notifier = "notifier"; + const config = config_adapter(yaml_config); + Assert.equal(config.notifier, "notifier"); + }); + + it("should get the access_control config", function() { + const yaml_config = build_yaml_config(); + yaml_config.access_control = "access_control"; + const config = config_adapter(yaml_config); + Assert.equal(config.access_control, "access_control"); + }); +}); diff --git a/test/unitary/test_config_adapter.js b/test/unitary/test_config_adapter.js deleted file mode 100644 index 5ffcc84a..00000000 --- a/test/unitary/test_config_adapter.js +++ /dev/null @@ -1,76 +0,0 @@ - -var assert = require('assert'); -var config_adapter = require('../../src/lib/config_adapter'); - -describe('test config adapter', function() { - it('should read the port from the yaml file', function() { - yaml_config = {}; - yaml_config.port = 7070; - var config = config_adapter(yaml_config); - assert.equal(config.port, 7070); - }); - - it('should default the port to 8080 if not provided', function() { - yaml_config = {}; - var config = config_adapter(yaml_config); - assert.equal(config.port, 8080); - }); - - it('should get the ldap attributes', function() { - yaml_config = {}; - yaml_config.ldap = {}; - yaml_config.ldap.url = 'http://ldap'; - yaml_config.ldap.user_search_base = 'ou=groups,dc=example,dc=com'; - yaml_config.ldap.user_search_filter = 'uid'; - yaml_config.ldap.user = 'admin'; - yaml_config.ldap.password = 'pass'; - - var config = config_adapter(yaml_config); - - assert.equal(config.ldap.url, 'http://ldap'); - assert.equal(config.ldap.user_search_base, 'ou=groups,dc=example,dc=com'); - assert.equal(config.ldap.user_search_filter, 'uid'); - assert.equal(config.ldap.user, 'admin'); - assert.equal(config.ldap.password, 'pass'); - }); - - it('should get the session attributes', function() { - yaml_config = {}; - yaml_config.session = {}; - yaml_config.session.domain = 'example.com'; - yaml_config.session.secret = 'secret'; - yaml_config.session.expiration = 3600; - - var config = config_adapter(yaml_config); - - assert.equal(config.session_domain, 'example.com'); - assert.equal(config.session_secret, 'secret'); - assert.equal(config.session_max_age, 3600); - }); - - it('should get the log level', function() { - yaml_config = {}; - yaml_config.logs_level = 'debug'; - - var config = config_adapter(yaml_config); - assert.equal(config.logs_level, 'debug'); - }); - - it('should get the notifier config', function() { - yaml_config = {}; - yaml_config.notifier = 'notifier'; - - var config = config_adapter(yaml_config); - - assert.equal(config.notifier, 'notifier'); - }); - - it('should get the access_control config', function() { - yaml_config = {}; - yaml_config.access_control = 'access_control'; - - var config = config_adapter(yaml_config); - - assert.equal(config.access_control, 'access_control'); - }); -}); diff --git a/test/unitary/test_data_persistence.js b/test/unitary/test_data_persistence.js index 35c2c980..ec6efbdb 100644 --- a/test/unitary/test_data_persistence.js +++ b/test/unitary/test_data_persistence.js @@ -12,8 +12,6 @@ var session = require('express-session'); var winston = require('winston'); var PORT = 8050; -var BASE_URL = 'http://localhost:' + PORT; - var requests = require('./requests')(PORT); diff --git a/test/unitary/test_server_config.js b/test/unitary/test_server_config.js index b5c26b5e..aadca125 100644 --- a/test/unitary/test_server_config.js +++ b/test/unitary/test_server_config.js @@ -41,6 +41,7 @@ describe('test server configuration', function() { it('should set cookie scope to domain set in the config', function() { config.session = {}; config.session.domain = 'example.com'; + config.session.secret = 'secret'; config.ldap = {}; config.ldap.url = 'http://ldap'; server.run(config, deps); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 00000000..40cc3822 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "module": "commonjs", + "target": "es6", + "noImplicitAny": true, + "moduleResolution": "node", + "sourceMap": true, + "outDir": "dist", + "baseUrl": ".", + "allowJs": true, + "paths": { + "*": [ + "node_modules/*", + "src/types/*" + ] + } + }, + "include": [ + "src/**/*" + ] +} diff --git a/tslint.json b/tslint.json new file mode 100644 index 00000000..c2c1b750 --- /dev/null +++ b/tslint.json @@ -0,0 +1,60 @@ +{ + "rules": { + "class-name": true, + "comment-format": [ + true, + "check-space" + ], + "indent": [ + true, + "spaces" + ], + "one-line": [ + true, + "check-open-brace", + "check-whitespace" + ], + "no-var-keyword": true, + "quotemark": [ + true, + "double", + "avoid-escape" + ], + "semicolon": [ + true, + "always", + "ignore-bound-class-methods" + ], + "whitespace": [ + true, + "check-branch", + "check-decl", + "check-operator", + "check-module", + "check-separator", + "check-type" + ], + "typedef-whitespace": [ + true, + { + "call-signature": "nospace", + "index-signature": "nospace", + "parameter": "nospace", + "property-declaration": "nospace", + "variable-declaration": "nospace" + }, + { + "call-signature": "onespace", + "index-signature": "onespace", + "parameter": "onespace", + "property-declaration": "onespace", + "variable-declaration": "onespace" + } + ], + "no-internal-module": true, + "no-trailing-whitespace": true, + "no-null-keyword": true, + "prefer-const": true, + "jsdoc-format": true + } +} From e1b7877c24a9c79bd645524a46fc134fd6a25d9c Mon Sep 17 00:00:00 2001 From: Clement Michaud Date: Sat, 13 May 2017 18:32:25 +0200 Subject: [PATCH 02/16] Fix packaging in travis --- .travis.yml | 6 ++++-- Gruntfile.js | 6 ++++++ package.json | 3 ++- 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index b1d4832f..b2f8cca8 100644 --- a/.travis.yml +++ b/.travis.yml @@ -19,8 +19,10 @@ addons: before_install: npm install -g npm@'>=2.13.5' script: -- npm test -- docker build -t clems4ever/authelia . +- npm install +- grunt test +- grunt build +- grunt docker-build - docker-compose build - docker-compose up -d - sleep 5 diff --git a/Gruntfile.js b/Gruntfile.js index f5a6b658..4b248405 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -13,6 +13,10 @@ module.exports = function(grunt) { "test": { cmd: "npm", args: ['run', 'test'] + }, + "docker-build": { + cmd: "docker", + args: ['build', '-t', 'clems4ever/authelia', '.'] } }, copy: { @@ -43,7 +47,9 @@ module.exports = function(grunt) { grunt.registerTask('default', ['build']); grunt.registerTask('res', ['copy:resources', 'copy:views', 'copy:public_html']); + grunt.registerTask('build', ['run:tslint', 'run:build-ts', 'res']); + grunt.registerTask('docker-build', ['run:docker-build']); grunt.registerTask('test', ['run:test']); }; diff --git a/package.json b/package.json index 8d52213a..cca2cb96 100644 --- a/package.json +++ b/package.json @@ -70,6 +70,7 @@ "sinon-promise": "^0.1.3", "tmp": "0.0.31", "ts-node": "^3.0.4", - "tslint": "^5.2.0" + "tslint": "^5.2.0", + "typescript": "^2.3.2" } } From 29604dc3bc743266e684a7f0efeae20e1a2119e9 Mon Sep 17 00:00:00 2001 From: Clement Michaud Date: Sat, 13 May 2017 18:40:39 +0200 Subject: [PATCH 03/16] Disable Docker temporarily in TravisCI --- .travis.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index b2f8cca8..70c3899a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -19,7 +19,6 @@ addons: before_install: npm install -g npm@'>=2.13.5' script: -- npm install - grunt test - grunt build - grunt docker-build From 923886667ddd47ced54f17ac31c026778ef0b77e Mon Sep 17 00:00:00 2001 From: Clement Michaud Date: Sun, 14 May 2017 00:42:13 +0200 Subject: [PATCH 04/16] moving authentication regulator to typescript --- src/lib/AuthenticationRegulator.ts | 44 +++++++++++++++++++ src/lib/authentication_regulator.js | 35 --------------- src/lib/server.ts | 2 +- src/types/authelia.d.ts | 1 - ...tor.js => AuthenticationRegulator.test.js} | 11 ++--- test/unitary/config_adapter.test.ts | 1 - 6 files changed, 51 insertions(+), 43 deletions(-) create mode 100644 src/lib/AuthenticationRegulator.ts delete mode 100644 src/lib/authentication_regulator.js rename test/unitary/{test_authentication_regulator.js => AuthenticationRegulator.test.js} (86%) diff --git a/src/lib/AuthenticationRegulator.ts b/src/lib/AuthenticationRegulator.ts new file mode 100644 index 00000000..77c121b2 --- /dev/null +++ b/src/lib/AuthenticationRegulator.ts @@ -0,0 +1,44 @@ + +import * as Promise from "bluebird"; + +const exceptions = require("./exceptions"); + +const REGULATION_TRACE_TYPE = "regulation"; +const MAX_AUTHENTICATION_COUNT_IN_TIME_RANGE = 3; + +interface DatedDocument { + date: Date; +} + +export class AuthenticationRegulator { + _user_data_store: any; + _lock_time_in_seconds: number; + + constructor(user_data_store: any, lock_time_in_seconds: number) { + this._user_data_store = user_data_store; + this._lock_time_in_seconds = lock_time_in_seconds; + } + + // Mark authentication + mark(userid: string, is_success: boolean): Promise { + return this._user_data_store.save_authentication_trace(userid, REGULATION_TRACE_TYPE, is_success); + } + + regulate(userid: string): Promise { + return this._user_data_store.get_last_authentication_traces(userid, REGULATION_TRACE_TYPE, false, 3) + .then((docs: Array) => { + if (docs.length < MAX_AUTHENTICATION_COUNT_IN_TIME_RANGE) { + // less than the max authorized number of authentication in time range, thus authorizing access + return Promise.resolve(); + } + + const oldest_doc = docs[MAX_AUTHENTICATION_COUNT_IN_TIME_RANGE - 1]; + const no_lock_min_date = new Date(new Date().getTime() - this._lock_time_in_seconds * 1000); + if (oldest_doc.date > no_lock_min_date) { + throw new exceptions.AuthenticationRegulationError("Max number of authentication. Please retry in few minutes."); + } + + return Promise.resolve(); + }); + } +} diff --git a/src/lib/authentication_regulator.js b/src/lib/authentication_regulator.js deleted file mode 100644 index e7e22190..00000000 --- a/src/lib/authentication_regulator.js +++ /dev/null @@ -1,35 +0,0 @@ - -module.exports = AuthenticationRegulator; - -var exceptions = require('./exceptions'); -var Promise = require('bluebird'); - -function AuthenticationRegulator(user_data_store, lock_time_in_seconds) { - this._user_data_store = user_data_store; - this._lock_time_in_seconds = lock_time_in_seconds; -} - -// Mark authentication -AuthenticationRegulator.prototype.mark = function(userid, is_success) { - return this._user_data_store.save_authentication_trace(userid, '1stfactor', is_success); -} - -AuthenticationRegulator.prototype.regulate = function(userid) { - var that = this; - return this._user_data_store.get_last_authentication_traces(userid, '1stfactor', false, 3) - .then(function(docs) { - if(docs.length < 3) { - return Promise.resolve(); - } - - var oldest_doc = docs[2]; - var no_lock_min_date = new Date(new Date().getTime() - - that._lock_time_in_seconds * 1000); - - if(oldest_doc.date > no_lock_min_date) { - throw new exceptions.AuthenticationRegulationError(); - } - - return Promise.resolve(); - }); -} diff --git a/src/lib/server.ts b/src/lib/server.ts index 279c3fbd..8e6ca8a8 100644 --- a/src/lib/server.ts +++ b/src/lib/server.ts @@ -3,10 +3,10 @@ import { authelia } from "../types/authelia"; import * as Express from "express"; import * as BodyParser from "body-parser"; import * as Path from "path"; +import { AuthenticationRegulator } from "./AuthenticationRegulator"; const UserDataStore = require("./user_data_store"); const Notifier = require("./notifier"); -const AuthenticationRegulator = require("./authentication_regulator"); const setup_endpoints = require("./setup_endpoints"); const config_adapter = require("./config_adapter"); const Ldap = require("./ldap"); diff --git a/src/types/authelia.d.ts b/src/types/authelia.d.ts index 9b222356..94950cbb 100644 --- a/src/types/authelia.d.ts +++ b/src/types/authelia.d.ts @@ -3,7 +3,6 @@ import * as winston from "winston"; import * as nedb from "nedb"; declare namespace authelia { - interface LdapConfiguration { url: string; base_dn: string; diff --git a/test/unitary/test_authentication_regulator.js b/test/unitary/AuthenticationRegulator.test.js similarity index 86% rename from test/unitary/test_authentication_regulator.js rename to test/unitary/AuthenticationRegulator.test.js index 18b46c9e..c06e91e2 100644 --- a/test/unitary/test_authentication_regulator.js +++ b/test/unitary/AuthenticationRegulator.test.js @@ -1,11 +1,12 @@ -var AuthenticationRegulator = require('../../src/lib/authentication_regulator'); -var UserDataStore = require('../../src/lib/user_data_store'); -var DataStore = require('nedb'); +import { AuthenticationRegulator } from "../../src/lib/AuthenticationRegulator"; +import * as UserDataStore from "../../src/lib/user_data_store"; +import * as DataStore from "nedb"; +import * as MockDate from "mockdate"; + var exceptions = require('../../src/lib/exceptions'); -var MockDate = require('mockdate'); -describe('test authentication regulator', function() { +describe.only('test authentication regulator', function() { it('should mark 2 authentication and regulate (resolve)', function() { var options = {}; options.inMemoryOnly = true; diff --git a/test/unitary/config_adapter.test.ts b/test/unitary/config_adapter.test.ts index 3db5cd58..51a4eef9 100644 --- a/test/unitary/config_adapter.test.ts +++ b/test/unitary/config_adapter.test.ts @@ -1,4 +1,3 @@ -import * as mocha from "mocha"; import * as Assert from "assert"; const config_adapter = require("../../src/lib/config_adapter"); From b0c6c61df5e731586d4622fc9b3db3b696b76da4 Mon Sep 17 00:00:00 2001 From: Clement Michaud Date: Tue, 16 May 2017 23:17:46 +0200 Subject: [PATCH 05/16] Migrate server to typescript --- Dockerfile | 2 +- package.json | 9 +- src/index.ts | 8 +- src/lib/AuthenticationRegulator.ts | 4 +- src/lib/Configuration.ts | 62 +++ src/lib/GlobalDependencies.ts | 11 + src/lib/Server.ts | 84 ++++ src/lib/TOTPSecret.ts | 6 + src/lib/UserDataStore.ts | 169 ++++++++ src/lib/config_adapter.ts | 18 +- src/lib/server.ts | 70 ---- src/lib/user_data_store.js | 124 ------ src/types/authdog.d.ts | 67 +++ src/types/authelia.d.ts | 61 --- src/types/nedb-async.d.ts | 12 + src/types/request-async.d.ts | 14 + test/unitary/AuthenticationRegulator.test.js | 71 ---- test/unitary/AuthenticationRegulator.test.ts | 73 ++++ test/unitary/Server.test.ts | 393 ++++++++++++++++++ test/unitary/UserDataStore.test.ts | 206 +++++++++ test/unitary/config_adapter.test.ts | 61 ++- test/unitary/data_persistence.test.ts | 179 ++++++++ test/unitary/server_config.test.ts | 72 ++++ test/unitary/test_data_persistence.js | 162 -------- test/unitary/test_server.js | 389 ----------------- test/unitary/test_server_config.js | 52 --- test/unitary/test_totp.js | 32 -- test/unitary/test_user_data_store.js | 212 ---------- test/unitary/totp.test.ts | 32 ++ .../authentication_audit.test.ts | 70 ++++ .../test_authentication_audit.js | 69 --- .../user_data_store/test_totp_secret.js | 65 --- .../user_data_store/totp_secret.test.ts | 72 ++++ tsconfig.json | 5 +- 34 files changed, 1596 insertions(+), 1340 deletions(-) create mode 100644 src/lib/Configuration.ts create mode 100644 src/lib/GlobalDependencies.ts create mode 100644 src/lib/Server.ts create mode 100644 src/lib/TOTPSecret.ts create mode 100644 src/lib/UserDataStore.ts delete mode 100644 src/lib/server.ts delete mode 100644 src/lib/user_data_store.js create mode 100644 src/types/authdog.d.ts delete mode 100644 src/types/authelia.d.ts create mode 100644 src/types/nedb-async.d.ts create mode 100644 src/types/request-async.d.ts delete mode 100644 test/unitary/AuthenticationRegulator.test.js create mode 100644 test/unitary/AuthenticationRegulator.test.ts create mode 100644 test/unitary/Server.test.ts create mode 100644 test/unitary/UserDataStore.test.ts create mode 100644 test/unitary/data_persistence.test.ts create mode 100644 test/unitary/server_config.test.ts delete mode 100644 test/unitary/test_data_persistence.js delete mode 100644 test/unitary/test_server.js delete mode 100644 test/unitary/test_server_config.js delete mode 100644 test/unitary/test_totp.js delete mode 100644 test/unitary/test_user_data_store.js create mode 100644 test/unitary/totp.test.ts create mode 100644 test/unitary/user_data_store/authentication_audit.test.ts delete mode 100644 test/unitary/user_data_store/test_authentication_audit.js delete mode 100644 test/unitary/user_data_store/test_totp_secret.js create mode 100644 test/unitary/user_data_store/totp_secret.test.ts diff --git a/Dockerfile b/Dockerfile index b535f8bc..eef8b58f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,7 +5,7 @@ WORKDIR /usr/src COPY package.json /usr/src/package.json RUN npm install --production -COPY src /usr/src +COPY dist/src /usr/src ENV PORT=80 EXPOSE 80 diff --git a/package.json b/package.json index cca2cb96..a5078e69 100644 --- a/package.json +++ b/package.json @@ -7,10 +7,8 @@ "authelia": "src/index.js" }, "scripts": { - "test": "./node_modules/.bin/mocha -r ts-node/register --recursive test/unitary", - "unit-test": "./node_modules/.bin/mocha --recursive test/unitary", + "test": "./node_modules/.bin/mocha --compilers ts:ts-node/register --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 --recursive test", "build-ts": "tsc", "watch-ts": "tsc -w", @@ -49,14 +47,19 @@ "devDependencies": { "@types/assert": "0.0.31", "@types/bluebird": "^3.5.3", + "@types/body-parser": "^1.16.3", "@types/express": "^4.0.35", "@types/express-session": "0.0.32", "@types/ldapjs": "^1.0.0", "@types/mocha": "^2.2.41", + "@types/mockdate": "^2.0.0", "@types/nedb": "^1.8.3", "@types/nodemailer": "^1.3.32", "@types/object-path": "^0.9.28", + "@types/request": "0.0.43", "@types/sinon": "^2.2.1", + "@types/speakeasy": "^2.0.1", + "@types/tmp": "0.0.33", "@types/winston": "^2.3.2", "@types/yamljs": "^0.2.30", "grunt": "^1.0.1", diff --git a/src/index.ts b/src/index.ts index 2de121e9..c16865f3 100755 --- a/src/index.ts +++ b/src/index.ts @@ -2,7 +2,7 @@ process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0"; -import * as server from "./lib/server"; +import Server from "./lib/Server"; const YAML = require("yamljs"); const config_path = process.argv[2]; @@ -26,4 +26,8 @@ const deps = { nedb: require("nedb") }; -server.run(yaml_config, deps); +const server = new Server(); +server.start(yaml_config, deps) +.then(() => { + console.log("The server is started!"); +}); diff --git a/src/lib/AuthenticationRegulator.ts b/src/lib/AuthenticationRegulator.ts index 77c121b2..16bd4340 100644 --- a/src/lib/AuthenticationRegulator.ts +++ b/src/lib/AuthenticationRegulator.ts @@ -11,8 +11,8 @@ interface DatedDocument { } export class AuthenticationRegulator { - _user_data_store: any; - _lock_time_in_seconds: number; + private _user_data_store: any; + private _lock_time_in_seconds: number; constructor(user_data_store: any, lock_time_in_seconds: number) { this._user_data_store = user_data_store; diff --git a/src/lib/Configuration.ts b/src/lib/Configuration.ts new file mode 100644 index 00000000..2ed31f56 --- /dev/null +++ b/src/lib/Configuration.ts @@ -0,0 +1,62 @@ + +export interface LdapConfiguration { + url: string; + base_dn: string; + additional_user_dn?: string; + user_name_attribute?: string; // cn by default + additional_group_dn?: string; + group_name_attribute?: string; // cn by default + user: string; // admin username + password: string; // admin password +} + +type UserName = string; +type GroupName = string; +type DomainPattern = string; + +type ACLDefaultRules = Array; +type ACLGroupsRules = Object; +type ACLUsersRules = Object; + +export interface ACLConfiguration { + default: ACLDefaultRules; + groups: ACLGroupsRules; + users: ACLUsersRules; +} + +interface SessionCookieConfiguration { + secret: string; + expiration?: number; + domain?: string; +} + +interface GMailNotifier { + user: string; + pass: string; +} + +type NotifierType = string; +export interface NotifiersConfiguration { + gmail: GMailNotifier; +} + +export interface UserConfiguration { + port?: number; + logs_level?: string; + ldap: LdapConfiguration; + session: SessionCookieConfiguration; + store_directory?: string; + notifier: NotifiersConfiguration; + access_control?: ACLConfiguration; +} + +export interface AppConfiguration { + port: number; + logs_level: string; + ldap: LdapConfiguration; + session: SessionCookieConfiguration; + store_in_memory?: boolean; + store_directory?: string; + notifier: NotifiersConfiguration; + access_control?: ACLConfiguration; +} diff --git a/src/lib/GlobalDependencies.ts b/src/lib/GlobalDependencies.ts new file mode 100644 index 00000000..4b71f273 --- /dev/null +++ b/src/lib/GlobalDependencies.ts @@ -0,0 +1,11 @@ +import * as winston from "winston"; + +export interface GlobalDependencies { + u2f: object; + nodemailer: any; + ldapjs: object; + session: any; + winston: winston.Winston; + speakeasy: object; + nedb: any; +} \ No newline at end of file diff --git a/src/lib/Server.ts b/src/lib/Server.ts new file mode 100644 index 00000000..8df410f6 --- /dev/null +++ b/src/lib/Server.ts @@ -0,0 +1,84 @@ + +import { UserConfiguration } from "./Configuration"; +import { GlobalDependencies } from "./GlobalDependencies"; +import * as Express from "express"; +import * as BodyParser from "body-parser"; +import * as Path from "path"; +import { AuthenticationRegulator } from "./AuthenticationRegulator"; +import UserDataStore from "./UserDataStore"; +import * as http from "http"; + +import config_adapter = require("./config_adapter"); + +const Notifier = require("./notifier"); +const setup_endpoints = require("./setup_endpoints"); +const Ldap = require("./ldap"); +const AccessControl = require("./access_control"); + +export default class Server { + private httpServer: http.Server; + + start(yaml_configuration: UserConfiguration, deps: GlobalDependencies): Promise { + const config = config_adapter(yaml_configuration); + + const view_directory = Path.resolve(__dirname, "../views"); + const public_html_directory = Path.resolve(__dirname, "../public_html"); + const datastore_options = { + directory: config.store_directory, + inMemory: config.store_in_memory + }; + + const app = Express(); + app.use(Express.static(public_html_directory)); + app.use(BodyParser.urlencoded({ extended: false })); + app.use(BodyParser.json()); + app.set("trust proxy", 1); // trust first proxy + + app.use(deps.session({ + secret: config.session.secret, + resave: false, + saveUninitialized: true, + cookie: { + secure: false, + maxAge: config.session.expiration, + domain: config.session.domain + }, + })); + + app.set("views", view_directory); + app.set("view engine", "ejs"); + + // by default the level of logs is info + deps.winston.level = config.logs_level || "info"; + + const five_minutes = 5 * 60; + const data_store = new UserDataStore(datastore_options); + const regulator = new AuthenticationRegulator(data_store, five_minutes); + const notifier = new Notifier(config.notifier, deps); + const ldap = new Ldap(deps, config.ldap); + const access_control = AccessControl(deps.winston, config.access_control); + + app.set("logger", deps.winston); + app.set("ldap", ldap); + app.set("totp engine", deps.speakeasy); + app.set("u2f", deps.u2f); + app.set("user data store", data_store); + app.set("notifier", notifier); + app.set("authentication regulator", regulator); + app.set("config", config); + app.set("access control", access_control); + setup_endpoints(app); + + return new Promise((resolve, reject) => { + this.httpServer = app.listen(config.port, function (err: string) { + console.log("Listening on %d...", config.port); + resolve(); + }); + }); + } + + stop() { + this.httpServer.close(); + } +} + diff --git a/src/lib/TOTPSecret.ts b/src/lib/TOTPSecret.ts new file mode 100644 index 00000000..e4a6b7d7 --- /dev/null +++ b/src/lib/TOTPSecret.ts @@ -0,0 +1,6 @@ + +export interface TOTPSecret { + base32: string; + ascii: string; + otpauth_url: string; +} \ No newline at end of file diff --git a/src/lib/UserDataStore.ts b/src/lib/UserDataStore.ts new file mode 100644 index 00000000..4aa6a05e --- /dev/null +++ b/src/lib/UserDataStore.ts @@ -0,0 +1,169 @@ +import * as Promise from "bluebird"; +import * as path from "path"; +import Nedb = require("nedb"); +import { NedbAsync } from "nedb"; +import { TOTPSecret } from "./TOTPSecret"; + +// Constants + +const U2F_META_COLLECTION_NAME = "u2f_meta"; +const IDENTITY_CHECK_TOKENS_COLLECTION_NAME = "identity_check_tokens"; +const AUTHENTICATION_TRACES_COLLECTION_NAME = "authentication_traces"; +const TOTP_SECRETS_COLLECTION_NAME = "totp_secrets"; + + +export interface TOTPSecretDocument { + userid: string; + secret: TOTPSecret; +} + +export interface U2FMetaDocument { + meta: object; + userid: string; + appid: string; +} + +export interface Options { + inMemoryOnly?: boolean; + directory?: string; +} + + +// Source + +export default class UserDataStore { + private _u2f_meta_collection: NedbAsync; + private _identity_check_tokens_collection: NedbAsync; + private _authentication_traces_collection: NedbAsync; + private _totp_secret_collection: NedbAsync; + + constructor(options?: Options) { + this._u2f_meta_collection = create_collection(U2F_META_COLLECTION_NAME, options); + this._identity_check_tokens_collection = + create_collection(IDENTITY_CHECK_TOKENS_COLLECTION_NAME, options); + this._authentication_traces_collection = + create_collection(AUTHENTICATION_TRACES_COLLECTION_NAME, options); + this._totp_secret_collection = + create_collection(TOTP_SECRETS_COLLECTION_NAME, options); + } + + set_u2f_meta(userid: string, appid: string, meta: Object): Promise { + const newDocument = { + userid: userid, + appid: appid, + meta: meta + }; + + const filter = { + userid: userid, + appid: appid + }; + + return this._u2f_meta_collection.updateAsync(filter, newDocument, { upsert: true }); + } + + get_u2f_meta(userid: string, appid: string): Promise { + const filter = { + userid: userid, + appid: appid + }; + return this._u2f_meta_collection.findOneAsync(filter); + } + + save_authentication_trace(userid: string, type: string, is_success: boolean) { + const newDocument = { + userid: userid, + date: new Date(), + is_success: is_success, + type: type + }; + + return this._authentication_traces_collection.insertAsync(newDocument); + } + + get_last_authentication_traces(userid: string, type: string, is_success: boolean, count: number): Promise { + const q = { + userid: userid, + type: type, + is_success: is_success + }; + + const query = this._authentication_traces_collection.find(q) + .sort({ date: -1 }).limit(count); + const query_promisified = Promise.promisify(query.exec, { context: query }); + return query_promisified(); + } + + issue_identity_check_token(userid: string, token: string, data: string | object, max_age: number): Promise { + const newDocument = { + userid: userid, + token: token, + content: { + userid: userid, + data: data + }, + max_date: new Date(new Date().getTime() + max_age) + }; + + return this._identity_check_tokens_collection.insertAsync(newDocument); + } + + consume_identity_check_token(token: string): Promise { + const query = { + token: token + }; + + return this._identity_check_tokens_collection.findOneAsync(query) + .then(function (doc) { + if (!doc) { + return Promise.reject("Registration token does not exist"); + } + + const max_date = doc.max_date; + const current_date = new Date(); + if (current_date > max_date) { + return Promise.reject("Registration token is not valid anymore"); + } + return Promise.resolve(doc.content); + }) + .then((content) => { + return Promise.join(this._identity_check_tokens_collection.removeAsync(query), + Promise.resolve(content)); + }) + .then((v) => { + return Promise.resolve(v[1]); + }); + } + + set_totp_secret(userid: string, secret: TOTPSecret): Promise { + const doc = { + userid: userid, + secret: secret + }; + + const query = { + userid: userid + }; + return this._totp_secret_collection.updateAsync(query, doc, { upsert: true }); + } + + get_totp_secret(userid: string): Promise { + const query = { + userid: userid + }; + return this._totp_secret_collection.findOneAsync(query); + } +} + +function create_collection(name: string, options: any): NedbAsync { + const datastore_options = { + inMemoryOnly: options.inMemoryOnly || false, + autoload: true, + filename: "" + }; + + if (options.directory) + datastore_options.filename = path.resolve(options.directory, name); + + return Promise.promisifyAll(new Nedb(datastore_options)) as NedbAsync; +} diff --git a/src/lib/config_adapter.ts b/src/lib/config_adapter.ts index 6c9721e3..5503f1c8 100644 --- a/src/lib/config_adapter.ts +++ b/src/lib/config_adapter.ts @@ -1,6 +1,6 @@ import * as ObjectPath from "object-path"; -import { authelia } from "../types/authelia"; +import { AppConfiguration, UserConfiguration, NotifiersConfiguration, ACLConfiguration, LdapConfiguration } from "./Configuration"; function get_optional(config: object, path: string, default_value: T): T { @@ -17,7 +17,7 @@ function ensure_key_existence(config: object, path: string): void { } } -export = function(yaml_config: object): authelia.Configuration { +export = function(yaml_config: UserConfiguration): AppConfiguration { ensure_key_existence(yaml_config, "ldap"); ensure_key_existence(yaml_config, "session.secret"); @@ -25,14 +25,16 @@ export = function(yaml_config: object): authelia.Configuration { return { port: port, - ldap: ObjectPath.get(yaml_config, "ldap"), - session_domain: ObjectPath.get(yaml_config, "session.domain"), - session_secret: ObjectPath.get(yaml_config, "session.secret"), - session_max_age: get_optional(yaml_config, "session.expiration", 3600000), // in ms + ldap: ObjectPath.get(yaml_config, "ldap"), + session: { + domain: ObjectPath.get(yaml_config, "session.domain"), + secret: ObjectPath.get(yaml_config, "session.secret"), + expiration: get_optional(yaml_config, "session.expiration", 3600000), // in ms + }, store_directory: get_optional(yaml_config, "store_directory", undefined), logs_level: get_optional(yaml_config, "logs_level", "info"), - notifier: ObjectPath.get(yaml_config, "notifier"), - access_control: ObjectPath.get(yaml_config, "access_control") + notifier: ObjectPath.get(yaml_config, "notifier"), + access_control: ObjectPath.get(yaml_config, "access_control") }; }; diff --git a/src/lib/server.ts b/src/lib/server.ts deleted file mode 100644 index 8e6ca8a8..00000000 --- a/src/lib/server.ts +++ /dev/null @@ -1,70 +0,0 @@ - -import { authelia } from "../types/authelia"; -import * as Express from "express"; -import * as BodyParser from "body-parser"; -import * as Path from "path"; -import { AuthenticationRegulator } from "./AuthenticationRegulator"; - -const UserDataStore = require("./user_data_store"); -const Notifier = require("./notifier"); -const setup_endpoints = require("./setup_endpoints"); -const config_adapter = require("./config_adapter"); -const Ldap = require("./ldap"); -const AccessControl = require("./access_control"); - -export function run(yaml_configuration: authelia.Configuration, deps: authelia.GlobalDependencies, fn?: () => undefined) { - const config = config_adapter(yaml_configuration); - - const view_directory = Path.resolve(__dirname, "../views"); - const public_html_directory = Path.resolve(__dirname, "../public_html"); - const datastore_options = { - directory: config.store_directory, - inMemory: config.store_in_memory - }; - - const app = Express(); - app.use(Express.static(public_html_directory)); - app.use(BodyParser.urlencoded({ extended: false })); - app.use(BodyParser.json()); - app.set("trust proxy", 1); // trust first proxy - - app.use(deps.session({ - secret: config.session_secret, - resave: false, - saveUninitialized: true, - cookie: { - secure: false, - maxAge: config.session_max_age, - domain: config.session_domain - }, - })); - - app.set("views", view_directory); - app.set("view engine", "ejs"); - - // by default the level of logs is info - deps.winston.level = config.logs_level || "info"; - - const five_minutes = 5 * 60; - const data_store = new UserDataStore(deps.nedb, datastore_options); - const regulator = new AuthenticationRegulator(data_store, five_minutes); - const notifier = new Notifier(config.notifier, deps); - const ldap = new Ldap(deps, config.ldap); - const access_control = AccessControl(deps.winston, config.access_control); - - app.set("logger", deps.winston); - app.set("ldap", ldap); - app.set("totp engine", deps.speakeasy); - app.set("u2f", deps.u2f); - app.set("user data store", data_store); - app.set("notifier", notifier); - app.set("authentication regulator", regulator); - app.set("config", config); - app.set("access control", access_control); - setup_endpoints(app); - - return app.listen(config.port, function(err: string) { - console.log("Listening on %d...", config.port); - if (fn) fn(); - }); -} diff --git a/src/lib/user_data_store.js b/src/lib/user_data_store.js deleted file mode 100644 index 8789e745..00000000 --- a/src/lib/user_data_store.js +++ /dev/null @@ -1,124 +0,0 @@ - -module.exports = UserDataStore; - -var Promise = require('bluebird'); -var path = require('path'); - -function UserDataStore(DataStore, options) { - this._u2f_meta_collection = create_collection('u2f_meta', options, DataStore); - this._identity_check_tokens_collection = - create_collection('identity_check_tokens', options, DataStore); - this._authentication_traces_collection = - create_collection('authentication_traces', options, DataStore); - this._totp_secret_collection = - create_collection('totp_secrets', options, DataStore); -} - -function create_collection(name, options, DataStore) { - var datastore_options = {}; - - if(options.directory) - datastore_options.filename = path.resolve(options.directory, name); - - datastore_options.inMemoryOnly = options.inMemoryOnly || false; - datastore_options.autoload = true; - return 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); -} - -UserDataStore.prototype.save_authentication_trace = function(userid, type, is_success) { - var newDocument = {}; - newDocument.userid = userid; - newDocument.date = new Date(); - newDocument.is_success = is_success; - newDocument.type = type; - - return this._authentication_traces_collection.insertAsync(newDocument); -} - -UserDataStore.prototype.get_last_authentication_traces = function(userid, type, is_success, count) { - var query = {}; - query.userid = userid; - query.type = type; - query.is_success = is_success; - - var query = this._authentication_traces_collection.find(query) - .sort({ date: -1 }).limit(count); - var query_promisified = Promise.promisify(query.exec, { context: query }); - return query_promisified(); -} - -UserDataStore.prototype.issue_identity_check_token = function(userid, token, data, max_age) { - var newDocument = {}; - newDocument.userid = userid; - newDocument.token = token; - newDocument.content = { userid: userid, data: data }; - newDocument.max_date = new Date(new Date().getTime() + max_age); - - return this._identity_check_tokens_collection.insertAsync(newDocument); -} - -UserDataStore.prototype.consume_identity_check_token = function(token) { - var query = {}; - query.token = token; - var that = this; - var doc_content; - - return this._identity_check_tokens_collection.findOneAsync(query) - .then(function(doc) { - if(!doc) { - return Promise.reject('Registration token does not exist'); - } - - var max_date = doc.max_date; - var current_date = new Date(); - if(current_date > max_date) { - return Promise.reject('Registration token is not valid anymore'); - } - - doc_content = doc.content; - return Promise.resolve(); - }) - .then(function() { - return that._identity_check_tokens_collection.removeAsync(query); - }) - .then(function() { - return Promise.resolve(doc_content); - }) -} - -UserDataStore.prototype.set_totp_secret = function(userid, secret) { - var doc = {} - doc.userid = userid; - doc.secret = secret; - - var query = {}; - query.userid = userid; - return this._totp_secret_collection.updateAsync(query, doc, { upsert: true }); -} - -UserDataStore.prototype.get_totp_secret = function(userid) { - var query = {}; - query.userid = userid; - return this._totp_secret_collection.findOneAsync(query); -} diff --git a/src/types/authdog.d.ts b/src/types/authdog.d.ts new file mode 100644 index 00000000..9cb1121f --- /dev/null +++ b/src/types/authdog.d.ts @@ -0,0 +1,67 @@ + +declare module "authdog" { + interface RegisterRequest { + challenge: string; + } + + interface RegisteredKey { + version: number; + keyHandle: string; + } + + type RegisteredKeys = Array; + type RegisterRequests = Array; + type AppId = string; + + interface RegistrationRequest { + appId: AppId; + type: string; + registerRequests: RegisterRequests; + registeredKeys: RegisteredKeys; + } + + interface Registration { + publicKey: string; + keyHandle: string; + certificate: string; + } + + interface ClientData { + challenge: string; + } + + interface RegistrationResponse { + clientData: ClientData; + registrationData: string; + } + + interface Options { + timeoutSeconds: number; + requestId: string; + } + + interface AuthenticationRequest { + appId: AppId; + type: string; + challenge: string; + registeredKeys: RegisteredKeys; + timeoutSeconds: number; + requestId: string; + } + + interface AuthenticationResponse { + keyHandle: string; + clientData: ClientData; + signatureData: string; + } + + interface Authentication { + userPresence: Uint8Array, + counter: Uint32Array + } + + export function startRegistration(appId: AppId, registeredKeys: RegisteredKeys, options?: Options): Promise; + export function finishRegistration(registrationRequest: RegistrationRequest, registrationResponse: RegistrationResponse): Promise; + export function startAuthentication(appId: AppId, registeredKeys: RegisteredKeys, options: Options): Promise; + export function finishAuthentication(challenge: string, deviceResponse: AuthenticationResponse, registeredKeys: RegisteredKeys): Promise; +} \ No newline at end of file diff --git a/src/types/authelia.d.ts b/src/types/authelia.d.ts deleted file mode 100644 index 94950cbb..00000000 --- a/src/types/authelia.d.ts +++ /dev/null @@ -1,61 +0,0 @@ - -import * as winston from "winston"; -import * as nedb from "nedb"; - -declare namespace authelia { - interface LdapConfiguration { - url: string; - base_dn: string; - additional_user_dn?: string; - user_name_attribute?: string; // cn by default - additional_group_dn?: string; - group_name_attribute?: string; // cn by default - user: string; // admin username - password: string; // admin password - } - - type UserName = string; - type GroupName = string; - type DomainPattern = string; - - type ACLDefaultRules = Array; - type ACLGroupsRules = Map; - type ACLUsersRules = Map; - - export interface ACLConfiguration { - default: ACLDefaultRules; - groups: ACLGroupsRules; - users: ACLUsersRules; - } - - interface SessionCookieConfiguration { - secret: string; - expiration: number; - domain: string - } - - type NotifierType = string; - export type NotifiersConfiguration = Map; - - export interface Configuration { - port: number; - logs_level: string; - ldap: LdapConfiguration | {}; - session_domain?: string; - session_secret: string; - session_max_age: number; - store_directory?: string; - notifier: NotifiersConfiguration; - access_control: ACLConfiguration; - } - - export interface GlobalDependencies { - u2f: object; - nodemailer: any; - ldapjs: object; - session: any; - winston: winston.Winston; - speakeasy: object; - nedb: object; - } -} \ No newline at end of file diff --git a/src/types/nedb-async.d.ts b/src/types/nedb-async.d.ts new file mode 100644 index 00000000..e5dc9926 --- /dev/null +++ b/src/types/nedb-async.d.ts @@ -0,0 +1,12 @@ +import Nedb = require("nedb"); +import * as Promise from "bluebird"; + +declare module "nedb" { + export class NedbAsync extends Nedb { + constructor(pathOrOptions?: string | Nedb.DataStoreOptions); + updateAsync(query: any, updateQuery: any, options?: Nedb.UpdateOptions): Promise; + findOneAsync(query: any): Promise; + insertAsync(newDoc: T): Promise; + removeAsync(query: any): Promise; + } +} \ No newline at end of file diff --git a/src/types/request-async.d.ts b/src/types/request-async.d.ts new file mode 100644 index 00000000..38a36822 --- /dev/null +++ b/src/types/request-async.d.ts @@ -0,0 +1,14 @@ +import * as Promise from "bluebird"; +import * as request from "request"; + +declare module "request" { + export interface RequestAsync extends RequestAPI { + getAsync(uri: string, options?: RequiredUriUrl): Promise; + getAsync(uri: string): Promise; + getAsync(options: RequiredUriUrl & CoreOptions): Promise; + + postAsync(uri: string, options?: CoreOptions): Promise; + postAsync(uri: string): Promise; + postAsync(options: RequiredUriUrl & CoreOptions): Promise; + } +} \ No newline at end of file diff --git a/test/unitary/AuthenticationRegulator.test.js b/test/unitary/AuthenticationRegulator.test.js deleted file mode 100644 index c06e91e2..00000000 --- a/test/unitary/AuthenticationRegulator.test.js +++ /dev/null @@ -1,71 +0,0 @@ - -import { AuthenticationRegulator } from "../../src/lib/AuthenticationRegulator"; -import * as UserDataStore from "../../src/lib/user_data_store"; -import * as DataStore from "nedb"; -import * as MockDate from "mockdate"; - -var exceptions = require('../../src/lib/exceptions'); - -describe.only('test authentication regulator', function() { - it('should mark 2 authentication and regulate (resolve)', function() { - var options = {}; - options.inMemoryOnly = true; - var data_store = new UserDataStore(DataStore, options); - var regulator = new AuthenticationRegulator(data_store, 10); - var user = 'user'; - - return regulator.mark(user, false) - .then(function() { - return regulator.mark(user, true); - }) - .then(function() { - return regulator.regulate(user); - }); - }); - - it('should mark 3 authentications and regulate (reject)', function(done) { - var options = {}; - options.inMemoryOnly = true; - var data_store = new UserDataStore(DataStore, options); - var regulator = new AuthenticationRegulator(data_store, 10); - var user = 'user'; - - regulator.mark(user, false) - .then(function() { - return regulator.mark(user, false); - }) - .then(function() { - return regulator.mark(user, false); - }) - .then(function() { - return regulator.regulate(user); - }) - .catch(exceptions.AuthenticationRegulationError, function() { - done(); - }) - }); - - it('should mark 3 authentications and regulate (resolve)', function(done) { - var options = {}; - options.inMemoryOnly = true; - var data_store = new UserDataStore(DataStore, options); - var regulator = new AuthenticationRegulator(data_store, 10); - var user = 'user'; - - MockDate.set('1/2/2000 00:00:00'); - regulator.mark(user, false) - .then(function() { - MockDate.set('1/2/2000 00:00:15'); - return regulator.mark(user, false); - }) - .then(function() { - return regulator.mark(user, false); - }) - .then(function() { - return regulator.regulate(user); - }) - .then(function() { - done(); - }) - }); -}); diff --git a/test/unitary/AuthenticationRegulator.test.ts b/test/unitary/AuthenticationRegulator.test.ts new file mode 100644 index 00000000..3ee9e1c2 --- /dev/null +++ b/test/unitary/AuthenticationRegulator.test.ts @@ -0,0 +1,73 @@ + +import { AuthenticationRegulator } from "../../src/lib/AuthenticationRegulator"; +import UserDataStore from "../../src/lib/UserDataStore"; +import * as MockDate from "mockdate"; + +const exceptions = require("../../src/lib/exceptions"); + +describe("test authentication regulator", function() { + it("should mark 2 authentication and regulate (resolve)", function() { + const options = { + inMemoryOnly: true + }; + const data_store = new UserDataStore(options); + const regulator = new AuthenticationRegulator(data_store, 10); + const user = "user"; + + return regulator.mark(user, false) + .then(function() { + return regulator.mark(user, true); + }) + .then(function() { + return regulator.regulate(user); + }); + }); + + it("should mark 3 authentications and regulate (reject)", function(done) { + const options = { + inMemoryOnly: true + }; + const data_store = new UserDataStore(options); + const regulator = new AuthenticationRegulator(data_store, 10); + const user = "user"; + + regulator.mark(user, false) + .then(function() { + return regulator.mark(user, false); + }) + .then(function() { + return regulator.mark(user, false); + }) + .then(function() { + return regulator.regulate(user); + }) + .catch(exceptions.AuthenticationRegulationError, function() { + done(); + }); + }); + + it("should mark 3 authentications and regulate (resolve)", function(done) { + const options = { + inMemoryOnly: true + }; + const data_store = new UserDataStore(options); + const regulator = new AuthenticationRegulator(data_store, 10); + const user = "user"; + + MockDate.set("1/2/2000 00:00:00"); + regulator.mark(user, false) + .then(function() { + MockDate.set("1/2/2000 00:00:15"); + return regulator.mark(user, false); + }) + .then(function() { + return regulator.mark(user, false); + }) + .then(function() { + return regulator.regulate(user); + }) + .then(function() { + done(); + }); + }); +}); diff --git a/test/unitary/Server.test.ts b/test/unitary/Server.test.ts new file mode 100644 index 00000000..cd9d0630 --- /dev/null +++ b/test/unitary/Server.test.ts @@ -0,0 +1,393 @@ + +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"; + + +const requestp = Promise.promisifyAll(request) as request.RequestAsync; +const assert = require("assert"); +const sinon = require("sinon"); +const MockDate = require("mockdate"); +const session = require("express-session"); +const winston = require("winston"); +const ldapjs = require("ldapjs"); + +const PORT = 8090; +const BASE_URL = "http://localhost:" + PORT; +const requests = require("./requests")(PORT); + +describe("test the server", function () { + let server: Server; + let transporter: object; + let u2f: any; + + 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", + user_name_attribute: "cn", + user: "cn=admin,dc=example,dc=com", + password: "password", + }, + session: { + secret: "session_secret", + expiration: 50000, + }, + store_in_memory: true, + notifier: { + gmail: { + user: "user@example.com", + pass: "password" + } + } + }; + + const ldap_client = { + bind: sinon.stub(), + search: sinon.stub(), + modify: sinon.stub(), + on: sinon.spy() + }; + const ldap = { + Change: sinon.spy(), + createClient: sinon.spy(function () { + return ldap_client; + }) + }; + + u2f = { + startRegistration: sinon.stub(), + finishRegistration: sinon.stub(), + startAuthentication: sinon.stub(), + finishAuthentication: sinon.stub() + }; + + transporter = { + sendMail: sinon.stub().yields() + }; + + const nodemailer = { + createTransport: sinon.spy(function () { + return transporter; + }) + }; + + const ldap_document = { + object: { + mail: "test_ok@example.com", + } + }; + + const search_res = { + on: sinon.spy(function (event: string, fn: (s: any) => void) { + if (event != "error") fn(ldap_document); + }) + }; + + ldap_client.bind.withArgs("cn=test_ok,ou=users,dc=example,dc=com", + "password").yields(undefined); + ldap_client.bind.withArgs("cn=admin,dc=example,dc=com", + "password").yields(undefined); + + ldap_client.bind.withArgs("cn=test_nok,ou=users,dc=example,dc=com", + "password").yields("error"); + + ldap_client.modify.yields(undefined); + ldap_client.search.yields(undefined, search_res); + + const deps = { + u2f: u2f, + nedb: nedb, + nodemailer: nodemailer, + ldapjs: ldap, + session: session, + winston: winston, + speakeasy: speakeasy + }; + + server = new Server(); + return server.start(config, deps); + }); + + afterEach(function () { + server.stop(); + }); + + describe("test GET /login", function () { + test_login(); + }); + + describe("test GET /logout", function () { + test_logout(); + }); + + describe("test GET /reset-password-form", function () { + test_reset_password_form(); + }); + + describe("test endpoints locks", function () { + function should_post_and_reply_with(url: string, status_code: number) { + return requestp.postAsync(url).then(function (response: request.RequestResponse) { + assert.equal(response.statusCode, status_code); + return Promise.resolve(); + }); + } + + function should_get_and_reply_with(url: string, status_code: number) { + return requestp.getAsync(url).then(function (response: request.RequestResponse) { + assert.equal(response.statusCode, status_code); + return Promise.resolve(); + }); + } + + function should_post_and_reply_with_403(url: string) { + return should_post_and_reply_with(url, 403); + } + function should_get_and_reply_with_403(url: string) { + return should_get_and_reply_with(url, 403); + } + + function should_post_and_reply_with_401(url: string) { + return should_post_and_reply_with(url, 401); + } + function should_get_and_reply_with_401(url: string) { + return should_get_and_reply_with(url, 401); + } + + function should_get_and_post_reply_with_403(url: string) { + const p1 = should_post_and_reply_with_403(url); + const p2 = should_get_and_reply_with_403(url); + return Promise.all([p1, p2]); + } + + it("should block /new-password", function () { + return should_post_and_reply_with_403(BASE_URL + "/new-password"); + }); + + it("should block /u2f-register", function () { + return should_get_and_post_reply_with_403(BASE_URL + "/u2f-register"); + }); + + it("should block /reset-password", function () { + return should_get_and_post_reply_with_403(BASE_URL + "/reset-password"); + }); + + it("should block /2ndfactor/u2f/register_request", function () { + return should_get_and_reply_with_403(BASE_URL + "/2ndfactor/u2f/register_request"); + }); + + it("should block /2ndfactor/u2f/register", function () { + return should_post_and_reply_with_403(BASE_URL + "/2ndfactor/u2f/register"); + }); + + it("should block /2ndfactor/u2f/sign_request", function () { + return should_get_and_reply_with_403(BASE_URL + "/2ndfactor/u2f/sign_request"); + }); + + it("should block /2ndfactor/u2f/sign", function () { + return should_post_and_reply_with_403(BASE_URL + "/2ndfactor/u2f/sign"); + }); + }); + + describe("test authentication and verification", function () { + test_authentication(); + test_reset_password(); + test_regulation(); + }); + + function test_reset_password_form() { + it("should serve the reset password form page", function (done) { + requestp.getAsync(BASE_URL + "/reset-password-form") + .then(function (response: request.RequestResponse) { + assert.equal(response.statusCode, 200); + done(); + }); + }); + } + + function test_login() { + it("should serve the login page", function (done) { + requestp.getAsync(BASE_URL + "/login") + .then(function (response: request.RequestResponse) { + assert.equal(response.statusCode, 200); + done(); + }); + }); + } + + function test_logout() { + it("should logout and redirect to /", function (done) { + requestp.getAsync(BASE_URL + "/logout") + .then(function (response: any) { + assert.equal(response.req.path, "/"); + done(); + }); + }); + } + + function test_authentication() { + it("should return status code 401 when user is not authenticated", function () { + return requestp.getAsync({ url: BASE_URL + "/verify" }) + .then(function (response: request.RequestResponse) { + assert.equal(response.statusCode, 401); + return Promise.resolve(); + }); + }); + + it("should return status code 204 when user is authenticated using totp", function () { + const j = requestp.jar(); + return requests.login(j) + .then(function (res: request.RequestResponse) { + assert.equal(res.statusCode, 200, "get login page failed"); + return requests.first_factor(j); + }) + .then(function (res: request.RequestResponse) { + assert.equal(res.statusCode, 204, "first factor failed"); + return requests.register_totp(j, transporter); + }) + .then(function (secret: string) { + const sec = JSON.parse(secret) as TOTPSecret; + const real_token = speakeasy.totp({ + secret: sec.base32, + encoding: "base32" + }); + return requests.totp(j, real_token); + }) + .then(function (res: request.RequestResponse) { + assert.equal(res.statusCode, 204, "second factor failed"); + return requests.verify(j); + }) + .then(function (res: request.RequestResponse) { + assert.equal(res.statusCode, 204, "verify failed"); + return Promise.resolve(); + }); + }); + + it("should keep session variables when login page is reloaded", function () { + const real_token = speakeasy.totp({ + secret: "totp_secret", + encoding: "base32" + }); + const j = requestp.jar(); + return requests.login(j) + .then(function (res: request.RequestResponse) { + assert.equal(res.statusCode, 200, "get login page failed"); + return requests.first_factor(j); + }) + .then(function (res: request.RequestResponse) { + assert.equal(res.statusCode, 204, "first factor failed"); + return requests.totp(j, real_token); + }) + .then(function (res: request.RequestResponse) { + assert.equal(res.statusCode, 204, "second factor failed"); + return requests.login(j); + }) + .then(function (res: request.RequestResponse) { + assert.equal(res.statusCode, 200, "login page loading failed"); + return requests.verify(j); + }) + .then(function (res: request.RequestResponse) { + assert.equal(res.statusCode, 204, "verify failed"); + return Promise.resolve(); + }) + .catch(function (err: Error) { + console.error(err); + }); + }); + + it("should return status code 204 when user is authenticated using u2f", function () { + const sign_request = {}; + const sign_status = {}; + const registration_request = {}; + const 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)); + + const j = requestp.jar(); + return requests.login(j) + .then(function (res: request.RequestResponse) { + assert.equal(res.statusCode, 200, "get login page failed"); + return requests.first_factor(j); + }) + .then(function (res: request.RequestResponse) { + assert.equal(res.statusCode, 204, "first factor failed"); + return requests.u2f_registration(j, transporter); + }) + .then(function (res: request.RequestResponse) { + assert.equal(res.statusCode, 204, "second factor, finish register failed"); + return requests.u2f_authentication(j); + }) + .then(function (res: request.RequestResponse) { + assert.equal(res.statusCode, 204, "second factor, finish sign failed"); + return requests.verify(j); + }) + .then(function (res: request.RequestResponse) { + assert.equal(res.statusCode, 204, "verify failed"); + return Promise.resolve(); + }); + }); + } + + function test_reset_password() { + it("should reset the password", function () { + const j = requestp.jar(); + return requests.login(j) + .then(function (res: request.RequestResponse) { + assert.equal(res.statusCode, 200, "get login page failed"); + return requests.first_factor(j); + }) + .then(function (res: request.RequestResponse) { + assert.equal(res.statusCode, 204, "first factor failed"); + return requests.reset_password(j, transporter, "user", "new-password"); + }) + .then(function (res: request.RequestResponse) { + assert.equal(res.statusCode, 204, "second factor, finish register failed"); + return Promise.resolve(); + }); + }); + } + + function test_regulation() { + it("should regulate authentication", function () { + const j = requestp.jar(); + MockDate.set("1/2/2017 00:00:00"); + return requests.login(j) + .then(function (res: request.RequestResponse) { + assert.equal(res.statusCode, 200, "get login page failed"); + return requests.failing_first_factor(j); + }) + .then(function (res: request.RequestResponse) { + assert.equal(res.statusCode, 401, "first factor failed"); + return requests.failing_first_factor(j); + }) + .then(function (res: request.RequestResponse) { + assert.equal(res.statusCode, 401, "first factor failed"); + return requests.failing_first_factor(j); + }) + .then(function (res: request.RequestResponse) { + assert.equal(res.statusCode, 401, "first factor failed"); + return requests.failing_first_factor(j); + }) + .then(function (res: request.RequestResponse) { + assert.equal(res.statusCode, 403, "first factor failed"); + MockDate.set("1/2/2017 00:30:00"); + return requests.failing_first_factor(j); + }) + .then(function (res: request.RequestResponse) { + assert.equal(res.statusCode, 401, "first factor failed"); + return Promise.resolve(); + }); + }); + } +}); + diff --git a/test/unitary/UserDataStore.test.ts b/test/unitary/UserDataStore.test.ts new file mode 100644 index 00000000..a631a946 --- /dev/null +++ b/test/unitary/UserDataStore.test.ts @@ -0,0 +1,206 @@ + +import UserDataStore from "../../src/lib/UserDataStore"; +import { U2FMetaDocument, Options } from "../../src/lib/UserDataStore"; + +import DataStore = require("nedb"); +import assert = require("assert"); +import Promise = require("bluebird"); +import sinon = require("sinon"); +import MockDate = require("mockdate"); + +describe("test user data store", () => { + let options: Options; + + beforeEach(function () { + options = { + inMemoryOnly: true + }; + }); + + + describe("test u2f meta", () => { + it("should save a u2f meta", function () { + const data_store = new UserDataStore(options); + + const userid = "user"; + const app_id = "https://localhost"; + const 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 () { + const options = { + inMemoryOnly: true + }; + + const data_store = new UserDataStore(options); + + const userid = "user"; + const app_id = "https://localhost"; + const 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 () { + const options = { + inMemoryOnly: true + }; + + const data_store = new UserDataStore(options); + + const userid = "user"; + const app_id = "https://localhost"; + const meta = { + publicKey: "pbk" + }; + + return data_store.set_u2f_meta(userid, app_id, meta) + .then(function (numUpdated: number) { + assert.equal(1, numUpdated); + return data_store.get_u2f_meta(userid, app_id); + }) + .then(function (doc: U2FMetaDocument) { + assert.deepEqual(meta, doc.meta); + assert.deepEqual(userid, doc.userid); + assert.deepEqual(app_id, doc.appid); + assert("_id" in doc); + return Promise.resolve(); + }); + }); + }); + + + describe("test u2f registration token", () => { + it("should save u2f registration token", function () { + const data_store = new UserDataStore(options); + + const userid = "user"; + const token = "token"; + const max_age = 60; + const content = "abc"; + + return data_store.issue_identity_check_token(userid, token, content, max_age) + .then(function (document) { + assert.equal(document.userid, userid); + assert.equal(document.token, token); + assert.deepEqual(document.content, { userid: "user", data: content }); + assert("max_date" in document); + assert("_id" in document); + return Promise.resolve(); + }) + .catch(function (err) { + console.error(err); + return Promise.reject(err); + }); + }); + + it("should save u2f registration token and consume it", function (done) { + const data_store = new UserDataStore(options); + + const userid = "user"; + const token = "token"; + const max_age = 50; + + data_store.issue_identity_check_token(userid, token, {}, max_age) + .then(function (document) { + return data_store.consume_identity_check_token(token); + }) + .then(function () { + done(); + }) + .catch(function (err) { + console.error(err); + }); + }); + + it("should not be able to consume registration token twice", function (done) { + const data_store = new UserDataStore(options); + + const userid = "user"; + const token = "token"; + const max_age = 50; + + data_store.issue_identity_check_token(userid, token, {}, max_age) + .then(function (document) { + return data_store.consume_identity_check_token(token); + }) + .then(function (document) { + return data_store.consume_identity_check_token(token); + }) + .catch(function (err) { + console.error(err); + done(); + }); + }); + + it("should fail when token does not exist", function () { + const data_store = new UserDataStore(options); + + const token = "token"; + + return data_store.consume_identity_check_token(token) + .then(function (document) { + return Promise.reject("Error while checking token"); + }) + .catch(function (err) { + return Promise.resolve(err); + }); + }); + + it("should fail when token expired", function (done) { + const data_store = new UserDataStore(options); + + const userid = "user"; + const token = "token"; + const max_age = 60; + MockDate.set("1/1/2000"); + + data_store.issue_identity_check_token(userid, token, {}, max_age) + .then(function () { + MockDate.set("1/2/2000"); + return data_store.consume_identity_check_token(token); + }) + .catch(function (err) { + MockDate.reset(); + done(); + }); + }); + + it("should save the userid and some data with the token", function (done) { + const data_store = new UserDataStore(options); + + const userid = "user"; + const token = "token"; + const max_age = 60; + MockDate.set("1/1/2000"); + const data = "abc"; + + data_store.issue_identity_check_token(userid, token, data, max_age) + .then(function () { + return data_store.consume_identity_check_token(token); + }) + .then(function (content) { + const expected_content = { + userid: "user", + data: "abc" + }; + assert.deepEqual(content, expected_content); + done(); + }); + }); + }); +}); diff --git a/test/unitary/config_adapter.test.ts b/test/unitary/config_adapter.test.ts index 51a4eef9..6f55ac2b 100644 --- a/test/unitary/config_adapter.test.ts +++ b/test/unitary/config_adapter.test.ts @@ -1,19 +1,31 @@ import * as Assert from "assert"; +import { UserConfiguration } from "../../src/lib/Configuration"; +import config_adapter = require("../../src/lib/config_adapter"); -const config_adapter = require("../../src/lib/config_adapter"); describe("test config adapter", function() { - function build_yaml_config(): any { + function build_yaml_config(): UserConfiguration { const yaml_config = { port: 8080, - ldap: {}, + ldap: { + url: "http://ldap", + base_dn: "cn=test,dc=example,dc=com", + user: "user", + password: "pass" + }, session: { domain: "example.com", secret: "secret", max_age: 40000 }, store_directory: "/mydirectory", - logs_level: "debug" + logs_level: "debug", + notifier: { + gmail: { + user: "user", + pass: "password" + } + } }; return yaml_config; } @@ -36,8 +48,9 @@ describe("test config adapter", function() { const yaml_config = build_yaml_config(); yaml_config.ldap = { url: "http://ldap", - user_search_base: "ou=groups,dc=example,dc=com", - user_search_filter: "uid", + base_dn: "cn=test,dc=example,dc=com", + additional_user_dn: "ou=users", + user_name_attribute: "uid", user: "admin", password: "pass" }; @@ -45,8 +58,8 @@ describe("test config adapter", function() { const config = config_adapter(yaml_config); Assert.equal(config.ldap.url, "http://ldap"); - Assert.equal(config.ldap.user_search_base, "ou=groups,dc=example,dc=com"); - Assert.equal(config.ldap.user_search_filter, "uid"); + Assert.equal(config.ldap.additional_user_dn, "ou=users"); + Assert.equal(config.ldap.user_name_attribute, "uid"); Assert.equal(config.ldap.user, "admin"); Assert.equal(config.ldap.password, "pass"); }); @@ -59,9 +72,9 @@ describe("test config adapter", function() { expiration: 3600 }; const config = config_adapter(yaml_config); - Assert.equal(config.session_domain, "example.com"); - Assert.equal(config.session_secret, "secret"); - Assert.equal(config.session_max_age, 3600); + Assert.equal(config.session.domain, "example.com"); + Assert.equal(config.session.secret, "secret"); + Assert.equal(config.session.expiration, 3600); }); it("should get the log level", function() { @@ -73,15 +86,33 @@ describe("test config adapter", function() { it("should get the notifier config", function() { const yaml_config = build_yaml_config(); - yaml_config.notifier = "notifier"; + yaml_config.notifier = { + gmail: { + user: "user", + pass: "pass" + } + }; const config = config_adapter(yaml_config); - Assert.equal(config.notifier, "notifier"); + Assert.deepEqual(config.notifier, { + gmail: { + user: "user", + pass: "pass" + } + }); }); it("should get the access_control config", function() { const yaml_config = build_yaml_config(); - yaml_config.access_control = "access_control"; + yaml_config.access_control = { + default: [], + users: {}, + groups: {} + }; const config = config_adapter(yaml_config); - Assert.equal(config.access_control, "access_control"); + Assert.deepEqual(config.access_control, { + default: [], + users: {}, + groups: {} + }); }); }); diff --git a/test/unitary/data_persistence.test.ts b/test/unitary/data_persistence.test.ts new file mode 100644 index 00000000..4f2154f5 --- /dev/null +++ b/test/unitary/data_persistence.test.ts @@ -0,0 +1,179 @@ + +import * as Promise from "bluebird"; +import * as request from "request"; + +import Server from "../../src/lib/Server"; +import { UserConfiguration } from "../../src/lib/Configuration"; +import { GlobalDependencies } from "../../src/lib/GlobalDependencies"; +import * as tmp from "tmp"; + + +const requestp = Promise.promisifyAll(request) as request.Request; +const assert = require("assert"); +const speakeasy = require("speakeasy"); +const sinon = require("sinon"); +const nedb = require("nedb"); +const session = require("express-session"); +const winston = require("winston"); + +const PORT = 8050; +const requests = require("./requests")(PORT); + +describe("test data persistence", function () { + let u2f: any; + let tmpDir: tmp.SynchrounousResult; + const ldap_client = { + bind: sinon.stub(), + search: sinon.stub(), + on: sinon.spy() + }; + const ldap = { + createClient: sinon.spy(function () { + return ldap_client; + }) + }; + + let config: UserConfiguration; + + before(function () { + u2f = { + startRegistration: sinon.stub(), + finishRegistration: sinon.stub(), + startAuthentication: sinon.stub(), + finishAuthentication: sinon.stub() + }; + + const search_doc = { + object: { + mail: "test_ok@example.com" + } + }; + + const search_res = { + on: sinon.spy(function (event: string, fn: (s: object) => void) { + if (event != "error") fn(search_doc); + }) + }; + + 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"); + ldap_client.search.yields(undefined, search_res); + + tmpDir = tmp.dirSync({ unsafeCleanup: true }); + config = { + port: PORT, + ldap: { + url: "ldap://127.0.0.1:389", + base_dn: "ou=users,dc=example,dc=com", + user: "user", + password: "password" + }, + session: { + secret: "session_secret", + expiration: 50000, + }, + store_directory: tmpDir.name, + notifier: { + gmail: { + user: "user@example.com", + pass: "password" + } + } + }; + }); + + after(function () { + tmpDir.removeCallback(); + }); + + it("should save a u2f meta and reload it after a restart of the server", function () { + let server: Server; + const sign_request = {}; + const sign_status = {}; + const registration_request = {}; + const 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)); + + const nodemailer = { + createTransport: sinon.spy(function () { + return transporter; + }) + }; + const transporter = { + sendMail: sinon.stub().yields() + }; + + const deps = { + u2f: u2f, + nedb: nedb, + nodemailer: nodemailer, + session: session, + winston: winston, + ldapjs: ldap, + speakeasy: speakeasy + } as GlobalDependencies; + + const j1 = request.jar(); + const j2 = request.jar(); + + return start_server(config, deps) + .then(function (s) { + server = s; + return requests.login(j1); + }) + .then(function (res) { + return requests.first_factor(j1); + }) + .then(function () { + return requests.u2f_registration(j1, transporter); + }) + .then(function () { + return requests.u2f_authentication(j1); + }) + .then(function () { + return stop_server(server); + }) + .then(function () { + return start_server(config, deps); + }) + .then(function (s) { + server = s; + return requests.login(j2); + }) + .then(function () { + return requests.first_factor(j2); + }) + .then(function () { + return requests.u2f_authentication(j2); + }) + .then(function (res) { + assert.equal(204, res.statusCode); + server.stop(); + return Promise.resolve(); + }) + .catch(function (err) { + console.error(err); + return Promise.reject(err); + }); + }); + + function start_server(config: UserConfiguration, deps: GlobalDependencies): Promise { + return new Promise(function (resolve, reject) { + const s = new Server(); + s.start(config, deps); + resolve(s); + }); + } + + function stop_server(s: Server) { + return new Promise(function (resolve, reject) { + s.stop(); + resolve(); + }); + } +}); diff --git a/test/unitary/server_config.test.ts b/test/unitary/server_config.test.ts new file mode 100644 index 00000000..6e4bf17f --- /dev/null +++ b/test/unitary/server_config.test.ts @@ -0,0 +1,72 @@ + +import * as assert from "assert"; +import * as sinon from "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 { AppConfiguration, UserConfiguration } from "../../src/lib/Configuration"; +import { GlobalDependencies } from "../../src/lib/GlobalDependencies"; +import Server from "../../src/lib/Server"; + + +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 = { + speakeasy: speakeasy, + u2f: u2f, + nedb: nedb, + winston: winston, + nodemailer: nodemailer, + 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 = { + session: { + domain: "example.com", + secret: "secret" + }, + ldap: { + url: "http://ldap", + user: "user", + password: "password" + }, + notifier: { + gmail: { + user: "user@example.com", + pass: "password" + } + } + } as UserConfiguration; + + 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/test_data_persistence.js b/test/unitary/test_data_persistence.js deleted file mode 100644 index ec6efbdb..00000000 --- a/test/unitary/test_data_persistence.js +++ /dev/null @@ -1,162 +0,0 @@ - -var server = require('../../src/lib/server'); - -var Promise = require('bluebird'); -var request = Promise.promisifyAll(require('request')); -var assert = require('assert'); -var speakeasy = require('speakeasy'); -var sinon = require('sinon'); -var tmp = require('tmp'); -var nedb = require('nedb'); -var session = require('express-session'); -var winston = require('winston'); - -var PORT = 8050; -var requests = require('./requests')(PORT); - - -describe('test data persistence', function() { - var u2f; - var tmpDir; - var ldap_client = { - bind: sinon.stub(), - search: sinon.stub(), - on: sinon.spy() - }; - var ldap = { - createClient: sinon.spy(function() { - return ldap_client; - }) - } - var config; - - before(function() { - u2f = {}; - u2f.startRegistration = sinon.stub(); - u2f.finishRegistration = sinon.stub(); - u2f.startAuthentication = sinon.stub(); - u2f.finishAuthentication = sinon.stub(); - - var search_doc = { - object: { - mail: 'test_ok@example.com' - } - }; - - var search_res = {}; - search_res.on = sinon.spy(function(event, fn) { - if(event != 'error') fn(search_doc); - }); - - 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'); - ldap_client.search.yields(undefined, search_res); - - tmpDir = tmp.dirSync({ unsafeCleanup: true }); - config = { - port: PORT, - totp_secret: 'totp_secret', - ldap: { - url: 'ldap://127.0.0.1:389', - base_dn: 'ou=users,dc=example,dc=com', - }, - session: { - secret: 'session_secret', - expiration: 50000, - }, - store_directory: tmpDir.name, - notifier: { gmail: { user: 'user@example.com', pass: 'password' } } - }; - }); - - 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 nodemailer = {}; - var transporter = { - sendMail: sinon.stub().yields() - }; - nodemailer.createTransport = sinon.spy(function() { - return transporter; - }); - - var deps = {}; - deps.u2f = u2f; - deps.nedb = nedb; - deps.nodemailer = nodemailer; - deps.session = session; - deps.winston = winston; - deps.ldapjs = ldap; - - var j1 = request.jar(); - var j2 = request.jar(); - - return start_server(config, deps) - .then(function(s) { - server = s; - return requests.login(j1); - }) - .then(function(res) { - return requests.first_factor(j1); - }) - .then(function() { - return requests.u2f_registration(j1, transporter); - }) - .then(function() { - return requests.u2f_authentication(j1); - }) - .then(function() { - return stop_server(server); - }) - .then(function() { - return start_server(config, deps) - }) - .then(function(s) { - server = s; - return requests.login(j2); - }) - .then(function() { - return requests.first_factor(j2); - }) - .then(function() { - return requests.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, deps) { - return new Promise(function(resolve, reject) { - var s = server.run(config, deps); - resolve(s); - }); - } - - function stop_server(s) { - return new Promise(function(resolve, reject) { - s.close(); - resolve(); - }); - } -}); diff --git a/test/unitary/test_server.js b/test/unitary/test_server.js deleted file mode 100644 index 3a94d7aa..00000000 --- a/test/unitary/test_server.js +++ /dev/null @@ -1,389 +0,0 @@ - -var server = require('../../src/lib/server'); -var Ldap = require('../../src/lib/ldap'); - -var Promise = require('bluebird'); -var request = Promise.promisifyAll(require('request')); -var assert = require('assert'); -var speakeasy = require('speakeasy'); -var sinon = require('sinon'); -var MockDate = require('mockdate'); -var session = require('express-session'); -var winston = require('winston'); -var speakeasy = require('speakeasy'); -var ldapjs = require('ldapjs'); - -var PORT = 8090; -var BASE_URL = 'http://localhost:' + PORT; -var requests = require('./requests')(PORT); - -describe('test the server', function() { - var _server - var deps; - var u2f, nedb; - var transporter; - var collection; - - beforeEach(function(done) { - var config = { - port: PORT, - totp_secret: 'totp_secret', - ldap: { - url: 'ldap://127.0.0.1:389', - base_dn: 'ou=users,dc=example,dc=com', - user_name_attribute: 'cn', - user: 'cn=admin,dc=example,dc=com', - password: 'password', - }, - session: { - secret: 'session_secret', - expiration: 50000, - }, - store_in_memory: true, - notifier: { - gmail: { - user: 'user@example.com', - pass: 'password' - } - } - }; - - var ldap_client = { - bind: sinon.stub(), - search: sinon.stub(), - modify: sinon.stub(), - on: sinon.spy() - }; - var ldap = { - Change: sinon.spy(), - createClient: sinon.spy(function() { - return ldap_client; - }) - }; - - u2f = {}; - u2f.startRegistration = sinon.stub(); - u2f.finishRegistration = sinon.stub(); - u2f.startAuthentication = sinon.stub(); - u2f.finishAuthentication = sinon.stub(); - - nedb = require('nedb'); - - transporter = {}; - transporter.sendMail = sinon.stub().yields(); - - var nodemailer = {}; - nodemailer.createTransport = sinon.spy(function() { - return transporter; -  }); - - ldap_document = { - object: { - mail: 'test_ok@example.com', - } - }; - - var search_res = {}; - search_res.on = sinon.spy(function(event, fn) { - if(event != 'error') fn(ldap_document); - }); - - ldap_client.bind.withArgs('cn=test_ok,ou=users,dc=example,dc=com', - 'password').yields(undefined); - ldap_client.bind.withArgs('cn=admin,dc=example,dc=com', - 'password').yields(undefined); - - ldap_client.bind.withArgs('cn=test_nok,ou=users,dc=example,dc=com', - 'password').yields('error'); - - ldap_client.modify.yields(undefined); - ldap_client.search.yields(undefined, search_res); - - var deps = {}; - deps.u2f = u2f; - deps.nedb = nedb; - deps.nodemailer = nodemailer; - deps.ldapjs = ldap; - deps.session = session; - deps.winston = winston; - deps.speakeasy = speakeasy; - - _server = server.run(config, deps, function() { - done(); - }); - }); - - afterEach(function() { - _server.close(); -  }); - - describe('test GET /login', function() { - test_login(); - }); - - describe('test GET /logout', function() { - test_logout(); - }); - - describe('test GET /reset-password-form', function() { - test_reset_password_form(); - }); - - describe('test endpoints locks', function() { - function should_post_and_reply_with(url, status_code) { - return request.postAsync(url).then(function(response) { - assert.equal(response.statusCode, status_code); - return Promise.resolve(); - }) - } - - function should_get_and_reply_with(url, status_code) { - return request.getAsync(url).then(function(response) { - assert.equal(response.statusCode, status_code); - return Promise.resolve(); - }) - } - - function should_post_and_reply_with_403(url) { - return should_post_and_reply_with(url, 403); -  } - function should_get_and_reply_with_403(url) { - return should_get_and_reply_with(url, 403); -  } - - function should_post_and_reply_with_401(url) { - return should_post_and_reply_with(url, 401); -  } - function should_get_and_reply_with_401(url) { - return should_get_and_reply_with(url, 401); -  } - - function should_get_and_post_reply_with_403(url) { - var p1 = should_post_and_reply_with_403(url); - var p2 = should_get_and_reply_with_403(url); - return Promise.all([p1, p2]); -  } - - it('should block /new-password', function() { - return should_post_and_reply_with_403(BASE_URL + '/new-password') - }); - - it('should block /u2f-register', function() { - return should_get_and_post_reply_with_403(BASE_URL + '/u2f-register'); - }); - - it('should block /reset-password', function() { - return should_get_and_post_reply_with_403(BASE_URL + '/reset-password'); - }); - - it('should block /2ndfactor/u2f/register_request', function() { - return should_get_and_reply_with_403(BASE_URL + '/2ndfactor/u2f/register_request'); - }); - - it('should block /2ndfactor/u2f/register', function() { - return should_post_and_reply_with_403(BASE_URL + '/2ndfactor/u2f/register'); - }); - - it('should block /2ndfactor/u2f/sign_request', function() { - return should_get_and_reply_with_403(BASE_URL + '/2ndfactor/u2f/sign_request'); - }); - - it('should block /2ndfactor/u2f/sign', function() { - return should_post_and_reply_with_403(BASE_URL + '/2ndfactor/u2f/sign'); - }); - }); - - describe('test authentication and verification', function() { - test_authentication(); - test_reset_password(); - test_regulation(); - }); - - function test_reset_password_form() { - it('should serve the reset password form page', function(done) { - request.getAsync(BASE_URL + '/reset-password-form') - .then(function(response) { - assert.equal(response.statusCode, 200); - done(); - }); - }); - } - - function test_login() { - it('should serve the login page', function(done) { - request.getAsync(BASE_URL + '/login') - .then(function(response) { - assert.equal(response.statusCode, 200); - done(); - }); - }); - } - - function test_logout() { - it('should logout and redirect to /', function(done) { - request.getAsync(BASE_URL + '/logout') - .then(function(response) { - assert.equal(response.req.path, '/'); - done(); - }); - }); - } - - function test_authentication() { - it('should return status code 401 when user is not authenticated', function() { - return request.getAsync({ url: BASE_URL + '/verify' }) - .then(function(response) { - assert.equal(response.statusCode, 401); - return Promise.resolve(); - }); - }); - - it('should return status code 204 when user is authenticated using totp', function() { - var j = request.jar(); - return requests.login(j) - .then(function(res) { - assert.equal(res.statusCode, 200, 'get login page failed'); - return requests.first_factor(j); - }) - .then(function(res) { - assert.equal(res.statusCode, 204, 'first factor failed'); - return requests.register_totp(j, transporter); - }) - .then(function(secret) { - var sec = JSON.parse(secret); - var real_token = speakeasy.totp({ - secret: sec.base32, - encoding: 'base32' - }); - return requests.totp(j, real_token); - }) - .then(function(res) { - assert.equal(res.statusCode, 204, 'second factor failed'); - return requests.verify(j); - }) - .then(function(res) { - assert.equal(res.statusCode, 204, 'verify failed'); - return Promise.resolve(); - }); - }); - - it('should keep session variables when login page is reloaded', function() { - var real_token = speakeasy.totp({ - secret: 'totp_secret', - encoding: 'base32' - }); - var j = request.jar(); - return requests.login(j) - .then(function(res) { - assert.equal(res.statusCode, 200, 'get login page failed'); - return requests.first_factor(j); - }) - .then(function(res) { - assert.equal(res.statusCode, 204, 'first factor failed'); - return requests.totp(j, real_token); - }) - .then(function(res) { - assert.equal(res.statusCode, 204, 'second factor failed'); - return requests.login(j); - }) - .then(function(res) { - assert.equal(res.statusCode, 200, 'login page loading failed'); - return requests.verify(j); - }) - .then(function(res) { - assert.equal(res.statusCode, 204, 'verify failed'); - return Promise.resolve(); - }) - .catch(function(err) { - console.error(err); -  }); - }); - - it('should return status code 204 when user is authenticated using u2f', function() { - 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 j = request.jar(); - return requests.login(j) - .then(function(res) { - assert.equal(res.statusCode, 200, 'get login page failed'); - return requests.first_factor(j); - }) - .then(function(res) { - assert.equal(res.statusCode, 204, 'first factor failed'); - return requests.u2f_registration(j, transporter); - }) - .then(function(res) { - assert.equal(res.statusCode, 204, 'second factor, finish register failed'); - return requests.u2f_authentication(j); - }) - .then(function(res) { - assert.equal(res.statusCode, 204, 'second factor, finish sign failed'); - return requests.verify(j); - }) - .then(function(res) { - assert.equal(res.statusCode, 204, 'verify failed'); - return Promise.resolve(); - }); - }); - } - - function test_reset_password() { - it('should reset the password', function() { - var j = request.jar(); - return requests.login(j) - .then(function(res) { - assert.equal(res.statusCode, 200, 'get login page failed'); - return requests.first_factor(j); - }) - .then(function(res) { - assert.equal(res.statusCode, 204, 'first factor failed'); - return requests.reset_password(j, transporter, 'user', 'new-password'); - }) - .then(function(res) { - assert.equal(res.statusCode, 204, 'second factor, finish register failed'); - return Promise.resolve(); - }); - }); - } - - function test_regulation() { - it('should regulate authentication', function() { - var j = request.jar(); - MockDate.set('1/2/2017 00:00:00'); - return requests.login(j) - .then(function(res) { - assert.equal(res.statusCode, 200, 'get login page failed'); - return requests.failing_first_factor(j); - }) - .then(function(res) { - assert.equal(res.statusCode, 401, 'first factor failed'); - return requests.failing_first_factor(j); - }) - .then(function(res) { - assert.equal(res.statusCode, 401, 'first factor failed'); - return requests.failing_first_factor(j); - }) - .then(function(res) { - assert.equal(res.statusCode, 401, 'first factor failed'); - return requests.failing_first_factor(j); - }) - .then(function(res) { - assert.equal(res.statusCode, 403, 'first factor failed'); - MockDate.set('1/2/2017 00:30:00'); - return requests.failing_first_factor(j); - }) - .then(function(res) { - assert.equal(res.statusCode, 401, 'first factor failed'); - return Promise.resolve(); - }) - }); - } -}); - diff --git a/test/unitary/test_server_config.js b/test/unitary/test_server_config.js deleted file mode 100644 index aadca125..00000000 --- a/test/unitary/test_server_config.js +++ /dev/null @@ -1,52 +0,0 @@ - -var sinon = require('sinon'); -var server = require('../../src/lib/server'); -var assert = require('assert'); - -describe('test server configuration', function() { - var deps; - var config; - - before(function() { - config = {}; - config.notifier = { - gmail: { - user: 'user@example.com', - pass: 'password' - } - } - - transporter = {}; - transporter.sendMail = sinon.stub().yields(); - - var nodemailer = {}; - nodemailer.createTransport = sinon.spy(function() { - return transporter; -  }); - - deps = {}; - deps.nedb = require('nedb'); - deps.winston = sinon.spy(); - deps.nodemailer = nodemailer; - deps.ldapjs = {}; - deps.ldapjs.createClient = sinon.spy(function() { - return { on: sinon.spy() }; - }); - deps.session = sinon.spy(function() { - return function(req, res, next) { next(); }; - }); - }); - - - it('should set cookie scope to domain set in the config', function() { - config.session = {}; - config.session.domain = 'example.com'; - config.session.secret = 'secret'; - config.ldap = {}; - config.ldap.url = 'http://ldap'; - server.run(config, deps); - - assert(deps.session.calledOnce); - assert.equal(deps.session.getCall(0).args[0].cookie.domain, 'example.com'); - }); -}); diff --git a/test/unitary/test_totp.js b/test/unitary/test_totp.js deleted file mode 100644 index c8648e7a..00000000 --- a/test/unitary/test_totp.js +++ /dev/null @@ -1,32 +0,0 @@ - -var totp = require('../../src/lib/totp'); -var sinon = require('sinon'); -var Promise = require('bluebird'); - -describe('test TOTP validation', function() { - it('should validate the TOTP token', function() { - var totp_secret = 'NBD2ZV64R9UV1O7K'; - var token = 'token'; - var totp_mock = sinon.mock(); - totp_mock.returns('token'); - var speakeasy_mock = { - totp: totp_mock - } - return totp.validate(speakeasy_mock, token, totp_secret); - }); - - it('should not validate a wrong TOTP token', function() { - var totp_secret = 'NBD2ZV64R9UV1O7K'; - var token = 'wrong token'; - var totp_mock = sinon.mock(); - totp_mock.returns('token'); - var speakeasy_mock = { - totp: totp_mock - } - return totp.validate(speakeasy_mock, token, totp_secret) - .catch(function() { - return Promise.resolve(); - }); - }); -}); - diff --git a/test/unitary/test_user_data_store.js b/test/unitary/test_user_data_store.js deleted file mode 100644 index a9a343ab..00000000 --- a/test/unitary/test_user_data_store.js +++ /dev/null @@ -1,212 +0,0 @@ - -var UserDataStore = require('../../src/lib/user_data_store'); -var DataStore = require('nedb'); -var assert = require('assert'); -var Promise = require('bluebird'); -var sinon = require('sinon'); -var MockDate = require('mockdate'); - -describe('test user data store', function() { - describe('test u2f meta', test_u2f_meta); - describe('test u2f registration token', test_u2f_registration_token); -}); - -function test_u2f_meta() { - 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(); - }); - }); -} - -function test_u2f_registration_token() { - it('should save u2f registration token', function() { - var options = {}; - options.inMemoryOnly = true; - - var data_store = new UserDataStore(DataStore, options); - - var userid = 'user'; - var token = 'token'; - var max_age = 60; - var content = 'abc'; - - return data_store.issue_identity_check_token(userid, token, content, max_age) - .then(function(document) { - assert.equal(document.userid, userid); - assert.equal(document.token, token); - assert.deepEqual(document.content, { userid: 'user', data: content }); - assert('max_date' in document); - assert('_id' in document); - return Promise.resolve(); - }) - .catch(function(err) { - console.error(err); - return Promise.reject(err); - }); - }); - - it('should save u2f registration token and consume it', function(done) { - var options = {}; - options.inMemoryOnly = true; - - var data_store = new UserDataStore(DataStore, options); - - var userid = 'user'; - var token = 'token'; - var max_age = 50; - - data_store.issue_identity_check_token(userid, token, {}, max_age) - .then(function(document) { - return data_store.consume_identity_check_token(token); - }) - .then(function() { - done(); - }) - .catch(function(err) { - console.error(err); - }); - }); - - it('should not be able to consume registration token twice', function(done) { - var options = {}; - options.inMemoryOnly = true; - - var data_store = new UserDataStore(DataStore, options); - - var userid = 'user'; - var token = 'token'; - var max_age = 50; - - data_store.issue_identity_check_token(userid, token, {}, max_age) - .then(function(document) { - return data_store.consume_identity_check_token(token); - }) - .then(function(document) { - return data_store.consume_identity_check_token(token); - }) - .catch(function(err) { - console.error(err); - done(); - }); - }); - - it('should fail when token does not exist', function() { - var options = {}; - options.inMemoryOnly = true; - - var data_store = new UserDataStore(DataStore, options); - - var token = 'token'; - - return data_store.consume_identity_check_token(token) - .then(function(document) { - return Promise.reject(); - }) - .catch(function(err) { - return Promise.resolve(err); - }); - }); - - it('should fail when token expired', function(done) { - var options = {}; - options.inMemoryOnly = true; - - var data_store = new UserDataStore(DataStore, options); - - var userid = 'user'; - var token = 'token'; - var max_age = 60; - MockDate.set('1/1/2000'); - - data_store.issue_identity_check_token(userid, token, {}, max_age) - .then(function() { - MockDate.set('1/2/2000'); - return data_store.consume_identity_check_token(token); - }) - .catch(function(err) { - MockDate.reset(); - done(); - }); - }); - - it('should save the userid and some data with the token', function(done) { - var options = {}; - options.inMemoryOnly = true; - - var data_store = new UserDataStore(DataStore, options); - - var userid = 'user'; - var token = 'token'; - var max_age = 60; - MockDate.set('1/1/2000'); - var data = 'abc'; - - data_store.issue_identity_check_token(userid, token, data, max_age) - .then(function() { - return data_store.consume_identity_check_token(token); - }) - .then(function(content) { - var expected_content = {}; - expected_content.userid = 'user'; - expected_content.data = 'abc'; - assert.deepEqual(content, expected_content); - done(); - }) - }); -} diff --git a/test/unitary/totp.test.ts b/test/unitary/totp.test.ts new file mode 100644 index 00000000..f797587d --- /dev/null +++ b/test/unitary/totp.test.ts @@ -0,0 +1,32 @@ + +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(); + }); + }); +}); + diff --git a/test/unitary/user_data_store/authentication_audit.test.ts b/test/unitary/user_data_store/authentication_audit.test.ts new file mode 100644 index 00000000..2dc5c930 --- /dev/null +++ b/test/unitary/user_data_store/authentication_audit.test.ts @@ -0,0 +1,70 @@ + +import * as assert from "assert"; +import * as Promise from "bluebird"; +import * as sinon from "sinon"; +import * as MockDate from "mockdate"; +import UserDataStore from "../../../src/lib/UserDataStore"; + +describe("test user data store", function() { + describe("test authentication traces", test_authentication_traces); +}); + +function test_authentication_traces() { + it("should save an authentication trace in db", function() { + const options = { + inMemoryOnly: true + }; + + const data_store = new UserDataStore(options); + const userid = "user"; + const type = "1stfactor"; + const is_success = false; + return data_store.save_authentication_trace(userid, type, is_success) + .then(function(doc) { + assert("_id" in doc); + assert.equal(doc.userid, "user"); + assert.equal(doc.is_success, false); + assert.equal(doc.type, "1stfactor"); + return Promise.resolve(); + }); + }); + + it("should return 3 last authentication traces", function() { + const options = { + inMemoryOnly: true + }; + + const data_store = new UserDataStore(options); + const userid = "user"; + const type = "1stfactor"; + const is_success = false; + MockDate.set("2/1/2000"); + return data_store.save_authentication_trace(userid, type, false) + .then(function(doc) { + MockDate.set("1/2/2000"); + return data_store.save_authentication_trace(userid, type, true); + }) + .then(function(doc) { + MockDate.set("1/7/2000"); + return data_store.save_authentication_trace(userid, type, false); + }) + .then(function(doc) { + MockDate.set("1/2/2000"); + return data_store.save_authentication_trace(userid, type, false); + }) + .then(function(doc) { + MockDate.set("1/5/2000"); + return data_store.save_authentication_trace(userid, type, false); + }) + .then(function(doc) { + return data_store.get_last_authentication_traces(userid, type, false, 3); + }) + .then(function(docs) { + assert.equal(docs.length, 3); + assert.deepEqual(docs[0].date, new Date("2/1/2000")); + assert.deepEqual(docs[1].date, new Date("1/7/2000")); + assert.deepEqual(docs[2].date, new Date("1/5/2000")); + return Promise.resolve(); + }); + }); +} diff --git a/test/unitary/user_data_store/test_authentication_audit.js b/test/unitary/user_data_store/test_authentication_audit.js deleted file mode 100644 index d317b480..00000000 --- a/test/unitary/user_data_store/test_authentication_audit.js +++ /dev/null @@ -1,69 +0,0 @@ - -var assert = require('assert'); -var Promise = require('bluebird'); -var sinon = require('sinon'); -var MockDate = require('mockdate'); -var UserDataStore = require('../../../src/lib/user_data_store'); -var DataStore = require('nedb'); - -describe('test user data store', function() { - describe('test authentication traces', test_authentication_traces); -}); - -function test_authentication_traces() { - it('should save an authentication trace in db', function() { - var options = {}; - options.inMemoryOnly = true; - - var data_store = new UserDataStore(DataStore, options); - var userid = 'user'; - var type = '1stfactor'; - var is_success = false; - return data_store.save_authentication_trace(userid, type, is_success) - .then(function(doc) { - assert('_id' in doc); - assert.equal(doc.userid, 'user'); - assert.equal(doc.is_success, false); - assert.equal(doc.type, '1stfactor'); - return Promise.resolve(); - }); - }); - - it('should return 3 last authentication traces', function() { - var options = {}; - options.inMemoryOnly = true; - - var data_store = new UserDataStore(DataStore, options); - var userid = 'user'; - var type = '1stfactor'; - var is_success = false; - MockDate.set('2/1/2000'); - return data_store.save_authentication_trace(userid, type, false) - .then(function(doc) { - MockDate.set('1/2/2000'); - return data_store.save_authentication_trace(userid, type, true); - }) - .then(function(doc) { - MockDate.set('1/7/2000'); - return data_store.save_authentication_trace(userid, type, false); - }) - .then(function(doc) { - MockDate.set('1/2/2000'); - return data_store.save_authentication_trace(userid, type, false); - }) - .then(function(doc) { - MockDate.set('1/5/2000'); - return data_store.save_authentication_trace(userid, type, false); - }) - .then(function(doc) { - return data_store.get_last_authentication_traces(userid, type, false, 3); - }) - .then(function(docs) { - assert.equal(docs.length, 3); - assert.deepEqual(docs[0].date, new Date('2/1/2000')); - assert.deepEqual(docs[1].date, new Date('1/7/2000')); - assert.deepEqual(docs[2].date, new Date('1/5/2000')); - return Promise.resolve(); - }) - }); -} diff --git a/test/unitary/user_data_store/test_totp_secret.js b/test/unitary/user_data_store/test_totp_secret.js deleted file mode 100644 index f08e4fff..00000000 --- a/test/unitary/user_data_store/test_totp_secret.js +++ /dev/null @@ -1,65 +0,0 @@ - -var assert = require('assert'); -var Promise = require('bluebird'); -var sinon = require('sinon'); -var MockDate = require('mockdate'); -var UserDataStore = require('../../../src/lib/user_data_store'); -var DataStore = require('nedb'); - -describe('test user data store', function() { - describe('test totp secrets store', test_totp_secrets); -}); - -function test_totp_secrets() { - it('should save and reload a totp secret', function() { - var options = {}; - options.inMemoryOnly = true; - - var data_store = new UserDataStore(DataStore, options); - var userid = 'user'; - var secret = {}; - secret.ascii = 'abc'; - secret.base32 = 'ABCDKZLEFZGREJK'; - - return data_store.set_totp_secret(userid, secret) - .then(function() { - return data_store.get_totp_secret(userid); - }) - .then(function(doc) { - assert('_id' in doc); - assert.equal(doc.userid, 'user'); - assert.equal(doc.secret.ascii, 'abc'); - assert.equal(doc.secret.base32, 'ABCDKZLEFZGREJK'); - return Promise.resolve(); - }); - }); - - it('should only remember last secret', function() { - var options = {}; - options.inMemoryOnly = true; - - var data_store = new UserDataStore(DataStore, options); - var userid = 'user'; - var secret1 = {}; - secret1.ascii = 'abc'; - secret1.base32 = 'ABCDKZLEFZGREJK'; - var secret2 = {}; - secret2.ascii = 'def'; - secret2.base32 = 'XYZABC'; - - return data_store.set_totp_secret(userid, secret1) - .then(function() { - return data_store.set_totp_secret(userid, secret2) - }) - .then(function() { - return data_store.get_totp_secret(userid); - }) - .then(function(doc) { - assert('_id' in doc); - assert.equal(doc.userid, 'user'); - assert.equal(doc.secret.ascii, 'def'); - assert.equal(doc.secret.base32, 'XYZABC'); - return Promise.resolve(); - }); - }); -} diff --git a/test/unitary/user_data_store/totp_secret.test.ts b/test/unitary/user_data_store/totp_secret.test.ts new file mode 100644 index 00000000..cddaa227 --- /dev/null +++ b/test/unitary/user_data_store/totp_secret.test.ts @@ -0,0 +1,72 @@ + +import * as assert from "assert"; +import * as Promise from "bluebird"; +import * as sinon from "sinon"; +import * as MockDate from "mockdate"; +import UserDataStore from "../../../src/lib/UserDataStore"; + +describe("test user data store", function() { + describe("test totp secrets store", test_totp_secrets); +}); + +function test_totp_secrets() { + it("should save and reload a totp secret", function() { + const options = { + inMemoryOnly: true + }; + + const data_store = new UserDataStore(options); + const userid = "user"; + const secret = { + ascii: "abc", + base32: "ABCDKZLEFZGREJK", + otpauth_url: "totp://test" + }; + + return data_store.set_totp_secret(userid, secret) + .then(function() { + return data_store.get_totp_secret(userid); + }) + .then(function(doc) { + assert("_id" in doc); + assert.equal(doc.userid, "user"); + assert.equal(doc.secret.ascii, "abc"); + assert.equal(doc.secret.base32, "ABCDKZLEFZGREJK"); + return Promise.resolve(); + }); + }); + + it("should only remember last secret", function() { + const options = { + inMemoryOnly: true + }; + + const data_store = new UserDataStore(options); + const userid = "user"; + const secret1 = { + ascii: "abc", + base32: "ABCDKZLEFZGREJK", + otpauth_url: "totp://test" + }; + const secret2 = { + ascii: "def", + base32: "XYZABC", + otpauth_url: "totp://test" + }; + + return data_store.set_totp_secret(userid, secret1) + .then(function() { + return data_store.set_totp_secret(userid, secret2); + }) + .then(function() { + return data_store.get_totp_secret(userid); + }) + .then(function(doc) { + assert("_id" in doc); + assert.equal(doc.userid, "user"); + assert.equal(doc.secret.ascii, "def"); + assert.equal(doc.secret.base32, "XYZABC"); + return Promise.resolve(); + }); + }); +} diff --git a/tsconfig.json b/tsconfig.json index 40cc3822..4d4d2aa5 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -10,12 +10,13 @@ "allowJs": true, "paths": { "*": [ - "node_modules/*", + "node_modules/@types/*", "src/types/*" ] } }, "include": [ - "src/**/*" + "src/**/*", + "test/**/*" ] } From 57278a73069bd8e7fefbfefcd5a10789f93fff37 Mon Sep 17 00:00:00 2001 From: Clement Michaud Date: Sat, 20 May 2017 09:49:05 +0200 Subject: [PATCH 06/16] Move notifiers to typescript --- package.json | 3 + src/lib/Configuration.ts | 20 +++--- src/lib/ConfigurationAdapter.ts | 42 +++++++++++ src/lib/Dependencies.ts | 22 ++++++ src/lib/GlobalDependencies.ts | 11 --- src/lib/Identity.ts | 6 ++ src/lib/Server.ts | 16 ++--- src/lib/config_adapter.ts | 40 ----------- src/lib/notifier.js | 24 ------- src/lib/notifiers/FileSystemNotifier.ts | 25 +++++++ src/lib/notifiers/GMailNotifier.ts | 44 ++++++++++++ src/lib/notifiers/INotifier.ts | 7 ++ src/lib/notifiers/NotifierFactory.ts | 22 ++++++ src/lib/notifiers/filesystem.js | 16 ----- src/lib/notifiers/gmail.js | 33 --------- test/unitary/Server.test.ts | 4 +- test/unitary/config_adapter.test.ts | 29 ++++---- test/unitary/data_persistence.test.ts | 6 +- test/unitary/mocks/nodemailer.ts | 7 ++ .../notifiers/FileSystemNotifier.test.ts | 42 +++++++++++ test/unitary/notifiers/GMailNotifier.test.ts | 39 +++++++++++ .../unitary/notifiers/NotifierFactory.test.ts | 39 +++++++++++ test/unitary/notifiers/test_fs.js | 37 ---------- test/unitary/notifiers/test_gmail.js | 36 ---------- test/unitary/notifiers/test_notifier.js | 35 ---------- test/unitary/server_config.test.ts | 10 +-- test/unitary/test_server_config.ts | 69 +++++++++++++++++++ 27 files changed, 411 insertions(+), 273 deletions(-) create mode 100644 src/lib/ConfigurationAdapter.ts create mode 100644 src/lib/Dependencies.ts delete mode 100644 src/lib/GlobalDependencies.ts create mode 100644 src/lib/Identity.ts delete mode 100644 src/lib/config_adapter.ts delete mode 100644 src/lib/notifier.js create mode 100644 src/lib/notifiers/FileSystemNotifier.ts create mode 100644 src/lib/notifiers/GMailNotifier.ts create mode 100644 src/lib/notifiers/INotifier.ts create mode 100644 src/lib/notifiers/NotifierFactory.ts delete mode 100644 src/lib/notifiers/filesystem.js delete mode 100644 src/lib/notifiers/gmail.js create mode 100644 test/unitary/mocks/nodemailer.ts create mode 100644 test/unitary/notifiers/FileSystemNotifier.test.ts create mode 100644 test/unitary/notifiers/GMailNotifier.test.ts create mode 100644 test/unitary/notifiers/NotifierFactory.test.ts delete mode 100644 test/unitary/notifiers/test_fs.js delete mode 100644 test/unitary/notifiers/test_gmail.js delete mode 100644 test/unitary/notifiers/test_notifier.js create mode 100644 test/unitary/test_server_config.ts diff --git a/package.json b/package.json index a5078e69..09cc9f23 100644 --- a/package.json +++ b/package.json @@ -48,6 +48,7 @@ "@types/assert": "0.0.31", "@types/bluebird": "^3.5.3", "@types/body-parser": "^1.16.3", + "@types/ejs": "^2.3.33", "@types/express": "^4.0.35", "@types/express-session": "0.0.32", "@types/ldapjs": "^1.0.0", @@ -56,6 +57,7 @@ "@types/nedb": "^1.8.3", "@types/nodemailer": "^1.3.32", "@types/object-path": "^0.9.28", + "@types/proxyquire": "^1.3.27", "@types/request": "0.0.43", "@types/sinon": "^2.2.1", "@types/speakeasy": "^2.0.1", @@ -67,6 +69,7 @@ "grunt-run": "^0.6.0", "mocha": "^3.2.0", "mockdate": "^2.0.1", + "proxyquire": "^1.8.0", "request": "^2.79.0", "should": "^11.1.1", "sinon": "^1.17.6", diff --git a/src/lib/Configuration.ts b/src/lib/Configuration.ts index 2ed31f56..d077f42e 100644 --- a/src/lib/Configuration.ts +++ b/src/lib/Configuration.ts @@ -30,14 +30,18 @@ interface SessionCookieConfiguration { domain?: string; } -interface GMailNotifier { - user: string; - pass: string; +export interface GmailNotifierConfiguration { + username: string; + password: string; } -type NotifierType = string; -export interface NotifiersConfiguration { - gmail: GMailNotifier; +export interface FileSystemNotifierConfiguration { + filename: string; +} + +export interface NotifierConfiguration { + gmail?: GmailNotifierConfiguration; + filesystem?: FileSystemNotifierConfiguration; } export interface UserConfiguration { @@ -46,7 +50,7 @@ export interface UserConfiguration { ldap: LdapConfiguration; session: SessionCookieConfiguration; store_directory?: string; - notifier: NotifiersConfiguration; + notifier: NotifierConfiguration; access_control?: ACLConfiguration; } @@ -57,6 +61,6 @@ export interface AppConfiguration { session: SessionCookieConfiguration; store_in_memory?: boolean; store_directory?: string; - notifier: NotifiersConfiguration; + notifier: NotifierConfiguration; access_control?: ACLConfiguration; } diff --git a/src/lib/ConfigurationAdapter.ts b/src/lib/ConfigurationAdapter.ts new file mode 100644 index 00000000..2742d135 --- /dev/null +++ b/src/lib/ConfigurationAdapter.ts @@ -0,0 +1,42 @@ + +import * as ObjectPath from "object-path"; +import { AppConfiguration, UserConfiguration, NotifierConfiguration, ACLConfiguration, LdapConfiguration } from "./Configuration"; + + +function get_optional(config: object, path: string, default_value: T): T { + let entry = default_value; + if (ObjectPath.has(config, path)) { + entry = ObjectPath.get(config, path); + } + return entry; +} + +function ensure_key_existence(config: object, path: string): void { + if (!ObjectPath.has(config, path)) { + throw new Error(`Configuration error: key '${path}' is missing in configuration file`); + } +} + +export default class ConfigurationAdapter { + static adapt(yaml_config: UserConfiguration): AppConfiguration { + ensure_key_existence(yaml_config, "ldap"); + ensure_key_existence(yaml_config, "session.secret"); + + const port = ObjectPath.get(yaml_config, "port", 8080); + + return { + port: port, + ldap: ObjectPath.get(yaml_config, "ldap"), + session: { + domain: ObjectPath.get(yaml_config, "session.domain"), + secret: ObjectPath.get(yaml_config, "session.secret"), + expiration: get_optional(yaml_config, "session.expiration", 3600000), // in ms + }, + store_directory: get_optional(yaml_config, "store_directory", undefined), + logs_level: get_optional(yaml_config, "logs_level", "info"), + notifier: ObjectPath.get(yaml_config, "notifier"), + access_control: ObjectPath.get(yaml_config, "access_control") + }; + } +} + diff --git a/src/lib/Dependencies.ts b/src/lib/Dependencies.ts new file mode 100644 index 00000000..0cfa344f --- /dev/null +++ b/src/lib/Dependencies.ts @@ -0,0 +1,22 @@ +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/GlobalDependencies.ts b/src/lib/GlobalDependencies.ts deleted file mode 100644 index 4b71f273..00000000 --- a/src/lib/GlobalDependencies.ts +++ /dev/null @@ -1,11 +0,0 @@ -import * as winston from "winston"; - -export interface GlobalDependencies { - u2f: object; - nodemailer: any; - ldapjs: object; - session: any; - winston: winston.Winston; - speakeasy: object; - nedb: any; -} \ No newline at end of file diff --git a/src/lib/Identity.ts b/src/lib/Identity.ts new file mode 100644 index 00000000..e985984e --- /dev/null +++ b/src/lib/Identity.ts @@ -0,0 +1,6 @@ + + +export interface Identity { + userid: string; + email: string; +} \ No newline at end of file diff --git a/src/lib/Server.ts b/src/lib/Server.ts index 8df410f6..360e90c6 100644 --- a/src/lib/Server.ts +++ b/src/lib/Server.ts @@ -1,16 +1,16 @@ import { UserConfiguration } from "./Configuration"; -import { GlobalDependencies } from "./GlobalDependencies"; +import { GlobalDependencies } from "./Dependencies"; +import { AuthenticationRegulator } from "./AuthenticationRegulator"; +import UserDataStore from "./UserDataStore"; +import ConfigurationAdapter from "./ConfigurationAdapter"; +import { NotifierFactory } from "./notifiers/NotifierFactory"; + import * as Express from "express"; import * as BodyParser from "body-parser"; import * as Path from "path"; -import { AuthenticationRegulator } from "./AuthenticationRegulator"; -import UserDataStore from "./UserDataStore"; import * as http from "http"; -import config_adapter = require("./config_adapter"); - -const Notifier = require("./notifier"); const setup_endpoints = require("./setup_endpoints"); const Ldap = require("./ldap"); const AccessControl = require("./access_control"); @@ -19,7 +19,7 @@ export default class Server { private httpServer: http.Server; start(yaml_configuration: UserConfiguration, deps: GlobalDependencies): Promise { - const config = config_adapter(yaml_configuration); + const config = ConfigurationAdapter.adapt(yaml_configuration); const view_directory = Path.resolve(__dirname, "../views"); const public_html_directory = Path.resolve(__dirname, "../public_html"); @@ -54,7 +54,7 @@ export default class Server { const five_minutes = 5 * 60; const data_store = new UserDataStore(datastore_options); const regulator = new AuthenticationRegulator(data_store, five_minutes); - const notifier = new Notifier(config.notifier, deps); + const notifier = NotifierFactory.build(config.notifier, deps); const ldap = new Ldap(deps, config.ldap); const access_control = AccessControl(deps.winston, config.access_control); diff --git a/src/lib/config_adapter.ts b/src/lib/config_adapter.ts deleted file mode 100644 index 5503f1c8..00000000 --- a/src/lib/config_adapter.ts +++ /dev/null @@ -1,40 +0,0 @@ - -import * as ObjectPath from "object-path"; -import { AppConfiguration, UserConfiguration, NotifiersConfiguration, ACLConfiguration, LdapConfiguration } from "./Configuration"; - - -function get_optional(config: object, path: string, default_value: T): T { - let entry = default_value; - if (ObjectPath.has(config, path)) { - entry = ObjectPath.get(config, path); - } - return entry; -} - -function ensure_key_existence(config: object, path: string): void { - if (!ObjectPath.has(config, path)) { - throw new Error(`Configuration error: key '${path}' is missing in configuration file`); - } -} - -export = function(yaml_config: UserConfiguration): AppConfiguration { - ensure_key_existence(yaml_config, "ldap"); - ensure_key_existence(yaml_config, "session.secret"); - - const port = ObjectPath.get(yaml_config, "port", 8080); - - return { - port: port, - ldap: ObjectPath.get(yaml_config, "ldap"), - session: { - domain: ObjectPath.get(yaml_config, "session.domain"), - secret: ObjectPath.get(yaml_config, "session.secret"), - expiration: get_optional(yaml_config, "session.expiration", 3600000), // in ms - }, - store_directory: get_optional(yaml_config, "store_directory", undefined), - logs_level: get_optional(yaml_config, "logs_level", "info"), - notifier: ObjectPath.get(yaml_config, "notifier"), - access_control: ObjectPath.get(yaml_config, "access_control") - }; -}; - diff --git a/src/lib/notifier.js b/src/lib/notifier.js deleted file mode 100644 index 84ba6060..00000000 --- a/src/lib/notifier.js +++ /dev/null @@ -1,24 +0,0 @@ - -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/FileSystemNotifier.ts b/src/lib/notifiers/FileSystemNotifier.ts new file mode 100644 index 00000000..10b62f8c --- /dev/null +++ b/src/lib/notifiers/FileSystemNotifier.ts @@ -0,0 +1,25 @@ + +import * as BluebirdPromise from "bluebird"; +import * as util from "util"; +import * as fs from "fs"; +import { INotifier } from "./INotifier"; +import { Identity } from "../Identity"; + +import { FileSystemNotifierConfiguration } from "../Configuration"; + +export class FileSystemNotifier extends INotifier { + private filename: string; + + constructor(options: FileSystemNotifierConfiguration) { + super(); + this.filename = options.filename; + } + + notify(identity: Identity, subject: string, link: string): BluebirdPromise { + const content = util.format("User: %s\nSubject: %s\nLink: %s", identity.userid, + subject, link); + const writeFilePromised = BluebirdPromise.promisify(fs.writeFile); + return writeFilePromised(this.filename, content); + } +} + diff --git a/src/lib/notifiers/GMailNotifier.ts b/src/lib/notifiers/GMailNotifier.ts new file mode 100644 index 00000000..030cd85d --- /dev/null +++ b/src/lib/notifiers/GMailNotifier.ts @@ -0,0 +1,44 @@ + +import * as Promise from "bluebird"; +import * as fs from "fs"; +import * as ejs from "ejs"; +import nodemailer = require("nodemailer"); + +import { NodemailerDependencies } from "../Dependencies"; +import { Identity } from "../Identity"; +import { INotifier } from "../notifiers/INotifier"; +import { GmailNotifierConfiguration } from "../Configuration"; + +const email_template = fs.readFileSync(__dirname + "/../../resources/email-template.ejs", "UTF-8"); + +export class GMailNotifier extends INotifier { + private transporter: any; + + constructor(options: GmailNotifierConfiguration, deps: NodemailerDependencies) { + super(); + const transporter = deps.createTransport({ + service: "gmail", + auth: { + user: options.username, + pass: options.password + } + }); + this.transporter = Promise.promisifyAll(transporter); + } + + notify(identity: Identity, subject: string, link: string): Promise { + const d = { + url: link, + button_title: "Continue", + title: subject + }; + + const mailOptions = { + from: "auth-server@open-intent.io", + to: identity.email, + subject: subject, + html: ejs.render(email_template, d) + }; + return this.transporter.sendMailAsync(mailOptions); + } +} diff --git a/src/lib/notifiers/INotifier.ts b/src/lib/notifiers/INotifier.ts new file mode 100644 index 00000000..84e94b3d --- /dev/null +++ b/src/lib/notifiers/INotifier.ts @@ -0,0 +1,7 @@ + +import * as BluebirdPromise from "bluebird"; +import { Identity } from "../Identity"; + +export abstract class INotifier { + abstract notify(identity: Identity, subject: string, link: string): BluebirdPromise; +} \ No newline at end of file diff --git a/src/lib/notifiers/NotifierFactory.ts b/src/lib/notifiers/NotifierFactory.ts new file mode 100644 index 00000000..fe166c72 --- /dev/null +++ b/src/lib/notifiers/NotifierFactory.ts @@ -0,0 +1,22 @@ + +import { NotifierConfiguration } from "..//Configuration"; +import { NotifierDependencies } from "../Dependencies"; +import { INotifier } from "./INotifier"; + +import { GMailNotifier } from "./GMailNotifier"; +import { FileSystemNotifier } from "./FileSystemNotifier"; + +export class NotifierFactory { + static build(options: NotifierConfiguration, deps: NotifierDependencies): INotifier { + if ("gmail" in options) { + return new GMailNotifier(options.gmail, deps.nodemailer); + } + else if ("filesystem" in options) { + return new FileSystemNotifier(options.filesystem); + } + } +} + + + + diff --git a/src/lib/notifiers/filesystem.js b/src/lib/notifiers/filesystem.js deleted file mode 100644 index a4a295ce..00000000 --- a/src/lib/notifiers/filesystem.js +++ /dev/null @@ -1,16 +0,0 @@ -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 deleted file mode 100644 index 5007d858..00000000 --- a/src/lib/notifiers/gmail.js +++ /dev/null @@ -1,33 +0,0 @@ -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/test/unitary/Server.test.ts b/test/unitary/Server.test.ts index cd9d0630..bc930f3b 100644 --- a/test/unitary/Server.test.ts +++ b/test/unitary/Server.test.ts @@ -44,8 +44,8 @@ describe("test the server", function () { store_in_memory: true, notifier: { gmail: { - user: "user@example.com", - pass: "password" + username: "user@example.com", + password: "password" } } }; diff --git a/test/unitary/config_adapter.test.ts b/test/unitary/config_adapter.test.ts index 6f55ac2b..0c8a651e 100644 --- a/test/unitary/config_adapter.test.ts +++ b/test/unitary/config_adapter.test.ts @@ -1,7 +1,6 @@ import * as Assert from "assert"; import { UserConfiguration } from "../../src/lib/Configuration"; -import config_adapter = require("../../src/lib/config_adapter"); - +import ConfigurationAdapter from "../../src/lib/ConfigurationAdapter"; describe("test config adapter", function() { function build_yaml_config(): UserConfiguration { @@ -22,8 +21,8 @@ describe("test config adapter", function() { logs_level: "debug", notifier: { gmail: { - user: "user", - pass: "password" + username: "user", + password: "password" } } }; @@ -33,14 +32,14 @@ describe("test config adapter", function() { it("should read the port from the yaml file", function() { const yaml_config = build_yaml_config(); yaml_config.port = 7070; - const config = config_adapter(yaml_config); + const config = ConfigurationAdapter.adapt(yaml_config); Assert.equal(config.port, 7070); }); it("should default the port to 8080 if not provided", function() { const yaml_config = build_yaml_config(); delete yaml_config.port; - const config = config_adapter(yaml_config); + const config = ConfigurationAdapter.adapt(yaml_config); Assert.equal(config.port, 8080); }); @@ -55,7 +54,7 @@ describe("test config adapter", function() { password: "pass" }; - const config = config_adapter(yaml_config); + const config = ConfigurationAdapter.adapt(yaml_config); Assert.equal(config.ldap.url, "http://ldap"); Assert.equal(config.ldap.additional_user_dn, "ou=users"); @@ -71,7 +70,7 @@ describe("test config adapter", function() { secret: "secret", expiration: 3600 }; - const config = config_adapter(yaml_config); + const config = ConfigurationAdapter.adapt(yaml_config); Assert.equal(config.session.domain, "example.com"); Assert.equal(config.session.secret, "secret"); Assert.equal(config.session.expiration, 3600); @@ -80,7 +79,7 @@ describe("test config adapter", function() { it("should get the log level", function() { const yaml_config = build_yaml_config(); yaml_config.logs_level = "debug"; - const config = config_adapter(yaml_config); + const config = ConfigurationAdapter.adapt(yaml_config); Assert.equal(config.logs_level, "debug"); }); @@ -88,15 +87,15 @@ describe("test config adapter", function() { const yaml_config = build_yaml_config(); yaml_config.notifier = { gmail: { - user: "user", - pass: "pass" + username: "user", + password: "pass" } }; - const config = config_adapter(yaml_config); + const config = ConfigurationAdapter.adapt(yaml_config); Assert.deepEqual(config.notifier, { gmail: { - user: "user", - pass: "pass" + username: "user", + password: "pass" } }); }); @@ -108,7 +107,7 @@ describe("test config adapter", function() { users: {}, groups: {} }; - const config = config_adapter(yaml_config); + const config = ConfigurationAdapter.adapt(yaml_config); Assert.deepEqual(config.access_control, { default: [], users: {}, diff --git a/test/unitary/data_persistence.test.ts b/test/unitary/data_persistence.test.ts index 4f2154f5..494bb0a7 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/GlobalDependencies"; +import { GlobalDependencies } from "../../src/lib/Dependencies"; import * as tmp from "tmp"; @@ -77,8 +77,8 @@ describe("test data persistence", function () { store_directory: tmpDir.name, notifier: { gmail: { - user: "user@example.com", - pass: "password" + username: "user@example.com", + password: "password" } } }; diff --git a/test/unitary/mocks/nodemailer.ts b/test/unitary/mocks/nodemailer.ts new file mode 100644 index 00000000..1e42d13e --- /dev/null +++ b/test/unitary/mocks/nodemailer.ts @@ -0,0 +1,7 @@ + +import sinon = require("sinon"); +import { Nodemailer } from "../../../src/lib/Dependencies"; + +export = { + createTransport: sinon.stub() +}; diff --git a/test/unitary/notifiers/FileSystemNotifier.test.ts b/test/unitary/notifiers/FileSystemNotifier.test.ts new file mode 100644 index 00000000..b5197157 --- /dev/null +++ b/test/unitary/notifiers/FileSystemNotifier.test.ts @@ -0,0 +1,42 @@ + +import * as sinon from "sinon"; +import * as assert from "assert"; +import { FileSystemNotifier } from "../../../src/lib/notifiers/FileSystemNotifier"; +import * as tmp from "tmp"; +import * as fs from "fs"; + +const NOTIFICATIONS_DIRECTORY = "notifications"; + +describe("test FS notifier", function() { + let tmpDir: tmp.SynchrounousResult; + before(function() { + tmpDir = tmp.dirSync({ unsafeCleanup: true }); + }); + + after(function() { + tmpDir.removeCallback(); + }); + + it("should write the notification in a file", function() { + const options = { + filename: tmpDir.name + "/" + NOTIFICATIONS_DIRECTORY + }; + + const sender = new FileSystemNotifier(options); + const subject = "subject"; + + const identity = { + userid: "user", + email: "user@example.com" + }; + + const url = "http://test.com"; + + return sender.notify(identity, subject, url) + .then(function() { + const content = fs.readFileSync(options.filename, "UTF-8"); + assert(content.length > 0); + return Promise.resolve(); + }); + }); +}); diff --git a/test/unitary/notifiers/GMailNotifier.test.ts b/test/unitary/notifiers/GMailNotifier.test.ts new file mode 100644 index 00000000..3e6f6ce4 --- /dev/null +++ b/test/unitary/notifiers/GMailNotifier.test.ts @@ -0,0 +1,39 @@ +import * as sinon from "sinon"; +import * as assert from "assert"; + +import nodemailerMock = require("../mocks/nodemailer"); +import GMailNotifier = require("../../../src/lib/notifiers/GMailNotifier"); + + +describe("test gmail notifier", function () { + it("should send an email", function () { + const transporter = { + sendMail: sinon.stub().yields() + }; + nodemailerMock.createTransport.returns(transporter); + + const options = { + username: "user_gmail", + password: "pass_gmail" + }; + + const sender = new GMailNotifier.GMailNotifier(options, nodemailerMock); + const subject = "subject"; + + const identity = { + userid: "user", + email: "user@example.com" + }; + + const url = "http://test.com"; + + return sender.notify(identity, subject, url) + .then(function () { + assert.equal(nodemailerMock.createTransport.getCall(0).args[0].auth.user, "user_gmail"); + assert.equal(nodemailerMock.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/NotifierFactory.test.ts b/test/unitary/notifiers/NotifierFactory.test.ts new file mode 100644 index 00000000..25600b0c --- /dev/null +++ b/test/unitary/notifiers/NotifierFactory.test.ts @@ -0,0 +1,39 @@ + +import * as sinon from "sinon"; +import * as BluebirdPromise from "bluebird"; +import * as assert from "assert"; + +import NodemailerMock = require("../mocks/nodemailer"); + +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"; + + +describe("test notifier", function() { + const deps: NotifierDependencies = { + nodemailer: NodemailerMock + }; + + it("should build a Gmail Notifier", function() { + const options = { + gmail: { + username: "abc", + password: "password" + } + }; + assert(NotifierFactory.build(options, deps) instanceof GMailNotifier); + }); + + it("should build a FS Notifier", function() { + const options = { + filesystem: { + filename: "abc" + } + }; + + assert(NotifierFactory.build(options, deps) instanceof FileSystemNotifier); + }); +}); diff --git a/test/unitary/notifiers/test_fs.js b/test/unitary/notifiers/test_fs.js deleted file mode 100644 index 60b16b8e..00000000 --- a/test/unitary/notifiers/test_fs.js +++ /dev/null @@ -1,37 +0,0 @@ -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 deleted file mode 100644 index bc65967a..00000000 --- a/test/unitary/notifiers/test_gmail.js +++ /dev/null @@ -1,36 +0,0 @@ -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 deleted file mode 100644 index efa4413d..00000000 --- a/test/unitary/notifiers/test_notifier.js +++ /dev/null @@ -1,35 +0,0 @@ - -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/server_config.test.ts b/test/unitary/server_config.test.ts index 6e4bf17f..c03072ce 100644 --- a/test/unitary/server_config.test.ts +++ b/test/unitary/server_config.test.ts @@ -8,7 +8,7 @@ import * as speakeasy from "speakeasy"; import * as u2f from "authdog"; import { AppConfiguration, UserConfiguration } from "../../src/lib/Configuration"; -import { GlobalDependencies } from "../../src/lib/GlobalDependencies"; +import { GlobalDependencies, Nodemailer } from "../../src/lib/Dependencies"; import Server from "../../src/lib/Server"; @@ -20,18 +20,18 @@ describe("test server configuration", function () { sendMail: sinon.stub().yields() }; - const nodemailer = { + const nodemailer: Nodemailer = { createTransport: sinon.spy(function () { return transporter; }) }; deps = { + nodemailer: nodemailer, speakeasy: speakeasy, u2f: u2f, nedb: nedb, winston: winston, - nodemailer: nodemailer, ldapjs: { createClient: sinon.spy(function () { return { on: sinon.spy() }; @@ -57,8 +57,8 @@ describe("test server configuration", function () { }, notifier: { gmail: { - user: "user@example.com", - pass: "password" + username: "user@example.com", + password: "password" } } } as UserConfiguration; diff --git a/test/unitary/test_server_config.ts b/test/unitary/test_server_config.ts new file mode 100644 index 00000000..2bc02178 --- /dev/null +++ b/test/unitary/test_server_config.ts @@ -0,0 +1,69 @@ + +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"); + }); +}); From 40e02d23bfd74d022e9b3de40d7f40725ab26315 Mon Sep 17 00:00:00 2001 From: Clement Michaud Date: Sat, 20 May 2017 17:30:42 +0200 Subject: [PATCH 07/16] Move access-control feature to typescript --- src/lib/Configuration.ts | 6 +- src/lib/ILogger.ts | 7 + src/lib/Server.ts | 8 +- src/lib/access_control.js | 84 --------- src/lib/access_control/AccessController.ts | 35 ++++ src/lib/access_control/PatternBuilder.ts | 61 +++++++ src/lib/routes/first_factor.js | 12 +- src/lib/routes/verify.js | 9 - .../access_control/AccessController.test.ts | 53 ++++++ .../access_control/PatternBuilder.test.ts | 120 +++++++++++++ test/unitary/routes/test_first_factor.js | 49 +----- test/unitary/routes/test_verify.js | 19 --- test/unitary/test_access_control.js | 160 ------------------ 13 files changed, 290 insertions(+), 333 deletions(-) create mode 100644 src/lib/ILogger.ts delete mode 100644 src/lib/access_control.js create mode 100644 src/lib/access_control/AccessController.ts create mode 100644 src/lib/access_control/PatternBuilder.ts create mode 100644 test/unitary/access_control/AccessController.test.ts create mode 100644 test/unitary/access_control/PatternBuilder.test.ts delete mode 100644 test/unitary/test_access_control.js diff --git a/src/lib/Configuration.ts b/src/lib/Configuration.ts index d077f42e..ece9acfc 100644 --- a/src/lib/Configuration.ts +++ b/src/lib/Configuration.ts @@ -14,9 +14,9 @@ type UserName = string; type GroupName = string; type DomainPattern = string; -type ACLDefaultRules = Array; -type ACLGroupsRules = Object; -type ACLUsersRules = Object; +export type ACLDefaultRules = DomainPattern[]; +export type ACLGroupsRules = { [group: string]: string[]; }; +export type ACLUsersRules = { [user: string]: string[]; }; export interface ACLConfiguration { default: ACLDefaultRules; diff --git a/src/lib/ILogger.ts b/src/lib/ILogger.ts new file mode 100644 index 00000000..96f03fe6 --- /dev/null +++ b/src/lib/ILogger.ts @@ -0,0 +1,7 @@ + +import * as winston from "winston"; + +export interface ILogger { + debug: winston.LeveledLogMethod; +} + diff --git a/src/lib/Server.ts b/src/lib/Server.ts index 360e90c6..d10e2bbe 100644 --- a/src/lib/Server.ts +++ b/src/lib/Server.ts @@ -11,9 +11,10 @@ import * as BodyParser from "body-parser"; import * as Path from "path"; import * as http from "http"; +import AccessController from "./access_control/AccessController"; + const setup_endpoints = require("./setup_endpoints"); const Ldap = require("./ldap"); -const AccessControl = require("./access_control"); export default class Server { private httpServer: http.Server; @@ -56,7 +57,7 @@ export default class Server { const regulator = new AuthenticationRegulator(data_store, five_minutes); const notifier = NotifierFactory.build(config.notifier, deps); const ldap = new Ldap(deps, config.ldap); - const access_control = AccessControl(deps.winston, config.access_control); + const accessController = new AccessController(config.access_control, deps.winston); app.set("logger", deps.winston); app.set("ldap", ldap); @@ -66,7 +67,8 @@ export default class Server { app.set("notifier", notifier); app.set("authentication regulator", regulator); app.set("config", config); - app.set("access control", access_control); + app.set("access controller", accessController); + setup_endpoints(app); return new Promise((resolve, reject) => { diff --git a/src/lib/access_control.js b/src/lib/access_control.js deleted file mode 100644 index e185eb7a..00000000 --- a/src/lib/access_control.js +++ /dev/null @@ -1,84 +0,0 @@ - -module.exports = function(logger, acl_config) { - return { - builder: new AccessControlBuilder(logger, acl_config), - matcher: new AccessControlMatcher(logger) - }; -} - -var objectPath = require('object-path'); - -// *************** PER DOMAIN MATCHER *************** -function AccessControlMatcher(logger) { - this.logger = logger; -} - -AccessControlMatcher.prototype.is_domain_allowed = function(domain, allowed_domains) { - // Allow all matcher - if(allowed_domains.length == 1 && allowed_domains[0] == '*') return true; - - this.logger.debug('ACL: trying to match %s with %s', domain, - JSON.stringify(allowed_domains)); - for(var i = 0; i < allowed_domains.length; ++i) { - var allowed_domain = allowed_domains[i]; - if(allowed_domain.startsWith('*') && - domain.endsWith(allowed_domain.substr(1))) { - return true; - } - else if(domain == allowed_domain) { - return true; - } - } - return false; -} - - -// *************** MATCHER BUILDER *************** -function AccessControlBuilder(logger, acl_config) { - this.logger = logger; - this.config = acl_config; -} - -AccessControlBuilder.prototype.extract_per_group = function(groups) { - var allowed_domains = []; - var groups_policy = objectPath.get(this.config, 'groups'); - if(groups_policy) { - for(var i=0; i(this.configuration, "groups"); + if (groups_policy) { + for (let i = 0; i < groups.length; ++i) { + const group = groups[i]; + if (group in groups_policy) { + const group_policy: string[] = groups_policy[group]; + allowed_domains = allowed_domains.concat(groups_policy[group]); + } + } + } + return allowed_domains; + } + + private buildFromUser(user: string): string[] { + let allowed_domains: string[] = []; + const users_policy = objectPath.get(this.configuration, "users"); + if (users_policy) { + if (user in users_policy) { + allowed_domains = allowed_domains.concat(users_policy[user]); + } + } + return allowed_domains; + } + + getAllowedDomains(user: string, groups: string[]): string[] { + if (!this.configuration) { + this.logger.debug("No access control rules found." + + "Default policy to allow all."); + return ["*"]; // No configuration means, no restrictions. + } + + let allowed_domains: string[] = []; + const default_policy = objectPath.get(this.configuration, "default"); + if (default_policy) { + allowed_domains = allowed_domains.concat(default_policy); + } + + allowed_domains = allowed_domains.concat(this.buildFromGroups(groups)); + allowed_domains = allowed_domains.concat(this.buildFromUser(user)); + + this.logger.debug("ACL: user \'%s\' is allowed access to %s", user, + JSON.stringify(allowed_domains)); + return allowed_domains; + } +} diff --git a/src/lib/routes/first_factor.js b/src/lib/routes/first_factor.js index 9736805f..32af80ff 100644 --- a/src/lib/routes/first_factor.js +++ b/src/lib/routes/first_factor.js @@ -37,7 +37,7 @@ function first_factor(req, res) { var ldap = req.app.get('ldap'); var config = req.app.get('config'); var regulator = req.app.get('authentication regulator'); - var acl_builder = req.app.get('access control').builder; + var accessController = req.app.get('access controller'); logger.info('1st factor: Starting authentication of user "%s"', username); logger.debug('1st factor: Start bind operation against LDAP'); @@ -63,15 +63,7 @@ function first_factor(req, res) { logger.debug('1st factor: Retrieved email are %s', emails); objectPath.set(req, 'session.auth_session.email', emails[0]); - if(config.access_control) { - allowed_domains = acl_builder.get_allowed_domains(username, groups); - } - else { - allowed_domains = acl_builder.get_any_domain(); - logger.debug('1st factor: no access control rules found.' + - 'Default policy to allow all.'); - } - objectPath.set(req, 'session.auth_session.allowed_domains', allowed_domains); + allowed_domains = accessController.isDomainAllowedForUser(username, groups); regulator.mark(username, true); res.status(204); diff --git a/src/lib/routes/verify.js b/src/lib/routes/verify.js index 0bea86e2..6ebbc852 100644 --- a/src/lib/routes/verify.js +++ b/src/lib/routes/verify.js @@ -19,17 +19,8 @@ function verify_filter(req, res) { if(!objectPath.has(req, 'session.auth_session.userid')) return Promise.reject('No userid variable'); - if(!objectPath.has(req, 'session.auth_session.allowed_domains')) - return Promise.reject('No allowed_domains variable'); - - // Get the session ACL matcher - var allowed_domains = objectPath.get(req, 'session.auth_session.allowed_domains'); var host = objectPath.get(req, 'headers.host'); var domain = host.split(':')[0]; - var acl_matcher = req.app.get('access control').matcher; - - if(!acl_matcher.is_domain_allowed(domain, allowed_domains)) - return Promise.reject('Access restricted by ACL rules'); if(!req.session.auth_session.first_factor || !req.session.auth_session.second_factor) diff --git a/test/unitary/access_control/AccessController.test.ts b/test/unitary/access_control/AccessController.test.ts new file mode 100644 index 00000000..9af32227 --- /dev/null +++ b/test/unitary/access_control/AccessController.test.ts @@ -0,0 +1,53 @@ + +import assert = require("assert"); +import winston = require("winston"); +import AccessController from "../../../src/lib/access_control/AccessController"; +import { ACLConfiguration } from "../../../src/lib/Configuration"; + +describe("test access control manager", function () { + let accessController: AccessController; + let configuration: ACLConfiguration; + + beforeEach(function () { + configuration = { + default: [], + users: {}, + groups: {} + }; + accessController = new AccessController(configuration, winston); + }); + + describe("check access control matching", function () { + beforeEach(function () { + configuration.default = ["home.example.com", "*.public.example.com"]; + configuration.users = { + user1: ["user1.example.com", "user1.mail.example.com"] + }; + configuration.groups = { + group1: ["secret2.example.com"], + group2: ["secret.example.com", "secret1.example.com"] + }; + }); + + it("should allow access to secret.example.com", function () { + assert(accessController.isDomainAllowedForUser("secret.example.com", "user", ["group1", "group2"])); + }); + + it("should deny access to secret3.example.com", function () { + assert(!accessController.isDomainAllowedForUser("secret3.example.com", "user", ["group1", "group2"])); + }); + + it("should allow access to home.example.com", function () { + assert(accessController.isDomainAllowedForUser("home.example.com", "user", ["group1", "group2"])); + }); + + it("should allow access to user1.example.com", function () { + assert(accessController.isDomainAllowedForUser("user1.example.com", "user1", ["group1", "group2"])); + }); + + it("should allow access *.public.example.com", function () { + assert(accessController.isDomainAllowedForUser("user.public.example.com", "nouser", [])); + assert(accessController.isDomainAllowedForUser("test.public.example.com", "nouser", [])); + }); + }); +}); diff --git a/test/unitary/access_control/PatternBuilder.test.ts b/test/unitary/access_control/PatternBuilder.test.ts new file mode 100644 index 00000000..a563556c --- /dev/null +++ b/test/unitary/access_control/PatternBuilder.test.ts @@ -0,0 +1,120 @@ + +import assert = require("assert"); +import winston = require("winston"); + +import PatternBuilder from "../../../src/lib/access_control/PatternBuilder"; +import { ACLConfiguration } from "../../../src/lib/Configuration"; + +describe("test access control manager", function () { + describe("test access control pattern builder when no configuration is provided", () => { + it("should allow access to the user", () => { + const patternBuilder = new PatternBuilder(undefined, winston); + + const allowed_domains = patternBuilder.getAllowedDomains("user", ["group1"]); + assert.deepEqual(allowed_domains, ["*"]); + }); + }); + + describe("test access control pattern builder", function () { + let patternBuilder: PatternBuilder; + let configuration: ACLConfiguration; + + + beforeEach(() => { + configuration = { + default: [], + users: {}, + groups: {} + }; + patternBuilder = new PatternBuilder(configuration, winston); + }); + + it("should deny all if nothing is defined in the config", function () { + const allowed_domains = patternBuilder.getAllowedDomains("user", ["group1", "group2"]); + assert.deepEqual(allowed_domains, []); + }); + + it("should allow domain test.example.com to all users if defined in" + + " default policy", function () { + configuration.default = ["test.example.com"]; + const allowed_domains = patternBuilder.getAllowedDomains("user", ["group1", "group2"]); + assert.deepEqual(allowed_domains, ["test.example.com"]); + }); + + it("should allow domain test.example.com to all users in group mygroup", function () { + const allowed_domains0 = patternBuilder.getAllowedDomains("user", ["group1", "group1"]); + assert.deepEqual(allowed_domains0, []); + + configuration.groups = { + mygroup: ["test.example.com"] + }; + + const allowed_domains1 = patternBuilder.getAllowedDomains("user", ["group1", "group2"]); + assert.deepEqual(allowed_domains1, []); + + const allowed_domains2 = patternBuilder.getAllowedDomains("user", ["group1", "mygroup"]); + assert.deepEqual(allowed_domains2, ["test.example.com"]); + }); + + it("should allow domain test.example.com based on per user config", function () { + const allowed_domains0 = patternBuilder.getAllowedDomains("user", ["group1"]); + assert.deepEqual(allowed_domains0, []); + + configuration.users = { + user1: ["test.example.com"] + }; + + const allowed_domains1 = patternBuilder.getAllowedDomains("user", ["group1", "mygroup"]); + assert.deepEqual(allowed_domains1, []); + + const allowed_domains2 = patternBuilder.getAllowedDomains("user1", ["group1", "mygroup"]); + assert.deepEqual(allowed_domains2, ["test.example.com"]); + }); + + it("should allow domains from user and groups", function () { + configuration.groups = { + group2: ["secret.example.com", "secret1.example.com"] + }; + configuration.users = { + user: ["test.example.com"] + }; + + const allowed_domains0 = patternBuilder.getAllowedDomains("user", ["group1", "group2"]); + assert.deepEqual(allowed_domains0, [ + "secret.example.com", + "secret1.example.com", + "test.example.com", + ]); + }); + + it("should allow domains from several groups", function () { + configuration.groups = { + group1: ["secret2.example.com"], + group2: ["secret.example.com", "secret1.example.com"] + }; + + const allowed_domains0 = patternBuilder.getAllowedDomains("user", ["group1", "group2"]); + assert.deepEqual(allowed_domains0, [ + "secret2.example.com", + "secret.example.com", + "secret1.example.com", + ]); + }); + + it("should allow domains from several groups and default policy", function () { + configuration.default = ["home.example.com"]; + configuration.groups = { + group1: ["secret2.example.com"], + group2: ["secret.example.com", "secret1.example.com"] + }; + + const allowed_domains0 = patternBuilder.getAllowedDomains("user", ["group1", "group2"]); + assert.deepEqual(allowed_domains0, [ + "home.example.com", + "secret2.example.com", + "secret.example.com", + "secret1.example.com", + ]); + }); + }); +}); diff --git a/test/unitary/routes/test_first_factor.js b/test/unitary/routes/test_first_factor.js index 7f500fc8..fffb8cab 100644 --- a/test/unitary/routes/test_first_factor.js +++ b/test/unitary/routes/test_first_factor.js @@ -6,7 +6,6 @@ var winston = require('winston'); var first_factor = require('../../../src/lib/routes/first_factor'); var exceptions = require('../../../src/lib/exceptions'); var Ldap = require('../../../src/lib/ldap'); -var AccessControl = require('../../../src/lib/access_control'); describe('test the first factor validation route', function() { var req, res; @@ -14,7 +13,7 @@ describe('test the first factor validation route', function() { var emails; var search_res_ok; var regulator; - var access_control; + var access_controller; var config; beforeEach(function() { @@ -36,14 +35,8 @@ describe('test the first factor validation route', function() { regulator.mark.returns(Promise.resolve()); regulator.regulate.returns(Promise.resolve()); - access_control = { - builder: { - get_allowed_domains: sinon.stub(), - get_any_domain: sinon.stub(), - }, - matcher: { - is_domain_allowed: sinon.stub() - } + access_controller = { + isDomainAllowedForUser: sinon.stub().returns(true) }; var app_get = sinon.stub(); @@ -51,7 +44,7 @@ describe('test the first factor validation route', function() { app_get.withArgs('config').returns(config); app_get.withArgs('logger').returns(winston); app_get.withArgs('authentication regulator').returns(regulator); - app_get.withArgs('access control').returns(access_control); + app_get.withArgs('access controller').returns(access_controller); req = { app: { @@ -87,40 +80,6 @@ describe('test the first factor validation route', function() { }); }); - describe('store the ACL matcher in the auth session', function() { - it('should store the allowed domains in the auth session', function() { - config.access_control = {}; - access_control.builder.get_allowed_domains.returns(['example.com', 'test.example.com']); - return new Promise(function(resolve, reject) { - res.send = sinon.spy(function(data) { - assert.deepEqual(['example.com', 'test.example.com'], - req.session.auth_session.allowed_domains); - assert.equal(204, res.status.getCall(0).args[0]); - resolve(); - }); - ldap_interface_mock.bind.withArgs('username').returns(Promise.resolve()); - ldap_interface_mock.get_emails.returns(Promise.resolve(emails)); - ldap_interface_mock.get_groups.returns(Promise.resolve(groups)); - first_factor(req, res); - }); - }); - - it('should store the allow all ACL matcher in the auth session', function() { - access_control.builder.get_any_domain.returns(['*']); - return new Promise(function(resolve, reject) { - res.send = sinon.spy(function(data) { - assert(req.session.auth_session.allowed_domains); - assert.equal(204, res.status.getCall(0).args[0]); - resolve(); - }); - ldap_interface_mock.bind.withArgs('username').returns(Promise.resolve()); - ldap_interface_mock.get_emails.returns(Promise.resolve(emails)); - ldap_interface_mock.get_groups.returns(Promise.resolve(groups)); - first_factor(req, res); - }); - }); - }); - it('should retrieve email from LDAP', function(done) { res.send = sinon.spy(function(data) { done(); }); ldap_interface_mock.bind.returns(Promise.resolve()); diff --git a/test/unitary/routes/test_verify.js b/test/unitary/routes/test_verify.js index e4987540..a3b7710a 100644 --- a/test/unitary/routes/test_verify.js +++ b/test/unitary/routes/test_verify.js @@ -93,25 +93,6 @@ describe('test authentication token verification', function() { return test_unauthorized(undefined); }); - it('should reply unauthorized when the domain is restricted', function() { - acl_matcher.is_domain_allowed.returns(false); - return test_unauthorized({ - first_factor: true, - second_factor: true, - userid: 'user', - allowed_domains: [] - }); - }); - - it('should reply authorized when the domain is allowed', function() { - return test_authorized({ - first_factor: true, - second_factor: true, - userid: 'user', - allowed_domains: ['secret.example.com'] - }); - }); - it('should not be authenticated when session is partially initialized', function() { return test_unauthorized({ first_factor: true }); }); diff --git a/test/unitary/test_access_control.js b/test/unitary/test_access_control.js deleted file mode 100644 index c0a496c7..00000000 --- a/test/unitary/test_access_control.js +++ /dev/null @@ -1,160 +0,0 @@ - -var assert = require('assert'); -var winston = require('winston'); -var AccessControl = require('../../src/lib/access_control'); - -describe('test access control manager', function() { - var access_control; - var acl_config; - var acl_builder; - var acl_matcher; - - beforeEach(function() { - acl_config = {}; - access_control = AccessControl(winston, acl_config); - acl_builder = access_control.builder; - acl_matcher = access_control.matcher; - }); - - describe('building user group access control matcher', function() { - it('should deny all if nothing is defined in the config', function() { - var allowed_domains = acl_builder.get_allowed_domains('user', ['group1', 'group2']); - assert.deepEqual(allowed_domains, []); - }); - - it('should allow domain test.example.com to all users if defined in' + - ' default policy', function() { - acl_config.default = ['test.example.com']; - - var allowed_domains = acl_builder.get_allowed_domains('user', ['group1', 'group2']); - assert.deepEqual(allowed_domains, ['test.example.com']); - }); - - it('should allow domain test.example.com to all users in group mygroup', function() { - var allowed_domains0 = acl_builder.get_allowed_domains('user', ['group1', 'group1']); - assert.deepEqual(allowed_domains0, []); - - acl_config.groups = { - mygroup: ['test.example.com'] - }; - - var allowed_domains1 = acl_builder.get_allowed_domains('user', ['group1', 'group2']); - assert.deepEqual(allowed_domains1, []); - - var allowed_domains2 = acl_builder.get_allowed_domains('user', ['group1', 'mygroup']); - assert.deepEqual(allowed_domains2, ['test.example.com']); - }); - - it('should allow domain test.example.com based on per user config', function() { - var allowed_domains0 = acl_builder.get_allowed_domains('user', ['group1']); - assert.deepEqual(allowed_domains0, []); - - acl_config.users = { - user1: ['test.example.com'] - }; - - var allowed_domains1 = acl_builder.get_allowed_domains('user', ['group1', 'mygroup']); - assert.deepEqual(allowed_domains1, []); - - var allowed_domains2 = acl_builder.get_allowed_domains('user1', ['group1', 'mygroup']); - assert.deepEqual(allowed_domains2, ['test.example.com']); - }); - - it('should allow domains from user and groups', function() { - acl_config.groups = { - group2: ['secret.example.com', 'secret1.example.com'] - }; - acl_config.users = { - user: ['test.example.com'] - }; - - var allowed_domains0 = acl_builder.get_allowed_domains('user', ['group1', 'group2']); - assert.deepEqual(allowed_domains0, [ - 'secret.example.com', - 'secret1.example.com', - 'test.example.com', - ]); - }); - - it('should allow domains from several groups', function() { - acl_config.groups = { - group1: ['secret2.example.com'], - group2: ['secret.example.com', 'secret1.example.com'] - }; - - var allowed_domains0 = acl_builder.get_allowed_domains('user', ['group1', 'group2']); - assert.deepEqual(allowed_domains0, [ - 'secret2.example.com', - 'secret.example.com', - 'secret1.example.com', - ]); - }); - - it('should allow domains from several groups and default policy', function() { - acl_config.default = ['home.example.com']; - acl_config.groups = { - group1: ['secret2.example.com'], - group2: ['secret.example.com', 'secret1.example.com'] - }; - - var allowed_domains0 = acl_builder.get_allowed_domains('user', ['group1', 'group2']); - assert.deepEqual(allowed_domains0, [ - 'home.example.com', - 'secret2.example.com', - 'secret.example.com', - 'secret1.example.com', - ]); - }); - }); - - describe('building user group access control matcher', function() { - it('should allow access to any subdomain', function() { - var allowed_domains = acl_builder.get_any_domain(); - assert(acl_matcher.is_domain_allowed('example.com', allowed_domains)); - assert(acl_matcher.is_domain_allowed('mail.example.com', allowed_domains)); - assert(acl_matcher.is_domain_allowed('test.example.com', allowed_domains)); - assert(acl_matcher.is_domain_allowed('user.mail.example.com', allowed_domains)); - assert(acl_matcher.is_domain_allowed('public.example.com', allowed_domains)); - assert(acl_matcher.is_domain_allowed('example2.com', allowed_domains)); - }); - }); - - describe('check access control matching', function() { - beforeEach(function() { - acl_config.default = ['home.example.com', '*.public.example.com']; - acl_config.users = { - user1: ['user1.example.com', 'user1.mail.example.com'] - }; - acl_config.groups = { - group1: ['secret2.example.com'], - group2: ['secret.example.com', 'secret1.example.com'] - }; - }); - - it('should allow access to secret.example.com', function() { - var allowed_domains = acl_builder.get_allowed_domains('user', ['group1', 'group2']); - assert(acl_matcher.is_domain_allowed('secret.example.com', allowed_domains)); - }); - - it('should deny access to secret3.example.com', function() { - var allowed_domains = acl_builder.get_allowed_domains('user', ['group1', 'group2']); - assert(!acl_matcher.is_domain_allowed('secret3.example.com', allowed_domains)); - }); - - it('should allow access to home.example.com', function() { - var allowed_domains = acl_builder.get_allowed_domains('user', ['group1', 'group2']); - assert(acl_matcher.is_domain_allowed('home.example.com', allowed_domains)); - }); - - it('should allow access to user1.example.com', function() { - var allowed_domains = acl_builder.get_allowed_domains('user1', ['group1', 'group2']); - assert(acl_matcher.is_domain_allowed('user1.example.com', allowed_domains)); - }); - - it('should allow access *.public.example.com', function() { - var allowed_domains = acl_builder.get_allowed_domains('nouser', []); - assert(acl_matcher.is_domain_allowed('user.public.example.com', allowed_domains)); - assert(acl_matcher.is_domain_allowed('test.public.example.com', allowed_domains)); - }); - }); -}); From bf74667726b403a986cafd915ccb2810fe705276 Mon Sep 17 00:00:00 2001 From: Clement Michaud Date: Sat, 20 May 2017 19:16:57 +0200 Subject: [PATCH 08/16] Move TOTP Validator and Generator to typescript --- package.json | 1 + src/lib/AuthenticationRegulator.ts | 2 +- src/lib/Dependencies.ts | 22 ------ src/lib/Server.ts | 11 ++- src/lib/TOTPGenerator.ts | 16 +++++ src/lib/TOTPValidator.ts | 23 +++++++ src/lib/UserDataStore.ts | 2 +- src/lib/notifiers/GMailNotifier.ts | 2 +- src/lib/notifiers/NotifierFactory.ts | 2 +- src/lib/routes/totp.js | 5 +- src/lib/routes/totp_register.js | 4 +- src/lib/totp.js | 22 ------ src/types/Dependencies.ts | 27 ++++++++ src/{lib => types}/TOTPSecret.ts | 0 test/unitary/AuthenticationRegulator.test.ts | 2 +- test/unitary/Server.test.ts | 11 ++- test/unitary/TOTPValidator.ts | 30 ++++++++ test/unitary/data_persistence.test.ts | 2 +- test/unitary/mocks/nodemailer.ts | 1 - test/unitary/mocks/speakeasy.ts | 7 ++ .../unitary/notifiers/NotifierFactory.test.ts | 2 +- test/unitary/routes/test_totp.js | 14 ++-- test/unitary/routes/test_totp_register.js | 12 +++- test/unitary/server_config.test.ts | 34 ++++----- test/unitary/test_server_config.ts | 69 ------------------- test/unitary/totp.test.ts | 32 --------- 26 files changed, 161 insertions(+), 194 deletions(-) delete mode 100644 src/lib/Dependencies.ts create mode 100644 src/lib/TOTPGenerator.ts create mode 100644 src/lib/TOTPValidator.ts delete mode 100644 src/lib/totp.js create mode 100644 src/types/Dependencies.ts rename src/{lib => types}/TOTPSecret.ts (100%) create mode 100644 test/unitary/TOTPValidator.ts create mode 100644 test/unitary/mocks/speakeasy.ts delete mode 100644 test/unitary/test_server_config.ts delete mode 100644 test/unitary/totp.test.ts 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(); - }); - }); -}); - From ba80bbd2193fe175d7ab3935643b47045a02cf52 Mon Sep 17 00:00:00 2001 From: Clement Michaud Date: Sat, 20 May 2017 19:21:45 +0200 Subject: [PATCH 09/16] Move setup_endpoints to typescript --- src/lib/RestApi.ts | 281 +++++++++++++++++++++++++++++++++++++ src/lib/Server.ts | 4 +- src/lib/setup_endpoints.js | 280 ------------------------------------ 3 files changed, 283 insertions(+), 282 deletions(-) create mode 100644 src/lib/RestApi.ts delete mode 100644 src/lib/setup_endpoints.js diff --git a/src/lib/RestApi.ts b/src/lib/RestApi.ts new file mode 100644 index 00000000..42bccbc2 --- /dev/null +++ b/src/lib/RestApi.ts @@ -0,0 +1,281 @@ + +import express = require("express"); + +const routes = require("./routes"); +const identity_check = require("./identity_check"); + +export default class RestApi { + static setup(app: express.Application): void { + /** + * @apiDefine UserSession + * @apiHeader {String} Cookie Cookie containing "connect.sid", the user + * session token. + */ + + /** + * @apiDefine InternalError + * @apiError (Error 500) {String} error Internal error message. + */ + + /** + * @apiDefine IdentityValidationPost + * + * @apiSuccess (Success 204) status Identity validation has been initiated. + * @apiError (Error 403) AccessDenied Access is denied. + * @apiError (Error 400) InvalidIdentity User identity is invalid. + * @apiError (Error 500) {String} error Internal error message. + * + * @apiDescription This request issue an identity validation token for the user + * bound to the session. It sends a challenge to the email address set in the user + * LDAP entry. The user must visit the sent URL to complete the validation and + * continue the registration process. + */ + + /** + * @apiDefine IdentityValidationGet + * @apiParam {String} identity_token The one-time identity validation token provided in the email. + * @apiSuccess (Success 200) {String} content The content of the page. + * @apiError (Error 403) AccessDenied Access is denied. + * @apiError (Error 500) {String} error Internal error message. + */ + + /** + * @api {get} /login Serve login page + * @apiName Login + * @apiGroup Pages + * @apiVersion 1.0.0 + * + * @apiParam {String} redirect Redirect to this URL when user is authenticated. + * @apiSuccess (Success 200) {String} Content The content of the login page. + * + * @apiDescription Create a user session and serve the login page along with + * a cookie. + */ + app.get("/login", routes.login); + + /** + * @api {get} /logout Server logout page + * @apiName Logout + * @apiGroup Pages + * @apiVersion 1.0.0 + * + * @apiParam {String} redirect Redirect to this URL when user is deauthenticated. + * @apiSuccess (Success 301) redirect Redirect to the URL. + * + * @apiDescription Deauthenticate the user and redirect him. + */ + app.get("/logout", routes.logout); + + /** + * @api {post} /totp-register Request TOTP registration + * @apiName RequestTOTPRegistration + * @apiGroup Registration + * @apiVersion 1.0.0 + * @apiUse UserSession + * @apiUse IdentityValidationPost + */ + /** + * @api {get} /totp-register Serve TOTP registration page + * @apiName ServeTOTPRegistrationPage + * @apiGroup Registration + * @apiVersion 1.0.0 + * @apiUse UserSession + * @apiUse IdentityValidationGet + * + * + * @apiDescription Serves the TOTP registration page that displays the secret. + * The secret is a QRCode and a base32 secret. + */ + identity_check(app, "/totp-register", routes.totp_register.icheck_interface); + + + /** + * @api {post} /u2f-register Request U2F registration + * @apiName RequestU2FRegistration + * @apiGroup Registration + * @apiVersion 1.0.0 + * @apiUse UserSession + * @apiUse IdentityValidationPost + */ + /** + * @api {get} /u2f-register Serve U2F registration page + * @apiName ServeU2FRegistrationPage + * @apiGroup Pages + * @apiVersion 1.0.0 + * @apiUse UserSession + * @apiUse IdentityValidationGet + * + * @apiDescription Serves the U2F registration page that asks the user to + * touch the token of the U2F device. + */ + identity_check(app, "/u2f-register", routes.u2f_register.icheck_interface); + + /** + * @api {post} /reset-password Request for password reset + * @apiName RequestPasswordReset + * @apiGroup Registration + * @apiVersion 1.0.0 + * @apiUse UserSession + * @apiUse IdentityValidationPost + */ + /** + * @api {get} /reset-password Serve password reset form. + * @apiName ServePasswordResetForm + * @apiGroup Pages + * @apiVersion 1.0.0 + * @apiUse UserSession + * @apiUse IdentityValidationGet + * + * @apiDescription Serves password reset form that allow the user to provide + * the new password. + */ + identity_check(app, "/reset-password", routes.reset_password.icheck_interface); + + app.get("/reset-password-form", function (req, res) { res.render("reset-password-form"); }); + + /** + * @api {post} /new-password Set LDAP password + * @apiName SetLDAPPassword + * @apiGroup Registration + * @apiVersion 1.0.0 + * @apiUse UserSession + * + * @apiParam {String} password New password + * + * @apiDescription Set a new password for the user. + */ + app.post("/new-password", routes.reset_password.post); + + /** + * @api {post} /new-totp-secret Generate TOTP secret + * @apiName GenerateTOTPSecret + * @apiGroup Registration + * @apiVersion 1.0.0 + * @apiUse UserSession + * + * @apiSuccess (Success 200) {String} base32 The base32 representation of the secret. + * @apiSuccess (Success 200) {String} ascii The ASCII representation of the secret. + * @apiSuccess (Success 200) {String} qrcode The QRCode of the secret in URI format. + * + * @apiError (Error 403) {String} error No user provided in the session or + * unexpected identity validation challenge in the session. + * @apiError (Error 500) {String} error Internal error message + * + * @apiDescription Generate a new TOTP secret and returns it. + */ + app.post("/new-totp-secret", routes.totp_register.post); + + /** + * @api {get} /verify Verify user authentication + * @apiName VerifyAuthentication + * @apiGroup Verification + * @apiVersion 1.0.0 + * @apiUse UserSession + * + * @apiSuccess (Success 204) status The user is authenticated. + * @apiError (Error 401) status The user is not authenticated. + * + * @apiDescription Verify that the user is authenticated, i.e., the two + * factors have been validated + */ + app.get("/verify", routes.verify); + + /** + * @api {post} /1stfactor LDAP authentication + * @apiName ValidateFirstFactor + * @apiGroup Authentication + * @apiVersion 1.0.0 + * @apiUse UserSession + * @apiUse InternalError + * + * @apiParam {String} username User username. + * @apiParam {String} password User password. + * + * @apiSuccess (Success 204) status 1st factor is validated. + * @apiError (Error 401) {none} error 1st factor is not validated. + * @apiError (Error 403) {none} error Access has been restricted after too + * many authentication attempts + * + * @apiDescription Verify credentials against the LDAP. + */ + app.post("/1stfactor", routes.first_factor); + + /** + * @api {post} /2ndfactor/totp TOTP authentication + * @apiName ValidateTOTPSecondFactor + * @apiGroup Authentication + * @apiVersion 1.0.0 + * @apiUse UserSession + * @apiUse InternalError + * + * @apiParam {String} token TOTP token. + * + * @apiSuccess (Success 204) status TOTP token is valid. + * @apiError (Error 401) {none} error TOTP token is invalid. + * + * @apiDescription Verify TOTP token. The user is authenticated upon success. + */ + app.post("/2ndfactor/totp", routes.second_factor.totp); + + /** + * @api {get} /2ndfactor/u2f/sign_request U2F Start authentication + * @apiName StartU2FAuthentication + * @apiGroup Authentication + * @apiVersion 1.0.0 + * @apiUse UserSession + * @apiUse InternalError + * + * @apiSuccess (Success 200) authentication_request The U2F authentication request. + * @apiError (Error 401) {none} error There is no key registered for user in session. + * + * @apiDescription Initiate an authentication request using a U2F device. + */ + app.get("/2ndfactor/u2f/sign_request", routes.second_factor.u2f.sign_request); + + /** + * @api {post} /2ndfactor/u2f/sign U2F Complete authentication + * @apiName CompleteU2FAuthentication + * @apiGroup Authentication + * @apiVersion 1.0.0 + * @apiUse UserSession + * @apiUse InternalError + * + * @apiSuccess (Success 204) status The U2F authentication succeeded. + * @apiError (Error 403) {none} error No authentication request has been provided. + * + * @apiDescription Complete authentication request of the U2F device. + */ + app.post("/2ndfactor/u2f/sign", routes.second_factor.u2f.sign); + + /** + * @api {get} /2ndfactor/u2f/register_request U2F Start device registration + * @apiName StartU2FRegistration + * @apiGroup Registration + * @apiVersion 1.0.0 + * @apiUse UserSession + * @apiUse InternalError + * + * @apiSuccess (Success 200) authentication_request The U2F registration request. + * @apiError (Error 403) {none} error Unexpected identity validation challenge. + * + * @apiDescription Initiate a U2F device registration request. + */ + app.get("/2ndfactor/u2f/register_request", routes.second_factor.u2f.register_request); + + /** + * @api {post} /2ndfactor/u2f/register U2F Complete device registration + * @apiName CompleteU2FRegistration + * @apiGroup Registration + * @apiVersion 1.0.0 + * @apiUse UserSession + * @apiUse InternalError + * + * @apiSuccess (Success 204) status The U2F registration succeeded. + * @apiError (Error 403) {none} error Unexpected identity validation challenge. + * @apiError (Error 403) {none} error No registration request has been provided. + * + * @apiDescription Complete U2F registration request. + */ + app.post("/2ndfactor/u2f/register", routes.second_factor.u2f.register); + } +} diff --git a/src/lib/Server.ts b/src/lib/Server.ts index 34d38056..1e8e8a95 100644 --- a/src/lib/Server.ts +++ b/src/lib/Server.ts @@ -7,6 +7,7 @@ import ConfigurationAdapter from "./ConfigurationAdapter"; import { NotifierFactory } from "./notifiers/NotifierFactory"; import TOTPValidator from "./TOTPValidator"; import TOTPGenerator from "./TOTPGenerator"; +import RestApi from "./RestApi"; import * as Express from "express"; import * as BodyParser from "body-parser"; @@ -15,7 +16,6 @@ import * as http from "http"; import AccessController from "./access_control/AccessController"; -const setup_endpoints = require("./setup_endpoints"); const Ldap = require("./ldap"); export default class Server { @@ -74,7 +74,7 @@ export default class Server { app.set("config", config); app.set("access controller", accessController); - setup_endpoints(app); + RestApi.setup(app); return new Promise((resolve, reject) => { this.httpServer = app.listen(config.port, function (err: string) { diff --git a/src/lib/setup_endpoints.js b/src/lib/setup_endpoints.js deleted file mode 100644 index efbed844..00000000 --- a/src/lib/setup_endpoints.js +++ /dev/null @@ -1,280 +0,0 @@ - -module.exports = setup_endpoints; - -var routes = require('./routes'); -var identity_check = require('./identity_check'); - -function setup_endpoints(app) { - /** - * @apiDefine UserSession - * @apiHeader {String} Cookie Cookie containing 'connect.sid', the user - * session token. - */ - - /** - * @apiDefine InternalError - * @apiError (Error 500) {String} error Internal error message. - */ - - /** - * @apiDefine IdentityValidationPost - * - * @apiSuccess (Success 204) status Identity validation has been initiated. - * @apiError (Error 403) AccessDenied Access is denied. - * @apiError (Error 400) InvalidIdentity User identity is invalid. - * @apiError (Error 500) {String} error Internal error message. - * - * @apiDescription This request issue an identity validation token for the user - * bound to the session. It sends a challenge to the email address set in the user - * LDAP entry. The user must visit the sent URL to complete the validation and - * continue the registration process. - */ - - /** - * @apiDefine IdentityValidationGet - * @apiParam {String} identity_token The one-time identity validation token provided in the email. - * @apiSuccess (Success 200) {String} content The content of the page. - * @apiError (Error 403) AccessDenied Access is denied. - * @apiError (Error 500) {String} error Internal error message. - */ - - /** - * @api {get} /login Serve login page - * @apiName Login - * @apiGroup Pages - * @apiVersion 1.0.0 - * - * @apiParam {String} redirect Redirect to this URL when user is authenticated. - * @apiSuccess (Success 200) {String} Content The content of the login page. - * - * @apiDescription Create a user session and serve the login page along with - * a cookie. - */ - app.get ('/login', routes.login); - - /** - * @api {get} /logout Server logout page - * @apiName Logout - * @apiGroup Pages - * @apiVersion 1.0.0 - * - * @apiParam {String} redirect Redirect to this URL when user is deauthenticated. - * @apiSuccess (Success 301) redirect Redirect to the URL. - * - * @apiDescription Deauthenticate the user and redirect him. - */ - app.get ('/logout', routes.logout); - - /** - * @api {post} /totp-register Request TOTP registration - * @apiName RequestTOTPRegistration - * @apiGroup Registration - * @apiVersion 1.0.0 - * @apiUse UserSession - * @apiUse IdentityValidationPost - */ - /** - * @api {get} /totp-register Serve TOTP registration page - * @apiName ServeTOTPRegistrationPage - * @apiGroup Registration - * @apiVersion 1.0.0 - * @apiUse UserSession - * @apiUse IdentityValidationGet - * - * - * @apiDescription Serves the TOTP registration page that displays the secret. - * The secret is a QRCode and a base32 secret. - */ - identity_check(app, '/totp-register', routes.totp_register.icheck_interface); - - - /** - * @api {post} /u2f-register Request U2F registration - * @apiName RequestU2FRegistration - * @apiGroup Registration - * @apiVersion 1.0.0 - * @apiUse UserSession - * @apiUse IdentityValidationPost - */ - /** - * @api {get} /u2f-register Serve U2F registration page - * @apiName ServeU2FRegistrationPage - * @apiGroup Pages - * @apiVersion 1.0.0 - * @apiUse UserSession - * @apiUse IdentityValidationGet - * - * @apiDescription Serves the U2F registration page that asks the user to - * touch the token of the U2F device. - */ - identity_check(app, '/u2f-register', routes.u2f_register.icheck_interface); - - /** - * @api {post} /reset-password Request for password reset - * @apiName RequestPasswordReset - * @apiGroup Registration - * @apiVersion 1.0.0 - * @apiUse UserSession - * @apiUse IdentityValidationPost - */ - /** - * @api {get} /reset-password Serve password reset form. - * @apiName ServePasswordResetForm - * @apiGroup Pages - * @apiVersion 1.0.0 - * @apiUse UserSession - * @apiUse IdentityValidationGet - * - * @apiDescription Serves password reset form that allow the user to provide - * the new password. - */ - identity_check(app, '/reset-password', routes.reset_password.icheck_interface); - - app.get ('/reset-password-form', function(req, res) { res.render('reset-password-form'); }); - - /** - * @api {post} /new-password Set LDAP password - * @apiName SetLDAPPassword - * @apiGroup Registration - * @apiVersion 1.0.0 - * @apiUse UserSession - * - * @apiParam {String} password New password - * - * @apiDescription Set a new password for the user. - */ - app.post ('/new-password', routes.reset_password.post); - - /** - * @api {post} /new-totp-secret Generate TOTP secret - * @apiName GenerateTOTPSecret - * @apiGroup Registration - * @apiVersion 1.0.0 - * @apiUse UserSession - * - * @apiSuccess (Success 200) {String} base32 The base32 representation of the secret. - * @apiSuccess (Success 200) {String} ascii The ASCII representation of the secret. - * @apiSuccess (Success 200) {String} qrcode The QRCode of the secret in URI format. - * - * @apiError (Error 403) {String} error No user provided in the session or - * unexpected identity validation challenge in the session. - * @apiError (Error 500) {String} error Internal error message - * - * @apiDescription Generate a new TOTP secret and returns it. - */ - app.post ('/new-totp-secret', routes.totp_register.post); - - /** - * @api {get} /verify Verify user authentication - * @apiName VerifyAuthentication - * @apiGroup Verification - * @apiVersion 1.0.0 - * @apiUse UserSession - * - * @apiSuccess (Success 204) status The user is authenticated. - * @apiError (Error 401) status The user is not authenticated. - * - * @apiDescription Verify that the user is authenticated, i.e., the two - * factors have been validated - */ - app.get ('/verify', routes.verify); - - /** - * @api {post} /1stfactor LDAP authentication - * @apiName ValidateFirstFactor - * @apiGroup Authentication - * @apiVersion 1.0.0 - * @apiUse UserSession - * @apiUse InternalError - * - * @apiParam {String} username User username. - * @apiParam {String} password User password. - * - * @apiSuccess (Success 204) status 1st factor is validated. - * @apiError (Error 401) {none} error 1st factor is not validated. - * @apiError (Error 403) {none} error Access has been restricted after too - * many authentication attempts - * - * @apiDescription Verify credentials against the LDAP. - */ - app.post ('/1stfactor', routes.first_factor); - - /** - * @api {post} /2ndfactor/totp TOTP authentication - * @apiName ValidateTOTPSecondFactor - * @apiGroup Authentication - * @apiVersion 1.0.0 - * @apiUse UserSession - * @apiUse InternalError - * - * @apiParam {String} token TOTP token. - * - * @apiSuccess (Success 204) status TOTP token is valid. - * @apiError (Error 401) {none} error TOTP token is invalid. - * - * @apiDescription Verify TOTP token. The user is authenticated upon success. - */ - app.post ('/2ndfactor/totp', routes.second_factor.totp); - - /** - * @api {get} /2ndfactor/u2f/sign_request U2F Start authentication - * @apiName StartU2FAuthentication - * @apiGroup Authentication - * @apiVersion 1.0.0 - * @apiUse UserSession - * @apiUse InternalError - * - * @apiSuccess (Success 200) authentication_request The U2F authentication request. - * @apiError (Error 401) {none} error There is no key registered for user in session. - * - * @apiDescription Initiate an authentication request using a U2F device. - */ - app.get ('/2ndfactor/u2f/sign_request', routes.second_factor.u2f.sign_request); - - /** - * @api {post} /2ndfactor/u2f/sign U2F Complete authentication - * @apiName CompleteU2FAuthentication - * @apiGroup Authentication - * @apiVersion 1.0.0 - * @apiUse UserSession - * @apiUse InternalError - * - * @apiSuccess (Success 204) status The U2F authentication succeeded. - * @apiError (Error 403) {none} error No authentication request has been provided. - * - * @apiDescription Complete authentication request of the U2F device. - */ - app.post ('/2ndfactor/u2f/sign', routes.second_factor.u2f.sign); - - /** - * @api {get} /2ndfactor/u2f/register_request U2F Start device registration - * @apiName StartU2FRegistration - * @apiGroup Registration - * @apiVersion 1.0.0 - * @apiUse UserSession - * @apiUse InternalError - * - * @apiSuccess (Success 200) authentication_request The U2F registration request. - * @apiError (Error 403) {none} error Unexpected identity validation challenge. - * - * @apiDescription Initiate a U2F device registration request. - */ - app.get ('/2ndfactor/u2f/register_request', routes.second_factor.u2f.register_request); - - /** - * @api {post} /2ndfactor/u2f/register U2F Complete device registration - * @apiName CompleteU2FRegistration - * @apiGroup Registration - * @apiVersion 1.0.0 - * @apiUse UserSession - * @apiUse InternalError - * - * @apiSuccess (Success 204) status The U2F registration succeeded. - * @apiError (Error 403) {none} error Unexpected identity validation challenge. - * @apiError (Error 403) {none} error No registration request has been provided. - * - * @apiDescription Complete U2F registration request. - */ - app.post ('/2ndfactor/u2f/register', routes.second_factor.u2f.register); -} - From bada70cf64bba8146387c8dd9df7685cd1509411 Mon Sep 17 00:00:00 2001 From: Clement Michaud Date: Sat, 20 May 2017 22:55:37 +0200 Subject: [PATCH 10/16] Move exceptions to typescript --- src/lib/AuthenticationRegulator.ts | 3 +- src/lib/Exceptions.ts | 49 +++++++ src/lib/Server.ts | 2 +- src/lib/exceptions.js | 45 ------ src/lib/identity_check.js | 2 +- src/lib/ldap.js | 4 +- src/lib/notifiers/FileSystemNotifier.ts | 2 +- src/lib/notifiers/GMailNotifier.ts | 8 +- src/lib/notifiers/INotifier.ts | 2 +- src/lib/notifiers/NotifierFactory.ts | 6 +- src/lib/routes.js | 2 +- src/lib/routes/FirstFactor.ts | 76 ++++++++++ src/lib/routes/first_factor.js | 95 ------------ src/lib/routes/reset_password.js | 2 +- src/lib/routes/totp.js | 2 +- src/types/Dependencies.ts | 11 +- src/{lib => types}/Identity.ts | 0 test/unitary/AuthenticationRegulator.test.ts | 5 +- test/unitary/mocks/AccessController.ts | 8 ++ test/unitary/mocks/AuthenticationRegulator.ts | 9 ++ test/unitary/mocks/Ldap.ts | 10 ++ test/unitary/mocks/express.ts | 11 ++ .../unitary/notifiers/NotifierFactory.test.ts | 13 +- test/unitary/routes/FirstFactor.test.ts | 136 ++++++++++++++++++ test/unitary/routes/test_first_factor.js | 136 ------------------ test/unitary/test_identity_check.js | 2 +- test/unitary/test_ldap.js | 2 +- 27 files changed, 328 insertions(+), 315 deletions(-) create mode 100644 src/lib/Exceptions.ts delete mode 100644 src/lib/exceptions.js create mode 100644 src/lib/routes/FirstFactor.ts delete mode 100644 src/lib/routes/first_factor.js rename src/{lib => types}/Identity.ts (100%) create mode 100644 test/unitary/mocks/AccessController.ts create mode 100644 test/unitary/mocks/AuthenticationRegulator.ts create mode 100644 test/unitary/mocks/Ldap.ts create mode 100644 test/unitary/mocks/express.ts create mode 100644 test/unitary/routes/FirstFactor.test.ts delete mode 100644 test/unitary/routes/test_first_factor.js diff --git a/src/lib/AuthenticationRegulator.ts b/src/lib/AuthenticationRegulator.ts index e55d860a..89b62892 100644 --- a/src/lib/AuthenticationRegulator.ts +++ b/src/lib/AuthenticationRegulator.ts @@ -1,7 +1,6 @@ import * as Promise from "bluebird"; - -const exceptions = require("./exceptions"); +import exceptions = require("./Exceptions"); const REGULATION_TRACE_TYPE = "regulation"; const MAX_AUTHENTICATION_COUNT_IN_TIME_RANGE = 3; diff --git a/src/lib/Exceptions.ts b/src/lib/Exceptions.ts new file mode 100644 index 00000000..0902e5dd --- /dev/null +++ b/src/lib/Exceptions.ts @@ -0,0 +1,49 @@ + + +export class LdapSeachError extends Error { + constructor(message?: string) { + super(message); + this.name = "LdapSeachError"; + Object.setPrototypeOf(this, LdapSeachError.prototype); + } +} + +export class LdapBindError extends Error { + constructor(message?: string) { + super(message); + this.name = "LdapBindError"; + Object.setPrototypeOf(this, LdapBindError.prototype); + } +} + +export class IdentityError extends Error { + constructor(message?: string) { + super(message); + this.name = "IdentityError"; + Object.setPrototypeOf(this, IdentityError.prototype); + } +} + +export class AccessDeniedError extends Error { + constructor(message?: string) { + super(message); + this.name = "AccessDeniedError"; + Object.setPrototypeOf(this, AccessDeniedError.prototype); + } +} + +export class AuthenticationRegulationError extends Error { + constructor(message?: string) { + super(message); + this.name = "AuthenticationRegulationError"; + Object.setPrototypeOf(this, AuthenticationRegulationError.prototype); + } +} + +export class InvalidTOTPError extends Error { + constructor(message?: string) { + super(message); + this.name = "InvalidTOTPError"; + Object.setPrototypeOf(this, InvalidTOTPError.prototype); + } +} diff --git a/src/lib/Server.ts b/src/lib/Server.ts index 1e8e8a95..4e81fa1b 100644 --- a/src/lib/Server.ts +++ b/src/lib/Server.ts @@ -57,7 +57,7 @@ export default class Server { const five_minutes = 5 * 60; const data_store = new UserDataStore(datastore_options); const regulator = new AuthenticationRegulator(data_store, five_minutes); - const notifier = NotifierFactory.build(config.notifier, deps); + const notifier = NotifierFactory.build(config.notifier, deps.nodemailer); const ldap = new Ldap(deps, config.ldap); const accessController = new AccessController(config.access_control, deps.winston); const totpValidator = new TOTPValidator(deps.speakeasy); diff --git a/src/lib/exceptions.js b/src/lib/exceptions.js deleted file mode 100644 index 411816ee..00000000 --- a/src/lib/exceptions.js +++ /dev/null @@ -1,45 +0,0 @@ - -module.exports = { - LdapSearchError: LdapSearchError, - LdapBindError: LdapBindError, - IdentityError: IdentityError, - AccessDeniedError: AccessDeniedError, - AuthenticationRegulationError: AuthenticationRegulationError, - InvalidTOTPError: InvalidTOTPError, -} - -function LdapSearchError(message) { - this.name = "LdapSearchError"; - this.message = (message || ""); -} -LdapSearchError.prototype = Object.create(Error.prototype); - -function LdapBindError(message) { - this.name = "LdapBindError"; - this.message = (message || ""); -} -LdapBindError.prototype = Object.create(Error.prototype); - -function IdentityError(message) { - this.name = "IdentityError"; - this.message = (message || ""); -} -IdentityError.prototype = Object.create(Error.prototype); - -function AccessDeniedError(message) { - this.name = "AccessDeniedError"; - this.message = (message || ""); -} -AccessDeniedError.prototype = Object.create(Error.prototype); - -function AuthenticationRegulationError(message) { - this.name = "AuthenticationRegulationError"; - this.message = (message || ""); -} -AuthenticationRegulationError.prototype = Object.create(Error.prototype); - -function InvalidTOTPError(message) { - this.name = "InvalidTOTPError"; - this.message = (message || ""); -} -InvalidTOTPError.prototype = Object.create(Error.prototype); diff --git a/src/lib/identity_check.js b/src/lib/identity_check.js index 0b03509a..72fd603a 100644 --- a/src/lib/identity_check.js +++ b/src/lib/identity_check.js @@ -3,7 +3,7 @@ var objectPath = require('object-path'); var randomstring = require('randomstring'); var Promise = require('bluebird'); var util = require('util'); -var exceptions = require('./exceptions'); +var exceptions = require('./Exceptions'); var fs = require('fs'); var ejs = require('ejs'); diff --git a/src/lib/ldap.js b/src/lib/ldap.js index 0007dffc..cc473e98 100644 --- a/src/lib/ldap.js +++ b/src/lib/ldap.js @@ -3,7 +3,7 @@ module.exports = Ldap; var util = require('util'); var Promise = require('bluebird'); -var exceptions = require('./exceptions'); +var exceptions = require('./Exceptions'); var Dovehash = require('dovehash'); function Ldap(deps, ldap_config) { @@ -70,7 +70,7 @@ Ldap.prototype._search_in_ldap = function(base, query) { }); }) .catch(function(err) { - reject(new exceptions.LdapSearchError(err)); + reject(err); }); }); } diff --git a/src/lib/notifiers/FileSystemNotifier.ts b/src/lib/notifiers/FileSystemNotifier.ts index 10b62f8c..2ab95531 100644 --- a/src/lib/notifiers/FileSystemNotifier.ts +++ b/src/lib/notifiers/FileSystemNotifier.ts @@ -3,7 +3,7 @@ import * as BluebirdPromise from "bluebird"; import * as util from "util"; import * as fs from "fs"; import { INotifier } from "./INotifier"; -import { Identity } from "../Identity"; +import { Identity } from "../../types/Identity"; import { FileSystemNotifierConfiguration } from "../Configuration"; diff --git a/src/lib/notifiers/GMailNotifier.ts b/src/lib/notifiers/GMailNotifier.ts index 949ecf0c..1cd11a3c 100644 --- a/src/lib/notifiers/GMailNotifier.ts +++ b/src/lib/notifiers/GMailNotifier.ts @@ -4,8 +4,8 @@ import * as fs from "fs"; import * as ejs from "ejs"; import nodemailer = require("nodemailer"); -import { NodemailerDependencies } from "../../types/Dependencies"; -import { Identity } from "../Identity"; +import { Nodemailer } from "../../types/Dependencies"; +import { Identity } from "../../types/Identity"; import { INotifier } from "../notifiers/INotifier"; import { GmailNotifierConfiguration } from "../Configuration"; @@ -14,9 +14,9 @@ const email_template = fs.readFileSync(__dirname + "/../../resources/email-templ export class GMailNotifier extends INotifier { private transporter: any; - constructor(options: GmailNotifierConfiguration, deps: NodemailerDependencies) { + constructor(options: GmailNotifierConfiguration, nodemailer: Nodemailer) { super(); - const transporter = deps.createTransport({ + const transporter = nodemailer.createTransport({ service: "gmail", auth: { user: options.username, diff --git a/src/lib/notifiers/INotifier.ts b/src/lib/notifiers/INotifier.ts index 84e94b3d..e413047a 100644 --- a/src/lib/notifiers/INotifier.ts +++ b/src/lib/notifiers/INotifier.ts @@ -1,6 +1,6 @@ import * as BluebirdPromise from "bluebird"; -import { Identity } from "../Identity"; +import { Identity } from "../../types/Identity"; export abstract class INotifier { abstract notify(identity: Identity, subject: string, link: string): BluebirdPromise; diff --git a/src/lib/notifiers/NotifierFactory.ts b/src/lib/notifiers/NotifierFactory.ts index 96f631f6..56986fdd 100644 --- a/src/lib/notifiers/NotifierFactory.ts +++ b/src/lib/notifiers/NotifierFactory.ts @@ -1,15 +1,15 @@ import { NotifierConfiguration } from "..//Configuration"; -import { NotifierDependencies } from "../../types/Dependencies"; +import { Nodemailer } from "../../types/Dependencies"; import { INotifier } from "./INotifier"; import { GMailNotifier } from "./GMailNotifier"; import { FileSystemNotifier } from "./FileSystemNotifier"; export class NotifierFactory { - static build(options: NotifierConfiguration, deps: NotifierDependencies): INotifier { + static build(options: NotifierConfiguration, nodemailer: Nodemailer): INotifier { if ("gmail" in options) { - return new GMailNotifier(options.gmail, deps.nodemailer); + return new GMailNotifier(options.gmail, nodemailer); } else if ("filesystem" in options) { return new FileSystemNotifier(options.filesystem); diff --git a/src/lib/routes.js b/src/lib/routes.js index 31655d4c..b2c9f3b2 100644 --- a/src/lib/routes.js +++ b/src/lib/routes.js @@ -1,5 +1,5 @@ -var first_factor = require('./routes/first_factor'); +var first_factor = require('./routes/FirstFactor'); var second_factor = require('./routes/second_factor'); var reset_password = require('./routes/reset_password'); var verify = require('./routes/verify'); diff --git a/src/lib/routes/FirstFactor.ts b/src/lib/routes/FirstFactor.ts new file mode 100644 index 00000000..9b24afe0 --- /dev/null +++ b/src/lib/routes/FirstFactor.ts @@ -0,0 +1,76 @@ + +import exceptions = require("../Exceptions"); +import objectPath = require("object-path"); +import Promise = require("bluebird"); +import express = require("express"); + +export = function(req: express.Request, res: express.Response) { + const username = req.body.username; + const password = req.body.password; + if (!username || !password) { + res.status(401); + res.send(); + return; + } + + const logger = req.app.get("logger"); + const ldap = req.app.get("ldap"); + const config = req.app.get("config"); + const regulator = req.app.get("authentication regulator"); + const accessController = req.app.get("access controller"); + + logger.info("1st factor: Starting authentication of user \"%s\"", username); + logger.debug("1st factor: Start bind operation against LDAP"); + logger.debug("1st factor: username=%s", username); + + regulator.regulate(username) + .then(function() { + return ldap.bind(username, password); + }) + .then(function() { + objectPath.set(req, "session.auth_session.userid", username); + objectPath.set(req, "session.auth_session.first_factor", true); + logger.info("1st factor: LDAP binding successful"); + logger.debug("1st factor: Retrieve email from LDAP"); + return Promise.join(ldap.get_emails(username), ldap.get_groups(username)); + }) + .then(function(data: string[2]) { + const emails = data[0]; + const groups = data[1]; + + if (!emails && emails.length <= 0) throw new Error("No email found"); + logger.debug("1st factor: Retrieved email are %s", emails); + objectPath.set(req, "session.auth_session.email", emails[0]); + + const isAllowed = accessController.isDomainAllowedForUser(username, groups); + if (!isAllowed) throw new Error("User not allowed to visit this domain"); + + regulator.mark(username, true); + res.status(204); + res.send(); + }) + .catch(exceptions.LdapSeachError, function(err: Error) { + logger.error("1st factor: Unable to retrieve email from LDAP", err); + res.status(500); + res.send(); + }) + .catch(exceptions.LdapBindError, function(err: Error) { + logger.error("1st factor: LDAP binding failed"); + logger.debug("1st factor: LDAP binding failed due to ", err); + regulator.mark(username, false); + res.status(401); + res.send("Bad credentials"); + }) + .catch(exceptions.AuthenticationRegulationError, function(err: Error) { + logger.error("1st factor: the regulator rejected the authentication of user %s", username); + logger.debug("1st factor: authentication rejected due to %s", err); + res.status(403); + res.send("Access has been restricted for a few minutes..."); + }) + .catch(function(err: Error) { + console.log(err.stack); + logger.error("1st factor: Unhandled error %s", err); + res.status(500); + res.send("Internal error"); + }); +}; diff --git a/src/lib/routes/first_factor.js b/src/lib/routes/first_factor.js deleted file mode 100644 index 32af80ff..00000000 --- a/src/lib/routes/first_factor.js +++ /dev/null @@ -1,95 +0,0 @@ - -module.exports = first_factor; - -var exceptions = require('../exceptions'); -var objectPath = require('object-path'); -var Promise = require('bluebird'); - -function get_allowed_domains(access_control, username, groups) { - var allowed_domains = []; - - for(var i = 0; i= 0) { - var domains = rule.allowed_domains; - allowed_domains = allowed_domains.concat(domains); - } - else if('user' in rule && username == rule['user']) { - var domains = rule.allowed_domains; - allowed_domains = allowed_domains.concat(domains); - } - } - } - return allowed_domains; -} - -function first_factor(req, res) { - var username = req.body.username; - var password = req.body.password; - if(!username || !password) { - res.status(401); - res.send(); - return; - } - - var logger = req.app.get('logger'); - var ldap = req.app.get('ldap'); - var config = req.app.get('config'); - var regulator = req.app.get('authentication regulator'); - var accessController = req.app.get('access controller'); - - logger.info('1st factor: Starting authentication of user "%s"', username); - logger.debug('1st factor: Start bind operation against LDAP'); - logger.debug('1st factor: username=%s', username); - - regulator.regulate(username) - .then(function() { - return ldap.bind(username, password); - }) - .then(function() { - objectPath.set(req, 'session.auth_session.userid', username); - objectPath.set(req, 'session.auth_session.first_factor', true); - logger.info('1st factor: LDAP binding successful'); - logger.debug('1st factor: Retrieve email from LDAP'); - return Promise.join(ldap.get_emails(username), ldap.get_groups(username)); - }) - .then(function(data) { - var emails = data[0]; - var groups = data[1]; - var allowed_domains; - - if(!emails && emails.length <= 0) throw new Error('No email found'); - logger.debug('1st factor: Retrieved email are %s', emails); - objectPath.set(req, 'session.auth_session.email', emails[0]); - - allowed_domains = accessController.isDomainAllowedForUser(username, groups); - - regulator.mark(username, true); - res.status(204); - res.send(); - }) - .catch(exceptions.LdapSearchError, function(err) { - logger.error('1st factor: Unable to retrieve email from LDAP', err); - res.status(500); - res.send(); - }) - .catch(exceptions.LdapBindError, function(err) { - logger.error('1st factor: LDAP binding failed'); - logger.debug('1st factor: LDAP binding failed due to ', err); - regulator.mark(username, false); - res.status(401); - res.send('Bad credentials'); - }) - .catch(exceptions.AuthenticationRegulationError, function(err) { - logger.error('1st factor: the regulator rejected the authentication of user %s', username); - logger.debug('1st factor: authentication rejected due to %s', err); - res.status(403); - res.send('Access has been restricted for a few minutes...'); - }) - .catch(function(err) { - logger.error('1st factor: Unhandled error %s', err); - res.status(500); - res.send('Internal error'); - }); -} diff --git a/src/lib/routes/reset_password.js b/src/lib/routes/reset_password.js index dd26bf5c..c635993c 100644 --- a/src/lib/routes/reset_password.js +++ b/src/lib/routes/reset_password.js @@ -1,7 +1,7 @@ var Promise = require('bluebird'); var objectPath = require('object-path'); -var exceptions = require('../exceptions'); +var exceptions = require('../Exceptions'); var CHALLENGE = 'reset-password'; var icheck_interface = { diff --git a/src/lib/routes/totp.js b/src/lib/routes/totp.js index 9fdfb93a..dc5c5721 100644 --- a/src/lib/routes/totp.js +++ b/src/lib/routes/totp.js @@ -2,7 +2,7 @@ module.exports = totp_fn; var objectPath = require('object-path'); -var exceptions = require('../../../src/lib/exceptions'); +var exceptions = require('../../../src/lib/Exceptions'); var UNAUTHORIZED_MESSAGE = 'Unauthorized access'; diff --git a/src/types/Dependencies.ts b/src/types/Dependencies.ts index 9d7de366..78c7e150 100644 --- a/src/types/Dependencies.ts +++ b/src/types/Dependencies.ts @@ -1,8 +1,9 @@ -import * as winston from "winston"; -import * as speakeasy from "speakeasy"; +import winston = require("winston"); +import speakeasy = require("speakeasy"); import nodemailer = require("nodemailer"); import session = require("express-session"); import nedb = require("nedb"); +import ldapjs = require("ldapjs"); export type Nodemailer = typeof nodemailer; export type Speakeasy = typeof speakeasy; @@ -18,10 +19,4 @@ export interface GlobalDependencies { 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/Identity.ts b/src/types/Identity.ts similarity index 100% rename from src/lib/Identity.ts rename to src/types/Identity.ts diff --git a/test/unitary/AuthenticationRegulator.test.ts b/test/unitary/AuthenticationRegulator.test.ts index 50a739d2..88707aff 100644 --- a/test/unitary/AuthenticationRegulator.test.ts +++ b/test/unitary/AuthenticationRegulator.test.ts @@ -1,9 +1,8 @@ import AuthenticationRegulator from "../../src/lib/AuthenticationRegulator"; import UserDataStore from "../../src/lib/UserDataStore"; -import * as MockDate from "mockdate"; - -const exceptions = require("../../src/lib/exceptions"); +import MockDate = require("mockdate"); +import exceptions = require("../../src/lib/Exceptions"); describe("test authentication regulator", function() { it("should mark 2 authentication and regulate (resolve)", function() { diff --git a/test/unitary/mocks/AccessController.ts b/test/unitary/mocks/AccessController.ts new file mode 100644 index 00000000..a0c97853 --- /dev/null +++ b/test/unitary/mocks/AccessController.ts @@ -0,0 +1,8 @@ + +import sinon = require("sinon"); + +export = function () { + return { + isDomainAllowedForUser: sinon.stub() + }; +}; diff --git a/test/unitary/mocks/AuthenticationRegulator.ts b/test/unitary/mocks/AuthenticationRegulator.ts new file mode 100644 index 00000000..d4464c45 --- /dev/null +++ b/test/unitary/mocks/AuthenticationRegulator.ts @@ -0,0 +1,9 @@ + +import sinon = require("sinon"); + +export = function () { + return { + mark: sinon.stub(), + regulate: sinon.stub() + }; +}; diff --git a/test/unitary/mocks/Ldap.ts b/test/unitary/mocks/Ldap.ts new file mode 100644 index 00000000..a44846c9 --- /dev/null +++ b/test/unitary/mocks/Ldap.ts @@ -0,0 +1,10 @@ + +import sinon = require("sinon"); + +export = function () { + return { + bind: sinon.stub(), + get_emails: sinon.stub(), + get_groups: sinon.stub() + }; +}; diff --git a/test/unitary/mocks/express.ts b/test/unitary/mocks/express.ts new file mode 100644 index 00000000..1f6712bc --- /dev/null +++ b/test/unitary/mocks/express.ts @@ -0,0 +1,11 @@ + +import sinon = require("sinon"); + +export = { + Response: function () { + return { + send: sinon.stub(), + status: sinon.stub() + }; + } +}; \ No newline at end of file diff --git a/test/unitary/notifiers/NotifierFactory.test.ts b/test/unitary/notifiers/NotifierFactory.test.ts index b0233d70..ff6200c0 100644 --- a/test/unitary/notifiers/NotifierFactory.test.ts +++ b/test/unitary/notifiers/NotifierFactory.test.ts @@ -9,14 +9,10 @@ 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/types/Dependencies"; +import nodemailerMock = require("../mocks/nodemailer"); -describe("test notifier", function() { - const deps: NotifierDependencies = { - nodemailer: NodemailerMock - }; - +describe("test notifier factory", function() { it("should build a Gmail Notifier", function() { const options = { gmail: { @@ -24,7 +20,8 @@ describe("test notifier", function() { password: "password" } }; - assert(NotifierFactory.build(options, deps) instanceof GMailNotifier); + nodemailerMock.createTransport.returns(sinon.spy()); + assert(NotifierFactory.build(options, nodemailerMock) instanceof GMailNotifier); }); it("should build a FS Notifier", function() { @@ -34,6 +31,6 @@ describe("test notifier", function() { } }; - assert(NotifierFactory.build(options, deps) instanceof FileSystemNotifier); + assert(NotifierFactory.build(options, nodemailerMock) instanceof FileSystemNotifier); }); }); diff --git a/test/unitary/routes/FirstFactor.test.ts b/test/unitary/routes/FirstFactor.test.ts new file mode 100644 index 00000000..f949a151 --- /dev/null +++ b/test/unitary/routes/FirstFactor.test.ts @@ -0,0 +1,136 @@ + +import sinon = require("sinon"); +import BluebirdPromise = require("bluebird"); +import assert = require("assert"); +import winston = require("winston"); + +import FirstFactor = require("../../../src/lib/routes/FirstFactor"); +import exceptions = require("../../../src/lib/Exceptions"); +import AuthenticationRegulatorMock = require("../mocks/AuthenticationRegulator"); +import AccessControllerMock = require("../mocks/AccessController"); +import LdapMock = require("../mocks/Ldap"); +import ExpressMock = require("../mocks/express"); + +describe("test the first factor validation route", function() { + let req: any; + let res: any; + let emails: string[]; + let groups: string[]; + let configuration; + let ldapMock: any; + let regulator: any; + let accessController: any; + + beforeEach(function() { + configuration = { + ldap: { + base_dn: "ou=users,dc=example,dc=com", + user_name_attribute: "uid" + } + }; + + emails = [ "test_ok@example.com" ]; + groups = [ "group1", "group2" ]; + + ldapMock = LdapMock(); + + accessController = AccessControllerMock(); + accessController.isDomainAllowedForUser.returns(true); + + regulator = AuthenticationRegulatorMock(); + regulator.regulate.returns(BluebirdPromise.resolve()); + regulator.mark.returns(BluebirdPromise.resolve()); + + const app_get = sinon.stub(); + app_get.withArgs("ldap").returns(ldapMock); + app_get.withArgs("configuration").returns(configuration); + app_get.withArgs("logger").returns(winston); + app_get.withArgs("authentication regulator").returns(regulator); + app_get.withArgs("access controller").returns(accessController); + + req = { + app: { + get: app_get + }, + body: { + username: "username", + password: "password" + }, + session: { + auth_session: { + FirstFactor: false, + second_factor: false + } + } + }; + res = ExpressMock.Response(); + }); + + it("should return status code 204 when LDAP binding succeeds", function() { + return new Promise(function(resolve, reject) { + res.send = sinon.spy(function() { + assert.equal("username", req.session.auth_session.userid); + assert.equal(204, res.status.getCall(0).args[0]); + resolve(); + }); + ldapMock.bind.withArgs("username").returns(BluebirdPromise.resolve()); + ldapMock.get_emails.returns(BluebirdPromise.resolve(emails)); + FirstFactor(req, res); + }); + }); + + it("should retrieve email from LDAP", function(done) { + res.send = sinon.spy(function() { done(); }); + ldapMock.bind.returns(BluebirdPromise.resolve()); + ldapMock.get_emails = sinon.stub().withArgs("usernam").returns(BluebirdPromise.resolve([{mail: ["test@example.com"] }])); + FirstFactor(req, res); + }); + + it("should set email as session variables", function() { + return new Promise(function(resolve, reject) { + res.send = sinon.spy(function() { + assert.equal("test_ok@example.com", req.session.auth_session.email); + resolve(); + }); + const emails = [ "test_ok@example.com" ]; + ldapMock.bind.returns(BluebirdPromise.resolve()); + ldapMock.get_emails.returns(BluebirdPromise.resolve(emails)); + FirstFactor(req, res); + }); + }); + + it("should return status code 401 when LDAP binding throws", function(done) { + res.send = sinon.spy(function() { + assert.equal(401, res.status.getCall(0).args[0]); + assert.equal(regulator.mark.getCall(0).args[0], "username"); + done(); + }); + ldapMock.bind.throws(new exceptions.LdapBindError("Bad credentials")); + FirstFactor(req, res); + }); + + it("should return status code 500 when LDAP search throws", function(done) { + res.send = sinon.spy(function() { + assert.equal(500, res.status.getCall(0).args[0]); + done(); + }); + ldapMock.bind.returns(BluebirdPromise.resolve()); + ldapMock.get_emails.throws(new exceptions.LdapSeachError("error while retrieving emails")); + FirstFactor(req, res); + }); + + it("should return status code 403 when regulator rejects authentication", function(done) { + const err = new exceptions.AuthenticationRegulationError("Authentication regulation..."); + regulator.regulate.returns(BluebirdPromise.reject(err)); + + res.send = sinon.spy(function() { + assert.equal(403, res.status.getCall(0).args[0]); + done(); + }); + ldapMock.bind.returns(BluebirdPromise.resolve()); + ldapMock.get_emails.returns(BluebirdPromise.resolve()); + FirstFactor(req, res); + }); +}); + + diff --git a/test/unitary/routes/test_first_factor.js b/test/unitary/routes/test_first_factor.js deleted file mode 100644 index fffb8cab..00000000 --- a/test/unitary/routes/test_first_factor.js +++ /dev/null @@ -1,136 +0,0 @@ - -var sinon = require('sinon'); -var Promise = require('bluebird'); -var assert = require('assert'); -var winston = require('winston'); -var first_factor = require('../../../src/lib/routes/first_factor'); -var exceptions = require('../../../src/lib/exceptions'); -var Ldap = require('../../../src/lib/ldap'); - -describe('test the first factor validation route', function() { - var req, res; - var ldap_interface_mock; - var emails; - var search_res_ok; - var regulator; - var access_controller; - var config; - - beforeEach(function() { - ldap_interface_mock = sinon.createStubInstance(Ldap); - config = { - ldap: { - base_dn: 'ou=users,dc=example,dc=com', - user_name_attribute: 'uid' - } - } - - emails = [ 'test_ok@example.com' ]; - groups = [ 'group1', 'group2' ]; - - regulator = {}; - regulator.mark = sinon.stub(); - regulator.regulate = sinon.stub(); - - regulator.mark.returns(Promise.resolve()); - regulator.regulate.returns(Promise.resolve()); - - access_controller = { - isDomainAllowedForUser: sinon.stub().returns(true) - }; - - var app_get = sinon.stub(); - app_get.withArgs('ldap').returns(ldap_interface_mock); - app_get.withArgs('config').returns(config); - app_get.withArgs('logger').returns(winston); - app_get.withArgs('authentication regulator').returns(regulator); - app_get.withArgs('access controller').returns(access_controller); - - req = { - app: { - get: app_get - }, - body: { - username: 'username', - password: 'password' - }, - session: { - auth_session: { - first_factor: false, - second_factor: false - } - } - } - res = { - send: sinon.spy(), - status: sinon.spy() - } - }); - - it('should return status code 204 when LDAP binding succeeds', function() { - return new Promise(function(resolve, reject) { - res.send = sinon.spy(function(data) { - assert.equal('username', req.session.auth_session.userid); - assert.equal(204, res.status.getCall(0).args[0]); - resolve(); - }); - ldap_interface_mock.bind.withArgs('username').returns(Promise.resolve()); - ldap_interface_mock.get_emails.returns(Promise.resolve(emails)); - first_factor(req, res); - }); - }); - - it('should retrieve email from LDAP', function(done) { - res.send = sinon.spy(function(data) { done(); }); - ldap_interface_mock.bind.returns(Promise.resolve()); - ldap_interface_mock.get_emails = sinon.stub().withArgs('usernam').returns(Promise.resolve([{mail: ['test@example.com'] }])); - first_factor(req, res); - }); - - it('should set email as session variables', function() { - return new Promise(function(resolve, reject) { - res.send = sinon.spy(function(data) { - assert.equal('test_ok@example.com', req.session.auth_session.email); - resolve(); - }); - var emails = [ 'test_ok@example.com' ]; - ldap_interface_mock.bind.returns(Promise.resolve()); - ldap_interface_mock.get_emails.returns(Promise.resolve(emails)); - first_factor(req, res); - }); - }); - - it('should return status code 401 when LDAP binding throws', function(done) { - res.send = sinon.spy(function(data) { - assert.equal(401, res.status.getCall(0).args[0]); - assert.equal(regulator.mark.getCall(0).args[0], 'username'); - done(); - }); - ldap_interface_mock.bind.throws(new exceptions.LdapBindError('Bad credentials')); - first_factor(req, res); - }); - - it('should return status code 500 when LDAP search throws', function(done) { - res.send = sinon.spy(function(data) { - assert.equal(500, res.status.getCall(0).args[0]); - done(); - }); - ldap_interface_mock.bind.returns(Promise.resolve()); - ldap_interface_mock.get_emails.throws(new exceptions.LdapSearchError('err')); - first_factor(req, res); - }); - - it('should return status code 403 when regulator rejects authentication', function(done) { - var err = new exceptions.AuthenticationRegulationError(); - regulator.regulate.returns(Promise.reject(err)); - res.send = sinon.spy(function(data) { - assert.equal(403, res.status.getCall(0).args[0]); - done(); - }); - ldap_interface_mock.bind.returns(Promise.resolve()); - ldap_interface_mock.get_emails.returns(Promise.resolve()); - first_factor(req, res); - }); -}); - - diff --git a/test/unitary/test_identity_check.js b/test/unitary/test_identity_check.js index 52e890dd..fde553c1 100644 --- a/test/unitary/test_identity_check.js +++ b/test/unitary/test_identity_check.js @@ -1,7 +1,7 @@ var sinon = require('sinon'); var identity_check = require('../../src/lib/identity_check'); -var exceptions = require('../../src/lib/exceptions'); +var exceptions = require('../../src/lib/Exceptions'); var assert = require('assert'); var winston = require('winston'); var Promise = require('bluebird'); diff --git a/test/unitary/test_ldap.js b/test/unitary/test_ldap.js index c7fff8f6..08085700 100644 --- a/test/unitary/test_ldap.js +++ b/test/unitary/test_ldap.js @@ -122,7 +122,7 @@ describe('test ldap validation', function() { var expected_doc = {}; expected_doc.mail = []; expected_doc.mail.push('user@example.com'); - ldap_client.search.yields('error'); + ldap_client.search.yields('Error while searching mails'); return ldap.get_emails('user') .catch(function() { From b54c181d275c7187108efd5841e61374ff37e967 Mon Sep 17 00:00:00 2001 From: Clement Michaud Date: Sun, 21 May 2017 01:15:34 +0200 Subject: [PATCH 11/16] Move ldap client to typescript --- src/lib/LdapClient.ts | 169 ++++++++++++++ src/lib/Server.ts | 5 +- src/lib/ldap.js | 154 ------------- src/lib/routes/reset_password.js | 4 +- src/types/Dependencies.ts | 3 +- src/types/dovehash.d.ts | 4 + src/types/ldapjs-async.d.ts | 11 + test/unitary/LdapClient.test.ts | 243 +++++++++++++++++++++ test/unitary/Server.test.ts | 2 +- test/unitary/mocks/Ldap.ts | 10 - test/unitary/mocks/LdapClient.ts | 20 ++ test/unitary/mocks/UserDataStore.ts | 18 ++ test/unitary/mocks/express.ts | 35 ++- test/unitary/mocks/ldapjs.ts | 28 +++ test/unitary/routes/FirstFactor.test.ts | 8 +- test/unitary/routes/reset_password.test.ts | 151 +++++++++++++ test/unitary/routes/test_reset_password.js | 162 -------------- test/unitary/test_ldap.js | 233 -------------------- 18 files changed, 681 insertions(+), 579 deletions(-) create mode 100644 src/lib/LdapClient.ts delete mode 100644 src/lib/ldap.js create mode 100644 src/types/dovehash.d.ts create mode 100644 src/types/ldapjs-async.d.ts create mode 100644 test/unitary/LdapClient.test.ts delete mode 100644 test/unitary/mocks/Ldap.ts create mode 100644 test/unitary/mocks/LdapClient.ts create mode 100644 test/unitary/mocks/UserDataStore.ts create mode 100644 test/unitary/mocks/ldapjs.ts create mode 100644 test/unitary/routes/reset_password.test.ts delete mode 100644 test/unitary/routes/test_reset_password.js delete mode 100644 test/unitary/test_ldap.js diff --git a/src/lib/LdapClient.ts b/src/lib/LdapClient.ts new file mode 100644 index 00000000..27638430 --- /dev/null +++ b/src/lib/LdapClient.ts @@ -0,0 +1,169 @@ + +import util = require("util"); +import BluebirdPromise = require("bluebird"); +import exceptions = require("./Exceptions"); +import Dovehash = require("dovehash"); +import ldapjs = require("ldapjs"); + +import { EventEmitter } from "events"; +import { LdapConfiguration } from "./Configuration"; +import { Ldapjs } from "../types/Dependencies"; +import { ILogger } from "./ILogger"; + +interface SearchEntry { + object: any; +} + +export class LdapClient { + options: LdapConfiguration; + ldapjs: Ldapjs; + logger: ILogger; + client: ldapjs.ClientAsync; + + constructor(options: LdapConfiguration, ldapjs: Ldapjs, logger: ILogger) { + this.options = options; + this.ldapjs = ldapjs; + this.logger = logger; + + this.connect(); + } + + connect(): void { + const ldap_client = this.ldapjs.createClient({ + url: this.options.url, + reconnect: true + }); + + ldap_client.on("error", function (err: Error) { + console.error("LDAP Error:", err.message); + }); + + this.client = BluebirdPromise.promisifyAll(ldap_client) as ldapjs.ClientAsync; + } + + private build_user_dn(username: string): string { + let user_name_attr = this.options.user_name_attribute; + // if not provided, default to cn + if (!user_name_attr) user_name_attr = "cn"; + + const additional_user_dn = this.options.additional_user_dn; + const base_dn = this.options.base_dn; + + let user_dn = util.format("%s=%s", user_name_attr, username); + if (additional_user_dn) user_dn += util.format(",%s", additional_user_dn); + user_dn += util.format(",%s", base_dn); + return user_dn; + } + + bind(username: string, password: string): BluebirdPromise { + const user_dn = this.build_user_dn(username); + + this.logger.debug("LDAP: Bind user %s", user_dn); + return this.client.bindAsync(user_dn, password) + .error(function (err) { + throw new exceptions.LdapBindError(err.message); + }); + } + + private search_in_ldap(base: string, query: ldapjs.SearchOptions): BluebirdPromise { + this.logger.debug("LDAP: Search for %s in %s", JSON.stringify(query), base); + return new BluebirdPromise((resolve, reject) => { + this.client.searchAsync(base, query) + .then(function (res: EventEmitter) { + const doc: SearchEntry[] = []; + res.on("searchEntry", function (entry: SearchEntry) { + doc.push(entry.object); + }); + res.on("error", function (err: Error) { + reject(err); + }); + res.on("end", function () { + resolve(doc); + }); + }) + .catch(function (err) { + reject(err); + }); + }); + } + + get_groups(username: string): BluebirdPromise { + const user_dn = this.build_user_dn(username); + + let group_name_attr = this.options.group_name_attribute; + if (!group_name_attr) group_name_attr = "cn"; + + const additional_group_dn = this.options.additional_group_dn; + const base_dn = this.options.base_dn; + + let group_dn = base_dn; + if (additional_group_dn) + group_dn = util.format("%s,", additional_group_dn) + group_dn; + + const query = { + scope: "sub", + attributes: [group_name_attr], + filter: "member=" + user_dn + }; + + const that = this; + this.logger.debug("LDAP: get groups of user %s", username); + return this.search_in_ldap(group_dn, query) + .then(function (docs) { + const groups = []; + for (let i = 0; i < docs.length; ++i) { + groups.push(docs[i].cn); + } + that.logger.debug("LDAP: got groups %s", groups); + return Promise.resolve(groups); + }); + } + + get_emails(username: string): BluebirdPromise { + const that = this; + const user_dn = this.build_user_dn(username); + + const query = { + scope: "base", + sizeLimit: 1, + attributes: ["mail"] + }; + + this.logger.debug("LDAP: get emails of user %s", username); + return this.search_in_ldap(user_dn, query) + .then(function (docs) { + const emails = []; + for (let i = 0; i < docs.length; ++i) { + if (typeof docs[i].mail === "string") + emails.push(docs[i].mail); + else { + emails.concat(docs[i].mail); + } + } + that.logger.debug("LDAP: got emails %s", emails); + return Promise.resolve(emails); + }); + } + + update_password(username: string, new_password: string): BluebirdPromise { + const user_dn = this.build_user_dn(username); + + const encoded_password = Dovehash.encode("SSHA", new_password); + const change = { + operation: "replace", + modification: { + userPassword: encoded_password + } + }; + + const that = this; + this.logger.debug("LDAP: update password of user %s", username); + + this.logger.debug("LDAP: bind admin"); + return this.client.bindAsync(this.options.user, this.options.password) + .then(function () { + that.logger.debug("LDAP: modify password"); + return that.client.modifyAsync(user_dn, change); + }); + } +} diff --git a/src/lib/Server.ts b/src/lib/Server.ts index 4e81fa1b..15c03edd 100644 --- a/src/lib/Server.ts +++ b/src/lib/Server.ts @@ -8,6 +8,7 @@ import { NotifierFactory } from "./notifiers/NotifierFactory"; import TOTPValidator from "./TOTPValidator"; import TOTPGenerator from "./TOTPGenerator"; import RestApi from "./RestApi"; +import { LdapClient } from "./LdapClient"; import * as Express from "express"; import * as BodyParser from "body-parser"; @@ -16,8 +17,6 @@ import * as http from "http"; import AccessController from "./access_control/AccessController"; -const Ldap = require("./ldap"); - export default class Server { private httpServer: http.Server; @@ -58,7 +57,7 @@ export default class Server { const data_store = new UserDataStore(datastore_options); const regulator = new AuthenticationRegulator(data_store, five_minutes); const notifier = NotifierFactory.build(config.notifier, deps.nodemailer); - const ldap = new Ldap(deps, config.ldap); + const ldap = new LdapClient(config.ldap, deps.ldapjs, deps.winston); const accessController = new AccessController(config.access_control, deps.winston); const totpValidator = new TOTPValidator(deps.speakeasy); const totpGenerator = new TOTPGenerator(deps.speakeasy); diff --git a/src/lib/ldap.js b/src/lib/ldap.js deleted file mode 100644 index cc473e98..00000000 --- a/src/lib/ldap.js +++ /dev/null @@ -1,154 +0,0 @@ - -module.exports = Ldap; - -var util = require('util'); -var Promise = require('bluebird'); -var exceptions = require('./Exceptions'); -var Dovehash = require('dovehash'); - -function Ldap(deps, ldap_config) { - this.ldap_config = ldap_config; - - this.ldapjs = deps.ldapjs; - this.logger = deps.winston; - - this.connect(); -} - -Ldap.prototype.connect = function() { - var ldap_client = this.ldapjs.createClient({ - url: this.ldap_config.url, - reconnect: true - }); - - ldap_client.on('error', function(err) { - console.error('LDAP Error:', err.message) - }); - - this.ldap_client = Promise.promisifyAll(ldap_client); -} - -Ldap.prototype._build_user_dn = function(username) { - var user_name_attr = this.ldap_config.user_name_attribute; - // if not provided, default to cn - if(!user_name_attr) user_name_attr = 'cn'; - - var additional_user_dn = this.ldap_config.additional_user_dn; - var base_dn = this.ldap_config.base_dn; - - var user_dn = util.format("%s=%s", user_name_attr, username); - if(additional_user_dn) user_dn += util.format(",%s", additional_user_dn); - user_dn += util.format(',%s', base_dn); - return user_dn; -} - -Ldap.prototype.bind = function(username, password) { - var user_dn = this._build_user_dn(username); - - this.logger.debug('LDAP: Bind user %s', user_dn); - return this.ldap_client.bindAsync(user_dn, password) - .error(function(err) { - throw new exceptions.LdapBindError(err.message); - }); -} - -Ldap.prototype._search_in_ldap = function(base, query) { - var that = this; - this.logger.debug('LDAP: Search for %s in %s', JSON.stringify(query), base); - return new Promise(function(resolve, reject) { - that.ldap_client.searchAsync(base, query) - .then(function(res) { - var doc = []; - res.on('searchEntry', function(entry) { - doc.push(entry.object); - }); - res.on('error', function(err) { - reject(new exceptions.LdapSearchError(err)); - }); - res.on('end', function(result) { - resolve(doc); - }); - }) - .catch(function(err) { - reject(err); - }); - }); -} - -Ldap.prototype.get_groups = function(username) { - var user_dn = this._build_user_dn(username); - - var group_name_attr = this.ldap_config.group_name_attribute; - if(!group_name_attr) group_name_attr = 'cn'; - - var additional_group_dn = this.ldap_config.additional_group_dn; - var base_dn = this.ldap_config.base_dn; - - var group_dn = base_dn; - if(additional_group_dn) - group_dn = util.format('%s,', additional_group_dn) + group_dn; - - var query = {}; - query.scope = 'sub'; - query.attributes = [group_name_attr]; - query.filter = 'member=' + user_dn ; - - var that = this; - this.logger.debug('LDAP: get groups of user %s', username); - return this._search_in_ldap(group_dn, query) - .then(function(docs) { - var groups = []; - for(var i = 0; i; + searchAsync(base: string, query: ldapjs.SearchOptions): Promise; + modifyAsync(userdn: string, change: ldapjs.Change): Promise; + } +} \ No newline at end of file diff --git a/test/unitary/LdapClient.test.ts b/test/unitary/LdapClient.test.ts new file mode 100644 index 00000000..82f49c7c --- /dev/null +++ b/test/unitary/LdapClient.test.ts @@ -0,0 +1,243 @@ + +import LdapClient = require("../../src/lib/LdapClient"); +import { LdapConfiguration } from "../../src/lib/Configuration"; + +import sinon = require("sinon"); +import BluebirdPromise = require("bluebird"); +import assert = require("assert"); +import ldapjs = require("ldapjs"); +import winston = require("winston"); +import { EventEmitter } from "events"; + +import { LdapjsMock, LdapjsClientMock } from "./mocks/ldapjs"; + + +describe("test ldap validation", function () { + let ldap: LdapClient.LdapClient; + let ldap_client: LdapjsClientMock; + let ldapjs: LdapjsMock; + let ldap_config: LdapConfiguration; + + beforeEach(function () { + ldap_client = { + bind: sinon.stub(), + search: sinon.stub(), + modify: sinon.stub(), + on: sinon.stub() + } as any; + + ldapjs = LdapjsMock(); + ldapjs.createClient.returns(ldap_client); + + ldap_config = { + url: "http://localhost:324", + user: "admin", + password: "password", + base_dn: "dc=example,dc=com", + additional_user_dn: "ou=users" + }; + + ldap = new LdapClient.LdapClient(ldap_config, ldapjs, winston); + return ldap.connect(); + }); + + describe("test binding", test_binding); + describe("test get emails from username", test_get_emails); + describe("test get groups from username", test_get_groups); + describe("test update password", test_update_password); + + function test_binding() { + function test_bind() { + const username = "username"; + const password = "password"; + return ldap.bind(username, password); + } + + it("should bind the user if good credentials provided", function () { + ldap_client.bind.yields(); + return test_bind(); + }); + + it("should bind the user with correct DN", function () { + ldap_config.user_name_attribute = "uid"; + const username = "user"; + const password = "password"; + ldap_client.bind.withArgs("uid=user,ou=users,dc=example,dc=com").yields(); + return ldap.bind(username, password); + }); + + it("should default to cn user search filter if no filter provided", function () { + const username = "user"; + const password = "password"; + ldap_client.bind.withArgs("cn=user,ou=users,dc=example,dc=com").yields(); + return ldap.bind(username, password); + }); + + it("should not bind the user if wrong credentials provided", function () { + ldap_client.bind.yields("wrong credentials"); + const promise = test_bind(); + return promise.catch(function () { + return Promise.resolve(); + }); + }); + } + + function test_get_emails() { + let res_emitter: any; + let expected_doc: any; + + beforeEach(function () { + expected_doc = { + object: { + mail: "user@example.com" + } + }; + + res_emitter = { + on: sinon.spy(function (event: string, fn: (doc: any) => void) { + if (event != "error") fn(expected_doc); + }) + }; + }); + + it("should retrieve the email of an existing user", function () { + ldap_client.search.yields(undefined, res_emitter); + + return ldap.get_emails("user") + .then(function (emails) { + assert.deepEqual(emails, [expected_doc.object.mail]); + return Promise.resolve(); + }); + }); + + it("should retrieve email for user with uid name attribute", function () { + ldap_config.user_name_attribute = "uid"; + ldap_client.search.withArgs("uid=username,ou=users,dc=example,dc=com").yields(undefined, res_emitter); + return ldap.get_emails("username") + .then(function (emails) { + assert.deepEqual(emails, ["user@example.com"]); + return Promise.resolve(); + }); + }); + + it("should fail on error with search method", function () { + const expected_doc = { + mail: ["user@example.com"] + }; + ldap_client.search.yields("Error while searching mails"); + + return ldap.get_emails("user") + .catch(function () { + return Promise.resolve(); + }); + }); + } + + function test_get_groups() { + let res_emitter: any; + let expected_doc1: any, expected_doc2: any; + + beforeEach(function () { + expected_doc1 = { + object: { + cn: "group1" + } + }; + + expected_doc2 = { + object: { + cn: "group2" + } + }; + + res_emitter = { + on: sinon.spy(function (event: string, fn: (doc: any) => void) { + if (event != "error") fn(expected_doc1); + if (event != "error") fn(expected_doc2); + }) + }; + }); + + it("should retrieve the groups of an existing user", function () { + ldap_client.search.yields(undefined, res_emitter); + return ldap.get_groups("user") + .then(function (groups) { + assert.deepEqual(groups, ["group1", "group2"]); + return Promise.resolve(); + }); + }); + + it("should reduce the scope to additional_group_dn", function (done) { + ldap_config.additional_group_dn = "ou=groups"; + ldap_client.search.yields(undefined, res_emitter); + ldap.get_groups("user") + .then(function() { + assert.equal(ldap_client.search.getCall(0).args[0], "ou=groups,dc=example,dc=com"); + done(); + }); + }); + + it("should use default group_name_attr if not provided", function (done) { + ldap_client.search.yields(undefined, res_emitter); + ldap.get_groups("user") + .then(function() { + assert.equal(ldap_client.search.getCall(0).args[0], "dc=example,dc=com"); + assert.equal(ldap_client.search.getCall(0).args[1].filter, "member=cn=user,ou=users,dc=example,dc=com"); + assert.deepEqual(ldap_client.search.getCall(0).args[1].attributes, ["cn"]); + done(); + }); + }); + + it("should fail on error with search method", function () { + ldap_client.search.yields("error"); + return ldap.get_groups("user") + .catch(function () { + return Promise.resolve(); + }); + }); + } + + function test_update_password() { + it("should update the password successfully", function () { + const change = { + operation: "replace", + modification: { + userPassword: "new-password" + } + }; + const userdn = "cn=user,ou=users,dc=example,dc=com"; + + ldap_client.bind.yields(undefined); + ldap_client.modify.yields(undefined); + + return ldap.update_password("user", "new-password") + .then(function () { + assert.deepEqual(ldap_client.modify.getCall(0).args[0], userdn); + assert.deepEqual(ldap_client.modify.getCall(0).args[1].operation, change.operation); + + const userPassword = ldap_client.modify.getCall(0).args[1].modification.userPassword; + assert(/{SSHA}/.test(userPassword)); + return Promise.resolve(); + }); + }); + + it("should fail when ldap throws an error", function () { + ldap_client.bind.yields(undefined); + ldap_client.modify.yields("Error"); + + return ldap.update_password("user", "new-password") + .catch(function () { + return Promise.resolve(); + }); + }); + + it("should update password of user using particular user name attribute", function () { + ldap_config.user_name_attribute = "uid"; + + ldap_client.bind.yields(undefined); + ldap_client.modify.withArgs("uid=username,ou=users,dc=example,dc=com").yields(); + return ldap.update_password("username", "newpass"); + }); + } +}); + diff --git a/test/unitary/Server.test.ts b/test/unitary/Server.test.ts index ed698aad..15105b26 100644 --- a/test/unitary/Server.test.ts +++ b/test/unitary/Server.test.ts @@ -1,6 +1,6 @@ import Server from "../../src/lib/Server"; -import Ldap = require("../../src/lib/ldap"); +import LdapClient = require("../../src/lib/LdapClient"); import Promise = require("bluebird"); import speakeasy = require("speakeasy"); diff --git a/test/unitary/mocks/Ldap.ts b/test/unitary/mocks/Ldap.ts deleted file mode 100644 index a44846c9..00000000 --- a/test/unitary/mocks/Ldap.ts +++ /dev/null @@ -1,10 +0,0 @@ - -import sinon = require("sinon"); - -export = function () { - return { - bind: sinon.stub(), - get_emails: sinon.stub(), - get_groups: sinon.stub() - }; -}; diff --git a/test/unitary/mocks/LdapClient.ts b/test/unitary/mocks/LdapClient.ts new file mode 100644 index 00000000..17495c69 --- /dev/null +++ b/test/unitary/mocks/LdapClient.ts @@ -0,0 +1,20 @@ + +import sinon = require("sinon"); + +export interface LdapClientMock { + bind: sinon.SinonStub; + get_emails: sinon.SinonStub; + get_groups: sinon.SinonStub; + search_in_ldap: sinon.SinonStub; + update_password: sinon.SinonStub; +} + +export function LdapClientMock(): LdapClientMock { + return { + bind: sinon.stub(), + get_emails: sinon.stub(), + get_groups: sinon.stub(), + search_in_ldap: sinon.stub(), + update_password: sinon.stub() + }; +} diff --git a/test/unitary/mocks/UserDataStore.ts b/test/unitary/mocks/UserDataStore.ts new file mode 100644 index 00000000..fc2774f1 --- /dev/null +++ b/test/unitary/mocks/UserDataStore.ts @@ -0,0 +1,18 @@ + +import sinon = require("sinon"); + +export interface UserDataStore { + set_u2f_meta: sinon.SinonStub; + get_u2f_meta: sinon.SinonStub; + issue_identity_check_token: sinon.SinonStub; + consume_identity_check_token: sinon.SinonStub; +} + +export function UserDataStore(): UserDataStore { + return { + set_u2f_meta: sinon.stub(), + get_u2f_meta: sinon.stub(), + issue_identity_check_token: sinon.stub(), + consume_identity_check_token: sinon.stub() + }; +} diff --git a/test/unitary/mocks/express.ts b/test/unitary/mocks/express.ts index 1f6712bc..31aec299 100644 --- a/test/unitary/mocks/express.ts +++ b/test/unitary/mocks/express.ts @@ -1,11 +1,30 @@ import sinon = require("sinon"); -export = { - Response: function () { - return { - send: sinon.stub(), - status: sinon.stub() - }; - } -}; \ No newline at end of file +export interface RequestMock { + app?: any; + body?: any; + session?: any; + headers?: any; +} + +export interface ResponseMock { + send: sinon.SinonStub | sinon.SinonSpy; + status: sinon.SinonStub; + json: sinon.SinonStub; +} + +export function RequestMock(): RequestMock { + return { + app: { + get: sinon.stub() + } + }; +} +export function ResponseMock(): ResponseMock { + return { + send: sinon.stub(), + status: sinon.stub(), + json: sinon.stub() + }; +} diff --git a/test/unitary/mocks/ldapjs.ts b/test/unitary/mocks/ldapjs.ts new file mode 100644 index 00000000..957f4a9e --- /dev/null +++ b/test/unitary/mocks/ldapjs.ts @@ -0,0 +1,28 @@ + +import sinon = require("sinon"); + +export interface LdapjsMock { + createClient: sinon.SinonStub; +} + +export interface LdapjsClientMock { + bind: sinon.SinonStub; + search: sinon.SinonStub; + modify: sinon.SinonStub; + on: sinon.SinonStub; +} + +export function LdapjsMock(): LdapjsMock { + return { + createClient: sinon.stub() + }; +} + +export function LdapjsClientMock(): LdapjsClientMock { + return { + bind: sinon.stub(), + search: sinon.stub(), + modify: sinon.stub(), + on: sinon.stub() + }; +} \ No newline at end of file diff --git a/test/unitary/routes/FirstFactor.test.ts b/test/unitary/routes/FirstFactor.test.ts index f949a151..9d0b1af3 100644 --- a/test/unitary/routes/FirstFactor.test.ts +++ b/test/unitary/routes/FirstFactor.test.ts @@ -8,7 +8,7 @@ import FirstFactor = require("../../../src/lib/routes/FirstFactor"); import exceptions = require("../../../src/lib/Exceptions"); import AuthenticationRegulatorMock = require("../mocks/AuthenticationRegulator"); import AccessControllerMock = require("../mocks/AccessController"); -import LdapMock = require("../mocks/Ldap"); +import { LdapClientMock } from "../mocks/LdapClient"; import ExpressMock = require("../mocks/express"); describe("test the first factor validation route", function() { @@ -17,7 +17,7 @@ describe("test the first factor validation route", function() { let emails: string[]; let groups: string[]; let configuration; - let ldapMock: any; + let ldapMock: LdapClientMock; let regulator: any; let accessController: any; @@ -32,7 +32,7 @@ describe("test the first factor validation route", function() { emails = [ "test_ok@example.com" ]; groups = [ "group1", "group2" ]; - ldapMock = LdapMock(); + ldapMock = LdapClientMock(); accessController = AccessControllerMock(); accessController.isDomainAllowedForUser.returns(true); @@ -63,7 +63,7 @@ describe("test the first factor validation route", function() { } } }; - res = ExpressMock.Response(); + res = ExpressMock.ResponseMock(); }); it("should return status code 204 when LDAP binding succeeds", function() { diff --git a/test/unitary/routes/reset_password.test.ts b/test/unitary/routes/reset_password.test.ts new file mode 100644 index 00000000..8ec9cbef --- /dev/null +++ b/test/unitary/routes/reset_password.test.ts @@ -0,0 +1,151 @@ + +import reset_password = require("../../../src/lib/routes/reset_password"); +import LdapClient = require("../../../src/lib/LdapClient"); +import sinon = require("sinon"); +import winston = require("winston"); +import assert = require("assert"); +import BluebirdPromise = require("bluebird"); + +import ExpressMock = require("../mocks/express"); +import { LdapClientMock } from "../mocks/LdapClient"; +import { UserDataStore } from "../mocks/UserDataStore"; + +describe("test reset password", function () { + let req: ExpressMock.RequestMock; + let res: ExpressMock.ResponseMock; + let user_data_store: UserDataStore; + let ldap_client: LdapClientMock; + let configuration: any; + + beforeEach(function () { + req = { + body: { + userid: "user" + }, + app: { + get: sinon.stub() + }, + session: { + auth_session: { + userid: "user", + email: "user@example.com", + first_factor: true, + second_factor: false + } + }, + headers: { + host: "localhost" + } + }; + + const options = { + inMemoryOnly: true + }; + + user_data_store = UserDataStore(); + user_data_store.set_u2f_meta.returns(Promise.resolve({})); + user_data_store.get_u2f_meta.returns(Promise.resolve({})); + user_data_store.issue_identity_check_token.returns(Promise.resolve({})); + user_data_store.consume_identity_check_token.returns(Promise.resolve({})); + req.app.get.withArgs("user data store").returns(user_data_store); + + + configuration = { + ldap: { + base_dn: "dc=example,dc=com", + user_name_attribute: "cn" + } + }; + + req.app.get.withArgs("logger").returns(winston); + req.app.get.withArgs("config").returns(configuration); + + ldap_client = LdapClientMock(); + req.app.get.withArgs("ldap").returns(ldap_client); + + res = ExpressMock.ResponseMock(); + }); + + describe("test reset password identity pre check", test_reset_password_check); + describe("test reset password post", test_reset_password_post); + + function test_reset_password_check() { + it("should fail when no userid is provided", function (done) { + req.body.userid = undefined; + reset_password.icheck_interface.pre_check_callback(req) + .catch(function (err: Error) { + done(); + }); + }); + + it("should fail if ldap fail", function (done) { + ldap_client.get_emails.returns(BluebirdPromise.reject("Internal error")); + reset_password.icheck_interface.pre_check_callback(req) + .catch(function (err: Error) { + done(); + }); + }); + + it("should perform a search in ldap to find email address", function (done) { + configuration.ldap.user_name_attribute = "uid"; + ldap_client.get_emails.returns(BluebirdPromise.resolve([])); + reset_password.icheck_interface.pre_check_callback(req) + .then(function () { + assert.equal("user", ldap_client.get_emails.getCall(0).args[0]); + done(); + }); + }); + + it("should returns identity when ldap replies", function (done) { + ldap_client.get_emails.returns(BluebirdPromise.resolve(["test@example.com"])); + reset_password.icheck_interface.pre_check_callback(req) + .then(function () { + done(); + }); + }); + } + + function test_reset_password_post() { + it("should update the password and reset auth_session for reauthentication", function (done) { + req.session.auth_session.identity_check = {}; + req.session.auth_session.identity_check.userid = "user"; + req.session.auth_session.identity_check.challenge = "reset-password"; + req.body = {}; + req.body.password = "new-password"; + + ldap_client.update_password.returns(BluebirdPromise.resolve()); + ldap_client.bind.returns(BluebirdPromise.resolve()); + res.send = sinon.spy(function () { + assert.equal(res.status.getCall(0).args[0], 204); + assert.equal(req.session.auth_session, undefined); + done(); + }); + reset_password.post(req, res); + }); + + it("should fail if identity_challenge does not exist", function (done) { + req.session.auth_session.identity_check = {}; + req.session.auth_session.identity_check.challenge = undefined; + res.send = sinon.spy(function () { + assert.equal(res.status.getCall(0).args[0], 403); + done(); + }); + reset_password.post(req, res); + }); + + it("should fail when ldap fails", function (done) { + req.session.auth_session.identity_check = {}; + req.session.auth_session.identity_check.challenge = "reset-password"; + req.body = {}; + req.body.password = "new-password"; + + ldap_client.bind.yields(undefined); + ldap_client.update_password.returns(BluebirdPromise.reject("Internal error with LDAP")); + res.send = sinon.spy(function () { + assert.equal(res.status.getCall(0).args[0], 500); + done(); + }); + reset_password.post(req, res); + }); + } +}); diff --git a/test/unitary/routes/test_reset_password.js b/test/unitary/routes/test_reset_password.js deleted file mode 100644 index efac684d..00000000 --- a/test/unitary/routes/test_reset_password.js +++ /dev/null @@ -1,162 +0,0 @@ -var reset_password = require('../../../src/lib/routes/reset_password'); -var Ldap = require('../../../src/lib/ldap'); - -var sinon = require('sinon'); -var winston = require('winston'); -var assert = require('assert'); - -describe('test reset password', function() { - var req, res; - var user_data_store; - var ldap_client; - var ldap; - - beforeEach(function() { - req = {} - req.body = {}; - req.body.userid = 'user'; - req.app = {}; - req.app.get = sinon.stub(); - req.app.get.withArgs('logger').returns(winston); - req.session = {}; - req.session.auth_session = {}; - req.session.auth_session.userid = 'user'; - req.session.auth_session.email = 'user@example.com'; - req.session.auth_session.first_factor = true; - req.session.auth_session.second_factor = false; - 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({})); - user_data_store.issue_identity_check_token = sinon.stub().returns(Promise.resolve({})); - user_data_store.consume_identity_check_token = sinon.stub().returns(Promise.resolve({})); - req.app.get.withArgs('user data store').returns(user_data_store); - - - config = {}; - config.ldap = {}; - config.ldap.base_dn = 'dc=example,dc=com'; - config.ldap.user_name_attribute = 'cn'; - req.app.get.withArgs('config').returns(config); - - ldap_client = {}; - ldap_client.bind = sinon.stub(); - ldap_client.search = sinon.stub(); - ldap_client.modify = sinon.stub(); - ldap_client.on = sinon.spy(); - - ldapjs = {}; - ldapjs.Change = sinon.spy(); - ldapjs.createClient = sinon.spy(function() { - return ldap_client; - }); - - deps = { - ldapjs: ldapjs, - winston: winston - }; - req.app.get.withArgs('ldap').returns(new Ldap(deps, config.ldap)); - - res = {}; - res.send = sinon.spy(); - res.json = sinon.spy(); - res.status = sinon.spy(); - }); - - describe('test reset password identity pre check', test_reset_password_check); - describe('test reset password post', test_reset_password_post); - - function test_reset_password_check() { - it('should fail when no userid is provided', function(done) { - req.body.userid = undefined; - reset_password.icheck_interface.pre_check_callback(req) - .catch(function(err) { - done(); - }); - }); - - it('should fail if ldap fail', function(done) { - ldap_client.search.yields('Internal error'); - reset_password.icheck_interface.pre_check_callback(req) - .catch(function(err) { - done(); - }); - }); - - it('should perform a search in ldap to find email address', function(done) { - config.ldap.user_name_attribute = 'uid'; - ldap_client.search = sinon.spy(function(dn) { - if(dn == 'uid=user,dc=example,dc=com') done(); - }); - reset_password.icheck_interface.pre_check_callback(req); - }); - - it('should returns identity when ldap replies', function(done) { - var doc = {}; - doc.object = {}; - doc.object.email = ['test@example.com']; - doc.object.userid = 'user'; - - var res = {}; - res.on = sinon.stub(); - res.on.withArgs('searchEntry').yields(doc); - res.on.withArgs('end').yields(); - - ldap_client.search.yields(undefined, res); - reset_password.icheck_interface.pre_check_callback(req) - .then(function() { - done(); - }); - }); - } - - function test_reset_password_post() { - it('should update the password and reset auth_session for reauthentication', function(done) { - req.session.auth_session.identity_check = {}; - req.session.auth_session.identity_check.userid = 'user'; - req.session.auth_session.identity_check.challenge = 'reset-password'; - req.body = {}; - req.body.password = 'new-password'; - - ldap_client.modify.yields(undefined); - ldap_client.bind.yields(undefined); - res.send = sinon.spy(function() { - assert.equal(ldap_client.modify.getCall(0).args[0], 'cn=user,dc=example,dc=com'); - assert.equal(res.status.getCall(0).args[0], 204); - assert.equal(req.session.auth_session, undefined); - done(); - }); - reset_password.post(req, res); - }); - - it('should fail if identity_challenge does not exist', function(done) { - req.session.auth_session.identity_check = {}; - req.session.auth_session.identity_check.challenge = undefined; - res.send = sinon.spy(function() { - assert.equal(res.status.getCall(0).args[0], 403); - done(); - }); - reset_password.post(req, res); - }); - - it('should fail when ldap fails', function(done) { - req.session.auth_session.identity_check = {}; - req.session.auth_session.identity_check.challenge = 'reset-password'; - req.body = {}; - req.body.password = 'new-password'; - - ldap_client.bind.yields(undefined); - ldap_client.modify.yields('Internal error with LDAP'); - res.send = sinon.spy(function() { - assert.equal(res.status.getCall(0).args[0], 500); - done(); - }); - reset_password.post(req, res); - }); - } -}); diff --git a/test/unitary/test_ldap.js b/test/unitary/test_ldap.js deleted file mode 100644 index 08085700..00000000 --- a/test/unitary/test_ldap.js +++ /dev/null @@ -1,233 +0,0 @@ - -var Ldap = require('../../src/lib/ldap'); -var sinon = require('sinon'); -var Promise = require('bluebird'); -var assert = require('assert'); -var ldapjs = require('ldapjs'); -var winston = require('winston'); - - -describe('test ldap validation', function() { - var ldap_client; - var ldap, ldapjs; - var ldap_config; - - beforeEach(function() { - ldap_client = { - bind: sinon.stub(), - search: sinon.stub(), - modify: sinon.stub(), - on: sinon.stub() - }; - - ldapjs = { - Change: sinon.spy(), - createClient: sinon.spy(function() { - return ldap_client; -  }) - } - ldap_config = { - url: 'http://localhost:324', - user: 'admin', - password: 'password', - base_dn: 'dc=example,dc=com', - additional_user_dn: 'ou=users' - }; - - var deps = {}; - deps.ldapjs = ldapjs; - deps.winston = winston; - - ldap = new Ldap(deps, ldap_config); - return ldap.connect(); - }); - - describe('test binding', test_binding); - describe('test get emails from username', test_get_emails); - describe('test get groups from username', test_get_groups); - describe('test update password', test_update_password); - - function test_binding() { - function test_bind() { - var username = "username"; - var password = "password"; - return ldap.bind(username, password); - } - - it('should bind the user if good credentials provided', function() { - ldap_client.bind.yields(); - return test_bind(); - }); - - it('should bind the user with correct DN', function() { - ldap_config.user_name_attribute = 'uid'; - var username = 'user'; - var password = 'password'; - ldap_client.bind.withArgs('uid=user,ou=users,dc=example,dc=com').yields(); - return ldap.bind(username, password); - }); - - it('should default to cn user search filter if no filter provided', function() { - var username = 'user'; - var password = 'password'; - ldap_client.bind.withArgs('cn=user,ou=users,dc=example,dc=com').yields(); - return ldap.bind(username, password); - }); - - it('should not bind the user if wrong credentials provided', function() { - ldap_client.bind.yields('wrong credentials'); - var promise = test_bind(); - return promise.catch(function() { - return Promise.resolve(); - }); - }); - } - - function test_get_emails() { - var res_emitter; - var expected_doc; - - beforeEach(function() { - expected_doc = {}; - expected_doc.object = {}; - expected_doc.object.mail = 'user@example.com'; - - res_emitter = {}; - res_emitter.on = sinon.spy(function(event, fn) { - if(event != 'error') fn(expected_doc) - }); - }); - - it('should retrieve the email of an existing user', function() { - ldap_client.search.yields(undefined, res_emitter); - - return ldap.get_emails('user') - .then(function(emails) { - assert.deepEqual(emails, [expected_doc.object.mail]); - return Promise.resolve(); - }) - }); - - it('should retrieve email for user with uid name attribute', function() { - ldap_config.user_name_attribute = 'uid'; - ldap_client.search.withArgs('uid=username,ou=users,dc=example,dc=com').yields(undefined, res_emitter); - return ldap.get_emails('username') - .then(function(emails) { - assert.deepEqual(emails, ['user@example.com']); - return Promise.resolve(); - }); - }); - - it('should fail on error with search method', function() { - var expected_doc = {}; - expected_doc.mail = []; - expected_doc.mail.push('user@example.com'); - ldap_client.search.yields('Error while searching mails'); - - return ldap.get_emails('user') - .catch(function() { - return Promise.resolve(); - }) - }); - } - - function test_get_groups() { - var res_emitter; - var expected_doc1, expected_doc2; - - beforeEach(function() { - expected_doc1 = {}; - expected_doc1.object = {}; - expected_doc1.object.cn = 'group1'; - - expected_doc2 = {}; - expected_doc2.object = {}; - expected_doc2.object.cn = 'group2'; - - res_emitter = {}; - res_emitter.on = sinon.spy(function(event, fn) { - if(event != 'error') fn(expected_doc1); - if(event != 'error') fn(expected_doc2); - }); - }); - - it('should retrieve the groups of an existing user', function() { - ldap_client.search.yields(undefined, res_emitter); - return ldap.get_groups('user') - .then(function(groups) { - assert.deepEqual(groups, ['group1', 'group2']); - return Promise.resolve(); - }); - }); - - it('should reduce the scope to additional_group_dn', function(done) { - ldap_config.additional_group_dn = 'ou=groups'; - ldap_client.search = sinon.spy(function(base_dn) { - assert.equal(base_dn, 'ou=groups,dc=example,dc=com'); - done(); - }); - ldap.get_groups('user'); - }); - - it('should use default group_name_attr if not provided', function(done) { - ldap_client.search = sinon.spy(function(base_dn, query) { - assert.equal(base_dn, 'dc=example,dc=com'); - assert.equal(query.filter, 'member=cn=user,ou=users,dc=example,dc=com'); - assert.deepEqual(query.attributes, ['cn']); - done(); - }); - ldap.get_groups('user'); - }); - - it('should fail on error with search method', function() { - ldap_client.search.yields('error'); - return ldap.get_groups('user') - .catch(function() { - return Promise.resolve(); - }) - }); - } - - function test_update_password() { - it('should update the password successfully', function() { - var change = {}; - change.operation = 'replace'; - change.modification = {}; - change.modification.userPassword = 'new-password'; - - var userdn = 'cn=user,ou=users,dc=example,dc=com'; - - ldap_client.bind.yields(undefined); - ldap_client.modify.yields(undefined); - - return ldap.update_password('user', 'new-password') - .then(function() { - assert.deepEqual(ldap_client.modify.getCall(0).args[0], userdn); - assert.deepEqual(ldapjs.Change.getCall(0).args[0].operation, change.operation); - - var userPassword = ldapjs.Change.getCall(0).args[0].modification.userPassword; - assert(/{SSHA}/.test(userPassword)); - return Promise.resolve(); - }) - }); - - it('should fail when ldap throws an error', function() { - ldap_client.bind.yields(undefined); - ldap_client.modify.yields('Error'); - - return ldap.update_password('user', 'new-password') - .catch(function() { - return Promise.resolve(); - }) - }); - - it('should update password of user using particular user name attribute', function() { - ldap_config.user_name_attribute = 'uid'; - - ldap_client.bind.yields(undefined); - ldap_client.modify.withArgs('uid=username,ou=users,dc=example,dc=com').yields(); - return ldap.update_password('username', 'newpass'); - }); - } -}); - From c98c07832dff8e880f07aac02ea46ebbdd7f5071 Mon Sep 17 00:00:00 2001 From: Clement Michaud Date: Sun, 21 May 2017 12:14:59 +0200 Subject: [PATCH 12/16] Move TOTP authenticator to typescript --- package.json | 2 +- src/lib/AuthenticationRegulator.ts | 10 +- src/lib/Exceptions.ts | 1 - src/lib/LdapClient.ts | 4 +- src/lib/Server.ts | 7 +- src/lib/TOTPValidator.ts | 2 +- src/lib/UserDataStore.ts | 70 +++++++------- src/lib/identity_check.js | 4 +- src/lib/notifiers/GMailNotifier.ts | 6 +- src/lib/routes/FirstFactor.ts | 96 +++++++++---------- src/lib/routes/TOTPAuthenticator.ts | 49 ++++++++++ src/lib/routes/reset_password.js | 6 +- src/lib/routes/second_factor.js | 5 +- src/lib/routes/totp.js | 49 ---------- src/lib/routes/totp_register.js | 8 +- src/lib/routes/u2f_register.js | 2 +- src/lib/routes/u2f_register_handler.js | 8 +- src/lib/routes/verify.js | 14 +-- src/types/authdog.d.ts | 10 +- src/types/ldapjs-async.d.ts | 8 +- src/types/nedb-async.d.ts | 10 +- src/types/request-async.d.ts | 14 +-- test/unitary/AuthenticationRegulator.test.ts | 7 +- ...TOTPValidator.ts => TOTPValidator.test.ts} | 6 +- test/unitary/UserDataStore.test.ts | 20 ++-- test/unitary/mocks/AccessController.ts | 8 +- test/unitary/mocks/AuthenticationRegulator.ts | 10 +- test/unitary/mocks/TOTPValidator.ts | 12 +++ test/unitary/mocks/UserDataStore.ts | 4 +- test/unitary/routes/FirstFactor.test.ts | 30 +++--- test/unitary/routes/TOTPAuthenticator.test.ts | 90 +++++++++++++++++ test/unitary/routes/test_totp.js | 87 ----------------- .../authentication_audit.test.ts | 5 +- .../user_data_store/totp_secret.test.ts | 5 +- tsconfig.json | 4 +- 35 files changed, 354 insertions(+), 319 deletions(-) create mode 100644 src/lib/routes/TOTPAuthenticator.ts delete mode 100644 src/lib/routes/totp.js rename test/unitary/{TOTPValidator.ts => TOTPValidator.test.ts} (82%) create mode 100644 test/unitary/mocks/TOTPValidator.ts create mode 100644 test/unitary/routes/TOTPAuthenticator.test.ts delete mode 100644 test/unitary/routes/test_totp.js diff --git a/package.json b/package.json index f5dc2941..1d68985b 100644 --- a/package.json +++ b/package.json @@ -47,7 +47,7 @@ }, "devDependencies": { "@types/assert": "0.0.31", - "@types/bluebird": "^3.5.3", + "@types/bluebird": "^3.5.4", "@types/body-parser": "^1.16.3", "@types/ejs": "^2.3.33", "@types/express": "^4.0.35", diff --git a/src/lib/AuthenticationRegulator.ts b/src/lib/AuthenticationRegulator.ts index 89b62892..60fbbcfd 100644 --- a/src/lib/AuthenticationRegulator.ts +++ b/src/lib/AuthenticationRegulator.ts @@ -1,5 +1,5 @@ -import * as Promise from "bluebird"; +import * as BluebirdPromise from "bluebird"; import exceptions = require("./Exceptions"); const REGULATION_TRACE_TYPE = "regulation"; @@ -19,16 +19,16 @@ export default class AuthenticationRegulator { } // Mark authentication - mark(userid: string, is_success: boolean): Promise { + mark(userid: string, is_success: boolean): BluebirdPromise { return this._user_data_store.save_authentication_trace(userid, REGULATION_TRACE_TYPE, is_success); } - regulate(userid: string): Promise { + regulate(userid: string): BluebirdPromise { return this._user_data_store.get_last_authentication_traces(userid, REGULATION_TRACE_TYPE, false, 3) .then((docs: Array) => { if (docs.length < MAX_AUTHENTICATION_COUNT_IN_TIME_RANGE) { // less than the max authorized number of authentication in time range, thus authorizing access - return Promise.resolve(); + return BluebirdPromise.resolve(); } const oldest_doc = docs[MAX_AUTHENTICATION_COUNT_IN_TIME_RANGE - 1]; @@ -37,7 +37,7 @@ export default class AuthenticationRegulator { throw new exceptions.AuthenticationRegulationError("Max number of authentication. Please retry in few minutes."); } - return Promise.resolve(); + return BluebirdPromise.resolve(); }); } } diff --git a/src/lib/Exceptions.ts b/src/lib/Exceptions.ts index 0902e5dd..38d054c0 100644 --- a/src/lib/Exceptions.ts +++ b/src/lib/Exceptions.ts @@ -1,5 +1,4 @@ - export class LdapSeachError extends Error { constructor(message?: string) { super(message); diff --git a/src/lib/LdapClient.ts b/src/lib/LdapClient.ts index 27638430..879cf953 100644 --- a/src/lib/LdapClient.ts +++ b/src/lib/LdapClient.ts @@ -115,7 +115,7 @@ export class LdapClient { groups.push(docs[i].cn); } that.logger.debug("LDAP: got groups %s", groups); - return Promise.resolve(groups); + return BluebirdPromise.resolve(groups); }); } @@ -141,7 +141,7 @@ export class LdapClient { } } that.logger.debug("LDAP: got emails %s", emails); - return Promise.resolve(emails); + return BluebirdPromise.resolve(emails); }); } diff --git a/src/lib/Server.ts b/src/lib/Server.ts index 15c03edd..e61edf74 100644 --- a/src/lib/Server.ts +++ b/src/lib/Server.ts @@ -9,6 +9,7 @@ import TOTPValidator from "./TOTPValidator"; import TOTPGenerator from "./TOTPGenerator"; import RestApi from "./RestApi"; import { LdapClient } from "./LdapClient"; +import BluebirdPromise = require("bluebird"); import * as Express from "express"; import * as BodyParser from "body-parser"; @@ -20,7 +21,7 @@ import AccessController from "./access_control/AccessController"; export default class Server { private httpServer: http.Server; - start(yaml_configuration: UserConfiguration, deps: GlobalDependencies): Promise { + start(yaml_configuration: UserConfiguration, deps: GlobalDependencies): BluebirdPromise { const config = ConfigurationAdapter.adapt(yaml_configuration); const view_directory = Path.resolve(__dirname, "../views"); @@ -54,7 +55,7 @@ export default class Server { deps.winston.level = config.logs_level || "info"; const five_minutes = 5 * 60; - const data_store = new UserDataStore(datastore_options); + const data_store = new UserDataStore(datastore_options, deps.nedb); const regulator = new AuthenticationRegulator(data_store, five_minutes); const notifier = NotifierFactory.build(config.notifier, deps.nodemailer); const ldap = new LdapClient(config.ldap, deps.ldapjs, deps.winston); @@ -75,7 +76,7 @@ export default class Server { RestApi.setup(app); - return new Promise((resolve, reject) => { + return new BluebirdPromise((resolve, reject) => { this.httpServer = app.listen(config.port, function (err: string) { console.log("Listening on %d...", config.port); resolve(); diff --git a/src/lib/TOTPValidator.ts b/src/lib/TOTPValidator.ts index 2b009884..09d8e15c 100644 --- a/src/lib/TOTPValidator.ts +++ b/src/lib/TOTPValidator.ts @@ -18,6 +18,6 @@ export default class TOTPValidator { }); if (token == real_token) return BluebirdPromise.resolve(); - return BluebirdPromise.reject("Wrong challenge"); + return BluebirdPromise.reject(new Error("Wrong challenge")); } } \ No newline at end of file diff --git a/src/lib/UserDataStore.ts b/src/lib/UserDataStore.ts index e3cda5ad..468f7080 100644 --- a/src/lib/UserDataStore.ts +++ b/src/lib/UserDataStore.ts @@ -1,8 +1,8 @@ -import * as Promise from "bluebird"; +import * as BluebirdPromise from "bluebird"; import * as path from "path"; -import Nedb = require("nedb"); import { NedbAsync } from "nedb"; import { TOTPSecret } from "../types/TOTPSecret"; +import { Nedb } from "../types/Dependencies"; // Constants @@ -36,18 +36,20 @@ export default class UserDataStore { private _identity_check_tokens_collection: NedbAsync; private _authentication_traces_collection: NedbAsync; private _totp_secret_collection: NedbAsync; + private nedb: Nedb; - constructor(options?: Options) { - this._u2f_meta_collection = create_collection(U2F_META_COLLECTION_NAME, options); + constructor(options: Options, nedb: Nedb) { + this.nedb = nedb; + this._u2f_meta_collection = this.create_collection(U2F_META_COLLECTION_NAME, options); this._identity_check_tokens_collection = - create_collection(IDENTITY_CHECK_TOKENS_COLLECTION_NAME, options); + this.create_collection(IDENTITY_CHECK_TOKENS_COLLECTION_NAME, options); this._authentication_traces_collection = - create_collection(AUTHENTICATION_TRACES_COLLECTION_NAME, options); + this.create_collection(AUTHENTICATION_TRACES_COLLECTION_NAME, options); this._totp_secret_collection = - create_collection(TOTP_SECRETS_COLLECTION_NAME, options); + this.create_collection(TOTP_SECRETS_COLLECTION_NAME, options); } - set_u2f_meta(userid: string, appid: string, meta: Object): Promise { + set_u2f_meta(userid: string, appid: string, meta: Object): BluebirdPromise { const newDocument = { userid: userid, appid: appid, @@ -62,7 +64,7 @@ export default class UserDataStore { return this._u2f_meta_collection.updateAsync(filter, newDocument, { upsert: true }); } - get_u2f_meta(userid: string, appid: string): Promise { + get_u2f_meta(userid: string, appid: string): BluebirdPromise { const filter = { userid: userid, appid: appid @@ -81,7 +83,7 @@ export default class UserDataStore { return this._authentication_traces_collection.insertAsync(newDocument); } - get_last_authentication_traces(userid: string, type: string, is_success: boolean, count: number): Promise { + get_last_authentication_traces(userid: string, type: string, is_success: boolean, count: number): BluebirdPromise { const q = { userid: userid, type: type, @@ -90,11 +92,11 @@ export default class UserDataStore { const query = this._authentication_traces_collection.find(q) .sort({ date: -1 }).limit(count); - const query_promisified = Promise.promisify(query.exec, { context: query }); + const query_promisified = BluebirdPromise.promisify(query.exec, { context: query }); return query_promisified(); } - issue_identity_check_token(userid: string, token: string, data: string | object, max_age: number): Promise { + issue_identity_check_token(userid: string, token: string, data: string | object, max_age: number): BluebirdPromise { const newDocument = { userid: userid, token: token, @@ -108,7 +110,7 @@ export default class UserDataStore { return this._identity_check_tokens_collection.insertAsync(newDocument); } - consume_identity_check_token(token: string): Promise { + consume_identity_check_token(token: string): BluebirdPromise { const query = { token: token }; @@ -116,26 +118,26 @@ export default class UserDataStore { return this._identity_check_tokens_collection.findOneAsync(query) .then(function (doc) { if (!doc) { - return Promise.reject("Registration token does not exist"); + return BluebirdPromise.reject("Registration token does not exist"); } const max_date = doc.max_date; const current_date = new Date(); if (current_date > max_date) { - return Promise.reject("Registration token is not valid anymore"); + return BluebirdPromise.reject("Registration token is not valid anymore"); } - return Promise.resolve(doc.content); + return BluebirdPromise.resolve(doc.content); }) .then((content) => { - return Promise.join(this._identity_check_tokens_collection.removeAsync(query), - Promise.resolve(content)); + return BluebirdPromise.join(this._identity_check_tokens_collection.removeAsync(query), + BluebirdPromise.resolve(content)); }) .then((v) => { - return Promise.resolve(v[1]); + return BluebirdPromise.resolve(v[1]); }); } - set_totp_secret(userid: string, secret: TOTPSecret): Promise { + set_totp_secret(userid: string, secret: TOTPSecret): BluebirdPromise { const doc = { userid: userid, secret: secret @@ -147,23 +149,23 @@ export default class UserDataStore { return this._totp_secret_collection.updateAsync(query, doc, { upsert: true }); } - get_totp_secret(userid: string): Promise { + get_totp_secret(userid: string): BluebirdPromise { const query = { userid: userid }; return this._totp_secret_collection.findOneAsync(query); } -} - -function create_collection(name: string, options: any): NedbAsync { - const datastore_options = { - inMemoryOnly: options.inMemoryOnly || false, - autoload: true, - filename: "" - }; - - if (options.directory) - datastore_options.filename = path.resolve(options.directory, name); - - return Promise.promisifyAll(new Nedb(datastore_options)) as NedbAsync; + + private create_collection(name: string, options: any): NedbAsync { + const datastore_options = { + inMemoryOnly: options.inMemoryOnly || false, + autoload: true, + filename: "" + }; + + if (options.directory) + datastore_options.filename = path.resolve(options.directory, name); + + return BluebirdPromise.promisifyAll(new this.nedb(datastore_options)) as NedbAsync; + } } diff --git a/src/lib/identity_check.js b/src/lib/identity_check.js index 72fd603a..0a1f68fe 100644 --- a/src/lib/identity_check.js +++ b/src/lib/identity_check.js @@ -1,7 +1,7 @@ var objectPath = require('object-path'); var randomstring = require('randomstring'); -var Promise = require('bluebird'); +var BluebirdPromise = require('bluebird'); var util = require('util'); var exceptions = require('./Exceptions'); var fs = require('fs'); @@ -27,7 +27,7 @@ IdentityCheck.prototype.issue_token = function(userid, content, logger) { this._logger.debug('identity_check: issue identity token %s for 5 minutes', token); return this._user_data_store.issue_identity_check_token(userid, token, content, five_minutes) .then(function() { - return Promise.resolve(token); + return BluebirdPromise.resolve(token); }); } diff --git a/src/lib/notifiers/GMailNotifier.ts b/src/lib/notifiers/GMailNotifier.ts index 1cd11a3c..ee6136ad 100644 --- a/src/lib/notifiers/GMailNotifier.ts +++ b/src/lib/notifiers/GMailNotifier.ts @@ -1,5 +1,5 @@ -import * as Promise from "bluebird"; +import * as BluebirdPromise from "bluebird"; import * as fs from "fs"; import * as ejs from "ejs"; import nodemailer = require("nodemailer"); @@ -23,10 +23,10 @@ export class GMailNotifier extends INotifier { pass: options.password } }); - this.transporter = Promise.promisifyAll(transporter); + this.transporter = BluebirdPromise.promisifyAll(transporter); } - notify(identity: Identity, subject: string, link: string): Promise { + notify(identity: Identity, subject: string, link: string): BluebirdPromise { const d = { url: link, button_title: "Continue", diff --git a/src/lib/routes/FirstFactor.ts b/src/lib/routes/FirstFactor.ts index 9b24afe0..3a67f468 100644 --- a/src/lib/routes/FirstFactor.ts +++ b/src/lib/routes/FirstFactor.ts @@ -1,10 +1,10 @@ import exceptions = require("../Exceptions"); import objectPath = require("object-path"); -import Promise = require("bluebird"); +import BluebirdPromise = require("bluebird"); import express = require("express"); -export = function(req: express.Request, res: express.Response) { +export = function (req: express.Request, res: express.Response) { const username = req.body.username; const password = req.body.password; if (!username || !password) { @@ -24,53 +24,53 @@ export = function(req: express.Request, res: express.Response) { logger.debug("1st factor: username=%s", username); regulator.regulate(username) - .then(function() { - return ldap.bind(username, password); - }) - .then(function() { - objectPath.set(req, "session.auth_session.userid", username); - objectPath.set(req, "session.auth_session.first_factor", true); - logger.info("1st factor: LDAP binding successful"); - logger.debug("1st factor: Retrieve email from LDAP"); - return Promise.join(ldap.get_emails(username), ldap.get_groups(username)); - }) - .then(function(data: string[2]) { - const emails = data[0]; - const groups = data[1]; + .then(function () { + return ldap.bind(username, password); + }) + .then(function () { + objectPath.set(req, "session.auth_session.userid", username); + objectPath.set(req, "session.auth_session.first_factor", true); + logger.info("1st factor: LDAP binding successful"); + logger.debug("1st factor: Retrieve email from LDAP"); + return BluebirdPromise.join(ldap.get_emails(username), ldap.get_groups(username)); + }) + .then(function (data: string[2]) { + const emails = data[0]; + const groups = data[1]; - if (!emails && emails.length <= 0) throw new Error("No email found"); - logger.debug("1st factor: Retrieved email are %s", emails); - objectPath.set(req, "session.auth_session.email", emails[0]); + if (!emails && emails.length <= 0) throw new Error("No email found"); + logger.debug("1st factor: Retrieved email are %s", emails); + objectPath.set(req, "session.auth_session.email", emails[0]); - const isAllowed = accessController.isDomainAllowedForUser(username, groups); - if (!isAllowed) throw new Error("User not allowed to visit this domain"); + const isAllowed = accessController.isDomainAllowedForUser(username, groups); + if (!isAllowed) throw new Error("User not allowed to visit this domain"); - regulator.mark(username, true); - res.status(204); - res.send(); - }) - .catch(exceptions.LdapSeachError, function(err: Error) { - logger.error("1st factor: Unable to retrieve email from LDAP", err); - res.status(500); - res.send(); - }) - .catch(exceptions.LdapBindError, function(err: Error) { - logger.error("1st factor: LDAP binding failed"); - logger.debug("1st factor: LDAP binding failed due to ", err); - regulator.mark(username, false); - res.status(401); - res.send("Bad credentials"); - }) - .catch(exceptions.AuthenticationRegulationError, function(err: Error) { - logger.error("1st factor: the regulator rejected the authentication of user %s", username); - logger.debug("1st factor: authentication rejected due to %s", err); - res.status(403); - res.send("Access has been restricted for a few minutes..."); - }) - .catch(function(err: Error) { - console.log(err.stack); - logger.error("1st factor: Unhandled error %s", err); - res.status(500); - res.send("Internal error"); - }); + regulator.mark(username, true); + res.status(204); + res.send(); + }) + .catch(exceptions.LdapSeachError, function (err: Error) { + logger.error("1st factor: Unable to retrieve email from LDAP", err); + res.status(500); + res.send(); + }) + .catch(exceptions.LdapBindError, function (err: Error) { + logger.error("1st factor: LDAP binding failed"); + logger.debug("1st factor: LDAP binding failed due to ", err); + regulator.mark(username, false); + res.status(401); + res.send("Bad credentials"); + }) + .catch(exceptions.AuthenticationRegulationError, function (err: Error) { + logger.error("1st factor: the regulator rejected the authentication of user %s", username); + logger.debug("1st factor: authentication rejected due to %s", err); + res.status(403); + res.send("Access has been restricted for a few minutes..."); + }) + .catch(function (err: Error) { + console.log(err.stack); + logger.error("1st factor: Unhandled error %s", err); + res.status(500); + res.send("Internal error"); + }); }; diff --git a/src/lib/routes/TOTPAuthenticator.ts b/src/lib/routes/TOTPAuthenticator.ts new file mode 100644 index 00000000..7f63f2ff --- /dev/null +++ b/src/lib/routes/TOTPAuthenticator.ts @@ -0,0 +1,49 @@ + +import exceptions = require("../Exceptions"); +import objectPath = require("object-path"); +import express = require("express"); +import { TOTPSecretDocument } from "../UserDataStore"; +import BluebirdPromise = require("bluebird"); + +const UNAUTHORIZED_MESSAGE = "Unauthorized access"; + +export = function(req: express.Request, res: express.Response) { + const logger = req.app.get("logger"); + const userid = objectPath.get(req, "session.auth_session.userid"); + logger.info("POST 2ndfactor totp: Initiate TOTP validation for user %s", userid); + + if (!userid) { + logger.error("POST 2ndfactor totp: No user id in the session"); + res.status(403); + res.send(); + return; + } + + const token = req.body.token; + const totpValidator = req.app.get("totp validator"); + const userDataStore = req.app.get("user data store"); + + logger.debug("POST 2ndfactor totp: Fetching secret for user %s", userid); + userDataStore.get_totp_secret(userid) + .then(function (doc: TOTPSecretDocument) { + logger.debug("POST 2ndfactor totp: TOTP secret is %s", JSON.stringify(doc)); + return totpValidator.validate(token, doc.secret.base32); + }) + .then(function () { + logger.debug("POST 2ndfactor totp: TOTP validation succeeded"); + objectPath.set(req, "session.auth_session.second_factor", true); + res.status(204); + res.send(); + }) + .catch(exceptions.InvalidTOTPError, function (err: Error) { + logger.error("POST 2ndfactor totp: Invalid TOTP token %s", err.message); + res.status(401); + res.send("Invalid TOTP token"); + }) + .catch(function (err: Error) { + console.log(err.stack); + logger.error("POST 2ndfactor totp: Internal error %s", err.message); + res.status(500); + res.send("Internal error"); + }); +}; diff --git a/src/lib/routes/reset_password.js b/src/lib/routes/reset_password.js index d2271d57..ab39ff5a 100644 --- a/src/lib/routes/reset_password.js +++ b/src/lib/routes/reset_password.js @@ -1,5 +1,5 @@ -var Promise = require('bluebird'); +var BluebirdPromise = require('bluebird'); var objectPath = require('object-path'); var exceptions = require('../Exceptions'); var CHALLENGE = 'reset-password'; @@ -19,7 +19,7 @@ module.exports = { function pre_check(req) { var userid = objectPath.get(req, 'body.userid'); if(!userid) { - return Promise.reject(new exceptions.AccessDeniedError("No user id provided")); + return BluebirdPromise.reject(new exceptions.AccessDeniedError("No user id provided")); } var ldap = req.app.get('ldap'); @@ -30,7 +30,7 @@ function pre_check(req) { var identity = {} identity.email = emails[0]; identity.userid = userid; - return Promise.resolve(identity); + return BluebirdPromise.resolve(identity); }); } diff --git a/src/lib/routes/second_factor.js b/src/lib/routes/second_factor.js index f57149dd..413b4337 100644 --- a/src/lib/routes/second_factor.js +++ b/src/lib/routes/second_factor.js @@ -1,9 +1,10 @@ var denyNotLogged = require('./deny_not_logged'); -var u2f = require('./u2f'); +var u2f = require('./u2f'); +var TOTPAuthenticator = require("./TOTPAuthenticator"); module.exports = { - totp: denyNotLogged(require('./totp')), + totp: denyNotLogged(TOTPAuthenticator), u2f: { register_request: u2f.register_request, register: u2f.register, diff --git a/src/lib/routes/totp.js b/src/lib/routes/totp.js deleted file mode 100644 index dc5c5721..00000000 --- a/src/lib/routes/totp.js +++ /dev/null @@ -1,49 +0,0 @@ - -module.exports = totp_fn; - -var objectPath = require('object-path'); -var exceptions = require('../../../src/lib/Exceptions'); - -var UNAUTHORIZED_MESSAGE = 'Unauthorized access'; - -function totp_fn(req, res) { - var logger = req.app.get('logger'); - var userid = objectPath.get(req, 'session.auth_session.userid'); - logger.info('POST 2ndfactor totp: Initiate TOTP validation for user %s', userid); - - if(!userid) { - logger.error('POST 2ndfactor totp: No user id in the session'); - res.status(403); - res.send(); - return; - } - - var token = req.body.token; - 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 totpValidator.validate(token, doc.secret.base32); - }) - .then(function() { - logger.debug('POST 2ndfactor totp: TOTP validation succeeded'); - objectPath.set(req, 'session.auth_session.second_factor', true); - res.status(204); - res.send(); - }, function(err) { - throw new exceptions.InvalidTOTPError(); - }) - .catch(exceptions.InvalidTOTPError, function(err) { - logger.error('POST 2ndfactor totp: Invalid TOTP token %s', err); - res.status(401); - res.send('Invalid TOTP token'); - }) - .catch(function(err) { - logger.error('POST 2ndfactor totp: Internal error %s', err); - res.status(500); - res.send('Internal error'); - }); -} diff --git a/src/lib/routes/totp_register.js b/src/lib/routes/totp_register.js index 37c7e7ce..0f163f36 100644 --- a/src/lib/routes/totp_register.js +++ b/src/lib/routes/totp_register.js @@ -1,5 +1,5 @@ var objectPath = require('object-path'); -var Promise = require('bluebird'); +var BluebirdPromise = require('bluebird'); var CHALLENGE = 'totp-register'; @@ -18,20 +18,20 @@ module.exports = { function pre_check(req) { var first_factor_passed = objectPath.get(req, 'session.auth_session.first_factor'); if(!first_factor_passed) { - return Promise.reject('Authentication required before registering TOTP secret key'); + return BluebirdPromise.reject('Authentication required before registering TOTP secret key'); } var userid = objectPath.get(req, 'session.auth_session.userid'); var email = objectPath.get(req, 'session.auth_session.email'); if(!(userid && email)) { - return Promise.reject('User ID or email is missing'); + return BluebirdPromise.reject('User ID or email is missing'); } var identity = {}; identity.email = email; identity.userid = userid; - return Promise.resolve(identity); + return BluebirdPromise.resolve(identity); } // Generate a secret and send it to the user diff --git a/src/lib/routes/u2f_register.js b/src/lib/routes/u2f_register.js index 5161d965..220a1716 100644 --- a/src/lib/routes/u2f_register.js +++ b/src/lib/routes/u2f_register.js @@ -10,7 +10,7 @@ module.exports = { var objectPath = require('object-path'); var u2f_common = require('./u2f_common'); -var Promise = require('bluebird'); +var BluebirdPromise = require('bluebird'); function register_request(req, res) { var logger = req.app.get('logger'); diff --git a/src/lib/routes/u2f_register_handler.js b/src/lib/routes/u2f_register_handler.js index 2c2600a3..f321dc52 100644 --- a/src/lib/routes/u2f_register_handler.js +++ b/src/lib/routes/u2f_register_handler.js @@ -1,6 +1,6 @@ var objectPath = require('object-path'); -var Promise = require('bluebird'); +var BluebirdPromise = require('bluebird'); var CHALLENGE = 'u2f-register'; @@ -19,19 +19,19 @@ module.exports = { function pre_check(req) { var first_factor_passed = objectPath.get(req, 'session.auth_session.first_factor'); if(!first_factor_passed) { - return Promise.reject('Authentication required before issuing a u2f registration request'); + return BluebirdPromise.reject('Authentication required before issuing a u2f registration request'); } var userid = objectPath.get(req, 'session.auth_session.userid'); var email = objectPath.get(req, 'session.auth_session.email'); if(!(userid && email)) { - return Promise.reject('User ID or email is missing'); + return BluebirdPromise.reject('User ID or email is missing'); } var identity = {}; identity.email = email; identity.userid = userid; - return Promise.resolve(identity); + return BluebirdPromise.resolve(identity); } diff --git a/src/lib/routes/verify.js b/src/lib/routes/verify.js index 6ebbc852..1b40649b 100644 --- a/src/lib/routes/verify.js +++ b/src/lib/routes/verify.js @@ -2,31 +2,31 @@ module.exports = verify; var objectPath = require('object-path'); -var Promise = require('bluebird'); +var BluebirdPromise = require('bluebird'); function verify_filter(req, res) { var logger = req.app.get('logger'); if(!objectPath.has(req, 'session.auth_session')) - return Promise.reject('No auth_session variable'); + return BluebirdPromise.reject('No auth_session variable'); if(!objectPath.has(req, 'session.auth_session.first_factor')) - return Promise.reject('No first factor variable'); + return BluebirdPromise.reject('No first factor variable'); if(!objectPath.has(req, 'session.auth_session.second_factor')) - return Promise.reject('No second factor variable'); + return BluebirdPromise.reject('No second factor variable'); if(!objectPath.has(req, 'session.auth_session.userid')) - return Promise.reject('No userid variable'); + return BluebirdPromise.reject('No userid variable'); var host = objectPath.get(req, 'headers.host'); var domain = host.split(':')[0]; if(!req.session.auth_session.first_factor || !req.session.auth_session.second_factor) - return Promise.reject('First or second factor not validated'); + return BluebirdPromise.reject('First or second factor not validated'); - return Promise.resolve(); + return BluebirdPromise.resolve(); } function verify(req, res) { diff --git a/src/types/authdog.d.ts b/src/types/authdog.d.ts index 9cb1121f..4405f6f1 100644 --- a/src/types/authdog.d.ts +++ b/src/types/authdog.d.ts @@ -1,4 +1,6 @@ +import BluebirdPromise = require("bluebird"); + declare module "authdog" { interface RegisterRequest { challenge: string; @@ -60,8 +62,8 @@ declare module "authdog" { counter: Uint32Array } - export function startRegistration(appId: AppId, registeredKeys: RegisteredKeys, options?: Options): Promise; - export function finishRegistration(registrationRequest: RegistrationRequest, registrationResponse: RegistrationResponse): Promise; - export function startAuthentication(appId: AppId, registeredKeys: RegisteredKeys, options: Options): Promise; - export function finishAuthentication(challenge: string, deviceResponse: AuthenticationResponse, registeredKeys: RegisteredKeys): Promise; + export function startRegistration(appId: AppId, registeredKeys: RegisteredKeys, options?: Options): BluebirdPromise; + export function finishRegistration(registrationRequest: RegistrationRequest, registrationResponse: RegistrationResponse): BluebirdPromise; + export function startAuthentication(appId: AppId, registeredKeys: RegisteredKeys, options: Options): BluebirdPromise; + export function finishAuthentication(challenge: string, deviceResponse: AuthenticationResponse, registeredKeys: RegisteredKeys): BluebirdPromise; } \ No newline at end of file diff --git a/src/types/ldapjs-async.d.ts b/src/types/ldapjs-async.d.ts index c2f03775..e5fad359 100644 --- a/src/types/ldapjs-async.d.ts +++ b/src/types/ldapjs-async.d.ts @@ -1,11 +1,11 @@ import ldapjs = require("ldapjs"); -import * as Promise from "bluebird"; +import * as BluebirdPromise from "bluebird"; import { EventEmitter } from "events"; declare module "ldapjs" { export interface ClientAsync { - bindAsync(username: string, password: string): Promise; - searchAsync(base: string, query: ldapjs.SearchOptions): Promise; - modifyAsync(userdn: string, change: ldapjs.Change): Promise; + bindAsync(username: string, password: string): BluebirdPromise; + searchAsync(base: string, query: ldapjs.SearchOptions): BluebirdPromise; + modifyAsync(userdn: string, change: ldapjs.Change): BluebirdPromise; } } \ No newline at end of file diff --git a/src/types/nedb-async.d.ts b/src/types/nedb-async.d.ts index e5dc9926..1f4fe042 100644 --- a/src/types/nedb-async.d.ts +++ b/src/types/nedb-async.d.ts @@ -1,12 +1,12 @@ import Nedb = require("nedb"); -import * as Promise from "bluebird"; +import BluebirdPromise = require("bluebird"); declare module "nedb" { export class NedbAsync extends Nedb { constructor(pathOrOptions?: string | Nedb.DataStoreOptions); - updateAsync(query: any, updateQuery: any, options?: Nedb.UpdateOptions): Promise; - findOneAsync(query: any): Promise; - insertAsync(newDoc: T): Promise; - removeAsync(query: any): Promise; + updateAsync(query: any, updateQuery: any, options?: Nedb.UpdateOptions): BluebirdPromise; + findOneAsync(query: any): BluebirdPromise; + insertAsync(newDoc: T): BluebirdPromise; + removeAsync(query: any): BluebirdPromise; } } \ No newline at end of file diff --git a/src/types/request-async.d.ts b/src/types/request-async.d.ts index 38a36822..164d6919 100644 --- a/src/types/request-async.d.ts +++ b/src/types/request-async.d.ts @@ -1,14 +1,14 @@ -import * as Promise from "bluebird"; +import * as BluebirdPromise from "bluebird"; import * as request from "request"; declare module "request" { export interface RequestAsync extends RequestAPI { - getAsync(uri: string, options?: RequiredUriUrl): Promise; - getAsync(uri: string): Promise; - getAsync(options: RequiredUriUrl & CoreOptions): Promise; + getAsync(uri: string, options?: RequiredUriUrl): BluebirdPromise; + getAsync(uri: string): BluebirdPromise; + getAsync(options: RequiredUriUrl & CoreOptions): BluebirdPromise; - postAsync(uri: string, options?: CoreOptions): Promise; - postAsync(uri: string): Promise; - postAsync(options: RequiredUriUrl & CoreOptions): Promise; + postAsync(uri: string, options?: CoreOptions): BluebirdPromise; + postAsync(uri: string): BluebirdPromise; + postAsync(options: RequiredUriUrl & CoreOptions): BluebirdPromise; } } \ No newline at end of file diff --git a/test/unitary/AuthenticationRegulator.test.ts b/test/unitary/AuthenticationRegulator.test.ts index 88707aff..27053790 100644 --- a/test/unitary/AuthenticationRegulator.test.ts +++ b/test/unitary/AuthenticationRegulator.test.ts @@ -3,13 +3,14 @@ import AuthenticationRegulator from "../../src/lib/AuthenticationRegulator"; import UserDataStore from "../../src/lib/UserDataStore"; import MockDate = require("mockdate"); import exceptions = require("../../src/lib/Exceptions"); +import nedb = require("nedb"); describe("test authentication regulator", function() { it("should mark 2 authentication and regulate (resolve)", function() { const options = { inMemoryOnly: true }; - const data_store = new UserDataStore(options); + const data_store = new UserDataStore(options, nedb); const regulator = new AuthenticationRegulator(data_store, 10); const user = "user"; @@ -26,7 +27,7 @@ describe("test authentication regulator", function() { const options = { inMemoryOnly: true }; - const data_store = new UserDataStore(options); + const data_store = new UserDataStore(options, nedb); const regulator = new AuthenticationRegulator(data_store, 10); const user = "user"; @@ -49,7 +50,7 @@ describe("test authentication regulator", function() { const options = { inMemoryOnly: true }; - const data_store = new UserDataStore(options); + const data_store = new UserDataStore(options, nedb); const regulator = new AuthenticationRegulator(data_store, 10); const user = "user"; diff --git a/test/unitary/TOTPValidator.ts b/test/unitary/TOTPValidator.test.ts similarity index 82% rename from test/unitary/TOTPValidator.ts rename to test/unitary/TOTPValidator.test.ts index e2d06f67..84baa040 100644 --- a/test/unitary/TOTPValidator.ts +++ b/test/unitary/TOTPValidator.test.ts @@ -18,12 +18,12 @@ describe("test TOTP validation", function() { return totpValidator.validate(token, totp_secret); }); - it("should not validate a wrong TOTP token", function() { + it("should not validate a wrong TOTP token", function(done) { const totp_secret = "NBD2ZV64R9UV1O7K"; const token = "wrong token"; - return totpValidator.validate(token, totp_secret) + totpValidator.validate(token, totp_secret) .catch(function() { - return Promise.resolve(); + done(); }); }); }); diff --git a/test/unitary/UserDataStore.test.ts b/test/unitary/UserDataStore.test.ts index a631a946..a7ce7dd9 100644 --- a/test/unitary/UserDataStore.test.ts +++ b/test/unitary/UserDataStore.test.ts @@ -2,7 +2,7 @@ import UserDataStore from "../../src/lib/UserDataStore"; import { U2FMetaDocument, Options } from "../../src/lib/UserDataStore"; -import DataStore = require("nedb"); +import nedb = require("nedb"); import assert = require("assert"); import Promise = require("bluebird"); import sinon = require("sinon"); @@ -20,7 +20,7 @@ describe("test user data store", () => { describe("test u2f meta", () => { it("should save a u2f meta", function () { - const data_store = new UserDataStore(options); + const data_store = new UserDataStore(options, nedb); const userid = "user"; const app_id = "https://localhost"; @@ -40,7 +40,7 @@ describe("test user data store", () => { inMemoryOnly: true }; - const data_store = new UserDataStore(options); + const data_store = new UserDataStore(options, nedb); const userid = "user"; const app_id = "https://localhost"; @@ -60,7 +60,7 @@ describe("test user data store", () => { inMemoryOnly: true }; - const data_store = new UserDataStore(options); + const data_store = new UserDataStore(options, nedb); const userid = "user"; const app_id = "https://localhost"; @@ -86,7 +86,7 @@ describe("test user data store", () => { describe("test u2f registration token", () => { it("should save u2f registration token", function () { - const data_store = new UserDataStore(options); + const data_store = new UserDataStore(options, nedb); const userid = "user"; const token = "token"; @@ -109,7 +109,7 @@ describe("test user data store", () => { }); it("should save u2f registration token and consume it", function (done) { - const data_store = new UserDataStore(options); + const data_store = new UserDataStore(options, nedb); const userid = "user"; const token = "token"; @@ -128,7 +128,7 @@ describe("test user data store", () => { }); it("should not be able to consume registration token twice", function (done) { - const data_store = new UserDataStore(options); + const data_store = new UserDataStore(options, nedb); const userid = "user"; const token = "token"; @@ -148,7 +148,7 @@ describe("test user data store", () => { }); it("should fail when token does not exist", function () { - const data_store = new UserDataStore(options); + const data_store = new UserDataStore(options, nedb); const token = "token"; @@ -162,7 +162,7 @@ describe("test user data store", () => { }); it("should fail when token expired", function (done) { - const data_store = new UserDataStore(options); + const data_store = new UserDataStore(options, nedb); const userid = "user"; const token = "token"; @@ -181,7 +181,7 @@ describe("test user data store", () => { }); it("should save the userid and some data with the token", function (done) { - const data_store = new UserDataStore(options); + const data_store = new UserDataStore(options, nedb); const userid = "user"; const token = "token"; diff --git a/test/unitary/mocks/AccessController.ts b/test/unitary/mocks/AccessController.ts index a0c97853..ce46c0b8 100644 --- a/test/unitary/mocks/AccessController.ts +++ b/test/unitary/mocks/AccessController.ts @@ -1,8 +1,12 @@ import sinon = require("sinon"); -export = function () { +export interface AccessControllerMock { + isDomainAllowedForUser: sinon.SinonStub; +} + +export function AccessControllerMock() { return { isDomainAllowedForUser: sinon.stub() }; -}; +} diff --git a/test/unitary/mocks/AuthenticationRegulator.ts b/test/unitary/mocks/AuthenticationRegulator.ts index d4464c45..2d789d94 100644 --- a/test/unitary/mocks/AuthenticationRegulator.ts +++ b/test/unitary/mocks/AuthenticationRegulator.ts @@ -1,9 +1,15 @@ import sinon = require("sinon"); -export = function () { + +export interface AuthenticationRegulatorMock { + mark: sinon.SinonStub; + regulate: sinon.SinonStub; +} + +export function AuthenticationRegulatorMock() { return { mark: sinon.stub(), regulate: sinon.stub() }; -}; +} diff --git a/test/unitary/mocks/TOTPValidator.ts b/test/unitary/mocks/TOTPValidator.ts new file mode 100644 index 00000000..56434c79 --- /dev/null +++ b/test/unitary/mocks/TOTPValidator.ts @@ -0,0 +1,12 @@ + +import sinon = require("sinon"); + +export interface TOTPValidatorMock { + validate: sinon.SinonStub; +} + +export function TOTPValidatorMock(): TOTPValidatorMock { + return { + validate: sinon.stub() + }; +} diff --git a/test/unitary/mocks/UserDataStore.ts b/test/unitary/mocks/UserDataStore.ts index fc2774f1..a2221efa 100644 --- a/test/unitary/mocks/UserDataStore.ts +++ b/test/unitary/mocks/UserDataStore.ts @@ -6,6 +6,7 @@ export interface UserDataStore { get_u2f_meta: sinon.SinonStub; issue_identity_check_token: sinon.SinonStub; consume_identity_check_token: sinon.SinonStub; + get_totp_secret: sinon.SinonStub; } export function UserDataStore(): UserDataStore { @@ -13,6 +14,7 @@ export function UserDataStore(): UserDataStore { set_u2f_meta: sinon.stub(), get_u2f_meta: sinon.stub(), issue_identity_check_token: sinon.stub(), - consume_identity_check_token: sinon.stub() + consume_identity_check_token: sinon.stub(), + get_totp_secret: sinon.stub() }; } diff --git a/test/unitary/routes/FirstFactor.test.ts b/test/unitary/routes/FirstFactor.test.ts index 9d0b1af3..8c48a0eb 100644 --- a/test/unitary/routes/FirstFactor.test.ts +++ b/test/unitary/routes/FirstFactor.test.ts @@ -12,14 +12,14 @@ import { LdapClientMock } from "../mocks/LdapClient"; import ExpressMock = require("../mocks/express"); describe("test the first factor validation route", function() { - let req: any; - let res: any; + let req: ExpressMock.RequestMock; + let res: ExpressMock.ResponseMock; let emails: string[]; let groups: string[]; let configuration; let ldapMock: LdapClientMock; - let regulator: any; - let accessController: any; + let regulator: AuthenticationRegulatorMock.AuthenticationRegulatorMock; + let accessController: AccessControllerMock.AccessControllerMock; beforeEach(function() { configuration = { @@ -34,10 +34,10 @@ describe("test the first factor validation route", function() { ldapMock = LdapClientMock(); - accessController = AccessControllerMock(); + accessController = AccessControllerMock.AccessControllerMock(); accessController.isDomainAllowedForUser.returns(true); - regulator = AuthenticationRegulatorMock(); + regulator = AuthenticationRegulatorMock.AuthenticationRegulatorMock(); regulator.regulate.returns(BluebirdPromise.resolve()); regulator.mark.returns(BluebirdPromise.resolve()); @@ -75,15 +75,15 @@ describe("test the first factor validation route", function() { }); ldapMock.bind.withArgs("username").returns(BluebirdPromise.resolve()); ldapMock.get_emails.returns(BluebirdPromise.resolve(emails)); - FirstFactor(req, res); + FirstFactor(req as any, res as any); }); }); it("should retrieve email from LDAP", function(done) { res.send = sinon.spy(function() { done(); }); ldapMock.bind.returns(BluebirdPromise.resolve()); - ldapMock.get_emails = sinon.stub().withArgs("usernam").returns(BluebirdPromise.resolve([{mail: ["test@example.com"] }])); - FirstFactor(req, res); + ldapMock.get_emails = sinon.stub().withArgs("username").returns(BluebirdPromise.resolve([{mail: ["test@example.com"] }])); + FirstFactor(req as any, res as any); }); it("should set email as session variables", function() { @@ -95,7 +95,7 @@ describe("test the first factor validation route", function() { const emails = [ "test_ok@example.com" ]; ldapMock.bind.returns(BluebirdPromise.resolve()); ldapMock.get_emails.returns(BluebirdPromise.resolve(emails)); - FirstFactor(req, res); + FirstFactor(req as any, res as any); }); }); @@ -105,8 +105,8 @@ describe("test the first factor validation route", function() { assert.equal(regulator.mark.getCall(0).args[0], "username"); done(); }); - ldapMock.bind.throws(new exceptions.LdapBindError("Bad credentials")); - FirstFactor(req, res); + ldapMock.bind.returns(BluebirdPromise.reject(new exceptions.LdapBindError("Bad credentials"))); + FirstFactor(req as any, res as any); }); it("should return status code 500 when LDAP search throws", function(done) { @@ -115,8 +115,8 @@ describe("test the first factor validation route", function() { done(); }); ldapMock.bind.returns(BluebirdPromise.resolve()); - ldapMock.get_emails.throws(new exceptions.LdapSeachError("error while retrieving emails")); - FirstFactor(req, res); + ldapMock.get_emails.returns(BluebirdPromise.reject(new exceptions.LdapSeachError("error while retrieving emails"))); + FirstFactor(req as any, res as any); }); it("should return status code 403 when regulator rejects authentication", function(done) { @@ -129,7 +129,7 @@ describe("test the first factor validation route", function() { }); ldapMock.bind.returns(BluebirdPromise.resolve()); ldapMock.get_emails.returns(BluebirdPromise.resolve()); - FirstFactor(req, res); + FirstFactor(req as any, res as any); }); }); diff --git a/test/unitary/routes/TOTPAuthenticator.test.ts b/test/unitary/routes/TOTPAuthenticator.test.ts new file mode 100644 index 00000000..eab66d67 --- /dev/null +++ b/test/unitary/routes/TOTPAuthenticator.test.ts @@ -0,0 +1,90 @@ + +import BluebirdPromise = require("bluebird"); +import sinon = require("sinon"); +import assert = require("assert"); +import winston = require("winston"); + +import exceptions = require("../../../src/lib/Exceptions"); +import TOTPAuthenticator = require("../../../src/lib/routes/TOTPAuthenticator"); + +import ExpressMock = require("../mocks/express"); +import UserDataStoreMock = require("../mocks/UserDataStore"); +import TOTPValidatorMock = require("../mocks/TOTPValidator"); + +describe("test totp route", function() { + let req: ExpressMock.RequestMock; + let res: ExpressMock.ResponseMock; + let totpValidator: TOTPValidatorMock.TOTPValidatorMock; + let userDataStore: UserDataStoreMock.UserDataStore; + + beforeEach(function() { + const app_get = sinon.stub(); + req = { + app: { + get: app_get + }, + body: { + token: "abc" + }, + session: { + auth_session: { + userid: "user", + first_factor: false, + second_factor: false + } + } + }; + res = ExpressMock.ResponseMock(); + + const config = { totp_secret: "secret" }; + totpValidator = TOTPValidatorMock.TOTPValidatorMock(); + + userDataStore = UserDataStoreMock.UserDataStore(); + + const doc = { + userid: "user", + secret: { + base32: "ABCDEF" + } + }; + userDataStore.get_totp_secret.returns(BluebirdPromise.resolve(doc)); + + app_get.withArgs("logger").returns(winston); + app_get.withArgs("totp validator").returns(totpValidator); + app_get.withArgs("config").returns(config); + app_get.withArgs("user data store").returns(userDataStore); + }); + + + it("should send status code 204 when totp is valid", function(done) { + totpValidator.validate.returns(Promise.resolve("ok")); + res.send = sinon.spy(function() { + // Second factor passed + assert.equal(true, req.session.auth_session.second_factor); + assert.equal(204, res.status.getCall(0).args[0]); + done(); + }); + TOTPAuthenticator(req as any, res as any); + }); + + it("should send status code 401 when totp is not valid", function(done) { + totpValidator.validate.returns(Promise.reject(new exceptions.InvalidTOTPError("Bad TOTP token"))); + res.send = sinon.spy(function() { + assert.equal(false, req.session.auth_session.second_factor); + assert.equal(401, res.status.getCall(0).args[0]); + done(); + }); + TOTPAuthenticator(req as any, res as any); + }); + + it("should send status code 401 when session has not been initiated", function(done) { + totpValidator.validate.returns(Promise.resolve("abc")); + res.send = sinon.spy(function() { + assert.equal(403, res.status.getCall(0).args[0]); + done(); + }); + req.session = {}; + TOTPAuthenticator(req as any, res as any); + }); +}); + diff --git a/test/unitary/routes/test_totp.js b/test/unitary/routes/test_totp.js deleted file mode 100644 index 18e161df..00000000 --- a/test/unitary/routes/test_totp.js +++ /dev/null @@ -1,87 +0,0 @@ - -var totp = require('../../../src/lib/routes/totp'); -var Promise = require('bluebird'); -var sinon = require('sinon'); -var assert = require('assert'); -var winston = require('winston'); - -describe('test totp route', function() { - var req, res; - var totpValidator; - var user_data_store; - - beforeEach(function() { - var app_get = sinon.stub(); - req = { - app: { - get: app_get - }, - body: { - token: 'abc' - }, - session: { - auth_session: { - userid: 'user', - first_factor: false, - second_factor: false - } - } - }; - res = { - send: sinon.spy(), - status: sinon.spy() - }; - - var config = { totp_secret: 'secret' }; - totpValidator = { - validate: sinon.stub() - } - - user_data_store = {}; - user_data_store.get_totp_secret = sinon.stub(); - - var doc = {}; - doc.userid = 'user'; - doc.secret = {}; - doc.secret.base32 = 'ABCDEF'; - user_data_store.get_totp_secret.returns(Promise.resolve(doc)); - - app_get.withArgs('logger').returns(winston); - 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) { - totpValidator.validate.returns(Promise.resolve("ok")); - res.send = sinon.spy(function() { - // Second factor passed - assert.equal(true, req.session.auth_session.second_factor) - assert.equal(204, res.status.getCall(0).args[0]); - done(); - }); - totp(req, res); - }); - - it('should send status code 401 when totp is not valid', function(done) { - 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]); - done(); - }); - totp(req, res); - }); - - it('should send status code 401 when session has not been initiated', function(done) { - totpValidator.validate.returns(Promise.resolve('abc')); - res.send = sinon.spy(function() { - assert.equal(403, res.status.getCall(0).args[0]); - done(); - }); - req.session = {}; - totp(req, res); - }); -}); - diff --git a/test/unitary/user_data_store/authentication_audit.test.ts b/test/unitary/user_data_store/authentication_audit.test.ts index 2dc5c930..8a8be4df 100644 --- a/test/unitary/user_data_store/authentication_audit.test.ts +++ b/test/unitary/user_data_store/authentication_audit.test.ts @@ -4,6 +4,7 @@ import * as Promise from "bluebird"; import * as sinon from "sinon"; import * as MockDate from "mockdate"; import UserDataStore from "../../../src/lib/UserDataStore"; +import nedb = require("nedb"); describe("test user data store", function() { describe("test authentication traces", test_authentication_traces); @@ -15,7 +16,7 @@ function test_authentication_traces() { inMemoryOnly: true }; - const data_store = new UserDataStore(options); + const data_store = new UserDataStore(options, nedb); const userid = "user"; const type = "1stfactor"; const is_success = false; @@ -34,7 +35,7 @@ function test_authentication_traces() { inMemoryOnly: true }; - const data_store = new UserDataStore(options); + const data_store = new UserDataStore(options, nedb); const userid = "user"; const type = "1stfactor"; const is_success = false; diff --git a/test/unitary/user_data_store/totp_secret.test.ts b/test/unitary/user_data_store/totp_secret.test.ts index cddaa227..bd5223ac 100644 --- a/test/unitary/user_data_store/totp_secret.test.ts +++ b/test/unitary/user_data_store/totp_secret.test.ts @@ -4,6 +4,7 @@ import * as Promise from "bluebird"; import * as sinon from "sinon"; import * as MockDate from "mockdate"; import UserDataStore from "../../../src/lib/UserDataStore"; +import nedb = require("nedb"); describe("test user data store", function() { describe("test totp secrets store", test_totp_secrets); @@ -15,7 +16,7 @@ function test_totp_secrets() { inMemoryOnly: true }; - const data_store = new UserDataStore(options); + const data_store = new UserDataStore(options, nedb); const userid = "user"; const secret = { ascii: "abc", @@ -41,7 +42,7 @@ function test_totp_secrets() { inMemoryOnly: true }; - const data_store = new UserDataStore(options); + const data_store = new UserDataStore(options, nedb); const userid = "user"; const secret1 = { ascii: "abc", diff --git a/tsconfig.json b/tsconfig.json index 4d4d2aa5..ebb0c747 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -10,8 +10,8 @@ "allowJs": true, "paths": { "*": [ - "node_modules/@types/*", - "src/types/*" + "src/types/*", + "node_modules/@types/*" ] } }, From fad23ff3beceac928f7fdb968de37608d15b8286 Mon Sep 17 00:00:00 2001 From: Clement Michaud Date: Sun, 21 May 2017 12:27:12 +0200 Subject: [PATCH 13/16] Move Authentication validator and routes to typescript --- src/lib/routes.js | 39 ------- src/lib/routes.ts | 41 +++++++ src/lib/routes/AuthenticationValidator.ts | 43 +++++++ src/lib/routes/verify.js | 44 ------- test/unitary/mocks/express.ts | 68 ++++++++++- .../routes/AuthenticationValidator.test.ts | 110 ++++++++++++++++++ test/unitary/routes/test_verify.js | 101 ---------------- 7 files changed, 261 insertions(+), 185 deletions(-) delete mode 100644 src/lib/routes.js create mode 100644 src/lib/routes.ts create mode 100644 src/lib/routes/AuthenticationValidator.ts delete mode 100644 src/lib/routes/verify.js create mode 100644 test/unitary/routes/AuthenticationValidator.test.ts delete mode 100644 test/unitary/routes/test_verify.js diff --git a/src/lib/routes.js b/src/lib/routes.js deleted file mode 100644 index b2c9f3b2..00000000 --- a/src/lib/routes.js +++ /dev/null @@ -1,39 +0,0 @@ - -var first_factor = require('./routes/FirstFactor'); -var second_factor = require('./routes/second_factor'); -var reset_password = require('./routes/reset_password'); -var verify = require('./routes/verify'); -var u2f_register_handler = require('./routes/u2f_register_handler'); -var totp_register = require('./routes/totp_register'); -var objectPath = require('object-path'); - -module.exports = { - login: serveLogin, - logout: serveLogout, - verify: verify, - first_factor: first_factor, - second_factor: second_factor, - reset_password: reset_password, - u2f_register: u2f_register_handler, - totp_register: totp_register, -} - -function serveLogin(req, res) { - if(!(objectPath.has(req, 'session.auth_session'))) { - req.session.auth_session = {}; - req.session.auth_session.first_factor = false; - req.session.auth_session.second_factor = false; - } - res.render('login'); -} - -function serveLogout(req, res) { - var redirect_param = req.query.redirect; - var redirect_url = redirect_param || '/'; - req.session.auth_session = { - first_factor: false, - second_factor: false - } - res.redirect(redirect_url); -} - diff --git a/src/lib/routes.ts b/src/lib/routes.ts new file mode 100644 index 00000000..1446fa59 --- /dev/null +++ b/src/lib/routes.ts @@ -0,0 +1,41 @@ + +import FirstFactor = require("./routes/FirstFactor"); +import second_factor = require("./routes/second_factor"); +import reset_password = require("./routes/reset_password"); +import AuthenticationValidator = require("./routes/AuthenticationValidator"); +import u2f_register_handler = require("./routes/u2f_register_handler"); +import totp_register = require("./routes/totp_register"); +import objectPath = require("object-path"); + +import express = require("express"); + +export = { + login: serveLogin, + logout: serveLogout, + verify: AuthenticationValidator, + first_factor: FirstFactor, + second_factor: second_factor, + reset_password: reset_password, + u2f_register: u2f_register_handler, + totp_register: totp_register, +}; + +function serveLogin(req: express.Request, res: express.Response) { + if (!(objectPath.has(req, "session.auth_session"))) { + req.session.auth_session = {}; + req.session.auth_session.first_factor = false; + req.session.auth_session.second_factor = false; + } + res.render("login"); +} + +function serveLogout(req: express.Request, res: express.Response) { + const redirect_param = req.query.redirect; + const redirect_url = redirect_param || "/"; + req.session.auth_session = { + first_factor: false, + second_factor: false + }; + res.redirect(redirect_url); +} + diff --git a/src/lib/routes/AuthenticationValidator.ts b/src/lib/routes/AuthenticationValidator.ts new file mode 100644 index 00000000..38b861da --- /dev/null +++ b/src/lib/routes/AuthenticationValidator.ts @@ -0,0 +1,43 @@ + +import objectPath = require("object-path"); +import BluebirdPromise = require("bluebird"); +import express = require("express"); + +function verify_filter(req: express.Request, res: express.Response) { + const logger = req.app.get("logger"); + + if (!objectPath.has(req, "session.auth_session")) + return BluebirdPromise.reject("No auth_session variable"); + + if (!objectPath.has(req, "session.auth_session.first_factor")) + return BluebirdPromise.reject("No first factor variable"); + + if (!objectPath.has(req, "session.auth_session.second_factor")) + return BluebirdPromise.reject("No second factor variable"); + + if (!objectPath.has(req, "session.auth_session.userid")) + return BluebirdPromise.reject("No userid variable"); + + const host = objectPath.get(req, "headers.host"); + const domain = host.split(":")[0]; + + if (!req.session.auth_session.first_factor || + !req.session.auth_session.second_factor) + return BluebirdPromise.reject("First or second factor not validated"); + + return BluebirdPromise.resolve(); +} + +export = function(req: express.Request, res: express.Response) { + verify_filter(req, res) + .then(function () { + res.status(204); + res.send(); + }) + .catch(function (err) { + req.app.get("logger").error(err); + res.status(401); + res.send(); + }); +}; + diff --git a/src/lib/routes/verify.js b/src/lib/routes/verify.js deleted file mode 100644 index 1b40649b..00000000 --- a/src/lib/routes/verify.js +++ /dev/null @@ -1,44 +0,0 @@ - -module.exports = verify; - -var objectPath = require('object-path'); -var BluebirdPromise = require('bluebird'); - -function verify_filter(req, res) { - var logger = req.app.get('logger'); - - if(!objectPath.has(req, 'session.auth_session')) - return BluebirdPromise.reject('No auth_session variable'); - - if(!objectPath.has(req, 'session.auth_session.first_factor')) - return BluebirdPromise.reject('No first factor variable'); - - if(!objectPath.has(req, 'session.auth_session.second_factor')) - return BluebirdPromise.reject('No second factor variable'); - - if(!objectPath.has(req, 'session.auth_session.userid')) - return BluebirdPromise.reject('No userid variable'); - - var host = objectPath.get(req, 'headers.host'); - var domain = host.split(':')[0]; - - if(!req.session.auth_session.first_factor || - !req.session.auth_session.second_factor) - return BluebirdPromise.reject('First or second factor not validated'); - - return BluebirdPromise.resolve(); -} - -function verify(req, res) { - verify_filter(req, res) - .then(function() { - res.status(204); - res.send(); - }) - .catch(function(err) { - req.app.get('logger').error(err); - res.status(401); - res.send(); - }); -} - diff --git a/test/unitary/mocks/express.ts b/test/unitary/mocks/express.ts index 31aec299..009cb4a3 100644 --- a/test/unitary/mocks/express.ts +++ b/test/unitary/mocks/express.ts @@ -1,17 +1,51 @@ import sinon = require("sinon"); +import express = require("express"); export interface RequestMock { app?: any; body?: any; session?: any; headers?: any; + get?: any; } export interface ResponseMock { send: sinon.SinonStub | sinon.SinonSpy; + sendStatus: sinon.SinonStub; + sendFile: sinon.SinonStub; + sendfile: sinon.SinonStub; status: sinon.SinonStub; json: sinon.SinonStub; + links: sinon.SinonStub; + jsonp: sinon.SinonStub; + download: sinon.SinonStub; + contentType: sinon.SinonStub; + type: sinon.SinonStub; + format: sinon.SinonStub; + attachment: sinon.SinonStub; + set: sinon.SinonStub; + header: sinon.SinonStub; + headersSent: boolean; + get: sinon.SinonStub; + clearCookie: sinon.SinonStub; + cookie: sinon.SinonStub; + location: sinon.SinonStub; + redirect: sinon.SinonStub; + render: sinon.SinonStub; + locals: sinon.SinonStub; + charset: string; + vary: sinon.SinonStub; + app: any; + write: sinon.SinonStub; + writeContinue: sinon.SinonStub; + writeHead: sinon.SinonStub; + statusCode: number; + statusMessage: string; + setHeader: sinon.SinonStub; + setTimeout: sinon.SinonStub; + sendDate: boolean; + getHeader: sinon.SinonStub; } export function RequestMock(): RequestMock { @@ -25,6 +59,38 @@ export function ResponseMock(): ResponseMock { return { send: sinon.stub(), status: sinon.stub(), - json: sinon.stub() + json: sinon.stub(), + sendStatus: sinon.stub(), + links: sinon.stub(), + jsonp: sinon.stub(), + sendFile: sinon.stub(), + sendfile: sinon.stub(), + download: sinon.stub(), + contentType: sinon.stub(), + type: sinon.stub(), + format: sinon.stub(), + attachment: sinon.stub(), + set: sinon.stub(), + header: sinon.stub(), + headersSent: true, + get: sinon.stub(), + clearCookie: sinon.stub(), + cookie: sinon.stub(), + location: sinon.stub(), + redirect: sinon.stub(), + render: sinon.stub(), + locals: sinon.stub(), + charset: "utf-8", + vary: sinon.stub(), + app: sinon.stub(), + write: sinon.stub(), + writeContinue: sinon.stub(), + writeHead: sinon.stub(), + statusCode: 200, + statusMessage: "message", + setHeader: sinon.stub(), + setTimeout: sinon.stub(), + sendDate: true, + getHeader: sinon.stub() }; } diff --git a/test/unitary/routes/AuthenticationValidator.test.ts b/test/unitary/routes/AuthenticationValidator.test.ts new file mode 100644 index 00000000..8d54c359 --- /dev/null +++ b/test/unitary/routes/AuthenticationValidator.test.ts @@ -0,0 +1,110 @@ + +import assert = require("assert"); +import AuthenticationValidator = require("../../../src/lib/routes/AuthenticationValidator"); +import sinon = require("sinon"); +import winston = require("winston"); + +import express = require("express"); + +import ExpressMock = require("../mocks/express"); +import AccessControllerMock = require("../mocks/AccessController"); + +describe("test authentication token verification", function () { + let req: ExpressMock.RequestMock; + let res: ExpressMock.ResponseMock; + let accessController: AccessControllerMock.AccessControllerMock; + + beforeEach(function () { + accessController = AccessControllerMock.AccessControllerMock(); + accessController.isDomainAllowedForUser.returns(true); + + req = ExpressMock.RequestMock(); + res = ExpressMock.ResponseMock(); + req.headers = {}; + req.headers.host = "secret.example.com"; + req.app.get = sinon.stub(); + req.app.get.withArgs("config").returns({}); + req.app.get.withArgs("logger").returns(winston); + req.app.get.withArgs("access controller").returns(accessController); + }); + + interface AuthenticationSession { + first_factor?: boolean; + second_factor?: boolean; + userid?: string; + groups?: string[]; + } + + it("should be already authenticated", function (done) { + req.session = {}; + req.session.auth_session = { + first_factor: true, + second_factor: true, + userid: "myuser", + } as AuthenticationSession; + + res.send = sinon.spy(function () { + assert.equal(204, res.status.getCall(0).args[0]); + done(); + }); + + AuthenticationValidator(req as express.Request, res as any); + }); + + describe("given different cases of session", function () { + function test_session(auth_session: AuthenticationSession, status_code: number) { + return new Promise(function (resolve, reject) { + req.session = {}; + req.session.auth_session = auth_session; + + res.send = sinon.spy(function () { + assert.equal(status_code, res.status.getCall(0).args[0]); + resolve(); + }); + + AuthenticationValidator(req as express.Request, res as any); + }); + } + + function test_unauthorized(auth_session: AuthenticationSession) { + return test_session(auth_session, 401); + } + + function test_authorized(auth_session: AuthenticationSession) { + return test_session(auth_session, 204); + } + + it("should not be authenticated when second factor is missing", function () { + return test_unauthorized({ + userid: "user", + first_factor: true, + second_factor: false + }); + }); + + it("should not be authenticated when first factor is missing", function () { + return test_unauthorized({ first_factor: false, second_factor: true }); + }); + + it("should not be authenticated when userid is missing", function () { + return test_unauthorized({ + first_factor: true, + second_factor: true, + groups: ["mygroup"], + }); + }); + + it("should not be authenticated when first and second factor are missing", function () { + return test_unauthorized({ first_factor: false, second_factor: false }); + }); + + it("should not be authenticated when session has not be initiated", function () { + return test_unauthorized(undefined); + }); + + it("should not be authenticated when session is partially initialized", function () { + return test_unauthorized({ first_factor: true }); + }); + }); +}); + diff --git a/test/unitary/routes/test_verify.js b/test/unitary/routes/test_verify.js deleted file mode 100644 index a3b7710a..00000000 --- a/test/unitary/routes/test_verify.js +++ /dev/null @@ -1,101 +0,0 @@ - -var assert = require('assert'); -var verify = require('../../../src/lib/routes/verify'); -var sinon = require('sinon'); -var winston = require('winston'); - -describe('test authentication token verification', function() { - var req, res; - var config_mock; - var acl_matcher; - - beforeEach(function() { - acl_matcher = { - is_domain_allowed: sinon.stub().returns(true) - }; - var access_control = { - matcher: acl_matcher - } - config_mock = {}; - req = {}; - res = {}; - req.headers = {}; - req.headers.host = 'secret.example.com'; - req.app = {}; - req.app.get = sinon.stub(); - req.app.get.withArgs('config').returns(config_mock); - req.app.get.withArgs('logger').returns(winston); - req.app.get.withArgs('access control').returns(access_control); - res.status = sinon.spy(); - }); - - it('should be already authenticated', function(done) { - req.session = {}; - req.session.auth_session = { - first_factor: true, - second_factor: true, - userid: 'myuser', - allowed_domains: ['*'] - }; - - res.send = sinon.spy(function() { - assert.equal(204, res.status.getCall(0).args[0]); - done(); - }); - - verify(req, res); - }); - - describe('given different cases of session', function() { - function test_session(auth_session, status_code) { - return new Promise(function(resolve, reject) { - req.session = {}; - req.session.auth_session = auth_session; - - res.send = sinon.spy(function() { - assert.equal(status_code, res.status.getCall(0).args[0]); - resolve(); - }); - - verify(req, res); - }); - } - - function test_unauthorized(auth_session) { - return test_session(auth_session, 401); - } - - function test_authorized(auth_session) { - return test_session(auth_session, 204); - } - - it('should not be authenticated when second factor is missing', function() { - return test_unauthorized({ first_factor: true, second_factor: false }); - }); - - it('should not be authenticated when first factor is missing', function() { - return test_unauthorized({ first_factor: false, second_factor: true }); - }); - - it('should not be authenticated when userid is missing', function() { - return test_unauthorized({ - first_factor: true, - second_factor: true, - group: 'mygroup', - }); - }); - - it('should not be authenticated when first and second factor are missing', function() { - return test_unauthorized({ first_factor: false, second_factor: false }); - }); - - it('should not be authenticated when session has not be initiated', function() { - return test_unauthorized(undefined); - }); - - it('should not be authenticated when session is partially initialized', function() { - return test_unauthorized({ first_factor: true }); - }); - }); -}); - From e3257b81a52ef7dedac49669a7a0a3fb13b9e5bd Mon Sep 17 00:00:00 2001 From: Clement Michaud Date: Sun, 21 May 2017 13:11:54 +0200 Subject: [PATCH 14/16] Move denyNotLogged function to typescript --- src/lib/routes/DenyNotLogged.ts | 18 +++++ src/lib/routes/deny_not_logged.js | 19 ----- src/lib/routes/second_factor.js | 8 +- test/unitary/mocks/express.ts | 2 +- test/unitary/routes/DenyNotLogged.test.ts | 82 ++++++++++++++++++++ test/unitary/routes/test_deny_not_logged.js | 83 --------------------- 6 files changed, 105 insertions(+), 107 deletions(-) create mode 100644 src/lib/routes/DenyNotLogged.ts delete mode 100644 src/lib/routes/deny_not_logged.js create mode 100644 test/unitary/routes/DenyNotLogged.test.ts delete mode 100644 test/unitary/routes/test_deny_not_logged.js diff --git a/src/lib/routes/DenyNotLogged.ts b/src/lib/routes/DenyNotLogged.ts new file mode 100644 index 00000000..fc30d836 --- /dev/null +++ b/src/lib/routes/DenyNotLogged.ts @@ -0,0 +1,18 @@ + +import objectPath = require("object-path"); +import express = require("express"); + +export = function denyNotLogged(callback: (req: express.Request, res: express.Response) => void) { + return function (req: express.Request, res: express.Response) { + const auth_session = req.session.auth_session; + const first_factor = objectPath.has(req, "session.auth_session.first_factor") + && req.session.auth_session.first_factor; + if (!first_factor) { + res.status(403); + res.send(); + return; + } + + callback(req, res); + }; +}; diff --git a/src/lib/routes/deny_not_logged.js b/src/lib/routes/deny_not_logged.js deleted file mode 100644 index d22faa03..00000000 --- a/src/lib/routes/deny_not_logged.js +++ /dev/null @@ -1,19 +0,0 @@ - -module.exports = denyNotLogged; - -var objectPath = require('object-path'); - -function denyNotLogged(next) { - return function(req, res) { - var auth_session = req.session.auth_session; - var first_factor = objectPath.has(req, 'session.auth_session.first_factor') - && req.session.auth_session.first_factor; - if(!first_factor) { - res.status(403); - res.send(); - return; - } - - next(req, res); - } -} diff --git a/src/lib/routes/second_factor.js b/src/lib/routes/second_factor.js index 413b4337..c84c3de2 100644 --- a/src/lib/routes/second_factor.js +++ b/src/lib/routes/second_factor.js @@ -1,18 +1,18 @@ -var denyNotLogged = require('./deny_not_logged'); +var DenyNotLogged = require('./DenyNotLogged'); var u2f = require('./u2f'); var TOTPAuthenticator = require("./TOTPAuthenticator"); module.exports = { - totp: denyNotLogged(TOTPAuthenticator), + totp: DenyNotLogged(TOTPAuthenticator), u2f: { register_request: u2f.register_request, register: u2f.register, register_handler_get: u2f.register_handler_get, register_handler_post: u2f.register_handler_post, - sign_request: denyNotLogged(u2f.sign_request), - sign: denyNotLogged(u2f.sign), + sign_request: DenyNotLogged(u2f.sign_request), + sign: DenyNotLogged(u2f.sign), } } diff --git a/test/unitary/mocks/express.ts b/test/unitary/mocks/express.ts index 009cb4a3..b06a1d5f 100644 --- a/test/unitary/mocks/express.ts +++ b/test/unitary/mocks/express.ts @@ -15,7 +15,7 @@ export interface ResponseMock { sendStatus: sinon.SinonStub; sendFile: sinon.SinonStub; sendfile: sinon.SinonStub; - status: sinon.SinonStub; + status: sinon.SinonStub | sinon.SinonSpy; json: sinon.SinonStub; links: sinon.SinonStub; jsonp: sinon.SinonStub; diff --git a/test/unitary/routes/DenyNotLogged.test.ts b/test/unitary/routes/DenyNotLogged.test.ts new file mode 100644 index 00000000..24678737 --- /dev/null +++ b/test/unitary/routes/DenyNotLogged.test.ts @@ -0,0 +1,82 @@ + +import sinon = require("sinon"); +import Promise = require("bluebird"); +import assert = require("assert"); +import express = require("express"); + +import ExpressMock = require("../mocks/express"); +import DenyNotLogged = require("../../../src/lib/routes/DenyNotLogged"); + +describe("test not logged", function () { + it("should return status code 403 when auth_session has not been previously created", function () { + return test_auth_session_not_created(); + }); + + it("should return status code 403 when auth_session has failed first factor", function () { + return test_auth_first_factor_not_validated(); + }); + + it("should return status code 204 when auth_session has succeeded first factor stage", function () { + return test_auth_with_first_factor_validated(); + }); +}); + +function test_auth_session_not_created() { + return new Promise(function (resolve, reject) { + const send = sinon.spy(resolve); + const status = sinon.spy(function (code: number) { + assert.equal(403, code); + }); + const req = ExpressMock.RequestMock(); + const res = ExpressMock.ResponseMock(); + req.session = {}; + res.send = send; + res.status = status; + + DenyNotLogged(reject)(req as any, res as any); + }); +} + +function test_auth_first_factor_not_validated() { + return new Promise(function (resolve, reject) { + const send = sinon.spy(resolve); + const status = sinon.spy(function (code: number) { + assert.equal(403, code); + }); + const req = { + session: { + auth_session: { + first_factor: false, + second_factor: false + } + } + }; + + const res = { + send: send, + status: status + }; + + DenyNotLogged(reject)(req as any, res as any); + }); +} + +function test_auth_with_first_factor_validated() { + return new Promise(function (resolve, reject) { + const req = { + session: { + auth_session: { + first_factor: true, + second_factor: false + } + } + }; + + const res = { + send: sinon.spy(), + status: sinon.spy() + }; + + DenyNotLogged(resolve)(req as any, res as any); + }); +} diff --git a/test/unitary/routes/test_deny_not_logged.js b/test/unitary/routes/test_deny_not_logged.js deleted file mode 100644 index 48a7007c..00000000 --- a/test/unitary/routes/test_deny_not_logged.js +++ /dev/null @@ -1,83 +0,0 @@ - -var sinon = require('sinon'); -var Promise = require('bluebird'); -var assert = require('assert'); - -var denyNotLogged = require('../../../src/lib/routes/deny_not_logged'); - -describe('test not logged', function() { - it('should return status code 403 when auth_session has not been previously created', function() { - return test_auth_session_not_created(); - }); - - it('should return status code 403 when auth_session has failed first factor', function() { - return test_auth_first_factor_not_validated(); - }); - - it('should return status code 204 when auth_session has succeeded first factor stage', function() { - return test_auth_with_first_factor_validated(); - }); -}); - -function test_auth_session_not_created() { - return new Promise(function(resolve, reject) { - var send = sinon.spy(resolve); - var status = sinon.spy(function(code) { - assert.equal(403, code); - }); - var req = { - session: {} - } - - var res = { - send: send, - status: status - } - - denyNotLogged(reject)(req, res); - }); -} - -function test_auth_first_factor_not_validated() { - return new Promise(function(resolve, reject) { - var send = sinon.spy(resolve); - var status = sinon.spy(function(code) { - assert.equal(403, code); - }); - var req = { - session: { - auth_session: { - first_factor: false, - second_factor: false - } - } - } - - var res = { - send: send, - status: status - } - - denyNotLogged(reject)(req, res); - }); -} - -function test_auth_with_first_factor_validated() { - return new Promise(function(resolve, reject) { - var req = { - session: { - auth_session: { - first_factor: true, - second_factor: false - } - } - } - - var res = { - send: sinon.spy(), - status: sinon.spy() - } - - denyNotLogged(resolve)(req, res); - }); -} From 9e89a690fb437480759c3e7944ea0754353b20b0 Mon Sep 17 00:00:00 2001 From: Clement Michaud Date: Sun, 21 May 2017 22:45:54 +0200 Subject: [PATCH 15/16] Finish migration to typescript --- package.json | 1 + src/lib/IdentityValidator.ts | 155 ++++++++++ src/lib/LdapClient.ts | 2 +- src/lib/RestApi.ts | 15 +- src/lib/Server.ts | 11 +- src/lib/UserDataStore.ts | 15 +- src/lib/access_control/AccessController.ts | 2 +- src/lib/access_control/PatternBuilder.ts | 2 +- src/lib/identity_check.js | 144 --------- src/lib/routes.ts | 16 +- src/lib/routes/DenyNotLogged.ts | 9 +- src/lib/routes/PasswordReset.ts | 81 +++++ src/lib/routes/SecondFactorRoutes.ts | 28 ++ src/lib/routes/TOTPRegistration.ts | 86 ++++++ src/lib/routes/U2FAuthenticationProcess.ts | 84 ++++++ src/lib/routes/U2FRegistration.ts | 51 ++++ src/lib/routes/U2FRegistrationProcess.ts | 89 ++++++ src/lib/routes/U2FRoutes.ts | 19 ++ src/lib/routes/reset_password.js | 70 ----- src/lib/routes/second_factor.js | 18 -- src/lib/routes/totp_register.js | 72 ----- src/lib/routes/u2f.js | 85 ------ src/lib/routes/u2f_common.js | 38 --- src/lib/routes/u2f_common.ts | 39 +++ src/lib/routes/u2f_register.js | 91 ------ src/lib/routes/u2f_register_handler.js | 37 --- src/{lib => types}/ILogger.ts | 0 test/unitary/IdentityValidator.test.ts | 213 +++++++++++++ test/unitary/mocks/IdentityValidator.ts | 35 +++ test/unitary/mocks/Notifier.ts | 12 + test/unitary/mocks/UserDataStore.ts | 4 +- test/unitary/mocks/authdog.ts | 19 ++ test/unitary/mocks/express.ts | 5 +- test/unitary/mocks/nodemailer.ts | 22 +- test/unitary/notifiers/GMailNotifier.test.ts | 3 +- .../unitary/notifiers/NotifierFactory.test.ts | 6 +- test/unitary/requests.js | 174 ----------- test/unitary/requests.ts | 179 +++++++++++ test/unitary/res_mock.js | 24 -- ...password.test.ts => PasswordReset.test.ts} | 16 +- test/unitary/routes/TOTPRegistration.test.ts | 137 +++++++++ test/unitary/routes/U2FRegistration.test.ts | 83 ++++++ test/unitary/routes/U2FRoutes.test.ts | 278 +++++++++++++++++ test/unitary/routes/test_totp_register.js | 136 --------- test/unitary/routes/test_u2f.js | 280 ------------------ test/unitary/routes/test_u2f_register.js | 78 ----- test/unitary/test_identity_check.js | 205 ------------- tsconfig.json | 1 - 48 files changed, 1671 insertions(+), 1499 deletions(-) create mode 100644 src/lib/IdentityValidator.ts delete mode 100644 src/lib/identity_check.js create mode 100644 src/lib/routes/PasswordReset.ts create mode 100644 src/lib/routes/SecondFactorRoutes.ts create mode 100644 src/lib/routes/TOTPRegistration.ts create mode 100644 src/lib/routes/U2FAuthenticationProcess.ts create mode 100644 src/lib/routes/U2FRegistration.ts create mode 100644 src/lib/routes/U2FRegistrationProcess.ts create mode 100644 src/lib/routes/U2FRoutes.ts delete mode 100644 src/lib/routes/reset_password.js delete mode 100644 src/lib/routes/second_factor.js delete mode 100644 src/lib/routes/totp_register.js delete mode 100644 src/lib/routes/u2f.js delete mode 100644 src/lib/routes/u2f_common.js create mode 100644 src/lib/routes/u2f_common.ts delete mode 100644 src/lib/routes/u2f_register.js delete mode 100644 src/lib/routes/u2f_register_handler.js rename src/{lib => types}/ILogger.ts (100%) create mode 100644 test/unitary/IdentityValidator.test.ts create mode 100644 test/unitary/mocks/IdentityValidator.ts create mode 100644 test/unitary/mocks/Notifier.ts create mode 100644 test/unitary/mocks/authdog.ts delete mode 100644 test/unitary/requests.js create mode 100644 test/unitary/requests.ts delete mode 100644 test/unitary/res_mock.js rename test/unitary/routes/{reset_password.test.ts => PasswordReset.test.ts} (90%) create mode 100644 test/unitary/routes/TOTPRegistration.test.ts create mode 100644 test/unitary/routes/U2FRegistration.test.ts create mode 100644 test/unitary/routes/U2FRoutes.test.ts delete mode 100644 test/unitary/routes/test_totp_register.js delete mode 100644 test/unitary/routes/test_u2f.js delete mode 100644 test/unitary/routes/test_u2f_register.js delete mode 100644 test/unitary/test_identity_check.js diff --git a/package.json b/package.json index 1d68985b..7af9aba6 100644 --- a/package.json +++ b/package.json @@ -59,6 +59,7 @@ "@types/nodemailer": "^1.3.32", "@types/object-path": "^0.9.28", "@types/proxyquire": "^1.3.27", + "@types/randomstring": "^1.1.5", "@types/request": "0.0.43", "@types/sinon": "^2.2.1", "@types/speakeasy": "^2.0.1", diff --git a/src/lib/IdentityValidator.ts b/src/lib/IdentityValidator.ts new file mode 100644 index 00000000..c89d5be1 --- /dev/null +++ b/src/lib/IdentityValidator.ts @@ -0,0 +1,155 @@ + +import objectPath = require("object-path"); +import randomstring = require("randomstring"); +import BluebirdPromise = require("bluebird"); +import util = require("util"); +import exceptions = require("./Exceptions"); +import fs = require("fs"); +import ejs = require("ejs"); +import UserDataStore from "./UserDataStore"; +import { ILogger } from "../types/ILogger"; +import express = require("express"); + +import Identity = require("../types/Identity"); +import { IdentityValidationRequestContent } from "./UserDataStore"; + +const filePath = __dirname + "/../resources/email-template.ejs"; +const email_template = fs.readFileSync(filePath, "utf8"); + + +// IdentityValidator allows user to go through a identity validation process in two steps: +// - Request an operation to be performed (password reset, registration). +// - Confirm operation with email. + +export interface IdentityValidable { + challenge(): string; + templateName(): string; + preValidation(req: express.Request): BluebirdPromise; + mailSubject(): string; +} + +export class IdentityValidator { + private userDataStore: UserDataStore; + private logger: ILogger; + + constructor(userDataStore: UserDataStore, logger: ILogger) { + this.userDataStore = userDataStore; + this.logger = logger; + } + + + static setup(app: express.Application, endpoint: string, handler: IdentityValidable, userDataStore: UserDataStore, logger: ILogger) { + const identityValidator = new IdentityValidator(userDataStore, logger); + app.get(endpoint, identityValidator.identity_check_get(endpoint, handler)); + app.post(endpoint, identityValidator.identity_check_post(endpoint, handler)); + } + + + private issue_token(userid: string, content: Object): BluebirdPromise { + const five_minutes = 4 * 60 * 1000; + const token = randomstring.generate({ length: 64 }); + const that = this; + + this.logger.debug("identity_check: issue identity token %s for 5 minutes", token); + return this.userDataStore.issue_identity_check_token(userid, token, content, five_minutes) + .then(function () { + return BluebirdPromise.resolve(token); + }); + } + + private consume_token(token: string): BluebirdPromise { + this.logger.debug("identity_check: consume token %s", token); + return this.userDataStore.consume_identity_check_token(token); + } + + private identity_check_get(endpoint: string, handler: IdentityValidable): express.RequestHandler { + const that = this; + return function (req: express.Request, res: express.Response) { + const logger = req.app.get("logger"); + const identity_token = objectPath.get(req, "query.identity_token"); + logger.info("GET identity_check: identity token provided is %s", identity_token); + + if (!identity_token) { + res.status(403); + res.send(); + return; + } + + that.consume_token(identity_token) + .then(function (content: IdentityValidationRequestContent) { + objectPath.set(req, "session.auth_session.identity_check", {}); + req.session.auth_session.identity_check.challenge = handler.challenge(); + req.session.auth_session.identity_check.userid = content.userid; + res.render(handler.templateName()); + }, function (err: Error) { + logger.error("GET identity_check: Error while consuming token %s", err); + throw new exceptions.AccessDeniedError("Access denied"); + }) + .catch(exceptions.AccessDeniedError, function (err: Error) { + logger.error("GET identity_check: Access Denied %s", err); + res.status(403); + res.send(); + }) + .catch(function (err: Error) { + logger.error("GET identity_check: Internal error %s", err); + res.status(500); + res.send(); + }); + }; + } + + + private identity_check_post(endpoint: string, handler: IdentityValidable): express.RequestHandler { + const that = this; + return function (req: express.Request, res: express.Response) { + const logger = req.app.get("logger"); + const notifier = req.app.get("notifier"); + let identity: Identity.Identity; + + handler.preValidation(req) + .then(function (id: Identity.Identity) { + identity = id; + const email_address = objectPath.get(identity, "email"); + const userid = objectPath.get(identity, "userid"); + + if (!(email_address && userid)) { + throw new exceptions.IdentityError("Missing user id or email address"); + } + + return that.issue_token(userid, undefined); + }, function (err: Error) { + throw new exceptions.AccessDeniedError(err.message); + }) + .then(function (token: string) { + const redirect_url = objectPath.get(req, "body.redirect"); + const original_url = util.format("https://%s%s", req.headers.host, req.headers["x-original-uri"]); + let link_url = util.format("%s?identity_token=%s", original_url, token); + if (redirect_url) { + link_url = util.format("%s&redirect=%s", link_url, redirect_url); + } + + logger.info("POST identity_check: notify to %s", identity.userid); + return notifier.notify(identity, handler.mailSubject(), link_url); + }) + .then(function () { + res.status(204); + res.send(); + }) + .catch(exceptions.IdentityError, function (err: Error) { + logger.error("POST identity_check: %s", err); + res.status(400); + res.send(); + }) + .catch(exceptions.AccessDeniedError, function (err: Error) { + logger.error("POST identity_check: %s", err); + res.status(403); + res.send(); + }) + .catch(function (err: Error) { + logger.error("POST identity_check: Error %s", err); + res.status(500); + res.send(); + }); + }; + } +} diff --git a/src/lib/LdapClient.ts b/src/lib/LdapClient.ts index 879cf953..d50414e2 100644 --- a/src/lib/LdapClient.ts +++ b/src/lib/LdapClient.ts @@ -8,7 +8,7 @@ import ldapjs = require("ldapjs"); import { EventEmitter } from "events"; import { LdapConfiguration } from "./Configuration"; import { Ldapjs } from "../types/Dependencies"; -import { ILogger } from "./ILogger"; +import { ILogger } from "../types/ILogger"; interface SearchEntry { object: any; diff --git a/src/lib/RestApi.ts b/src/lib/RestApi.ts index 42bccbc2..558321b2 100644 --- a/src/lib/RestApi.ts +++ b/src/lib/RestApi.ts @@ -1,11 +1,12 @@ import express = require("express"); - -const routes = require("./routes"); -const identity_check = require("./identity_check"); +import routes = require("./routes"); +import IdentityValidator = require("./IdentityValidator"); +import UserDataStore from "./UserDataStore"; +import { ILogger } from "../types/ILogger"; export default class RestApi { - static setup(app: express.Application): void { + static setup(app: express.Application, userDataStore: UserDataStore, logger: ILogger): void { /** * @apiDefine UserSession * @apiHeader {String} Cookie Cookie containing "connect.sid", the user @@ -86,7 +87,7 @@ export default class RestApi { * @apiDescription Serves the TOTP registration page that displays the secret. * The secret is a QRCode and a base32 secret. */ - identity_check(app, "/totp-register", routes.totp_register.icheck_interface); + IdentityValidator.IdentityValidator.setup(app, "/totp-register", routes.totp_register.icheck_interface, userDataStore, logger); /** @@ -108,7 +109,7 @@ export default class RestApi { * @apiDescription Serves the U2F registration page that asks the user to * touch the token of the U2F device. */ - identity_check(app, "/u2f-register", routes.u2f_register.icheck_interface); + IdentityValidator.IdentityValidator.setup(app, "/u2f-register", routes.u2f_register.icheck_interface, userDataStore, logger); /** * @api {post} /reset-password Request for password reset @@ -129,7 +130,7 @@ export default class RestApi { * @apiDescription Serves password reset form that allow the user to provide * the new password. */ - identity_check(app, "/reset-password", routes.reset_password.icheck_interface); + IdentityValidator.IdentityValidator.setup(app, "/reset-password", routes.reset_password.icheck_interface, userDataStore, logger); app.get("/reset-password-form", function (req, res) { res.render("reset-password-form"); }); diff --git a/src/lib/Server.ts b/src/lib/Server.ts index e61edf74..da54cd38 100644 --- a/src/lib/Server.ts +++ b/src/lib/Server.ts @@ -10,6 +10,7 @@ import TOTPGenerator from "./TOTPGenerator"; import RestApi from "./RestApi"; import { LdapClient } from "./LdapClient"; import BluebirdPromise = require("bluebird"); +import { IdentityValidator } from "./IdentityValidator"; import * as Express from "express"; import * as BodyParser from "body-parser"; @@ -55,26 +56,28 @@ export default class Server { deps.winston.level = config.logs_level || "info"; const five_minutes = 5 * 60; - const data_store = new UserDataStore(datastore_options, deps.nedb); - const regulator = new AuthenticationRegulator(data_store, five_minutes); + const userDataStore = new UserDataStore(datastore_options, deps.nedb); + const regulator = new AuthenticationRegulator(userDataStore, five_minutes); const notifier = NotifierFactory.build(config.notifier, deps.nodemailer); const ldap = new LdapClient(config.ldap, deps.ldapjs, deps.winston); const accessController = new AccessController(config.access_control, deps.winston); const totpValidator = new TOTPValidator(deps.speakeasy); const totpGenerator = new TOTPGenerator(deps.speakeasy); + const identityValidator = new IdentityValidator(userDataStore, deps.winston); app.set("logger", deps.winston); app.set("ldap", ldap); app.set("totp validator", totpValidator); app.set("totp generator", totpGenerator); app.set("u2f", deps.u2f); - app.set("user data store", data_store); + app.set("user data store", userDataStore); app.set("notifier", notifier); app.set("authentication regulator", regulator); app.set("config", config); app.set("access controller", accessController); + app.set("identity validator", identityValidator); - RestApi.setup(app); + RestApi.setup(app, userDataStore, deps.winston); return new BluebirdPromise((resolve, reject) => { this.httpServer = app.listen(config.port, function (err: string) { diff --git a/src/lib/UserDataStore.ts b/src/lib/UserDataStore.ts index 468f7080..57f10508 100644 --- a/src/lib/UserDataStore.ts +++ b/src/lib/UserDataStore.ts @@ -28,6 +28,17 @@ export interface Options { directory?: string; } +export interface IdentityValidationRequestContent { + userid: string; + data: string; +} + +export interface IdentityValidationRequestDocument { + userid: string; + token: string; + content: IdentityValidationRequestContent; + max_date: Date; +} // Source @@ -54,7 +65,7 @@ export default class UserDataStore { userid: userid, appid: appid, meta: meta - }; + } as U2FMetaDocument; const filter = { userid: userid, @@ -110,7 +121,7 @@ export default class UserDataStore { return this._identity_check_tokens_collection.insertAsync(newDocument); } - consume_identity_check_token(token: string): BluebirdPromise { + consume_identity_check_token(token: string): BluebirdPromise { const query = { token: token }; diff --git a/src/lib/access_control/AccessController.ts b/src/lib/access_control/AccessController.ts index 9ec0b469..a9a18114 100644 --- a/src/lib/access_control/AccessController.ts +++ b/src/lib/access_control/AccessController.ts @@ -1,7 +1,7 @@ import { ACLConfiguration } from "../Configuration"; import PatternBuilder from "./PatternBuilder"; -import { ILogger } from "../ILogger"; +import { ILogger } from "../../types/ILogger"; export default class AccessController { private logger: ILogger; diff --git a/src/lib/access_control/PatternBuilder.ts b/src/lib/access_control/PatternBuilder.ts index dcdb4514..7b1966e6 100644 --- a/src/lib/access_control/PatternBuilder.ts +++ b/src/lib/access_control/PatternBuilder.ts @@ -1,5 +1,5 @@ -import { ILogger } from "../ILogger"; +import { ILogger } from "../../types/ILogger"; import { ACLConfiguration, ACLGroupsRules, ACLUsersRules, ACLDefaultRules } from "../Configuration"; import objectPath = require("object-path"); diff --git a/src/lib/identity_check.js b/src/lib/identity_check.js deleted file mode 100644 index 0a1f68fe..00000000 --- a/src/lib/identity_check.js +++ /dev/null @@ -1,144 +0,0 @@ - -var objectPath = require('object-path'); -var randomstring = require('randomstring'); -var BluebirdPromise = require('bluebird'); -var util = require('util'); -var exceptions = require('./Exceptions'); -var fs = require('fs'); -var ejs = require('ejs'); - -module.exports = identity_check; - -var filePath = __dirname + '/../resources/email-template.ejs'; -var email_template = fs.readFileSync(filePath, 'utf8'); - -// IdentityCheck class - -function IdentityCheck(user_data_store, logger) { - this._user_data_store = user_data_store; - this._logger = logger; -} - -IdentityCheck.prototype.issue_token = function(userid, content, logger) { - var five_minutes = 4 * 60 * 1000; - var token = randomstring.generate({ length: 64 }); - var that = this; - - this._logger.debug('identity_check: issue identity token %s for 5 minutes', token); - return this._user_data_store.issue_identity_check_token(userid, token, content, five_minutes) - .then(function() { - return BluebirdPromise.resolve(token); - }); -} - -IdentityCheck.prototype.consume_token = function(token, logger) { - this._logger.debug('identity_check: consume token %s', token); - return this._user_data_store.consume_identity_check_token(token) -} - - -// The identity_check middleware that allows the user two perform a two step validation -// using the user email - -function identity_check(app, endpoint, icheck_interface) { - app.get(endpoint, identity_check_get(endpoint, icheck_interface)); - app.post(endpoint, identity_check_post(endpoint, icheck_interface)); -} - - -function identity_check_get(endpoint, icheck_interface) { - return function(req, res) { - var logger = req.app.get('logger'); - var identity_token = objectPath.get(req, 'query.identity_token'); - logger.info('GET identity_check: identity token provided is %s', identity_token); - - if(!identity_token) { - res.status(403); - res.send(); - return; - } - - 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, logger); - - identity_check.consume_token(identity_token, logger) - .then(function(content) { - objectPath.set(req, 'session.auth_session.identity_check', {}); - req.session.auth_session.identity_check.challenge = icheck_interface.challenge; - req.session.auth_session.identity_check.userid = content.userid; - res.render(icheck_interface.render_template); - }, function(err) { - logger.error('GET identity_check: Error while consuming token %s', err); - throw new exceptions.AccessDeniedError('Access denied'); - }) - .catch(exceptions.AccessDeniedError, function(err) { - logger.error('GET identity_check: Access Denied %s', err); - res.status(403); - res.send(); -  }) - .catch(function(err) { - logger.error('GET identity_check: Internal error %s', err); - res.status(500); - res.send(); - }); - } -} - - -function identity_check_post(endpoint, icheck_interface) { - return function(req, res) { - var logger = req.app.get('logger'); - var notifier = req.app.get('notifier'); - var user_data_store = req.app.get('user data store'); - var identity_check = new IdentityCheck(user_data_store, logger); - var identity; - - icheck_interface.pre_check_callback(req) - .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'); - } - - return identity_check.issue_token(userid, undefined, logger); - }, function(err) { - throw new exceptions.AccessDeniedError(err); - }) - .then(function(token) { - var redirect_url = objectPath.get(req, 'body.redirect'); - 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); - if(redirect_url) { - link_url = util.format('%s&redirect=%s', link_url, redirect_url); - } - - 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); - res.send(); - }) - .catch(exceptions.IdentityError, function(err) { - logger.error('POST identity_check: %s', err); - res.status(400); - res.send(); - }) - .catch(exceptions.AccessDeniedError, function(err) { - logger.error('POST identity_check: %s', err); - res.status(403); - res.send(); - }) - .catch(function(err) { - logger.error('POST identity_check: Error %s', err); - res.status(500); - res.send(); - }); - } -} - - diff --git a/src/lib/routes.ts b/src/lib/routes.ts index 1446fa59..4c2d680d 100644 --- a/src/lib/routes.ts +++ b/src/lib/routes.ts @@ -1,10 +1,10 @@ import FirstFactor = require("./routes/FirstFactor"); -import second_factor = require("./routes/second_factor"); -import reset_password = require("./routes/reset_password"); +import SecondFactorRoutes = require("./routes/SecondFactorRoutes"); +import PasswordReset = require("./routes/PasswordReset"); import AuthenticationValidator = require("./routes/AuthenticationValidator"); -import u2f_register_handler = require("./routes/u2f_register_handler"); -import totp_register = require("./routes/totp_register"); +import U2FRegistration = require("./routes/U2FRegistration"); +import TOTPRegistration = require("./routes/TOTPRegistration"); import objectPath = require("object-path"); import express = require("express"); @@ -14,10 +14,10 @@ export = { logout: serveLogout, verify: AuthenticationValidator, first_factor: FirstFactor, - second_factor: second_factor, - reset_password: reset_password, - u2f_register: u2f_register_handler, - totp_register: totp_register, + second_factor: SecondFactorRoutes, + reset_password: PasswordReset, + u2f_register: U2FRegistration, + totp_register: TOTPRegistration, }; function serveLogin(req: express.Request, res: express.Response) { diff --git a/src/lib/routes/DenyNotLogged.ts b/src/lib/routes/DenyNotLogged.ts index fc30d836..2c2b71d9 100644 --- a/src/lib/routes/DenyNotLogged.ts +++ b/src/lib/routes/DenyNotLogged.ts @@ -2,8 +2,10 @@ import objectPath = require("object-path"); import express = require("express"); -export = function denyNotLogged(callback: (req: express.Request, res: express.Response) => void) { - return function (req: express.Request, res: express.Response) { +type ExpressRequest = (req: express.Request, res: express.Response, next?: express.NextFunction) => void; + +export = function(callback: ExpressRequest): ExpressRequest { + return function (req: express.Request, res: express.Response, next: express.NextFunction) { const auth_session = req.session.auth_session; const first_factor = objectPath.has(req, "session.auth_session.first_factor") && req.session.auth_session.first_factor; @@ -12,7 +14,6 @@ export = function denyNotLogged(callback: (req: express.Request, res: express.Re res.send(); return; } - - callback(req, res); + callback(req, res, next); }; }; diff --git a/src/lib/routes/PasswordReset.ts b/src/lib/routes/PasswordReset.ts new file mode 100644 index 00000000..25b8e107 --- /dev/null +++ b/src/lib/routes/PasswordReset.ts @@ -0,0 +1,81 @@ + +import BluebirdPromise = require("bluebird"); +import objectPath = require("object-path"); +import exceptions = require("../Exceptions"); +import express = require("express"); +import { Identity } from "../../types/Identity"; +import { IdentityValidable } from "../IdentityValidator"; + +const CHALLENGE = "reset-password"; + +class PasswordResetHandler implements IdentityValidable { + challenge(): string { + return CHALLENGE; + } + + templateName(): string { + return "reset-password"; + } + + preValidation(req: express.Request): BluebirdPromise { + const userid = objectPath.get(req, "body.userid"); + if (!userid) { + return BluebirdPromise.reject(new exceptions.AccessDeniedError("No user id provided")); + } + + const ldap = req.app.get("ldap"); + return ldap.get_emails(userid) + .then(function (emails: string[]) { + if (!emails && emails.length <= 0) throw new Error("No email found"); + + const identity = { + email: emails[0], + userid: userid + }; + return BluebirdPromise.resolve(identity); + }); + } + + mailSubject(): string { + return "Reset your password"; + } +} + +function protect(fn: express.RequestHandler) { + return function (req: express.Request, res: express.Response) { + const challenge = objectPath.get(req, "session.auth_session.identity_check.challenge"); + if (challenge != CHALLENGE) { + res.status(403); + res.send(); + return; + } + fn(req, res, undefined); + }; +} + +function post(req: express.Request, res: express.Response) { + const logger = req.app.get("logger"); + const ldap = req.app.get("ldap"); + const new_password = objectPath.get(req, "body.password"); + const userid = objectPath.get(req, "session.auth_session.identity_check.userid"); + + logger.info("POST reset-password: User %s wants to reset his/her password", userid); + + ldap.update_password(userid, new_password) + .then(function () { + logger.info("POST reset-password: Password reset for user %s", userid); + objectPath.set(req, "session.auth_session", undefined); + res.status(204); + res.send(); + }) + .catch(function (err: Error) { + logger.error("POST reset-password: Error while resetting the password of user %s. %s", userid, err); + res.status(500); + res.send(); + }); +} + +export = { + icheck_interface: new PasswordResetHandler(), + post: protect(post) +}; diff --git a/src/lib/routes/SecondFactorRoutes.ts b/src/lib/routes/SecondFactorRoutes.ts new file mode 100644 index 00000000..f8698c2f --- /dev/null +++ b/src/lib/routes/SecondFactorRoutes.ts @@ -0,0 +1,28 @@ + +import DenyNotLogged = require("./DenyNotLogged"); +import U2FRoutes = require("./U2FRoutes"); +import TOTPAuthenticator = require("./TOTPAuthenticator"); + +import express = require("express"); + +interface SecondFactorRoutes { + totp: express.RequestHandler; + u2f: { + register_request: express.RequestHandler; + register: express.RequestHandler; + sign_request: express.RequestHandler; + sign: express.RequestHandler; + }; +} + +export = { + totp: DenyNotLogged(TOTPAuthenticator), + u2f: { + register_request: U2FRoutes.register_request, + register: U2FRoutes.register, + + sign_request: DenyNotLogged(U2FRoutes.sign_request), + sign: DenyNotLogged(U2FRoutes.sign), + } +} as SecondFactorRoutes; + diff --git a/src/lib/routes/TOTPRegistration.ts b/src/lib/routes/TOTPRegistration.ts new file mode 100644 index 00000000..1be58181 --- /dev/null +++ b/src/lib/routes/TOTPRegistration.ts @@ -0,0 +1,86 @@ +import objectPath = require("object-path"); +import BluebirdPromise = require("bluebird"); +import express = require("express"); +import exceptions = require("../Exceptions"); +import { Identity } from "../../types/Identity"; +import { IdentityValidable } from "../IdentityValidator"; + +const CHALLENGE = "totp-register"; +const TEMPLATE_NAME = "totp-register"; + + +class TOTPRegistrationHandler implements IdentityValidable { + challenge(): string { + return CHALLENGE; + } + + templateName(): string { + return TEMPLATE_NAME; + } + + preValidation(req: express.Request): BluebirdPromise { + const first_factor_passed = objectPath.get(req, "session.auth_session.first_factor"); + if (!first_factor_passed) { + return BluebirdPromise.reject("Authentication required before registering TOTP secret key"); + } + + const userid = objectPath.get(req, "session.auth_session.userid"); + const email = objectPath.get(req, "session.auth_session.email"); + + if (!(userid && email)) { + return BluebirdPromise.reject("User ID or email is missing"); + } + + const identity = { + email: email, + userid: userid + }; + return BluebirdPromise.resolve(identity); + } + + mailSubject(): string { + return "Register your TOTP secret key"; + } +} + +// Generate a secret and send it to the user +function post(req: express.Request, res: express.Response) { + const logger = req.app.get("logger"); + const userid = objectPath.get(req, "session.auth_session.identity_check.userid"); + const challenge = objectPath.get(req, "session.auth_session.identity_check.challenge"); + + if (challenge != CHALLENGE || !userid) { + res.status(403); + res.send(); + return; + } + + const user_data_store = req.app.get("user data store"); + const totpGenerator = req.app.get("totp generator"); + const secret = totpGenerator.generate(); + + logger.debug("POST new-totp-secret: save the TOTP secret in DB"); + user_data_store.set_totp_secret(userid, secret) + .then(function () { + const doc = { + otpauth_url: secret.otpauth_url, + base32: secret.base32, + ascii: secret.ascii + }; + objectPath.set(req, "session", undefined); + + res.status(200); + res.json(doc); + }) + .catch(function (err: Error) { + logger.error("POST new-totp-secret: Internal error %s", err); + res.status(500); + res.send(); + }); +} + + +export = { + icheck_interface: new TOTPRegistrationHandler(), + post: post, +}; diff --git a/src/lib/routes/U2FAuthenticationProcess.ts b/src/lib/routes/U2FAuthenticationProcess.ts new file mode 100644 index 00000000..84c8690b --- /dev/null +++ b/src/lib/routes/U2FAuthenticationProcess.ts @@ -0,0 +1,84 @@ + +import u2f_register_handler = require("./U2FRegistration"); +import objectPath = require("object-path"); +import u2f_common = require("./u2f_common"); +import BluebirdPromise = require("bluebird"); +import express = require("express"); +import authdog = require("../../types/authdog"); +import UserDataStore, { U2FMetaDocument } from "../UserDataStore"; + + +function retrieve_u2f_meta(req: express.Request, userDataStore: UserDataStore) { + const userid = req.session.auth_session.userid; + const appid = u2f_common.extract_app_id(req); + return userDataStore.get_u2f_meta(userid, appid); +} + + +function sign_request(req: express.Request, res: express.Response) { + const logger = req.app.get("logger"); + const userDataStore = req.app.get("user data store"); + + retrieve_u2f_meta(req, userDataStore) + .then(function (doc: U2FMetaDocument) { + if (!doc) { + u2f_common.reply_with_missing_registration(res); + return; + } + + const u2f = req.app.get("u2f"); + const meta = doc.meta; + const appid = u2f_common.extract_app_id(req); + logger.info("U2F sign_request: Start authentication to app %s", appid); + return u2f.startAuthentication(appid, [meta]); + }) + .then(function (authRequest: authdog.AuthenticationRequest) { + logger.info("U2F sign_request: Store authentication request and reply"); + req.session.auth_session.sign_request = authRequest; + res.status(200); + res.json(authRequest); + }) + .catch(function (err: Error) { + logger.info("U2F sign_request: %s", err); + res.status(500); + res.send(); + }); +} + + +function sign(req: express.Request, res: express.Response) { + if (!objectPath.has(req, "session.auth_session.sign_request")) { + u2f_common.reply_with_unauthorized(res); + return; + } + + const logger = req.app.get("logger"); + const userDataStore = req.app.get("user data store"); + + retrieve_u2f_meta(req, userDataStore) + .then(function (doc: U2FMetaDocument) { + const appid = u2f_common.extract_app_id(req); + const u2f = req.app.get("u2f"); + const authRequest = req.session.auth_session.sign_request; + const meta = doc.meta; + logger.info("U2F sign: Finish authentication"); + return u2f.finishAuthentication(authRequest, req.body, [meta]); + }) + .then(function (authenticationStatus: authdog.Authentication) { + logger.info("U2F sign: Authentication successful"); + req.session.auth_session.second_factor = true; + res.status(204); + res.send(); + }) + .catch(function (err: Error) { + logger.error("U2F sign: %s", err); + res.status(500); + res.send(); + }); +} + + +export = { + sign_request: sign_request, + sign: sign +}; diff --git a/src/lib/routes/U2FRegistration.ts b/src/lib/routes/U2FRegistration.ts new file mode 100644 index 00000000..d8126c46 --- /dev/null +++ b/src/lib/routes/U2FRegistration.ts @@ -0,0 +1,51 @@ + +import objectPath = require("object-path"); +import BluebirdPromise = require("bluebird"); +import express = require("express"); + +import { IdentityValidable } from "../IdentityValidator"; +import { Identity } from "../../types/Identity"; + +const CHALLENGE = "u2f-register"; +const TEMPLATE_NAME = "u2f-register"; +const MAIL_SUBJECT = "Register your U2F device"; + + +class U2FRegistrationHandler implements IdentityValidable { + challenge(): string { + return CHALLENGE; + } + + templateName(): string { + return TEMPLATE_NAME; + } + + preValidation(req: express.Request): BluebirdPromise { + const first_factor_passed = objectPath.get(req, "session.auth_session.first_factor"); + if (!first_factor_passed) { + return BluebirdPromise.reject("Authentication required before issuing a u2f registration request"); + } + + const userid = objectPath.get(req, "session.auth_session.userid"); + const email = objectPath.get(req, "session.auth_session.email"); + + if (!(userid && email)) { + return BluebirdPromise.reject("User ID or email is missing"); + } + + const identity = { + email: email, + userid: userid + }; + return BluebirdPromise.resolve(identity); + } + + mailSubject(): string { + return MAIL_SUBJECT; + } +} + +export = { + icheck_interface: new U2FRegistrationHandler(), +}; + diff --git a/src/lib/routes/U2FRegistrationProcess.ts b/src/lib/routes/U2FRegistrationProcess.ts new file mode 100644 index 00000000..1737e256 --- /dev/null +++ b/src/lib/routes/U2FRegistrationProcess.ts @@ -0,0 +1,89 @@ + +import u2f_register_handler = require("./U2FRegistration"); +import objectPath = require("object-path"); +import u2f_common = require("./u2f_common"); +import BluebirdPromise = require("bluebird"); +import express = require("express"); +import authdog = require("../../types/authdog"); + +function register_request(req: express.Request, res: express.Response) { + const logger = req.app.get("logger"); + const challenge = objectPath.get(req, "session.auth_session.identity_check.challenge"); + if (challenge != "u2f-register") { + res.status(403); + res.send(); + return; + } + + const u2f = req.app.get("u2f"); + const appid = u2f_common.extract_app_id(req); + + logger.debug("U2F register_request: headers=%s", JSON.stringify(req.headers)); + logger.info("U2F register_request: Starting registration of app %s", appid); + u2f.startRegistration(appid, []) + .then(function (registrationRequest: authdog.AuthenticationRequest) { + logger.info("U2F register_request: Sending back registration request"); + req.session.auth_session.register_request = registrationRequest; + res.status(200); + res.json(registrationRequest); + }) + .catch(function (err: Error) { + logger.error("U2F register_request: %s", err); + res.status(500); + res.send("Unable to start registration request"); + }); +} + +function register(req: express.Request, res: express.Response) { + const registrationRequest = objectPath.get(req, "session.auth_session.register_request"); + const challenge = objectPath.get(req, "session.auth_session.identity_check.challenge"); + + if (!registrationRequest) { + res.status(403); + res.send(); + return; + } + + if (!(registrationRequest && challenge == "u2f-register")) { + res.status(403); + res.send(); + return; + } + + + const user_data_storage = req.app.get("user data store"); + const u2f = req.app.get("u2f"); + const userid = req.session.auth_session.userid; + const appid = u2f_common.extract_app_id(req); + const logger = req.app.get("logger"); + + logger.info("U2F register: Finishing registration"); + logger.debug("U2F register: register_request=%s", JSON.stringify(registrationRequest)); + logger.debug("U2F register: body=%s", JSON.stringify(req.body)); + + u2f.finishRegistration(registrationRequest, req.body) + .then(function (registrationStatus: authdog.Registration) { + logger.info("U2F register: Store registration and reply"); + const meta = { + keyHandle: registrationStatus.keyHandle, + publicKey: registrationStatus.publicKey, + certificate: registrationStatus.certificate + }; + return user_data_storage.set_u2f_meta(userid, appid, meta); + }) + .then(function () { + objectPath.set(req, "session.auth_session.identity_check", undefined); + res.status(204); + res.send(); + }) + .catch(function (err: Error) { + logger.error("U2F register: %s", err); + res.status(500); + res.send("Unable to register"); + }); +} + +export = { + register_request: register_request, + register: register +}; diff --git a/src/lib/routes/U2FRoutes.ts b/src/lib/routes/U2FRoutes.ts new file mode 100644 index 00000000..50c150ee --- /dev/null +++ b/src/lib/routes/U2FRoutes.ts @@ -0,0 +1,19 @@ + +import U2FRegistrationProcess = require("./U2FRegistrationProcess"); +import U2FAuthenticationProcess = require("./U2FAuthenticationProcess"); + +import express = require("express"); + +interface U2FRoutes { + register_request: express.RequestHandler; + register: express.RequestHandler; + sign_request: express.RequestHandler; + sign: express.RequestHandler; +} + +export = { + register_request: U2FRegistrationProcess.register_request, + register: U2FRegistrationProcess.register, + sign_request: U2FAuthenticationProcess.sign_request, + sign: U2FAuthenticationProcess.sign, +} as U2FRoutes; diff --git a/src/lib/routes/reset_password.js b/src/lib/routes/reset_password.js deleted file mode 100644 index ab39ff5a..00000000 --- a/src/lib/routes/reset_password.js +++ /dev/null @@ -1,70 +0,0 @@ - -var BluebirdPromise = require('bluebird'); -var objectPath = require('object-path'); -var exceptions = require('../Exceptions'); -var CHALLENGE = 'reset-password'; - -var icheck_interface = { - challenge: CHALLENGE, - render_template: 'reset-password', - pre_check_callback: pre_check, - email_subject: 'Reset your password', -} - -module.exports = { - icheck_interface: icheck_interface, - post: protect(post) -} - -function pre_check(req) { - var userid = objectPath.get(req, 'body.userid'); - if(!userid) { - return BluebirdPromise.reject(new exceptions.AccessDeniedError("No user id provided")); - } - - var ldap = req.app.get('ldap'); - return ldap.get_emails(userid) - .then(function(emails) { - if(!emails && emails.length <= 0) throw new Error('No email found'); - - var identity = {} - identity.email = emails[0]; - identity.userid = userid; - return BluebirdPromise.resolve(identity); - }); -} - -function protect(fn) { - return function(req, res) { - var challenge = objectPath.get(req, 'session.auth_session.identity_check.challenge'); - if(challenge != CHALLENGE) { - res.status(403); - res.send(); - return; - } - fn(req, res); -  } -} - -function post(req, res) { - var logger = req.app.get('logger'); - var ldap = req.app.get('ldap'); - var new_password = objectPath.get(req, 'body.password'); - var userid = objectPath.get(req, 'session.auth_session.identity_check.userid'); - - logger.info('POST reset-password: User %s wants to reset his/her password', userid); - - ldap.update_password(userid, new_password) - .then(function() { - logger.info('POST reset-password: Password reset for user %s', userid); - objectPath.set(req, 'session.auth_session', undefined); - res.status(204); - res.send(); - }) - .catch(function(err) { - logger.error('POST reset-password: Error while resetting the password of user %s. %s', userid, err); - res.status(500); - res.send(); - }); -} - diff --git a/src/lib/routes/second_factor.js b/src/lib/routes/second_factor.js deleted file mode 100644 index c84c3de2..00000000 --- a/src/lib/routes/second_factor.js +++ /dev/null @@ -1,18 +0,0 @@ - -var DenyNotLogged = require('./DenyNotLogged'); -var u2f = require('./u2f'); -var TOTPAuthenticator = require("./TOTPAuthenticator"); - -module.exports = { - totp: DenyNotLogged(TOTPAuthenticator), - u2f: { - register_request: u2f.register_request, - register: u2f.register, - register_handler_get: u2f.register_handler_get, - register_handler_post: u2f.register_handler_post, - - sign_request: DenyNotLogged(u2f.sign_request), - sign: DenyNotLogged(u2f.sign), - } -} - diff --git a/src/lib/routes/totp_register.js b/src/lib/routes/totp_register.js deleted file mode 100644 index 0f163f36..00000000 --- a/src/lib/routes/totp_register.js +++ /dev/null @@ -1,72 +0,0 @@ -var objectPath = require('object-path'); -var BluebirdPromise = require('bluebird'); - -var CHALLENGE = 'totp-register'; - -var icheck_interface = { - challenge: CHALLENGE, - render_template: 'totp-register', - pre_check_callback: pre_check, - email_subject: 'Register your TOTP secret key', -} - -module.exports = { - icheck_interface: icheck_interface, - post: post, -} - -function pre_check(req) { - var first_factor_passed = objectPath.get(req, 'session.auth_session.first_factor'); - if(!first_factor_passed) { - return BluebirdPromise.reject('Authentication required before registering TOTP secret key'); - } - - var userid = objectPath.get(req, 'session.auth_session.userid'); - var email = objectPath.get(req, 'session.auth_session.email'); - - if(!(userid && email)) { - return BluebirdPromise.reject('User ID or email is missing'); - } - - var identity = {}; - identity.email = email; - identity.userid = userid; - return BluebirdPromise.resolve(identity); -} - -// Generate a secret and send it to the user -function post(req, res) { - var logger = req.app.get('logger'); - var userid = objectPath.get(req, 'session.auth_session.identity_check.userid'); - var challenge = objectPath.get(req, 'session.auth_session.identity_check.challenge'); - - if(challenge != CHALLENGE || !userid) { - res.status(403); - res.send(); - return; - } - - var user_data_store = req.app.get('user data store'); - 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) - .then(function() { - var doc = {}; - doc.otpauth_url = secret.otpauth_url; - doc.base32 = secret.base32; - doc.ascii = secret.ascii; - - objectPath.set(req, 'session', undefined); - - res.status(200); - res.json(doc); - }) - .catch(function(err) { - logger.error('POST new-totp-secret: Internal error %s', err); - res.status(500); - res.send(); - }); -} - diff --git a/src/lib/routes/u2f.js b/src/lib/routes/u2f.js deleted file mode 100644 index 604159e2..00000000 --- a/src/lib/routes/u2f.js +++ /dev/null @@ -1,85 +0,0 @@ - -var u2f_register = require('./u2f_register'); -var u2f_common = require('./u2f_common'); -var objectPath = require('object-path'); - -module.exports = { - register_request: u2f_register.register_request, - register: u2f_register.register, - register_handler_get: u2f_register.register_handler_get, - register_handler_post: u2f_register.register_handler_post, - - sign_request: sign_request, - sign: sign, -} - - -function retrieve_u2f_meta(req, user_data_storage) { - var userid = req.session.auth_session.userid; - var appid = u2f_common.extract_app_id(req); - return user_data_storage.get_u2f_meta(userid, appid); -} - - -function sign_request(req, res) { - var logger = req.app.get('logger'); - var user_data_storage = req.app.get('user data store'); - - retrieve_u2f_meta(req, user_data_storage) - .then(function(doc) { - if(!doc) { - u2f_common.reply_with_missing_registration(res); - return; - } - - var u2f = req.app.get('u2f'); - var meta = doc.meta; - var appid = u2f_common.extract_app_id(req); - logger.info('U2F sign_request: Start authentication to app %s', appid); - return u2f.startAuthentication(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); - }) - .catch(function(err) { - logger.info('U2F sign_request: %s', err); - res.status(500); - res.send(); - }); -} - - -function sign(req, res) { - if(!objectPath.has(req, 'session.auth_session.sign_request')) { - u2f_common.reply_with_unauthorized(res); - return; - } - - var logger = req.app.get('logger'); - var user_data_storage = req.app.get('user data store'); - - retrieve_u2f_meta(req, user_data_storage) - .then(function(doc) { - var appid = u2f_common.extract_app_id(req); - var u2f = req.app.get('u2f'); - var authRequest = req.session.auth_session.sign_request; - var meta = doc.meta; - logger.info('U2F sign: Finish authentication'); - return u2f.finishAuthentication(authRequest, req.body, [meta]) - }) - .then(function(authenticationStatus) { - logger.info('U2F sign: Authentication successful'); - req.session.auth_session.second_factor = true; - res.status(204); - res.send(); - }) - .catch(function(err) { - logger.error('U2F sign: %s', err); - res.status(500); - res.send(); - }); -} - diff --git a/src/lib/routes/u2f_common.js b/src/lib/routes/u2f_common.js deleted file mode 100644 index 4b7e4602..00000000 --- a/src/lib/routes/u2f_common.js +++ /dev/null @@ -1,38 +0,0 @@ - -module.exports = { - extract_app_id: extract_app_id, - extract_original_url: extract_original_url, - extract_referrer: extract_referrer, - reply_with_internal_error: reply_with_internal_error, - reply_with_missing_registration: reply_with_missing_registration, - reply_with_unauthorized: reply_with_unauthorized -} - -var util = require('util'); - -function extract_app_id(req) { - return util.format('https://%s', req.headers.host); -} - -function extract_original_url(req) { - return util.format('https://%s%s', req.headers.host, req.headers['x-original-uri']); -} - -function extract_referrer(req) { - return req.headers.referrer; -} - -function reply_with_internal_error(res, msg) { - res.status(500); - res.send(msg) -} - -function reply_with_missing_registration(res) { - res.status(401); - res.send('Please register before authenticate'); -} - -function reply_with_unauthorized(res) { - res.status(401); - res.send(); -} diff --git a/src/lib/routes/u2f_common.ts b/src/lib/routes/u2f_common.ts new file mode 100644 index 00000000..cb13bd01 --- /dev/null +++ b/src/lib/routes/u2f_common.ts @@ -0,0 +1,39 @@ + +import util = require("util"); +import express = require("express"); + +function extract_app_id(req: express.Request) { + return util.format("https://%s", req.headers.host); +} + +function extract_original_url(req: express.Request) { + return util.format("https://%s%s", req.headers.host, req.headers["x-original-uri"]); +} + +function extract_referrer(req: express.Request) { + return req.headers.referrer; +} + +function reply_with_internal_error(res: express.Response, msg: string) { + res.status(500); + res.send(msg); +} + +function reply_with_missing_registration(res: express.Response) { + res.status(401); + res.send("Please register before authenticate"); +} + +function reply_with_unauthorized(res: express.Response) { + res.status(401); + res.send(); +} + +export = { + extract_app_id: extract_app_id, + extract_original_url: extract_original_url, + extract_referrer: extract_referrer, + reply_with_internal_error: reply_with_internal_error, + reply_with_missing_registration: reply_with_missing_registration, + reply_with_unauthorized: reply_with_unauthorized +}; \ No newline at end of file diff --git a/src/lib/routes/u2f_register.js b/src/lib/routes/u2f_register.js deleted file mode 100644 index 220a1716..00000000 --- a/src/lib/routes/u2f_register.js +++ /dev/null @@ -1,91 +0,0 @@ - -var u2f_register_handler = require('./u2f_register_handler'); - -module.exports = { - register_request: register_request, - register: register, - register_handler_get: u2f_register_handler.get, - register_handler_post: u2f_register_handler.post -} - -var objectPath = require('object-path'); -var u2f_common = require('./u2f_common'); -var BluebirdPromise = require('bluebird'); - -function register_request(req, res) { - var logger = req.app.get('logger'); - var challenge = objectPath.get(req, 'session.auth_session.identity_check.challenge'); - if(challenge != 'u2f-register') { - res.status(403); - res.send(); - return; - } - - var u2f = req.app.get('u2f'); - var appid = u2f_common.extract_app_id(req); - - logger.debug('U2F register_request: headers=%s', JSON.stringify(req.headers)); - logger.info('U2F register_request: Starting registration of app %s', appid); - u2f.startRegistration(appid, []) - .then(function(registrationRequest) { - logger.info('U2F register_request: Sending back registration request'); - req.session.auth_session.register_request = registrationRequest; - res.status(200); - res.json(registrationRequest); - }) - .catch(function(err) { - logger.error('U2F register_request: %s', err); - res.status(500); - res.send('Unable to start registration request'); - }); -} - -function register(req, res) { - var registrationRequest = objectPath.get(req, 'session.auth_session.register_request'); - var challenge = objectPath.get(req, 'session.auth_session.identity_check.challenge'); - - if(!registrationRequest) { - res.status(403); - res.send(); - return; - } - - if(!(registrationRequest && challenge == 'u2f-register')) { - res.status(403); - res.send(); - return; - } - - - var user_data_storage = req.app.get('user data store'); - var u2f = req.app.get('u2f'); - var userid = req.session.auth_session.userid; - var appid = u2f_common.extract_app_id(req); - var logger = req.app.get('logger'); - - logger.info('U2F register: Finishing registration'); - logger.debug('U2F register: register_request=%s', JSON.stringify(registrationRequest)); - logger.debug('U2F register: body=%s', JSON.stringify(req.body)); - - u2f.finishRegistration(registrationRequest, req.body) - .then(function(registrationStatus) { - logger.info('U2F register: Store registration and reply'); - var meta = { - keyHandle: registrationStatus.keyHandle, - publicKey: registrationStatus.publicKey, - certificate: registrationStatus.certificate - } - return user_data_storage.set_u2f_meta(userid, appid, meta); - }) - .then(function() { - objectPath.set(req, 'session.auth_session.identity_check', undefined); - res.status(204); - res.send(); - }) - .catch(function(err) { - logger.error('U2F register: %s', err); - res.status(500); - res.send('Unable to register'); - }); -} - diff --git a/src/lib/routes/u2f_register_handler.js b/src/lib/routes/u2f_register_handler.js deleted file mode 100644 index f321dc52..00000000 --- a/src/lib/routes/u2f_register_handler.js +++ /dev/null @@ -1,37 +0,0 @@ - -var objectPath = require('object-path'); -var BluebirdPromise = require('bluebird'); - -var CHALLENGE = 'u2f-register'; - -var icheck_interface = { - challenge: CHALLENGE, - render_template: 'u2f-register', - pre_check_callback: pre_check, - email_subject: 'Register your U2F device', -} - -module.exports = { - icheck_interface: icheck_interface, -} - - -function pre_check(req) { - var first_factor_passed = objectPath.get(req, 'session.auth_session.first_factor'); - if(!first_factor_passed) { - return BluebirdPromise.reject('Authentication required before issuing a u2f registration request'); - } - - var userid = objectPath.get(req, 'session.auth_session.userid'); - var email = objectPath.get(req, 'session.auth_session.email'); - - if(!(userid && email)) { - return BluebirdPromise.reject('User ID or email is missing'); - } - - var identity = {}; - identity.email = email; - identity.userid = userid; - return BluebirdPromise.resolve(identity); -} - diff --git a/src/lib/ILogger.ts b/src/types/ILogger.ts similarity index 100% rename from src/lib/ILogger.ts rename to src/types/ILogger.ts diff --git a/test/unitary/IdentityValidator.test.ts b/test/unitary/IdentityValidator.test.ts new file mode 100644 index 00000000..6527e659 --- /dev/null +++ b/test/unitary/IdentityValidator.test.ts @@ -0,0 +1,213 @@ + +import sinon = require("sinon"); +import IdentityValidator = require("../../src/lib/IdentityValidator"); +import exceptions = require("../../src/lib/Exceptions"); +import assert = require("assert"); +import winston = require("winston"); +import Promise = require("bluebird"); +import express = require("express"); +import BluebirdPromise = require("bluebird"); + +import ExpressMock = require("./mocks/express"); +import UserDataStoreMock = require("./mocks/UserDataStore"); +import NotifierMock = require("./mocks/Notifier"); +import IdentityValidatorMock = require("./mocks/IdentityValidator"); + + +describe("test identity check process", function() { + let req: ExpressMock.RequestMock; + let res: ExpressMock.ResponseMock; + let userDataStore: UserDataStoreMock.UserDataStore; + let notifier: NotifierMock.NotifierMock; + let app: express.Application; + let app_get: sinon.SinonStub; + let app_post: sinon.SinonStub; + let identityValidable: IdentityValidatorMock.IdentityValidableMock; + + beforeEach(function() { + req = ExpressMock.RequestMock(); + res = ExpressMock.ResponseMock(); + + userDataStore = UserDataStoreMock.UserDataStore(); + userDataStore.issue_identity_check_token = sinon.stub(); + userDataStore.issue_identity_check_token.returns(Promise.resolve()); + userDataStore.consume_identity_check_token = sinon.stub(); + userDataStore.consume_identity_check_token.returns(Promise.resolve({ userid: "user" })); + + notifier = NotifierMock.NotifierMock(); + notifier.notify = sinon.stub().returns(Promise.resolve()); + + req.headers = {}; + req.session = {}; + req.session.auth_session = {}; + + req.query = {}; + req.app = {}; + req.app.get = sinon.stub(); + req.app.get.withArgs("logger").returns(winston); + req.app.get.withArgs("user data store").returns(userDataStore); + req.app.get.withArgs("notifier").returns(notifier); + + app = express(); + app_get = sinon.stub(app, "get"); + app_post = sinon.stub(app, "post"); + + identityValidable = IdentityValidatorMock.IdentityValidableMock(); + }); + + afterEach(function() { + app_get.restore(); + app_post.restore(); + }); + + it("should register a POST and GET endpoint", function() { + const endpoint = "/test"; + const icheck_interface = {}; + + IdentityValidator.IdentityValidator.setup(app, endpoint, identityValidable, userDataStore as any, winston); + + assert(app_get.calledOnce); + assert(app_get.calledWith(endpoint)); + + assert(app_post.calledOnce); + assert(app_post.calledWith(endpoint)); + }); + + describe("test POST", test_post_handler); + describe("test GET", test_get_handler); + + function test_post_handler() { + it("should send 403 if pre check rejects", function(done) { + const endpoint = "/protected"; + + identityValidable.preValidation.returns(Promise.reject("No access")); + IdentityValidator.IdentityValidator.setup(app, endpoint, identityValidable, userDataStore as any, winston); + + res.send = sinon.spy(function() { + assert.equal(res.status.getCall(0).args[0], 403); + done(); + }); + + const handler = app_post.getCall(0).args[1]; + handler(req, res); + }); + + it("should send 400 if email is missing in provided identity", function(done) { + const endpoint = "/protected"; + const identity = { userid: "abc" }; + + identityValidable.preValidation.returns(Promise.resolve(identity)); + IdentityValidator.IdentityValidator.setup(app, endpoint, identityValidable, userDataStore as any, winston); + + res.send = sinon.spy(function() { + assert.equal(res.status.getCall(0).args[0], 400); + done(); + }); + + const handler = app_post.getCall(0).args[1]; + handler(req, res); + }); + + it("should send 400 if userid is missing in provided identity", function(done) { + const endpoint = "/protected"; + const identity = { email: "abc@example.com" }; + + identityValidable.preValidation.returns(Promise.resolve(identity)); + IdentityValidator.IdentityValidator.setup(app, endpoint, identityValidable, userDataStore as any, winston); + + res.send = sinon.spy(function() { + assert.equal(res.status.getCall(0).args[0], 400); + done(); + }); + const handler = app_post.getCall(0).args[1]; + handler(req, res); + }); + + it("should issue a token, send an email and return 204", function(done) { + const endpoint = "/protected"; + const identity = { userid: "user", email: "abc@example.com" }; + req.headers.host = "localhost"; + req.headers["x-original-uri"] = "/auth/test"; + + identityValidable.preValidation.returns(Promise.resolve(identity)); + IdentityValidator.IdentityValidator.setup(app, endpoint, identityValidable, userDataStore as any, winston); + + res.send = sinon.spy(function() { + assert.equal(res.status.getCall(0).args[0], 204); + assert(notifier.notify.calledOnce); + assert(userDataStore.issue_identity_check_token.calledOnce); + assert.equal(userDataStore.issue_identity_check_token.getCall(0).args[0], "user"); + assert.equal(userDataStore.issue_identity_check_token.getCall(0).args[3], 240000); + done(); + }); + const handler = app_post.getCall(0).args[1]; + handler(req, res); + }); + } + + function test_get_handler() { + it("should send 403 if no identity_token is provided", function(done) { + const endpoint = "/protected"; + + IdentityValidator.IdentityValidator.setup(app, endpoint, identityValidable, userDataStore as any, winston); + + res.send = sinon.spy(function() { + assert.equal(res.status.getCall(0).args[0], 403); + done(); + }); + const handler = app_get.getCall(0).args[1]; + handler(req, res); + }); + + it("should render template if identity_token is provided and still valid", function(done) { + req.query.identity_token = "token"; + const endpoint = "/protected"; + identityValidable.templateName.returns("template"); + + IdentityValidator.IdentityValidator.setup(app, endpoint, identityValidable, userDataStore as any, winston); + + res.render = sinon.spy(function(template: string) { + assert.equal(template, "template"); + done(); + }); + const handler = app_get.getCall(0).args[1]; + handler(req, res); + }); + + it("should return 403 if identity_token is provided but invalid", function(done) { + req.query.identity_token = "token"; + const endpoint = "/protected"; + + identityValidable.templateName.returns("template"); + userDataStore.consume_identity_check_token + .returns(Promise.reject("Invalid token")); + + IdentityValidator.IdentityValidator.setup(app, endpoint, identityValidable, userDataStore as any, winston); + + res.send = sinon.spy(function(template: string) { + assert.equal(res.status.getCall(0).args[0], 403); + done(); + }); + const handler = app_get.getCall(0).args[1]; + handler(req, res); + }); + + it("should set the identity_check session object even if session does not exist yet", function(done) { + req.query.identity_token = "token"; + const endpoint = "/protected"; + + req.session = {}; + identityValidable.templateName.returns("template"); + + IdentityValidator.IdentityValidator.setup(app, endpoint, identityValidable, userDataStore as any, winston); + + res.render = sinon.spy(function(template: string) { + assert.equal(req.session.auth_session.identity_check.userid, "user"); + assert.equal(template, "template"); + done(); + }); + const handler = app_get.getCall(0).args[1]; + handler(req, res); + }); + } +}); diff --git a/test/unitary/mocks/IdentityValidator.ts b/test/unitary/mocks/IdentityValidator.ts new file mode 100644 index 00000000..fd341770 --- /dev/null +++ b/test/unitary/mocks/IdentityValidator.ts @@ -0,0 +1,35 @@ + +import sinon = require("sinon"); +import { IdentityValidable } from "../../../src/lib/IdentityValidator"; +import express = require("express"); +import BluebirdPromise = require("bluebird"); +import { Identity } from "../../../src/types/Identity"; + + +export interface IdentityValidableMock { + challenge: sinon.SinonStub; + templateName: sinon.SinonStub; + preValidation: sinon.SinonStub; + mailSubject: sinon.SinonStub; +} + +export function IdentityValidableMock() { + return { + challenge: sinon.stub(), + templateName: sinon.stub(), + preValidation: sinon.stub(), + mailSubject: sinon.stub() + }; +} + +export interface IdentityValidatorMock { + consume_token: sinon.SinonStub; + issue_token: sinon.SinonStub; +} + +export function IdentityValidatorMock() { + return { + consume_token: sinon.stub(), + issue_token: sinon.stub() + }; +} \ No newline at end of file diff --git a/test/unitary/mocks/Notifier.ts b/test/unitary/mocks/Notifier.ts new file mode 100644 index 00000000..95160408 --- /dev/null +++ b/test/unitary/mocks/Notifier.ts @@ -0,0 +1,12 @@ + +import sinon = require("sinon"); + +export interface NotifierMock { + notify: sinon.SinonStub; +} + +export function NotifierMock(): NotifierMock { + return { + notify: sinon.stub() + }; +} diff --git a/test/unitary/mocks/UserDataStore.ts b/test/unitary/mocks/UserDataStore.ts index a2221efa..4a4daa6a 100644 --- a/test/unitary/mocks/UserDataStore.ts +++ b/test/unitary/mocks/UserDataStore.ts @@ -7,6 +7,7 @@ export interface UserDataStore { issue_identity_check_token: sinon.SinonStub; consume_identity_check_token: sinon.SinonStub; get_totp_secret: sinon.SinonStub; + set_totp_secret: sinon.SinonStub; } export function UserDataStore(): UserDataStore { @@ -15,6 +16,7 @@ export function UserDataStore(): UserDataStore { get_u2f_meta: sinon.stub(), issue_identity_check_token: sinon.stub(), consume_identity_check_token: sinon.stub(), - get_totp_secret: sinon.stub() + get_totp_secret: sinon.stub(), + set_totp_secret: sinon.stub() }; } diff --git a/test/unitary/mocks/authdog.ts b/test/unitary/mocks/authdog.ts new file mode 100644 index 00000000..843a9e7c --- /dev/null +++ b/test/unitary/mocks/authdog.ts @@ -0,0 +1,19 @@ + +import sinon = require("sinon"); +import authdog = require("authdog"); + +export interface AuthdogMock { + startRegistration: sinon.SinonStub; + finishRegistration: sinon.SinonStub; + startAuthentication: sinon.SinonStub; + finishAuthentication: sinon.SinonStub; +} + +export function AuthdogMock(): AuthdogMock { + return { + startRegistration: sinon.stub(), + finishAuthentication: sinon.stub(), + startAuthentication: sinon.stub(), + finishRegistration: sinon.stub() + }; +} diff --git a/test/unitary/mocks/express.ts b/test/unitary/mocks/express.ts index b06a1d5f..daa3e170 100644 --- a/test/unitary/mocks/express.ts +++ b/test/unitary/mocks/express.ts @@ -8,6 +8,7 @@ export interface RequestMock { session?: any; headers?: any; get?: any; + query?: any; } export interface ResponseMock { @@ -16,7 +17,7 @@ export interface ResponseMock { sendFile: sinon.SinonStub; sendfile: sinon.SinonStub; status: sinon.SinonStub | sinon.SinonSpy; - json: sinon.SinonStub; + json: sinon.SinonStub | sinon.SinonSpy; links: sinon.SinonStub; jsonp: sinon.SinonStub; download: sinon.SinonStub; @@ -32,7 +33,7 @@ export interface ResponseMock { cookie: sinon.SinonStub; location: sinon.SinonStub; redirect: sinon.SinonStub; - render: sinon.SinonStub; + render: sinon.SinonStub | sinon.SinonSpy; locals: sinon.SinonStub; charset: string; vary: sinon.SinonStub; diff --git a/test/unitary/mocks/nodemailer.ts b/test/unitary/mocks/nodemailer.ts index 09d3564d..61ec1c94 100644 --- a/test/unitary/mocks/nodemailer.ts +++ b/test/unitary/mocks/nodemailer.ts @@ -1,6 +1,22 @@ import sinon = require("sinon"); -export = { - createTransport: sinon.stub() -}; +export interface NodemailerMock { + createTransport: sinon.SinonStub; +} + +export function NodemailerMock(): NodemailerMock { + return { + createTransport: sinon.stub() + }; +} + +export interface NodemailerTransporterMock { + sendMail: sinon.SinonStub; +} + +export function NodemailerTransporterMock() { + return { + sendMail: sinon.stub() + }; +} diff --git a/test/unitary/notifiers/GMailNotifier.test.ts b/test/unitary/notifiers/GMailNotifier.test.ts index 3e6f6ce4..feaae479 100644 --- a/test/unitary/notifiers/GMailNotifier.test.ts +++ b/test/unitary/notifiers/GMailNotifier.test.ts @@ -1,7 +1,7 @@ import * as sinon from "sinon"; import * as assert from "assert"; -import nodemailerMock = require("../mocks/nodemailer"); +import NodemailerMock = require("../mocks/nodemailer"); import GMailNotifier = require("../../../src/lib/notifiers/GMailNotifier"); @@ -10,6 +10,7 @@ describe("test gmail notifier", function () { const transporter = { sendMail: sinon.stub().yields() }; + const nodemailerMock = NodemailerMock.NodemailerMock(); nodemailerMock.createTransport.returns(transporter); const options = { diff --git a/test/unitary/notifiers/NotifierFactory.test.ts b/test/unitary/notifiers/NotifierFactory.test.ts index ff6200c0..d327a9ba 100644 --- a/test/unitary/notifiers/NotifierFactory.test.ts +++ b/test/unitary/notifiers/NotifierFactory.test.ts @@ -3,16 +3,15 @@ import * as sinon from "sinon"; import * as BluebirdPromise from "bluebird"; import * as assert from "assert"; -import NodemailerMock = require("../mocks/nodemailer"); - import { NotifierFactory } from "../../../src/lib/notifiers/NotifierFactory"; import { GMailNotifier } from "../../../src/lib/notifiers/GMailNotifier"; import { FileSystemNotifier } from "../../../src/lib/notifiers/FileSystemNotifier"; -import nodemailerMock = require("../mocks/nodemailer"); +import NodemailerMock = require("../mocks/nodemailer"); describe("test notifier factory", function() { + let nodemailerMock: NodemailerMock.NodemailerMock; it("should build a Gmail Notifier", function() { const options = { gmail: { @@ -20,6 +19,7 @@ describe("test notifier factory", function() { password: "password" } }; + nodemailerMock = NodemailerMock.NodemailerMock(); nodemailerMock.createTransport.returns(sinon.spy()); assert(NotifierFactory.build(options, nodemailerMock) instanceof GMailNotifier); }); diff --git a/test/unitary/requests.js b/test/unitary/requests.js deleted file mode 100644 index f66b16e4..00000000 --- a/test/unitary/requests.js +++ /dev/null @@ -1,174 +0,0 @@ - -var Promise = require('bluebird'); -var request = Promise.promisifyAll(require('request')); -var assert = require('assert'); - -module.exports = function(port) { - var PORT = port; - var BASE_URL = 'http://localhost:' + PORT; - - function execute_reset_password(jar, transporter, user, new_password) { - return request.postAsync({ - url: BASE_URL + '/reset-password', - jar: jar, - form: { userid: user } - }) - .then(function(res) { - assert.equal(res.statusCode, 204); - var html_content = transporter.sendMail.getCall(0).args[0].html; - var regexp = /identity_token=([a-zA-Z0-9]+)/; - var token = regexp.exec(html_content)[1]; - // console.log(html_content, token); - return request.getAsync({ - url: BASE_URL + '/reset-password?identity_token=' + token, - jar: jar - }) - }) - .then(function(res) { - assert.equal(res.statusCode, 200); - return request.postAsync({ - url: BASE_URL + '/new-password', - jar: jar, - form: { - password: new_password - } - }); - }); - } - - function execute_register_totp(jar, transporter) { - return request.postAsync({ - url: BASE_URL + '/totp-register', - jar: jar - }) - .then(function(res) { - assert.equal(res.statusCode, 204); - var html_content = transporter.sendMail.getCall(0).args[0].html; - var regexp = /identity_token=([a-zA-Z0-9]+)/; - var token = regexp.exec(html_content)[1]; - // console.log(html_content, token); - return request.getAsync({ - url: BASE_URL + '/totp-register?identity_token=' + token, - jar: jar - }) - }) - .then(function(res) { - assert.equal(res.statusCode, 200); - return request.postAsync({ - url : BASE_URL + '/new-totp-secret', - jar: jar, - }) - }) - .then(function(res) { - console.log(res.statusCode); - console.log(res.body); - assert.equal(res.statusCode, 200); - return Promise.resolve(res.body); - }); - } - - function execute_totp(jar, token) { - return request.postAsync({ - url: BASE_URL + '/2ndfactor/totp', - jar: jar, - form: { - token: token - } - }); - } - - function execute_u2f_authentication(jar) { - return request.getAsync({ - url: BASE_URL + '/2ndfactor/u2f/sign_request', - jar: jar - }) - .then(function(res) { - assert.equal(res.statusCode, 200); - return request.postAsync({ - url: BASE_URL + '/2ndfactor/u2f/sign', - jar: jar, - form: { - } - }); - }); - } - - 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 }) - } - - function execute_u2f_registration(jar, transporter) { - return request.postAsync({ - url: BASE_URL + '/u2f-register', - jar: jar - }) - .then(function(res) { - assert.equal(res.statusCode, 204); - var html_content = transporter.sendMail.getCall(0).args[0].html; - var regexp = /identity_token=([a-zA-Z0-9]+)/; - var token = regexp.exec(html_content)[1]; - // console.log(html_content, token); - return request.getAsync({ - url: BASE_URL + '/u2f-register?identity_token=' + token, - jar: jar - }) - }) - .then(function(res) { - assert.equal(res.statusCode, 200); - return request.getAsync({ - url: BASE_URL + '/2ndfactor/u2f/register_request', - jar: jar, - }); - }) - .then(function(res) { - assert.equal(res.statusCode, 200); - return request.postAsync({ - url: BASE_URL + '/2ndfactor/u2f/register', - jar: jar, - form: { - s: 'test' - } - }); - }); - } - - function execute_first_factor(jar) { - return request.postAsync({ - url: BASE_URL + '/1stfactor', - jar: jar, - form: { - username: 'test_ok', - password: 'password' - } - }); - } - - function execute_failing_first_factor(jar) { - return request.postAsync({ - url: BASE_URL + '/1stfactor', - jar: jar, - form: { - username: 'test_nok', - password: 'password' - } - }); - } - - return { - login: execute_login, - verify: execute_verification, - reset_password: execute_reset_password, - u2f_authentication: execute_u2f_authentication, - u2f_registration: execute_u2f_registration, - first_factor: execute_first_factor, - failing_first_factor: execute_failing_first_factor, - totp: execute_totp, - register_totp: execute_register_totp, - } - -} - diff --git a/test/unitary/requests.ts b/test/unitary/requests.ts new file mode 100644 index 00000000..221f4b37 --- /dev/null +++ b/test/unitary/requests.ts @@ -0,0 +1,179 @@ + +import BluebirdPromise = require("bluebird"); +import request = require("request"); +import assert = require("assert"); +import express = require("express"); +import nodemailer = require("nodemailer"); + +import NodemailerMock = require("./mocks/nodemailer"); + +const requestAsync = BluebirdPromise.promisifyAll(request) as request.RequestAsync; + +export = function (port: number) { + const PORT = port; + const BASE_URL = "http://localhost:" + PORT; + + function execute_reset_password(jar: request.CookieJar, transporter: NodemailerMock.NodemailerTransporterMock, user: string, new_password: string) { + return requestAsync.postAsync({ + url: BASE_URL + "/reset-password", + jar: jar, + form: { userid: user } + }) + .then(function (res: request.RequestResponse) { + assert.equal(res.statusCode, 204); + const html_content = transporter.sendMail.getCall(0).args[0].html; + const regexp = /identity_token=([a-zA-Z0-9]+)/; + const token = regexp.exec(html_content)[1]; + // console.log(html_content, token); + return requestAsync.getAsync({ + url: BASE_URL + "/reset-password?identity_token=" + token, + jar: jar + }); + }) + .then(function (res: request.RequestResponse) { + assert.equal(res.statusCode, 200); + return requestAsync.postAsync({ + url: BASE_URL + "/new-password", + jar: jar, + form: { + password: new_password + } + }); + }); + } + + function execute_register_totp(jar: request.CookieJar, transporter: NodemailerMock.NodemailerTransporterMock) { + return requestAsync.postAsync({ + url: BASE_URL + "/totp-register", + jar: jar + }) + .then(function (res: request.RequestResponse) { + assert.equal(res.statusCode, 204); + const html_content = transporter.sendMail.getCall(0).args[0].html; + const regexp = /identity_token=([a-zA-Z0-9]+)/; + const token = regexp.exec(html_content)[1]; + // console.log(html_content, token); + return requestAsync.getAsync({ + url: BASE_URL + "/totp-register?identity_token=" + token, + jar: jar + }); + }) + .then(function (res: request.RequestResponse) { + assert.equal(res.statusCode, 200); + return requestAsync.postAsync({ + url: BASE_URL + "/new-totp-secret", + jar: jar, + }); + }) + .then(function (res: request.RequestResponse) { + console.log(res.statusCode); + console.log(res.body); + assert.equal(res.statusCode, 200); + return Promise.resolve(res.body); + }); + } + + function execute_totp(jar: request.CookieJar, token: string) { + return requestAsync.postAsync({ + url: BASE_URL + "/2ndfactor/totp", + jar: jar, + form: { + token: token + } + }); + } + + function execute_u2f_authentication(jar: request.CookieJar) { + return requestAsync.getAsync({ + url: BASE_URL + "/2ndfactor/u2f/sign_request", + jar: jar + }) + .then(function (res: request.RequestResponse) { + assert.equal(res.statusCode, 200); + return requestAsync.postAsync({ + url: BASE_URL + "/2ndfactor/u2f/sign", + jar: jar, + form: { + } + }); + }); + } + + function execute_verification(jar: request.CookieJar) { + return requestAsync.getAsync({ url: BASE_URL + "/verify", jar: jar }); + } + + function execute_login(jar: request.CookieJar) { + return requestAsync.getAsync({ url: BASE_URL + "/login", jar: jar }); + } + + function execute_u2f_registration(jar: request.CookieJar, transporter: NodemailerMock.NodemailerTransporterMock) { + return requestAsync.postAsync({ + url: BASE_URL + "/u2f-register", + jar: jar + }) + .then(function (res: request.RequestResponse) { + assert.equal(res.statusCode, 204); + const html_content = transporter.sendMail.getCall(0).args[0].html; + const regexp = /identity_token=([a-zA-Z0-9]+)/; + const token = regexp.exec(html_content)[1]; + // console.log(html_content, token); + return requestAsync.getAsync({ + url: BASE_URL + "/u2f-register?identity_token=" + token, + jar: jar + }); + }) + .then(function (res: request.RequestResponse) { + assert.equal(res.statusCode, 200); + return requestAsync.getAsync({ + url: BASE_URL + "/2ndfactor/u2f/register_request", + jar: jar, + }); + }) + .then(function (res: request.RequestResponse) { + assert.equal(res.statusCode, 200); + return requestAsync.postAsync({ + url: BASE_URL + "/2ndfactor/u2f/register", + jar: jar, + form: { + s: "test" + } + }); + }); + } + + function execute_first_factor(jar: request.CookieJar) { + return requestAsync.postAsync({ + url: BASE_URL + "/1stfactor", + jar: jar, + form: { + username: "test_ok", + password: "password" + } + }); + } + + function execute_failing_first_factor(jar: request.CookieJar) { + return requestAsync.postAsync({ + url: BASE_URL + "/1stfactor", + jar: jar, + form: { + username: "test_nok", + password: "password" + } + }); + } + + return { + login: execute_login, + verify: execute_verification, + reset_password: execute_reset_password, + u2f_authentication: execute_u2f_authentication, + u2f_registration: execute_u2f_registration, + first_factor: execute_first_factor, + failing_first_factor: execute_failing_first_factor, + totp: execute_totp, + register_totp: execute_register_totp, + }; +}; + diff --git a/test/unitary/res_mock.js b/test/unitary/res_mock.js deleted file mode 100644 index 1ac02a50..00000000 --- a/test/unitary/res_mock.js +++ /dev/null @@ -1,24 +0,0 @@ - -module.exports = create_res_mock; - -var sinon = require('sinon'); -var sinonPromise = require('sinon-promise'); -sinonPromise(sinon); - -function create_res_mock() { - var status_mock = sinon.mock(); - var send_mock = sinon.mock(); - var set_mock = sinon.mock(); - var cookie_mock = sinon.mock(); - var render_mock = sinon.mock(); - var redirect_mock = sinon.mock(); - - return { - status: status_mock, - send: send_mock, - set: set_mock, - cookie: cookie_mock, - render: render_mock, - redirect: redirect_mock - }; -} diff --git a/test/unitary/routes/reset_password.test.ts b/test/unitary/routes/PasswordReset.test.ts similarity index 90% rename from test/unitary/routes/reset_password.test.ts rename to test/unitary/routes/PasswordReset.test.ts index 8ec9cbef..cb1ec07d 100644 --- a/test/unitary/routes/reset_password.test.ts +++ b/test/unitary/routes/PasswordReset.test.ts @@ -1,5 +1,5 @@ -import reset_password = require("../../../src/lib/routes/reset_password"); +import PasswordReset = require("../../../src/lib/routes/PasswordReset"); import LdapClient = require("../../../src/lib/LdapClient"); import sinon = require("sinon"); import winston = require("winston"); @@ -72,7 +72,7 @@ describe("test reset password", function () { function test_reset_password_check() { it("should fail when no userid is provided", function (done) { req.body.userid = undefined; - reset_password.icheck_interface.pre_check_callback(req) + PasswordReset.icheck_interface.preValidation(req as any) .catch(function (err: Error) { done(); }); @@ -80,7 +80,7 @@ describe("test reset password", function () { it("should fail if ldap fail", function (done) { ldap_client.get_emails.returns(BluebirdPromise.reject("Internal error")); - reset_password.icheck_interface.pre_check_callback(req) + PasswordReset.icheck_interface.preValidation(req as any) .catch(function (err: Error) { done(); }); @@ -89,7 +89,7 @@ describe("test reset password", function () { it("should perform a search in ldap to find email address", function (done) { configuration.ldap.user_name_attribute = "uid"; ldap_client.get_emails.returns(BluebirdPromise.resolve([])); - reset_password.icheck_interface.pre_check_callback(req) + PasswordReset.icheck_interface.preValidation(req as any) .then(function () { assert.equal("user", ldap_client.get_emails.getCall(0).args[0]); done(); @@ -98,7 +98,7 @@ describe("test reset password", function () { it("should returns identity when ldap replies", function (done) { ldap_client.get_emails.returns(BluebirdPromise.resolve(["test@example.com"])); - reset_password.icheck_interface.pre_check_callback(req) + PasswordReset.icheck_interface.preValidation(req as any) .then(function () { done(); }); @@ -120,7 +120,7 @@ describe("test reset password", function () { assert.equal(req.session.auth_session, undefined); done(); }); - reset_password.post(req, res); + PasswordReset.post(req as any, res as any); }); it("should fail if identity_challenge does not exist", function (done) { @@ -130,7 +130,7 @@ describe("test reset password", function () { assert.equal(res.status.getCall(0).args[0], 403); done(); }); - reset_password.post(req, res); + PasswordReset.post(req as any, res as any); }); it("should fail when ldap fails", function (done) { @@ -145,7 +145,7 @@ describe("test reset password", function () { assert.equal(res.status.getCall(0).args[0], 500); done(); }); - reset_password.post(req, res); + PasswordReset.post(req as any, res as any); }); } }); diff --git a/test/unitary/routes/TOTPRegistration.test.ts b/test/unitary/routes/TOTPRegistration.test.ts new file mode 100644 index 00000000..4667b618 --- /dev/null +++ b/test/unitary/routes/TOTPRegistration.test.ts @@ -0,0 +1,137 @@ +import sinon = require("sinon"); +import winston = require("winston"); +import TOTPRegistration = require("../../../src/lib/routes/TOTPRegistration"); +import assert = require("assert"); +import BluebirdPromise = require("bluebird"); + +import ExpressMock = require("../mocks/express"); +import UserDataStoreMock = require("../mocks/UserDataStore"); + +describe("test totp register", function () { + let req: ExpressMock.RequestMock; + let res: ExpressMock.ResponseMock; + let userDataStore: UserDataStoreMock.UserDataStore; + + beforeEach(function () { + req = ExpressMock.RequestMock(); + req.app.get = sinon.stub(); + req.app.get.withArgs("logger").returns(winston); + req.session = {}; + req.session.auth_session = {}; + req.session.auth_session.userid = "user"; + req.session.auth_session.email = "user@example.com"; + req.session.auth_session.first_factor = true; + req.session.auth_session.second_factor = false; + req.headers = {}; + req.headers.host = "localhost"; + + const options = { + inMemoryOnly: true + }; + + userDataStore = UserDataStoreMock.UserDataStore(); + userDataStore.set_u2f_meta = sinon.stub().returns(Promise.resolve({})); + userDataStore.get_u2f_meta = sinon.stub().returns(Promise.resolve({})); + userDataStore.issue_identity_check_token = sinon.stub().returns(Promise.resolve({})); + userDataStore.consume_identity_check_token = sinon.stub().returns(Promise.resolve({})); + userDataStore.set_totp_secret = sinon.stub().returns(Promise.resolve({})); + req.app.get.withArgs("user data store").returns(userDataStore); + + res = ExpressMock.ResponseMock(); + }); + + describe("test totp registration check", test_registration_check); + describe("test totp post secret", test_post_secret); + + function test_registration_check() { + it("should fail if first_factor has not been passed", function (done) { + req.session.auth_session.first_factor = false; + TOTPRegistration.icheck_interface.preValidation(req as any) + .catch(function (err) { + done(); + }); + }); + + it("should fail if userid is missing", function (done) { + req.session.auth_session.first_factor = false; + req.session.auth_session.userid = undefined; + + TOTPRegistration.icheck_interface.preValidation(req as any) + .catch(function (err) { + done(); + }); + }); + + it("should fail if email is missing", function (done) { + req.session.auth_session.first_factor = false; + req.session.auth_session.email = undefined; + + TOTPRegistration.icheck_interface.preValidation(req as any) + .catch(function (err) { + done(); + }); + }); + + it("should succeed if first factor passed, userid and email are provided", function (done) { + TOTPRegistration.icheck_interface.preValidation(req as any) + .then(function (err) { + done(); + }); + }); + } + + function test_post_secret() { + it("should send the secret in json format", function (done) { + 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"; + res.json = sinon.spy(function () { + done(); + }); + TOTPRegistration.post(req as any, res as any); + }); + + it("should clear the session for reauthentication", function (done) { + 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"; + res.json = sinon.spy(function () { + assert.equal(req.session, undefined); + done(); + }); + TOTPRegistration.post(req as any, res as any); + }); + + it("should return 403 if the identity check challenge is not set", function (done) { + req.session.auth_session.identity_check = {}; + req.session.auth_session.identity_check.challenge = undefined; + res.send = sinon.spy(function () { + assert.equal(res.status.getCall(0).args[0], 403); + done(); + }); + TOTPRegistration.post(req as any, res as any); + }); + + it("should return 500 if db throws", function (done) { + 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"; + userDataStore.set_totp_secret.returns(BluebirdPromise.reject("internal error")); + + res.send = sinon.spy(function () { + assert.equal(res.status.getCall(0).args[0], 500); + done(); + }); + TOTPRegistration.post(req as any, res as any); + }); + } +}); diff --git a/test/unitary/routes/U2FRegistration.test.ts b/test/unitary/routes/U2FRegistration.test.ts new file mode 100644 index 00000000..a89faad3 --- /dev/null +++ b/test/unitary/routes/U2FRegistration.test.ts @@ -0,0 +1,83 @@ +import sinon = require("sinon"); +import winston = require("winston"); +import u2f_register = require("../../../src/lib/routes/U2FRegistration"); +import assert = require("assert"); + +import ExpressMock = require("../mocks/express"); +import UserDataStoreMock = require("../mocks/UserDataStore"); + +describe("test register handler", function() { + let req: ExpressMock.RequestMock; + let res: ExpressMock.ResponseMock; + let user_data_store: UserDataStoreMock.UserDataStore; + + beforeEach(function() { + req = ExpressMock.RequestMock; + req.app = {}; + req.app.get = sinon.stub(); + req.app.get.withArgs("logger").returns(winston); + req.session = {}; + req.session.auth_session = {}; + req.session.auth_session.userid = "user"; + req.session.auth_session.email = "user@example.com"; + req.session.auth_session.first_factor = true; + req.session.auth_session.second_factor = false; + req.headers = {}; + req.headers.host = "localhost"; + + const options = { + inMemoryOnly: true + }; + + user_data_store = UserDataStoreMock.UserDataStore(); + user_data_store.set_u2f_meta = sinon.stub().returns(Promise.resolve({})); + user_data_store.get_u2f_meta = sinon.stub().returns(Promise.resolve({})); + user_data_store.issue_identity_check_token = sinon.stub().returns(Promise.resolve({})); + user_data_store.consume_identity_check_token = sinon.stub().returns(Promise.resolve({})); + req.app.get.withArgs("user data store").returns(user_data_store); + + res = ExpressMock.ResponseMock(); + res.send = sinon.spy(); + res.json = sinon.spy(); + res.status = sinon.spy(); + }); + + describe("test u2f registration check", test_registration_check); + + function test_registration_check() { + it("should fail if first_factor has not been passed", function(done) { + req.session.auth_session.first_factor = false; + u2f_register.icheck_interface.preValidation(req as any) + .catch(function(err: Error) { + done(); + }); + }); + + it("should fail if userid is missing", function(done) { + req.session.auth_session.first_factor = false; + req.session.auth_session.userid = undefined; + + u2f_register.icheck_interface.preValidation(req as any) + .catch(function(err: Error) { + done(); + }); + }); + + it("should fail if email is missing", function(done) { + req.session.auth_session.first_factor = false; + req.session.auth_session.email = undefined; + + u2f_register.icheck_interface.preValidation(req as any) + .catch(function(err) { + done(); + }); + }); + + it("should succeed if first factor passed, userid and email are provided", function(done) { + u2f_register.icheck_interface.preValidation(req as any) + .then(function(err) { + done(); + }); + }); + } +}); diff --git a/test/unitary/routes/U2FRoutes.test.ts b/test/unitary/routes/U2FRoutes.test.ts new file mode 100644 index 00000000..5274351c --- /dev/null +++ b/test/unitary/routes/U2FRoutes.test.ts @@ -0,0 +1,278 @@ + +import sinon = require("sinon"); +import Promise = require("bluebird"); +import assert = require("assert"); +import u2f = require("../../../src/lib/routes/U2FRoutes"); +import winston = require("winston"); + +import ExpressMock = require("../mocks/express"); +import UserDataStoreMock = require("../mocks/UserDataStore"); +import AuthdogMock = require("../mocks/authdog"); + +describe("test u2f routes", function () { + let req: ExpressMock.RequestMock; + let res: ExpressMock.ResponseMock; + let user_data_store: UserDataStoreMock.UserDataStore; + + beforeEach(function () { + req = ExpressMock.RequestMock(); + req.app = {}; + req.app.get = sinon.stub(); + req.app.get.withArgs("logger").returns(winston); + req.session = {}; + req.session.auth_session = {}; + req.session.auth_session.userid = "user"; + req.session.auth_session.first_factor = true; + req.session.auth_session.second_factor = false; + req.session.auth_session.identity_check = {}; + req.session.auth_session.identity_check.challenge = "u2f-register"; + req.session.auth_session.register_request = {}; + req.headers = {}; + req.headers.host = "localhost"; + + const options = { + inMemoryOnly: true + }; + + user_data_store = UserDataStoreMock.UserDataStore(); + 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 = ExpressMock.ResponseMock(); + res.send = sinon.spy(); + res.json = sinon.spy(); + res.status = sinon.spy(); + }); + + describe("test registration request", test_registration_request); + describe("test registration", test_registration); + 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) { + const expectedRequest = { + test: "abc" + }; + res.json = sinon.spy(function (data: any) { + assert.equal(200, res.status.getCall(0).args[0]); + assert.deepEqual(expectedRequest, data); + done(); + }); + const user_key_container = {}; + const u2f_mock = AuthdogMock.AuthdogMock(); + u2f_mock.startRegistration.returns(Promise.resolve(expectedRequest)); + + req.app.get.withArgs("u2f").returns(u2f_mock); + u2f.register_request(req as any, res as any, undefined); + }); + + it("should return internal error on registration request", function (done) { + res.send = sinon.spy(function (data: any) { + assert.equal(500, res.status.getCall(0).args[0]); + done(); + }); + const user_key_container = {}; + const u2f_mock = AuthdogMock.AuthdogMock(); + u2f_mock.startRegistration.returns(Promise.reject("Internal error")); + + req.app.get.withArgs("u2f").returns(u2f_mock); + u2f.register_request(req as any, res as any, undefined); + }); + + it("should return forbidden if identity has not been verified", function (done) { + res.send = sinon.spy(function (data: any) { + assert.equal(403, res.status.getCall(0).args[0]); + done(); + }); + req.session.auth_session.identity_check = undefined; + u2f.register_request(req as any, res as any, undefined); + }); + } + + function test_registration() { + it("should save u2f meta and return status code 200", function (done) { + const expectedStatus = { + keyHandle: "keyHandle", + publicKey: "pbk", + certificate: "cert" + }; + res.send = sinon.spy(function (data: any) { + assert.equal("user", user_data_store.set_u2f_meta.getCall(0).args[0]); + assert.equal(req.session.auth_session.identity_check, undefined); + done(); + }); + const u2f_mock = AuthdogMock.AuthdogMock(); + u2f_mock.finishRegistration.returns(Promise.resolve(expectedStatus)); + + req.session.auth_session.register_request = {}; + req.app.get.withArgs("u2f").returns(u2f_mock); + u2f.register(req as any, res as any, undefined); + }); + + it("should return unauthorized on finishRegistration error", function (done) { + res.send = sinon.spy(function (data: any) { + assert.equal(500, res.status.getCall(0).args[0]); + done(); + }); + const user_key_container = {}; + const u2f_mock = AuthdogMock.AuthdogMock(); + u2f_mock.finishRegistration.returns(Promise.reject("Internal error")); + + req.session.auth_session.register_request = "abc"; + req.app.get.withArgs("u2f").returns(u2f_mock); + u2f.register(req as any, res as any, undefined); + }); + + it("should return 403 when register_request is not provided", function (done) { + res.send = sinon.spy(function (data: any) { + assert.equal(403, res.status.getCall(0).args[0]); + done(); + }); + const user_key_container = {}; + const u2f_mock = AuthdogMock.AuthdogMock(); + u2f_mock.finishRegistration.returns(Promise.resolve()); + + req.session.auth_session.register_request = undefined; + req.app.get.withArgs("u2f").returns(u2f_mock); + u2f.register(req as any, res as any, undefined); + }); + + it("should return forbidden error when no auth request has been initiated", function (done) { + res.send = sinon.spy(function (data: any) { + assert.equal(403, res.status.getCall(0).args[0]); + done(); + }); + const user_key_container = {}; + const u2f_mock = AuthdogMock.AuthdogMock(); + u2f_mock.finishRegistration.returns(Promise.resolve()); + + req.session.auth_session.register_request = undefined; + req.app.get.withArgs("u2f").returns(u2f_mock); + u2f.register(req as any, res as any, undefined); + }); + + it("should return forbidden error when identity has not been verified", function (done) { + res.send = sinon.spy(function (data: any) { + assert.equal(403, res.status.getCall(0).args[0]); + done(); + }); + req.session.auth_session.identity_check = undefined; + u2f.register(req as any, res as any, undefined); + }); + } + + function test_signing_request() { + it("should send back the sign request and save it in the session", function (done) { + const expectedRequest = { + test: "abc" + }; + res.json = sinon.spy(function (data: any) { + assert.deepEqual(expectedRequest, req.session.auth_session.sign_request); + assert.equal(200, res.status.getCall(0).args[0]); + assert.deepEqual(expectedRequest, data); + done(); + }); + const user_key_container = { + user: {} + }; + const u2f_mock = AuthdogMock.AuthdogMock(); + u2f_mock.startAuthentication.returns(Promise.resolve(expectedRequest)); + + req.app.get.withArgs("u2f").returns(u2f_mock); + u2f.sign_request(req as any, res as any, undefined); + }); + + it("should return unauthorized error on registration request error", function (done) { + res.send = sinon.spy(function (data: any) { + assert.equal(500, res.status.getCall(0).args[0]); + done(); + }); + const user_key_container = { + user: {} + }; + const u2f_mock = AuthdogMock.AuthdogMock(); + u2f_mock.startAuthentication.returns(Promise.reject("Internal error")); + + req.app.get.withArgs("u2f").returns(u2f_mock); + u2f.sign_request(req as any, res as any, undefined); + }); + + it("should send unauthorized error when no registration exists", function (done) { + const expectedRequest = { + test: "abc" + }; + res.send = sinon.spy(function (data: any) { + assert.equal(401, res.status.getCall(0).args[0]); + done(); + }); + const user_key_container = {}; // no entry means no registration + const u2f_mock = AuthdogMock.AuthdogMock(); + 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.sign_request(req as any, res as any, undefined); + }); + } + + function test_signing() { + it("should return status code 204", function (done) { + const user_key_container = { + user: {} + }; + const expectedStatus = { + keyHandle: "keyHandle", + publicKey: "pbk", + certificate: "cert" + }; + res.send = sinon.spy(function (data: any) { + assert(204, res.status.getCall(0).args[0]); + assert(req.session.auth_session.second_factor); + done(); + }); + const u2f_mock = AuthdogMock.AuthdogMock(); + u2f_mock.finishAuthentication.returns(Promise.resolve(expectedStatus)); + + req.session.auth_session.sign_request = {}; + req.app.get.withArgs("u2f").returns(u2f_mock); + u2f.sign(req as any, res as any, undefined); + }); + + it("should return unauthorized error on registration request internal error", function (done) { + res.send = sinon.spy(function (data: any) { + assert.equal(500, res.status.getCall(0).args[0]); + done(); + }); + const user_key_container = { + user: {} + }; + + const u2f_mock = AuthdogMock.AuthdogMock(); + u2f_mock.finishAuthentication.returns(Promise.reject("Internal error")); + + req.session.auth_session.sign_request = {}; + req.app.get.withArgs("u2f").returns(u2f_mock); + u2f.sign(req as any, res as any, undefined); + }); + + it("should return unauthorized error when no sign request has been initiated", function (done) { + res.send = sinon.spy(function (data: any) { + assert.equal(401, res.status.getCall(0).args[0]); + done(); + }); + const user_key_container = {}; + const u2f_mock = AuthdogMock.AuthdogMock(); + u2f_mock.finishAuthentication.returns(Promise.resolve()); + + req.app.get.withArgs("u2f").returns(u2f_mock); + u2f.sign(req as any, res as any, undefined); + }); + } +}); + diff --git a/test/unitary/routes/test_totp_register.js b/test/unitary/routes/test_totp_register.js deleted file mode 100644 index 6ef33c69..00000000 --- a/test/unitary/routes/test_totp_register.js +++ /dev/null @@ -1,136 +0,0 @@ -var sinon = require('sinon'); -var winston = require('winston'); -var totp_register = require('../../../src/lib/routes/totp_register'); -var assert = require('assert'); -var Promise = require('bluebird'); - -describe('test totp register', function() { - var req, res; - var user_data_store; - - beforeEach(function() { - req = {} - req.app = {}; - req.app.get = sinon.stub(); - req.app.get.withArgs('logger').returns(winston); - req.session = {}; - req.session.auth_session = {}; - req.session.auth_session.userid = 'user'; - req.session.auth_session.email = 'user@example.com'; - req.session.auth_session.first_factor = true; - req.session.auth_session.second_factor = false; - 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({})); - user_data_store.issue_identity_check_token = sinon.stub().returns(Promise.resolve({})); - user_data_store.consume_identity_check_token = sinon.stub().returns(Promise.resolve({})); - user_data_store.set_totp_secret = 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(); - }); - - describe('test totp registration check', test_registration_check); - describe('test totp post secret', test_post_secret); - - function test_registration_check() { - it('should fail if first_factor has not been passed', function(done) { - req.session.auth_session.first_factor = false; - totp_register.icheck_interface.pre_check_callback(req) - .catch(function(err) { - done(); - }); - }); - - it('should fail if userid is missing', function(done) { - req.session.auth_session.first_factor = false; - req.session.auth_session.userid = undefined; - - totp_register.icheck_interface.pre_check_callback(req) - .catch(function(err) { - done(); - }); - }); - - it('should fail if email is missing', function(done) { - req.session.auth_session.first_factor = false; - req.session.auth_session.email = undefined; - - totp_register.icheck_interface.pre_check_callback(req) - .catch(function(err) { - done(); - }); - }); - - it('should succeed if first factor passed, userid and email are provided', function(done) { - totp_register.icheck_interface.pre_check_callback(req) - .then(function(err) { - done(); - }); - }); - } - - function test_post_secret() { - it('should send the secret in json format', function(done) { - 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'; - res.json = sinon.spy(function() { - done(); - }); - totp_register.post(req, res); - }); - - it('should clear the session for reauthentication', function(done) { - 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'; - res.json = sinon.spy(function() { - assert.equal(req.session, undefined); - done(); - }); - totp_register.post(req, res); - }); - - it('should return 403 if the identity check challenge is not set', function(done) { - req.session.auth_session.identity_check = {}; - req.session.auth_session.identity_check.challenge = undefined; - res.send = sinon.spy(function() { - assert.equal(res.status.getCall(0).args[0], 403); - done(); - }); - totp_register.post(req, res); - }); - - it('should return 500 if db throws', function(done) { - 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'; - user_data_store.set_totp_secret.returns(new Promise.reject('internal error')); - - res.send = sinon.spy(function() { - assert.equal(res.status.getCall(0).args[0], 500); - done(); - }); - totp_register.post(req, res); - }); -  } -}); diff --git a/test/unitary/routes/test_u2f.js b/test/unitary/routes/test_u2f.js deleted file mode 100644 index f26cb673..00000000 --- a/test/unitary/routes/test_u2f.js +++ /dev/null @@ -1,280 +0,0 @@ - -var sinon = require('sinon'); -var Promise = require('bluebird'); -var assert = require('assert'); -var u2f = require('../../../src/lib/routes/u2f'); -var winston = require('winston'); - -describe('test u2f routes', function() { - var req, res; - var user_data_store; - - beforeEach(function() { - req = {} - req.app = {}; - req.app.get = sinon.stub(); - req.app.get.withArgs('logger').returns(winston); - req.session = {}; - req.session.auth_session = {}; - req.session.auth_session.userid = 'user'; - req.session.auth_session.first_factor = true; - req.session.auth_session.second_factor = false; - req.session.auth_session.identity_check = {}; - req.session.auth_session.identity_check.challenge = 'u2f-register'; - req.session.auth_session.register_request = {}; - 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(); - }) - - describe('test registration request', test_registration_request); - describe('test registration', test_registration); - 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 = { - test: 'abc' - }; - res.json = sinon.spy(function(data) { - assert.equal(200, res.status.getCall(0).args[0]); - assert.deepEqual(expectedRequest, data); - done(); - }); - var user_key_container = {}; - var u2f_mock = {}; - u2f_mock.startRegistration = sinon.stub(); - u2f_mock.startRegistration.returns(Promise.resolve(expectedRequest)); - - req.app.get.withArgs('u2f').returns(u2f_mock); - u2f.register_request(req, res); - }); - - it('should return internal error on registration request', function(done) { - res.send = sinon.spy(function(data) { - assert.equal(500, res.status.getCall(0).args[0]); - done(); - }); - var user_key_container = {}; - var u2f_mock = {}; - u2f_mock.startRegistration = sinon.stub(); - u2f_mock.startRegistration.returns(Promise.reject('Internal error')); - - req.app.get.withArgs('u2f').returns(u2f_mock); - u2f.register_request(req, res); - }); - - it('should return forbidden if identity has not been verified', function(done) { - res.send = sinon.spy(function(data) { - assert.equal(403, res.status.getCall(0).args[0]); - done(); - }); - req.session.auth_session.identity_check = undefined; - u2f.register_request(req, res); - }); - } - - function test_registration() { - 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.equal('user', user_data_store.set_u2f_meta.getCall(0).args[0]) - assert.equal(req.session.auth_session.identity_check, undefined); - done(); - }); - var u2f_mock = {}; - u2f_mock.finishRegistration = sinon.stub(); - u2f_mock.finishRegistration.returns(Promise.resolve(expectedStatus)); - - req.session.auth_session.register_request = {}; - req.app.get.withArgs('u2f').returns(u2f_mock); - u2f.register(req, res); - }); - - it('should return unauthorized on finishRegistration error', function(done) { - res.send = sinon.spy(function(data) { - assert.equal(500, res.status.getCall(0).args[0]); - done(); - }); - var user_key_container = {}; - var u2f_mock = {}; - u2f_mock.finishRegistration = sinon.stub(); - u2f_mock.finishRegistration.returns(Promise.reject('Internal error')); - - req.session.auth_session.register_request = 'abc'; - req.app.get.withArgs('u2f').returns(u2f_mock); - u2f.register(req, res); - }); - - it('should return 403 when register_request is not provided', function(done) { - res.send = sinon.spy(function(data) { - assert.equal(403, res.status.getCall(0).args[0]); - done(); - }); - var user_key_container = {}; - var u2f_mock = {}; - u2f_mock.finishRegistration = sinon.stub(); - u2f_mock.finishRegistration.returns(Promise.resolve()); - - req.session.auth_session.register_request = undefined; - req.app.get.withArgs('u2f').returns(u2f_mock); - u2f.register(req, res); - }); - - it('should return forbidden error when no auth request has been initiated', function(done) { - res.send = sinon.spy(function(data) { - assert.equal(403, res.status.getCall(0).args[0]); - done(); - }); - var user_key_container = {}; - var u2f_mock = {}; - u2f_mock.finishRegistration = sinon.stub(); - u2f_mock.finishRegistration.returns(Promise.resolve()); - - req.session.auth_session.register_request = undefined; - req.app.get.withArgs('u2f').returns(u2f_mock); - u2f.register(req, res); - }); - - it('should return forbidden error when identity has not been verified', function(done) { - res.send = sinon.spy(function(data) { - assert.equal(403, res.status.getCall(0).args[0]); - done(); - }); - req.session.auth_session.identity_check = undefined; - u2f.register(req, res); - }); - } - - function test_signing_request() { - it('should send back the sign request and save it in the session', function(done) { - var expectedRequest = { - test: 'abc' - }; - res.json = sinon.spy(function(data) { - assert.deepEqual(expectedRequest, req.session.auth_session.sign_request); - assert.equal(200, res.status.getCall(0).args[0]); - assert.deepEqual(expectedRequest, data); - done(); - }); - var user_key_container = {}; - user_key_container['user'] = {}; // simulate a registration - var u2f_mock = {}; - u2f_mock.startAuthentication = sinon.stub(); - u2f_mock.startAuthentication.returns(Promise.resolve(expectedRequest)); - - req.app.get.withArgs('u2f').returns(u2f_mock); - u2f.sign_request(req, res); - }); - - it('should return unauthorized error on registration request error', function(done) { - res.send = sinon.spy(function(data) { - assert.equal(500, res.status.getCall(0).args[0]); - done(); - }); - var user_key_container = {}; - user_key_container['user'] = {}; // simulate a registration - var u2f_mock = {}; - u2f_mock.startAuthentication = sinon.stub(); - u2f_mock.startAuthentication.returns(Promise.reject('Internal error')); - - req.app.get.withArgs('u2f').returns(u2f_mock); - u2f.sign_request(req, res); - }); - - it('should send unauthorized error when no registration exists', function(done) { - var expectedRequest = { - test: 'abc' - }; - res.send = sinon.spy(function(data) { - assert.equal(401, res.status.getCall(0).args[0]); - done(); - }); - var user_key_container = {}; // no entry means no registration - var u2f_mock = {}; - 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.sign_request(req, res); - }); - } - - function test_signing() { - it('should return status code 204', function(done) { - var user_key_container = {}; - user_key_container['user'] = {}; - var expectedStatus = { - keyHandle: 'keyHandle', - publicKey: 'pbk', - certificate: 'cert' - }; - res.send = sinon.spy(function(data) { - assert(204, res.status.getCall(0).args[0]); - assert(req.session.auth_session.second_factor); - done(); - }); - var u2f_mock = {}; - u2f_mock.finishAuthentication = sinon.stub(); - u2f_mock.finishAuthentication.returns(Promise.resolve(expectedStatus)); - - req.session.auth_session.sign_request = {}; - req.app.get.withArgs('u2f').returns(u2f_mock); - u2f.sign(req, res); - }); - - it('should return unauthorized error on registration request internal error', function(done) { - res.send = sinon.spy(function(data) { - assert.equal(500, res.status.getCall(0).args[0]); - done(); - }); - var user_key_container = {}; - user_key_container['user'] = {}; - - var u2f_mock = {}; - u2f_mock.finishAuthentication = sinon.stub(); - u2f_mock.finishAuthentication.returns(Promise.reject('Internal error')); - - req.session.auth_session.sign_request = {}; - req.app.get.withArgs('u2f').returns(u2f_mock); - u2f.sign(req, res); - }); - - it('should return unauthorized error when no sign request has been initiated', function(done) { - res.send = sinon.spy(function(data) { - assert.equal(401, res.status.getCall(0).args[0]); - done(); - }); - var user_key_container = {}; - var u2f_mock = {}; - u2f_mock.finishAuthentication = sinon.stub(); - u2f_mock.finishAuthentication.returns(Promise.resolve()); - - req.app.get.withArgs('u2f').returns(u2f_mock); - u2f.sign(req, res); - }); - } -}); - diff --git a/test/unitary/routes/test_u2f_register.js b/test/unitary/routes/test_u2f_register.js deleted file mode 100644 index c3860ea5..00000000 --- a/test/unitary/routes/test_u2f_register.js +++ /dev/null @@ -1,78 +0,0 @@ -var sinon = require('sinon'); -var winston = require('winston'); -var u2f_register = require('../../../src/lib/routes/u2f_register_handler'); -var assert = require('assert'); - -describe('test register handler', function() { - var req, res; - var user_data_store; - - beforeEach(function() { - req = {} - req.app = {}; - req.app.get = sinon.stub(); - req.app.get.withArgs('logger').returns(winston); - req.session = {}; - req.session.auth_session = {}; - req.session.auth_session.userid = 'user'; - req.session.auth_session.email = 'user@example.com'; - req.session.auth_session.first_factor = true; - req.session.auth_session.second_factor = false; - 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({})); - user_data_store.issue_identity_check_token = sinon.stub().returns(Promise.resolve({})); - user_data_store.consume_identity_check_token = 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(); - }); - - describe('test u2f registration check', test_registration_check); - - function test_registration_check() { - it('should fail if first_factor has not been passed', function(done) { - req.session.auth_session.first_factor = false; - u2f_register.icheck_interface.pre_check_callback(req) - .catch(function(err) { - done(); - }); - }); - - it('should fail if userid is missing', function(done) { - req.session.auth_session.first_factor = false; - req.session.auth_session.userid = undefined; - - u2f_register.icheck_interface.pre_check_callback(req) - .catch(function(err) { - done(); - }); - }); - - it('should fail if email is missing', function(done) { - req.session.auth_session.first_factor = false; - req.session.auth_session.email = undefined; - - u2f_register.icheck_interface.pre_check_callback(req) - .catch(function(err) { - done(); - }); - }); - - it('should succeed if first factor passed, userid and email are provided', function(done) { - u2f_register.icheck_interface.pre_check_callback(req) - .then(function(err) { - done(); - }); - }); - } -}); diff --git a/test/unitary/test_identity_check.js b/test/unitary/test_identity_check.js deleted file mode 100644 index fde553c1..00000000 --- a/test/unitary/test_identity_check.js +++ /dev/null @@ -1,205 +0,0 @@ - -var sinon = require('sinon'); -var identity_check = require('../../src/lib/identity_check'); -var exceptions = require('../../src/lib/Exceptions'); -var assert = require('assert'); -var winston = require('winston'); -var Promise = require('bluebird'); - -describe('test identity check process', function() { - var req, res, app, icheck_interface; - var user_data_store; - var notifier; - - beforeEach(function() { - req = {}; - res = {}; - - app = {}; - icheck_interface = {}; - icheck_interface.pre_check_callback = sinon.stub(); - - user_data_store = {}; - user_data_store.issue_identity_check_token = sinon.stub(); - user_data_store.issue_identity_check_token.returns(Promise.resolve()); - user_data_store.consume_identity_check_token = sinon.stub(); - user_data_store.consume_identity_check_token.returns(Promise.resolve({ userid: 'user' })); - - notifier = {}; - notifier.notify = sinon.stub().returns(Promise.resolve()); - - req.headers = {}; - req.session = {}; - req.session.auth_session = {}; - - req.query = {}; - req.app = {}; - 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('notifier').returns(notifier); - - res.status = sinon.spy(); - res.send = sinon.spy(); - res.redirect = sinon.spy(); - res.render = sinon.spy(); - - app.get = sinon.spy(); - app.post = sinon.spy(); - }); - - it('should register a POST and GET endpoint', function() { - var app = {}; - app.get = sinon.spy(); - app.post = sinon.spy(); - var endpoint = '/test'; - var icheck_interface = {}; - - identity_check(app, endpoint, icheck_interface); - - assert(app.get.calledOnce); - assert(app.get.calledWith(endpoint)); - - assert(app.post.calledOnce); - assert(app.post.calledWith(endpoint)); - }); - - describe('test POST', test_post_handler); - describe('test GET', test_get_handler); - - function test_post_handler() { - it('should send 403 if pre check rejects', function(done) { - var endpoint = '/protected'; - - icheck_interface.pre_check_callback.returns(Promise.reject('No access')); - identity_check(app, endpoint, icheck_interface); - - res.send = sinon.spy(function() { - assert.equal(res.status.getCall(0).args[0], 403); - done(); - }); - - var handler = app.post.getCall(0).args[1]; - handler(req, res); - }); - - it('should send 400 if email is missing in provided identity', function(done) { - var endpoint = '/protected'; - var identity = { userid: 'abc' }; - - icheck_interface.pre_check_callback.returns(Promise.resolve(identity)); - identity_check(app, endpoint, icheck_interface); - - res.send = sinon.spy(function() { - assert.equal(res.status.getCall(0).args[0], 400); - done(); - }); - - var handler = app.post.getCall(0).args[1]; - handler(req, res); - }); - - it('should send 400 if userid is missing in provided identity', function(done) { - var endpoint = '/protected'; - var identity = { email: 'abc@example.com' }; - - icheck_interface.pre_check_callback.returns(Promise.resolve(identity)); - identity_check(app, endpoint, icheck_interface); - - res.send = sinon.spy(function() { - assert.equal(res.status.getCall(0).args[0], 400); - done(); - }); - var handler = app.post.getCall(0).args[1]; - handler(req, res); - }); - - it('should issue a token, send an email and return 204', function(done) { - var endpoint = '/protected'; - var identity = { userid: 'user', email: 'abc@example.com' }; - req.headers.host = 'localhost'; - req.headers['x-original-uri'] = '/auth/test'; - - icheck_interface.pre_check_callback.returns(Promise.resolve(identity)); - identity_check(app, endpoint, icheck_interface); - - res.send = sinon.spy(function() { - assert.equal(res.status.getCall(0).args[0], 204); - 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); - done(); - }); - var handler = app.post.getCall(0).args[1]; - handler(req, res); - }); - } - - function test_get_handler() { - it('should send 403 if no identity_token is provided', function(done) { - var endpoint = '/protected'; - - identity_check(app, endpoint, icheck_interface); - - res.send = sinon.spy(function() { - assert.equal(res.status.getCall(0).args[0], 403); - done(); - }); - var handler = app.get.getCall(0).args[1]; - handler(req, res); - }); - - it('should render template if identity_token is provided and still valid', function(done) { - req.query.identity_token = 'token'; - var endpoint = '/protected'; - - icheck_interface.render_template = 'template'; - - identity_check(app, endpoint, icheck_interface); - - res.render = sinon.spy(function(template) { - assert.equal(template, 'template'); - done(); - }); - var handler = app.get.getCall(0).args[1]; - handler(req, res); - }); - - it('should return 403 if identity_token is provided but invalid', function(done) { - req.query.identity_token = 'token'; - var endpoint = '/protected'; - - icheck_interface.render_template = 'template'; - user_data_store.consume_identity_check_token - .returns(Promise.reject('Invalid token')); - - identity_check(app, endpoint, icheck_interface); - - res.send = sinon.spy(function(template) { - assert.equal(res.status.getCall(0).args[0], 403); - done(); - }); - var handler = app.get.getCall(0).args[1]; - handler(req, res); - }); - - it('should set the identity_check session object even if session does not exist yet', function(done) { - req.query.identity_token = 'token'; - var endpoint = '/protected'; - - req.session = {}; - icheck_interface.render_template = 'template'; - - identity_check(app, endpoint, icheck_interface); - - res.render = sinon.spy(function(template) { - assert.equal(req.session.auth_session.identity_check.userid, 'user'); - assert.equal(template, 'template'); - done(); - }); - var handler = app.get.getCall(0).args[1]; - handler(req, res); - }); - } -}); diff --git a/tsconfig.json b/tsconfig.json index ebb0c747..547417dd 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -7,7 +7,6 @@ "sourceMap": true, "outDir": "dist", "baseUrl": ".", - "allowJs": true, "paths": { "*": [ "src/types/*", From 9fddcc7e93c26372205c048cae02a2d9c1451977 Mon Sep 17 00:00:00 2001 From: Clement Michaud Date: Sun, 21 May 2017 23:32:09 +0200 Subject: [PATCH 16/16] Fix issue with domain access during first factor phase --- src/lib/Exceptions.ts | 8 ++++ src/lib/routes/AuthenticationValidator.ts | 14 +++++- src/lib/routes/FirstFactor.ts | 28 +++++++----- .../routes/AuthenticationValidator.test.ts | 14 ++++++ test/unitary/routes/FirstFactor.test.ts | 43 ++++++++++--------- 5 files changed, 74 insertions(+), 33 deletions(-) diff --git a/src/lib/Exceptions.ts b/src/lib/Exceptions.ts index 38d054c0..56fcbb7d 100644 --- a/src/lib/Exceptions.ts +++ b/src/lib/Exceptions.ts @@ -46,3 +46,11 @@ export class InvalidTOTPError extends Error { Object.setPrototypeOf(this, InvalidTOTPError.prototype); } } + +export class DomainAccessDenied extends Error { + constructor(message?: string) { + super(message); + this.name = "DomainAccessDenied"; + Object.setPrototypeOf(this, DomainAccessDenied.prototype); + } +} diff --git a/src/lib/routes/AuthenticationValidator.ts b/src/lib/routes/AuthenticationValidator.ts index 38b861da..d5ae1178 100644 --- a/src/lib/routes/AuthenticationValidator.ts +++ b/src/lib/routes/AuthenticationValidator.ts @@ -2,9 +2,12 @@ import objectPath = require("object-path"); import BluebirdPromise = require("bluebird"); import express = require("express"); +import AccessController from "../access_control/AccessController"; +import exceptions = require("../Exceptions"); function verify_filter(req: express.Request, res: express.Response) { const logger = req.app.get("logger"); + const accessController: AccessController = req.app.get("access controller"); if (!objectPath.has(req, "session.auth_session")) return BluebirdPromise.reject("No auth_session variable"); @@ -18,17 +21,24 @@ function verify_filter(req: express.Request, res: express.Response) { if (!objectPath.has(req, "session.auth_session.userid")) return BluebirdPromise.reject("No userid variable"); + const username = objectPath.get(req, "session.auth_session.userid"); + const groups = objectPath.get(req, "session.auth_session.groups"); + const host = objectPath.get(req, "headers.host"); const domain = host.split(":")[0]; + const isAllowed = accessController.isDomainAllowedForUser(domain, username, groups); + if (!isAllowed) return BluebirdPromise.reject( + new exceptions.DomainAccessDenied("User '" + username + "' does not have access to " + domain)); + if (!req.session.auth_session.first_factor || !req.session.auth_session.second_factor) - return BluebirdPromise.reject("First or second factor not validated"); + return BluebirdPromise.reject(new exceptions.AccessDeniedError("First or second factor not validated")); return BluebirdPromise.resolve(); } -export = function(req: express.Request, res: express.Response) { +export = function (req: express.Request, res: express.Response) { verify_filter(req, res) .then(function () { res.status(204); diff --git a/src/lib/routes/FirstFactor.ts b/src/lib/routes/FirstFactor.ts index 3a67f468..7d33afc9 100644 --- a/src/lib/routes/FirstFactor.ts +++ b/src/lib/routes/FirstFactor.ts @@ -3,10 +3,13 @@ import exceptions = require("../Exceptions"); import objectPath = require("object-path"); import BluebirdPromise = require("bluebird"); import express = require("express"); +import AccessController from "../access_control/AccessController"; +import AuthenticationRegulator from "../AuthenticationRegulator"; +import { LdapClient } from "../LdapClient"; export = function (req: express.Request, res: express.Response) { - const username = req.body.username; - const password = req.body.password; + const username: string = req.body.username; + const password: string = req.body.password; if (!username || !password) { res.status(401); res.send(); @@ -14,10 +17,10 @@ export = function (req: express.Request, res: express.Response) { } const logger = req.app.get("logger"); - const ldap = req.app.get("ldap"); + const ldap: LdapClient = req.app.get("ldap"); const config = req.app.get("config"); - const regulator = req.app.get("authentication regulator"); - const accessController = req.app.get("access controller"); + const regulator: AuthenticationRegulator = req.app.get("authentication regulator"); + const accessController: AccessController = req.app.get("access controller"); logger.info("1st factor: Starting authentication of user \"%s\"", username); logger.debug("1st factor: Start bind operation against LDAP"); @@ -34,16 +37,14 @@ export = function (req: express.Request, res: express.Response) { logger.debug("1st factor: Retrieve email from LDAP"); return BluebirdPromise.join(ldap.get_emails(username), ldap.get_groups(username)); }) - .then(function (data: string[2]) { - const emails = data[0]; - const groups = data[1]; + .then(function (data: [string[], string[]]) { + const emails: string[] = data[0]; + const groups: string[] = data[1]; if (!emails && emails.length <= 0) throw new Error("No email found"); logger.debug("1st factor: Retrieved email are %s", emails); objectPath.set(req, "session.auth_session.email", emails[0]); - - const isAllowed = accessController.isDomainAllowedForUser(username, groups); - if (!isAllowed) throw new Error("User not allowed to visit this domain"); + objectPath.set(req, "session.auth_session.groups", groups); regulator.mark(username, true); res.status(204); @@ -67,6 +68,11 @@ export = function (req: express.Request, res: express.Response) { res.status(403); res.send("Access has been restricted for a few minutes..."); }) + .catch(exceptions.DomainAccessDenied, (err: Error) => { + logger.error("1st factor: ", err); + res.status(401); + res.send("Access denied..."); + }) .catch(function (err: Error) { console.log(err.stack); logger.error("1st factor: Unhandled error %s", err); diff --git a/test/unitary/routes/AuthenticationValidator.test.ts b/test/unitary/routes/AuthenticationValidator.test.ts index 8d54c359..33f27f4a 100644 --- a/test/unitary/routes/AuthenticationValidator.test.ts +++ b/test/unitary/routes/AuthenticationValidator.test.ts @@ -105,6 +105,20 @@ describe("test authentication token verification", function () { it("should not be authenticated when session is partially initialized", function () { return test_unauthorized({ first_factor: true }); }); + + it.only("should not be authenticated when domain is not allowed for user", function () { + req.headers.host = "test.example.com"; + + accessController.isDomainAllowedForUser.returns(false); + accessController.isDomainAllowedForUser.withArgs("test.example.com", "user", ["group1", "group2"]).returns(true); + + return test_authorized({ + first_factor: true, + second_factor: true, + userid: "user", + groups: ["group1", "group2"] + }); + }); }); }); diff --git a/test/unitary/routes/FirstFactor.test.ts b/test/unitary/routes/FirstFactor.test.ts index 8c48a0eb..0ee5b07e 100644 --- a/test/unitary/routes/FirstFactor.test.ts +++ b/test/unitary/routes/FirstFactor.test.ts @@ -11,7 +11,7 @@ import AccessControllerMock = require("../mocks/AccessController"); import { LdapClientMock } from "../mocks/LdapClient"; import ExpressMock = require("../mocks/express"); -describe("test the first factor validation route", function() { +describe("test the first factor validation route", function () { let req: ExpressMock.RequestMock; let res: ExpressMock.ResponseMock; let emails: string[]; @@ -21,7 +21,7 @@ describe("test the first factor validation route", function() { let regulator: AuthenticationRegulatorMock.AuthenticationRegulatorMock; let accessController: AccessControllerMock.AccessControllerMock; - beforeEach(function() { + beforeEach(function () { configuration = { ldap: { base_dn: "ou=users,dc=example,dc=com", @@ -29,8 +29,8 @@ describe("test the first factor validation route", function() { } }; - emails = [ "test_ok@example.com" ]; - groups = [ "group1", "group2" ]; + emails = ["test_ok@example.com"]; + groups = ["group1", "group2" ]; ldapMock = LdapClientMock(); @@ -61,14 +61,17 @@ describe("test the first factor validation route", function() { FirstFactor: false, second_factor: false } + }, + headers: { + host: "home.example.com" } }; res = ExpressMock.ResponseMock(); }); - it("should return status code 204 when LDAP binding succeeds", function() { - return new Promise(function(resolve, reject) { - res.send = sinon.spy(function() { + it("should return status code 204 when LDAP binding succeeds", function () { + return new Promise(function (resolve, reject) { + res.send = sinon.spy(function () { assert.equal("username", req.session.auth_session.userid); assert.equal(204, res.status.getCall(0).args[0]); resolve(); @@ -79,28 +82,28 @@ describe("test the first factor validation route", function() { }); }); - it("should retrieve email from LDAP", function(done) { - res.send = sinon.spy(function() { done(); }); + it("should retrieve email from LDAP", function (done) { + res.send = sinon.spy(function () { done(); }); ldapMock.bind.returns(BluebirdPromise.resolve()); - ldapMock.get_emails = sinon.stub().withArgs("username").returns(BluebirdPromise.resolve([{mail: ["test@example.com"] }])); + ldapMock.get_emails = sinon.stub().withArgs("username").returns(BluebirdPromise.resolve([{ mail: ["test@example.com"] }])); FirstFactor(req as any, res as any); }); - it("should set email as session variables", function() { - return new Promise(function(resolve, reject) { - res.send = sinon.spy(function() { + it("should set email as session variables", function () { + return new Promise(function (resolve, reject) { + res.send = sinon.spy(function () { assert.equal("test_ok@example.com", req.session.auth_session.email); resolve(); }); - const emails = [ "test_ok@example.com" ]; + const emails = ["test_ok@example.com"]; ldapMock.bind.returns(BluebirdPromise.resolve()); ldapMock.get_emails.returns(BluebirdPromise.resolve(emails)); FirstFactor(req as any, res as any); }); }); - it("should return status code 401 when LDAP binding throws", function(done) { - res.send = sinon.spy(function() { + it("should return status code 401 when LDAP binding throws", function (done) { + res.send = sinon.spy(function () { assert.equal(401, res.status.getCall(0).args[0]); assert.equal(regulator.mark.getCall(0).args[0], "username"); done(); @@ -109,8 +112,8 @@ describe("test the first factor validation route", function() { FirstFactor(req as any, res as any); }); - it("should return status code 500 when LDAP search throws", function(done) { - res.send = sinon.spy(function() { + it("should return status code 500 when LDAP search throws", function (done) { + res.send = sinon.spy(function () { assert.equal(500, res.status.getCall(0).args[0]); done(); }); @@ -119,11 +122,11 @@ describe("test the first factor validation route", function() { FirstFactor(req as any, res as any); }); - it("should return status code 403 when regulator rejects authentication", function(done) { + it("should return status code 403 when regulator rejects authentication", function (done) { const err = new exceptions.AuthenticationRegulationError("Authentication regulation..."); regulator.regulate.returns(BluebirdPromise.reject(err)); - res.send = sinon.spy(function() { + res.send = sinon.spy(function () { assert.equal(403, res.status.getCall(0).args[0]); done(); });