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/.travis.yml b/.travis.yml index b1d4832f..70c3899a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -19,8 +19,9 @@ addons: before_install: npm install -g npm@'>=2.13.5' script: -- npm test -- docker build -t clems4ever/authelia . +- grunt test +- grunt build +- grunt docker-build - docker-compose build - docker-compose up -d - sleep 5 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/Gruntfile.js b/Gruntfile.js new file mode 100644 index 00000000..4b248405 --- /dev/null +++ b/Gruntfile.js @@ -0,0 +1,55 @@ +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'] + }, + "docker-build": { + cmd: "docker", + args: ['build', '-t', 'clems4ever/authelia', '.'] + } + }, + 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('docker-build', ['run:docker-build']); + + grunt.registerTask('test', ['run:test']); +}; diff --git a/package.json b/package.json index 563d58a8..7af9aba6 100644 --- a/package.json +++ b/package.json @@ -7,11 +7,14 @@ "authelia": "src/index.js" }, "scripts": { - "test": "./node_modules/.bin/mocha --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", + "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", - "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 +46,39 @@ "yamljs": "^0.2.8" }, "devDependencies": { + "@types/assert": "0.0.31", + "@types/bluebird": "^3.5.4", + "@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", + "@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/proxyquire": "^1.3.27", + "@types/randomstring": "^1.1.5", + "@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", + "grunt-contrib-copy": "^1.0.0", + "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", "sinon-promise": "^0.1.3", - "tmp": "0.0.31" + "tmp": "0.0.31", + "ts-node": "^3.0.4", + "tslint": "^5.2.0", + "typescript": "^2.3.2" } } 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..c16865f3 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,33 @@ +#! /usr/bin/env node + +process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0"; + +import 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") +}; + +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 new file mode 100644 index 00000000..60fbbcfd --- /dev/null +++ b/src/lib/AuthenticationRegulator.ts @@ -0,0 +1,43 @@ + +import * as BluebirdPromise from "bluebird"; +import exceptions = require("./Exceptions"); + +const REGULATION_TRACE_TYPE = "regulation"; +const MAX_AUTHENTICATION_COUNT_IN_TIME_RANGE = 3; + +interface DatedDocument { + date: Date; +} + +export default class AuthenticationRegulator { + 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; + this._lock_time_in_seconds = lock_time_in_seconds; + } + + // Mark authentication + mark(userid: string, is_success: boolean): BluebirdPromise { + return this._user_data_store.save_authentication_trace(userid, REGULATION_TRACE_TYPE, is_success); + } + + 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 BluebirdPromise.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 BluebirdPromise.resolve(); + }); + } +} diff --git a/src/lib/Configuration.ts b/src/lib/Configuration.ts new file mode 100644 index 00000000..ece9acfc --- /dev/null +++ b/src/lib/Configuration.ts @@ -0,0 +1,66 @@ + +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; + +export type ACLDefaultRules = DomainPattern[]; +export type ACLGroupsRules = { [group: string]: string[]; }; +export type ACLUsersRules = { [user: string]: string[]; }; + +export interface ACLConfiguration { + default: ACLDefaultRules; + groups: ACLGroupsRules; + users: ACLUsersRules; +} + +interface SessionCookieConfiguration { + secret: string; + expiration?: number; + domain?: string; +} + +export interface GmailNotifierConfiguration { + username: string; + password: string; +} + +export interface FileSystemNotifierConfiguration { + filename: string; +} + +export interface NotifierConfiguration { + gmail?: GmailNotifierConfiguration; + filesystem?: FileSystemNotifierConfiguration; +} + +export interface UserConfiguration { + port?: number; + logs_level?: string; + ldap: LdapConfiguration; + session: SessionCookieConfiguration; + store_directory?: string; + notifier: NotifierConfiguration; + access_control?: ACLConfiguration; +} + +export interface AppConfiguration { + port: number; + logs_level: string; + ldap: LdapConfiguration; + session: SessionCookieConfiguration; + store_in_memory?: boolean; + store_directory?: string; + 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/Exceptions.ts b/src/lib/Exceptions.ts new file mode 100644 index 00000000..56fcbb7d --- /dev/null +++ b/src/lib/Exceptions.ts @@ -0,0 +1,56 @@ + +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); + } +} + +export class DomainAccessDenied extends Error { + constructor(message?: string) { + super(message); + this.name = "DomainAccessDenied"; + Object.setPrototypeOf(this, DomainAccessDenied.prototype); + } +} 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 new file mode 100644 index 00000000..d50414e2 --- /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 "../types/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 BluebirdPromise.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 BluebirdPromise.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/RestApi.ts b/src/lib/RestApi.ts new file mode 100644 index 00000000..558321b2 --- /dev/null +++ b/src/lib/RestApi.ts @@ -0,0 +1,282 @@ + +import express = require("express"); +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, userDataStore: UserDataStore, logger: ILogger): 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. + */ + IdentityValidator.IdentityValidator.setup(app, "/totp-register", routes.totp_register.icheck_interface, userDataStore, logger); + + + /** + * @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. + */ + IdentityValidator.IdentityValidator.setup(app, "/u2f-register", routes.u2f_register.icheck_interface, userDataStore, logger); + + /** + * @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. + */ + 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"); }); + + /** + * @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 new file mode 100644 index 00000000..da54cd38 --- /dev/null +++ b/src/lib/Server.ts @@ -0,0 +1,94 @@ + +import { UserConfiguration } from "./Configuration"; +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 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"; +import * as Path from "path"; +import * as http from "http"; + +import AccessController from "./access_control/AccessController"; + +export default class Server { + private httpServer: http.Server; + + start(yaml_configuration: UserConfiguration, deps: GlobalDependencies): BluebirdPromise { + const config = ConfigurationAdapter.adapt(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 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", 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, userDataStore, deps.winston); + + return new BluebirdPromise((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/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..09d8e15c --- /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(new Error("Wrong challenge")); + } +} \ No newline at end of file diff --git a/src/lib/UserDataStore.ts b/src/lib/UserDataStore.ts new file mode 100644 index 00000000..57f10508 --- /dev/null +++ b/src/lib/UserDataStore.ts @@ -0,0 +1,182 @@ +import * as BluebirdPromise from "bluebird"; +import * as path from "path"; +import { NedbAsync } from "nedb"; +import { TOTPSecret } from "../types/TOTPSecret"; +import { Nedb } from "../types/Dependencies"; + +// 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; +} + +export interface IdentityValidationRequestContent { + userid: string; + data: string; +} + +export interface IdentityValidationRequestDocument { + userid: string; + token: string; + content: IdentityValidationRequestContent; + max_date: Date; +} + +// 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; + private nedb: Nedb; + + 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 = + this.create_collection(IDENTITY_CHECK_TOKENS_COLLECTION_NAME, options); + this._authentication_traces_collection = + this.create_collection(AUTHENTICATION_TRACES_COLLECTION_NAME, options); + this._totp_secret_collection = + this.create_collection(TOTP_SECRETS_COLLECTION_NAME, options); + } + + set_u2f_meta(userid: string, appid: string, meta: Object): BluebirdPromise { + const newDocument = { + userid: userid, + appid: appid, + meta: meta + } as U2FMetaDocument; + + const filter = { + userid: userid, + appid: appid + }; + + return this._u2f_meta_collection.updateAsync(filter, newDocument, { upsert: true }); + } + + get_u2f_meta(userid: string, appid: string): BluebirdPromise { + 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): BluebirdPromise { + 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 = BluebirdPromise.promisify(query.exec, { context: query }); + return query_promisified(); + } + + issue_identity_check_token(userid: string, token: string, data: string | object, max_age: number): BluebirdPromise { + 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): BluebirdPromise { + const query = { + token: token + }; + + return this._identity_check_tokens_collection.findOneAsync(query) + .then(function (doc) { + if (!doc) { + 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 BluebirdPromise.reject("Registration token is not valid anymore"); + } + return BluebirdPromise.resolve(doc.content); + }) + .then((content) => { + return BluebirdPromise.join(this._identity_check_tokens_collection.removeAsync(query), + BluebirdPromise.resolve(content)); + }) + .then((v) => { + return BluebirdPromise.resolve(v[1]); + }); + } + + set_totp_secret(userid: string, secret: TOTPSecret): BluebirdPromise { + 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): BluebirdPromise { + const query = { + userid: userid + }; + return this._totp_secret_collection.findOneAsync(query); + } + + 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/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/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/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/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 deleted file mode 100644 index bef35534..00000000 --- a/src/lib/identity_check.js +++ /dev/null @@ -1,144 +0,0 @@ - -var objectPath = require('object-path'); -var randomstring = require('randomstring'); -var Promise = 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 Promise.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(); - }) - .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: IdentityError %s', err); - res.status(400); - res.send(); - }) - .catch(exceptions.AccessDeniedError, function(err) { - logger.error('POST identity_check: AccessDeniedError %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/ldap.js b/src/lib/ldap.js deleted file mode 100644 index 0007dffc..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(new exceptions.LdapSearchError(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 { + 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..ee6136ad --- /dev/null +++ b/src/lib/notifiers/GMailNotifier.ts @@ -0,0 +1,44 @@ + +import * as BluebirdPromise from "bluebird"; +import * as fs from "fs"; +import * as ejs from "ejs"; +import nodemailer = require("nodemailer"); + +import { Nodemailer } from "../../types/Dependencies"; +import { Identity } from "../../types/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, nodemailer: Nodemailer) { + super(); + const transporter = nodemailer.createTransport({ + service: "gmail", + auth: { + user: options.username, + pass: options.password + } + }); + this.transporter = BluebirdPromise.promisifyAll(transporter); + } + + notify(identity: Identity, subject: string, link: string): BluebirdPromise { + 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..e413047a --- /dev/null +++ b/src/lib/notifiers/INotifier.ts @@ -0,0 +1,7 @@ + +import * as BluebirdPromise from "bluebird"; +import { Identity } from "../../types/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..56986fdd --- /dev/null +++ b/src/lib/notifiers/NotifierFactory.ts @@ -0,0 +1,22 @@ + +import { NotifierConfiguration } from "..//Configuration"; +import { Nodemailer } from "../../types/Dependencies"; +import { INotifier } from "./INotifier"; + +import { GMailNotifier } from "./GMailNotifier"; +import { FileSystemNotifier } from "./FileSystemNotifier"; + +export class NotifierFactory { + static build(options: NotifierConfiguration, nodemailer: Nodemailer): INotifier { + if ("gmail" in options) { + return new GMailNotifier(options.gmail, 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/src/lib/routes.js b/src/lib/routes.js deleted file mode 100644 index 31655d4c..00000000 --- a/src/lib/routes.js +++ /dev/null @@ -1,39 +0,0 @@ - -var first_factor = require('./routes/first_factor'); -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..4c2d680d --- /dev/null +++ b/src/lib/routes.ts @@ -0,0 +1,41 @@ + +import FirstFactor = require("./routes/FirstFactor"); +import SecondFactorRoutes = require("./routes/SecondFactorRoutes"); +import PasswordReset = require("./routes/PasswordReset"); +import AuthenticationValidator = require("./routes/AuthenticationValidator"); +import U2FRegistration = require("./routes/U2FRegistration"); +import TOTPRegistration = require("./routes/TOTPRegistration"); +import objectPath = require("object-path"); + +import express = require("express"); + +export = { + login: serveLogin, + logout: serveLogout, + verify: AuthenticationValidator, + first_factor: FirstFactor, + second_factor: SecondFactorRoutes, + reset_password: PasswordReset, + u2f_register: U2FRegistration, + totp_register: TOTPRegistration, +}; + +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..d5ae1178 --- /dev/null +++ b/src/lib/routes/AuthenticationValidator.ts @@ -0,0 +1,53 @@ + +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"); + + 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 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(new exceptions.AccessDeniedError("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/DenyNotLogged.ts b/src/lib/routes/DenyNotLogged.ts new file mode 100644 index 00000000..2c2b71d9 --- /dev/null +++ b/src/lib/routes/DenyNotLogged.ts @@ -0,0 +1,19 @@ + +import objectPath = require("object-path"); +import express = require("express"); + +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; + if (!first_factor) { + res.status(403); + res.send(); + return; + } + callback(req, res, next); + }; +}; diff --git a/src/lib/routes/FirstFactor.ts b/src/lib/routes/FirstFactor.ts new file mode 100644 index 00000000..7d33afc9 --- /dev/null +++ b/src/lib/routes/FirstFactor.ts @@ -0,0 +1,82 @@ + +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: string = req.body.username; + const password: string = req.body.password; + if (!username || !password) { + res.status(401); + res.send(); + return; + } + + const logger = req.app.get("logger"); + const ldap: LdapClient = req.app.get("ldap"); + const config = req.app.get("config"); + 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"); + 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 BluebirdPromise.join(ldap.get_emails(username), ldap.get_groups(username)); + }) + .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]); + objectPath.set(req, "session.auth_session.groups", groups); + + 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(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); + res.status(500); + res.send("Internal error"); + }); +}; 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/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/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/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/first_factor.js b/src/lib/routes/first_factor.js deleted file mode 100644 index 9736805f..00000000 --- a/src/lib/routes/first_factor.js +++ /dev/null @@ -1,103 +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 acl_builder = req.app.get('access control').builder; - - 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]); - - 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); - - 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 deleted file mode 100644 index dd26bf5c..00000000 --- a/src/lib/routes/reset_password.js +++ /dev/null @@ -1,72 +0,0 @@ - -var Promise = 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) { - var err = new exceptions.AccessDeniedError(); - return Promise.reject(err); - } - - 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 Promise.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 f57149dd..00000000 --- a/src/lib/routes/second_factor.js +++ /dev/null @@ -1,17 +0,0 @@ - -var denyNotLogged = require('./deny_not_logged'); -var u2f = require('./u2f'); - -module.exports = { - totp: denyNotLogged(require('./totp')), - 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.js b/src/lib/routes/totp.js deleted file mode 100644 index 6621ba42..00000000 --- a/src/lib/routes/totp.js +++ /dev/null @@ -1,50 +0,0 @@ - -module.exports = totp; - -var totp = require('../totp'); -var objectPath = require('object-path'); -var exceptions = require('../../../src/lib/exceptions'); - -var UNAUTHORIZED_MESSAGE = 'Unauthorized access'; - -function totp(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 totp_engine = req.app.get('totp engine'); - 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) - }) - .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 deleted file mode 100644 index 4ca2094f..00000000 --- a/src/lib/routes/totp_register.js +++ /dev/null @@ -1,72 +0,0 @@ -var objectPath = require('object-path'); -var Promise = 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 Promise.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'); - } - - var identity = {}; - identity.email = email; - identity.userid = userid; - return Promise.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 totp = req.app.get('totp engine'); - var secret = totp.generateSecret(); - - 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 5161d965..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 Promise = 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 2c2600a3..00000000 --- a/src/lib/routes/u2f_register_handler.js +++ /dev/null @@ -1,37 +0,0 @@ - -var objectPath = require('object-path'); -var Promise = 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 Promise.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'); - } - - var identity = {}; - identity.email = email; - identity.userid = userid; - return Promise.resolve(identity); -} - diff --git a/src/lib/routes/verify.js b/src/lib/routes/verify.js deleted file mode 100644 index 0bea86e2..00000000 --- a/src/lib/routes/verify.js +++ /dev/null @@ -1,53 +0,0 @@ - -module.exports = verify; - -var objectPath = require('object-path'); -var Promise = 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'); - - if(!objectPath.has(req, 'session.auth_session.first_factor')) - return Promise.reject('No first factor variable'); - - if(!objectPath.has(req, 'session.auth_session.second_factor')) - return Promise.reject('No second factor variable'); - - 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) - return Promise.reject('First or second factor not validated'); - - return Promise.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/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/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); -} - 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/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/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/Dependencies.ts b/src/types/Dependencies.ts new file mode 100644 index 00000000..3047938f --- /dev/null +++ b/src/types/Dependencies.ts @@ -0,0 +1,23 @@ +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; +export type Winston = typeof winston; +export type Session = typeof session; +export type Nedb = typeof nedb; +export type Ldapjs = typeof ldapjs; + +export interface GlobalDependencies { + u2f: object; + nodemailer: Nodemailer; + ldapjs: Ldapjs; + session: Session; + winston: Winston; + speakeasy: Speakeasy; + nedb: Nedb; +} \ No newline at end of file diff --git a/src/types/ILogger.ts b/src/types/ILogger.ts new file mode 100644 index 00000000..96f03fe6 --- /dev/null +++ b/src/types/ILogger.ts @@ -0,0 +1,7 @@ + +import * as winston from "winston"; + +export interface ILogger { + debug: winston.LeveledLogMethod; +} + diff --git a/src/types/Identity.ts b/src/types/Identity.ts new file mode 100644 index 00000000..e985984e --- /dev/null +++ b/src/types/Identity.ts @@ -0,0 +1,6 @@ + + +export interface Identity { + userid: string; + email: string; +} \ No newline at end of file diff --git a/src/types/TOTPSecret.ts b/src/types/TOTPSecret.ts new file mode 100644 index 00000000..e4a6b7d7 --- /dev/null +++ b/src/types/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/types/authdog.d.ts b/src/types/authdog.d.ts new file mode 100644 index 00000000..4405f6f1 --- /dev/null +++ b/src/types/authdog.d.ts @@ -0,0 +1,69 @@ + +import BluebirdPromise = require("bluebird"); + +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): 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/dovehash.d.ts b/src/types/dovehash.d.ts new file mode 100644 index 00000000..c354609c --- /dev/null +++ b/src/types/dovehash.d.ts @@ -0,0 +1,4 @@ + +declare module "dovehash" { + function encode(algo: string, text: string): string; +} \ No newline at end of file diff --git a/src/types/ldapjs-async.d.ts b/src/types/ldapjs-async.d.ts new file mode 100644 index 00000000..e5fad359 --- /dev/null +++ b/src/types/ldapjs-async.d.ts @@ -0,0 +1,11 @@ +import ldapjs = require("ldapjs"); +import * as BluebirdPromise from "bluebird"; +import { EventEmitter } from "events"; + +declare module "ldapjs" { + export interface ClientAsync { + 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 new file mode 100644 index 00000000..1f4fe042 --- /dev/null +++ b/src/types/nedb-async.d.ts @@ -0,0 +1,12 @@ +import Nedb = require("nedb"); +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): 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 new file mode 100644 index 00000000..164d6919 --- /dev/null +++ b/src/types/request-async.d.ts @@ -0,0 +1,14 @@ +import * as BluebirdPromise from "bluebird"; +import * as request from "request"; + +declare module "request" { + export interface RequestAsync extends RequestAPI { + getAsync(uri: string, options?: RequiredUriUrl): BluebirdPromise; + getAsync(uri: string): BluebirdPromise; + getAsync(options: RequiredUriUrl & CoreOptions): BluebirdPromise; + + 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 new file mode 100644 index 00000000..27053790 --- /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 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, nedb); + 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, nedb); + 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, nedb); + 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/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/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 new file mode 100644 index 00000000..15105b26 --- /dev/null +++ b/test/unitary/Server.test.ts @@ -0,0 +1,392 @@ + +import Server from "../../src/lib/Server"; +import LdapClient = require("../../src/lib/LdapClient"); + +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; +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, + 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: { + username: "user@example.com", + password: "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/TOTPValidator.test.ts b/test/unitary/TOTPValidator.test.ts new file mode 100644 index 00000000..84baa040 --- /dev/null +++ b/test/unitary/TOTPValidator.test.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(done) { + const totp_secret = "NBD2ZV64R9UV1O7K"; + const token = "wrong token"; + totpValidator.validate(token, totp_secret) + .catch(function() { + done(); + }); + }); +}); + diff --git a/test/unitary/UserDataStore.test.ts b/test/unitary/UserDataStore.test.ts new file mode 100644 index 00000000..a7ce7dd9 --- /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 nedb = 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, nedb); + + 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, nedb); + + 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, nedb); + + 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, nedb); + + 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, nedb); + + 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, nedb); + + 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, nedb); + + 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, nedb); + + 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, nedb); + + 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/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/config_adapter.test.ts b/test/unitary/config_adapter.test.ts new file mode 100644 index 00000000..0c8a651e --- /dev/null +++ b/test/unitary/config_adapter.test.ts @@ -0,0 +1,117 @@ +import * as Assert from "assert"; +import { UserConfiguration } from "../../src/lib/Configuration"; +import ConfigurationAdapter from "../../src/lib/ConfigurationAdapter"; + +describe("test config adapter", function() { + function build_yaml_config(): UserConfiguration { + const yaml_config = { + port: 8080, + 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", + notifier: { + gmail: { + username: "user", + password: "password" + } + } + }; + 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 = 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 = ConfigurationAdapter.adapt(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", + base_dn: "cn=test,dc=example,dc=com", + additional_user_dn: "ou=users", + user_name_attribute: "uid", + user: "admin", + password: "pass" + }; + + const config = ConfigurationAdapter.adapt(yaml_config); + + Assert.equal(config.ldap.url, "http://ldap"); + 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"); + }); + + 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 = ConfigurationAdapter.adapt(yaml_config); + 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() { + const yaml_config = build_yaml_config(); + yaml_config.logs_level = "debug"; + const config = ConfigurationAdapter.adapt(yaml_config); + Assert.equal(config.logs_level, "debug"); + }); + + it("should get the notifier config", function() { + const yaml_config = build_yaml_config(); + yaml_config.notifier = { + gmail: { + username: "user", + password: "pass" + } + }; + const config = ConfigurationAdapter.adapt(yaml_config); + Assert.deepEqual(config.notifier, { + gmail: { + username: "user", + password: "pass" + } + }); + }); + + it("should get the access_control config", function() { + const yaml_config = build_yaml_config(); + yaml_config.access_control = { + default: [], + users: {}, + groups: {} + }; + const config = ConfigurationAdapter.adapt(yaml_config); + 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..1e721872 --- /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/types/Dependencies"; +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: { + username: "user@example.com", + password: "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/mocks/AccessController.ts b/test/unitary/mocks/AccessController.ts new file mode 100644 index 00000000..ce46c0b8 --- /dev/null +++ b/test/unitary/mocks/AccessController.ts @@ -0,0 +1,12 @@ + +import sinon = require("sinon"); + +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 new file mode 100644 index 00000000..2d789d94 --- /dev/null +++ b/test/unitary/mocks/AuthenticationRegulator.ts @@ -0,0 +1,15 @@ + +import sinon = require("sinon"); + + +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/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/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/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/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 new file mode 100644 index 00000000..4a4daa6a --- /dev/null +++ b/test/unitary/mocks/UserDataStore.ts @@ -0,0 +1,22 @@ + +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; + get_totp_secret: sinon.SinonStub; + set_totp_secret: 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(), + 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 new file mode 100644 index 00000000..daa3e170 --- /dev/null +++ b/test/unitary/mocks/express.ts @@ -0,0 +1,97 @@ + +import sinon = require("sinon"); +import express = require("express"); + +export interface RequestMock { + app?: any; + body?: any; + session?: any; + headers?: any; + get?: any; + query?: any; +} + +export interface ResponseMock { + send: sinon.SinonStub | sinon.SinonSpy; + sendStatus: sinon.SinonStub; + sendFile: sinon.SinonStub; + sendfile: sinon.SinonStub; + status: sinon.SinonStub | sinon.SinonSpy; + json: sinon.SinonStub | sinon.SinonSpy; + 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 | sinon.SinonSpy; + 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 { + return { + app: { + get: sinon.stub() + } + }; +} +export function ResponseMock(): ResponseMock { + return { + send: sinon.stub(), + status: 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/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/mocks/nodemailer.ts b/test/unitary/mocks/nodemailer.ts new file mode 100644 index 00000000..61ec1c94 --- /dev/null +++ b/test/unitary/mocks/nodemailer.ts @@ -0,0 +1,22 @@ + +import sinon = require("sinon"); + +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/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/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..feaae479 --- /dev/null +++ b/test/unitary/notifiers/GMailNotifier.test.ts @@ -0,0 +1,40 @@ +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() + }; + const nodemailerMock = NodemailerMock.NodemailerMock(); + 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..d327a9ba --- /dev/null +++ b/test/unitary/notifiers/NotifierFactory.test.ts @@ -0,0 +1,36 @@ + +import * as sinon from "sinon"; +import * as BluebirdPromise from "bluebird"; +import * as assert from "assert"; + +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"); + + +describe("test notifier factory", function() { + let nodemailerMock: NodemailerMock.NodemailerMock; + it("should build a Gmail Notifier", function() { + const options = { + gmail: { + username: "abc", + password: "password" + } + }; + nodemailerMock = NodemailerMock.NodemailerMock(); + nodemailerMock.createTransport.returns(sinon.spy()); + assert(NotifierFactory.build(options, nodemailerMock) instanceof GMailNotifier); + }); + + it("should build a FS Notifier", function() { + const options = { + filesystem: { + filename: "abc" + } + }; + + assert(NotifierFactory.build(options, nodemailerMock) 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/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/AuthenticationValidator.test.ts b/test/unitary/routes/AuthenticationValidator.test.ts new file mode 100644 index 00000000..33f27f4a --- /dev/null +++ b/test/unitary/routes/AuthenticationValidator.test.ts @@ -0,0 +1,124 @@ + +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 }); + }); + + 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/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/FirstFactor.test.ts b/test/unitary/routes/FirstFactor.test.ts new file mode 100644 index 00000000..0ee5b07e --- /dev/null +++ b/test/unitary/routes/FirstFactor.test.ts @@ -0,0 +1,139 @@ + +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 { LdapClientMock } from "../mocks/LdapClient"; +import ExpressMock = require("../mocks/express"); + +describe("test the first factor validation route", function () { + let req: ExpressMock.RequestMock; + let res: ExpressMock.ResponseMock; + let emails: string[]; + let groups: string[]; + let configuration; + let ldapMock: LdapClientMock; + let regulator: AuthenticationRegulatorMock.AuthenticationRegulatorMock; + let accessController: AccessControllerMock.AccessControllerMock; + + 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 = LdapClientMock(); + + accessController = AccessControllerMock.AccessControllerMock(); + accessController.isDomainAllowedForUser.returns(true); + + regulator = AuthenticationRegulatorMock.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 + } + }, + 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 () { + 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 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("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 () { + 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 as any, res as any); + }); + }); + + 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.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) { + res.send = sinon.spy(function () { + assert.equal(500, res.status.getCall(0).args[0]); + done(); + }); + ldapMock.bind.returns(BluebirdPromise.resolve()); + 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) { + 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 as any, res as any); + }); +}); + + diff --git a/test/unitary/routes/PasswordReset.test.ts b/test/unitary/routes/PasswordReset.test.ts new file mode 100644 index 00000000..cb1ec07d --- /dev/null +++ b/test/unitary/routes/PasswordReset.test.ts @@ -0,0 +1,151 @@ + +import PasswordReset = require("../../../src/lib/routes/PasswordReset"); +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; + PasswordReset.icheck_interface.preValidation(req as any) + .catch(function (err: Error) { + done(); + }); + }); + + it("should fail if ldap fail", function (done) { + ldap_client.get_emails.returns(BluebirdPromise.reject("Internal error")); + PasswordReset.icheck_interface.preValidation(req as any) + .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([])); + PasswordReset.icheck_interface.preValidation(req as any) + .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"])); + PasswordReset.icheck_interface.preValidation(req as any) + .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(); + }); + PasswordReset.post(req as any, res as any); + }); + + 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(); + }); + PasswordReset.post(req as any, res as any); + }); + + 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(); + }); + PasswordReset.post(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/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_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); - }); -} diff --git a/test/unitary/routes/test_first_factor.js b/test/unitary/routes/test_first_factor.js deleted file mode 100644 index 7f500fc8..00000000 --- a/test/unitary/routes/test_first_factor.js +++ /dev/null @@ -1,177 +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'); -var AccessControl = require('../../../src/lib/access_control'); - -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_control; - 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_control = { - builder: { - get_allowed_domains: sinon.stub(), - get_any_domain: sinon.stub(), - }, - matcher: { - is_domain_allowed: sinon.stub() - } - }; - - 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 control').returns(access_control); - - 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); - }); - }); - - 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()); - 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/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/routes/test_totp.js b/test/unitary/routes/test_totp.js deleted file mode 100644 index 1fd0fc4d..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 totp_engine; - 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' }; - totp_engine = { - totp: 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 engine').returns(totp_engine); - 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'); - 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) { - totp_engine.totp.returns('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) { - totp_engine.totp.returns('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/routes/test_totp_register.js b/test/unitary/routes/test_totp_register.js deleted file mode 100644 index 784449df..00000000 --- a/test/unitary/routes/test_totp_register.js +++ /dev/null @@ -1,130 +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 engine').returns(require('speakeasy')); - 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 engine').returns(require('speakeasy')); - 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 engine').returns(require('speakeasy')); - 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/routes/test_verify.js b/test/unitary/routes/test_verify.js deleted file mode 100644 index e4987540..00000000 --- a/test/unitary/routes/test_verify.js +++ /dev/null @@ -1,120 +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 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/server_config.test.ts b/test/unitary/server_config.test.ts new file mode 100644 index 00000000..350d2d66 --- /dev/null +++ b/test/unitary/server_config.test.ts @@ -0,0 +1,72 @@ + +import assert = require("assert"); +import sinon = require ("sinon"); +import nedb = require("nedb"); +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/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 createTransport = sinon.stub(nodemailer, "createTransport"); + createTransport.returns(transporter); + + sessionMock = sinon.spy(session); + + deps = { + nodemailer: nodemailer, + speakeasy: speakeasy, + u2f: u2f, + nedb: nedb, + winston: winston, + ldapjs: { + createClient: sinon.spy(function () { + return { on: sinon.spy() }; + }) + }, + session: sessionMock as any + }; + }); + + + 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: { + username: "user@example.com", + password: "password" + } + } + } as UserConfiguration; + + const server = new Server(); + server.start(config, deps); + + assert(sessionMock.calledOnce); + assert.equal(sessionMock.getCall(0).args[0].cookie.domain, "example.com"); + }); +}); 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)); - }); - }); -}); diff --git a/test/unitary/test_authentication_regulator.js b/test/unitary/test_authentication_regulator.js deleted file mode 100644 index 18b46c9e..00000000 --- a/test/unitary/test_authentication_regulator.js +++ /dev/null @@ -1,70 +0,0 @@ - -var AuthenticationRegulator = require('../../src/lib/authentication_regulator'); -var UserDataStore = require('../../src/lib/user_data_store'); -var DataStore = require('nedb'); -var exceptions = require('../../src/lib/exceptions'); -var MockDate = require('mockdate'); - -describe('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/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 deleted file mode 100644 index 35c2c980..00000000 --- a/test/unitary/test_data_persistence.js +++ /dev/null @@ -1,164 +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 BASE_URL = 'http://localhost:' + PORT; - -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_identity_check.js b/test/unitary/test_identity_check.js deleted file mode 100644 index 52e890dd..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/test/unitary/test_ldap.js b/test/unitary/test_ldap.js deleted file mode 100644 index c7fff8f6..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'); - - 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'); - }); - } -}); - 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 b5c26b5e..00000000 --- a/test/unitary/test_server_config.js +++ /dev/null @@ -1,51 +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.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/user_data_store/authentication_audit.test.ts b/test/unitary/user_data_store/authentication_audit.test.ts new file mode 100644 index 00000000..8a8be4df --- /dev/null +++ b/test/unitary/user_data_store/authentication_audit.test.ts @@ -0,0 +1,71 @@ + +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"; +import nedb = 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() { + const options = { + inMemoryOnly: true + }; + + const data_store = new UserDataStore(options, nedb); + 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, nedb); + 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..bd5223ac --- /dev/null +++ b/test/unitary/user_data_store/totp_secret.test.ts @@ -0,0 +1,73 @@ + +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"; +import nedb = 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() { + const options = { + inMemoryOnly: true + }; + + const data_store = new UserDataStore(options, nedb); + 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, nedb); + 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 new file mode 100644 index 00000000..547417dd --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "module": "commonjs", + "target": "es6", + "noImplicitAny": true, + "moduleResolution": "node", + "sourceMap": true, + "outDir": "dist", + "baseUrl": ".", + "paths": { + "*": [ + "src/types/*", + "node_modules/@types/*" + ] + } + }, + "include": [ + "src/**/*", + "test/**/*" + ] +} 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 + } +}