diff --git a/Dockerfile b/Dockerfile index b535f8bc..eef8b58f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,7 +5,7 @@ WORKDIR /usr/src COPY package.json /usr/src/package.json RUN npm install --production -COPY src /usr/src +COPY dist/src /usr/src ENV PORT=80 EXPOSE 80 diff --git a/package.json b/package.json index cca2cb96..a5078e69 100644 --- a/package.json +++ b/package.json @@ -7,10 +7,8 @@ "authelia": "src/index.js" }, "scripts": { - "test": "./node_modules/.bin/mocha -r ts-node/register --recursive test/unitary", - "unit-test": "./node_modules/.bin/mocha --recursive test/unitary", + "test": "./node_modules/.bin/mocha --compilers ts:ts-node/register --recursive test/unitary", "int-test": "./node_modules/.bin/mocha --recursive test/integration", - "all-test": "./node_modules/.bin/mocha --recursive test", "coverage": "./node_modules/.bin/istanbul cover _mocha -- -R spec --recursive test", "build-ts": "tsc", "watch-ts": "tsc -w", @@ -49,14 +47,19 @@ "devDependencies": { "@types/assert": "0.0.31", "@types/bluebird": "^3.5.3", + "@types/body-parser": "^1.16.3", "@types/express": "^4.0.35", "@types/express-session": "0.0.32", "@types/ldapjs": "^1.0.0", "@types/mocha": "^2.2.41", + "@types/mockdate": "^2.0.0", "@types/nedb": "^1.8.3", "@types/nodemailer": "^1.3.32", "@types/object-path": "^0.9.28", + "@types/request": "0.0.43", "@types/sinon": "^2.2.1", + "@types/speakeasy": "^2.0.1", + "@types/tmp": "0.0.33", "@types/winston": "^2.3.2", "@types/yamljs": "^0.2.30", "grunt": "^1.0.1", diff --git a/src/index.ts b/src/index.ts index 2de121e9..c16865f3 100755 --- a/src/index.ts +++ b/src/index.ts @@ -2,7 +2,7 @@ process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0"; -import * as server from "./lib/server"; +import Server from "./lib/Server"; const YAML = require("yamljs"); const config_path = process.argv[2]; @@ -26,4 +26,8 @@ const deps = { nedb: require("nedb") }; -server.run(yaml_config, deps); +const server = new Server(); +server.start(yaml_config, deps) +.then(() => { + console.log("The server is started!"); +}); diff --git a/src/lib/AuthenticationRegulator.ts b/src/lib/AuthenticationRegulator.ts index 77c121b2..16bd4340 100644 --- a/src/lib/AuthenticationRegulator.ts +++ b/src/lib/AuthenticationRegulator.ts @@ -11,8 +11,8 @@ interface DatedDocument { } export class AuthenticationRegulator { - _user_data_store: any; - _lock_time_in_seconds: number; + private _user_data_store: any; + private _lock_time_in_seconds: number; constructor(user_data_store: any, lock_time_in_seconds: number) { this._user_data_store = user_data_store; diff --git a/src/lib/Configuration.ts b/src/lib/Configuration.ts new file mode 100644 index 00000000..2ed31f56 --- /dev/null +++ b/src/lib/Configuration.ts @@ -0,0 +1,62 @@ + +export interface LdapConfiguration { + url: string; + base_dn: string; + additional_user_dn?: string; + user_name_attribute?: string; // cn by default + additional_group_dn?: string; + group_name_attribute?: string; // cn by default + user: string; // admin username + password: string; // admin password +} + +type UserName = string; +type GroupName = string; +type DomainPattern = string; + +type ACLDefaultRules = Array; +type ACLGroupsRules = Object; +type ACLUsersRules = Object; + +export interface ACLConfiguration { + default: ACLDefaultRules; + groups: ACLGroupsRules; + users: ACLUsersRules; +} + +interface SessionCookieConfiguration { + secret: string; + expiration?: number; + domain?: string; +} + +interface GMailNotifier { + user: string; + pass: string; +} + +type NotifierType = string; +export interface NotifiersConfiguration { + gmail: GMailNotifier; +} + +export interface UserConfiguration { + port?: number; + logs_level?: string; + ldap: LdapConfiguration; + session: SessionCookieConfiguration; + store_directory?: string; + notifier: NotifiersConfiguration; + access_control?: ACLConfiguration; +} + +export interface AppConfiguration { + port: number; + logs_level: string; + ldap: LdapConfiguration; + session: SessionCookieConfiguration; + store_in_memory?: boolean; + store_directory?: string; + notifier: NotifiersConfiguration; + access_control?: ACLConfiguration; +} diff --git a/src/lib/GlobalDependencies.ts b/src/lib/GlobalDependencies.ts new file mode 100644 index 00000000..4b71f273 --- /dev/null +++ b/src/lib/GlobalDependencies.ts @@ -0,0 +1,11 @@ +import * as winston from "winston"; + +export interface GlobalDependencies { + u2f: object; + nodemailer: any; + ldapjs: object; + session: any; + winston: winston.Winston; + speakeasy: object; + nedb: any; +} \ No newline at end of file diff --git a/src/lib/Server.ts b/src/lib/Server.ts new file mode 100644 index 00000000..8df410f6 --- /dev/null +++ b/src/lib/Server.ts @@ -0,0 +1,84 @@ + +import { UserConfiguration } from "./Configuration"; +import { GlobalDependencies } from "./GlobalDependencies"; +import * as Express from "express"; +import * as BodyParser from "body-parser"; +import * as Path from "path"; +import { AuthenticationRegulator } from "./AuthenticationRegulator"; +import UserDataStore from "./UserDataStore"; +import * as http from "http"; + +import config_adapter = require("./config_adapter"); + +const Notifier = require("./notifier"); +const setup_endpoints = require("./setup_endpoints"); +const Ldap = require("./ldap"); +const AccessControl = require("./access_control"); + +export default class Server { + private httpServer: http.Server; + + start(yaml_configuration: UserConfiguration, deps: GlobalDependencies): Promise { + const config = config_adapter(yaml_configuration); + + const view_directory = Path.resolve(__dirname, "../views"); + const public_html_directory = Path.resolve(__dirname, "../public_html"); + const datastore_options = { + directory: config.store_directory, + inMemory: config.store_in_memory + }; + + const app = Express(); + app.use(Express.static(public_html_directory)); + app.use(BodyParser.urlencoded({ extended: false })); + app.use(BodyParser.json()); + app.set("trust proxy", 1); // trust first proxy + + app.use(deps.session({ + secret: config.session.secret, + resave: false, + saveUninitialized: true, + cookie: { + secure: false, + maxAge: config.session.expiration, + domain: config.session.domain + }, + })); + + app.set("views", view_directory); + app.set("view engine", "ejs"); + + // by default the level of logs is info + deps.winston.level = config.logs_level || "info"; + + const five_minutes = 5 * 60; + const data_store = new UserDataStore(datastore_options); + const regulator = new AuthenticationRegulator(data_store, five_minutes); + const notifier = new Notifier(config.notifier, deps); + const ldap = new Ldap(deps, config.ldap); + const access_control = AccessControl(deps.winston, config.access_control); + + app.set("logger", deps.winston); + app.set("ldap", ldap); + app.set("totp engine", deps.speakeasy); + app.set("u2f", deps.u2f); + app.set("user data store", data_store); + app.set("notifier", notifier); + app.set("authentication regulator", regulator); + app.set("config", config); + app.set("access control", access_control); + setup_endpoints(app); + + return new Promise((resolve, reject) => { + this.httpServer = app.listen(config.port, function (err: string) { + console.log("Listening on %d...", config.port); + resolve(); + }); + }); + } + + stop() { + this.httpServer.close(); + } +} + diff --git a/src/lib/TOTPSecret.ts b/src/lib/TOTPSecret.ts new file mode 100644 index 00000000..e4a6b7d7 --- /dev/null +++ b/src/lib/TOTPSecret.ts @@ -0,0 +1,6 @@ + +export interface TOTPSecret { + base32: string; + ascii: string; + otpauth_url: string; +} \ No newline at end of file diff --git a/src/lib/UserDataStore.ts b/src/lib/UserDataStore.ts new file mode 100644 index 00000000..4aa6a05e --- /dev/null +++ b/src/lib/UserDataStore.ts @@ -0,0 +1,169 @@ +import * as Promise from "bluebird"; +import * as path from "path"; +import Nedb = require("nedb"); +import { NedbAsync } from "nedb"; +import { TOTPSecret } from "./TOTPSecret"; + +// Constants + +const U2F_META_COLLECTION_NAME = "u2f_meta"; +const IDENTITY_CHECK_TOKENS_COLLECTION_NAME = "identity_check_tokens"; +const AUTHENTICATION_TRACES_COLLECTION_NAME = "authentication_traces"; +const TOTP_SECRETS_COLLECTION_NAME = "totp_secrets"; + + +export interface TOTPSecretDocument { + userid: string; + secret: TOTPSecret; +} + +export interface U2FMetaDocument { + meta: object; + userid: string; + appid: string; +} + +export interface Options { + inMemoryOnly?: boolean; + directory?: string; +} + + +// Source + +export default class UserDataStore { + private _u2f_meta_collection: NedbAsync; + private _identity_check_tokens_collection: NedbAsync; + private _authentication_traces_collection: NedbAsync; + private _totp_secret_collection: NedbAsync; + + constructor(options?: Options) { + this._u2f_meta_collection = create_collection(U2F_META_COLLECTION_NAME, options); + this._identity_check_tokens_collection = + create_collection(IDENTITY_CHECK_TOKENS_COLLECTION_NAME, options); + this._authentication_traces_collection = + create_collection(AUTHENTICATION_TRACES_COLLECTION_NAME, options); + this._totp_secret_collection = + create_collection(TOTP_SECRETS_COLLECTION_NAME, options); + } + + set_u2f_meta(userid: string, appid: string, meta: Object): Promise { + const newDocument = { + userid: userid, + appid: appid, + meta: meta + }; + + const filter = { + userid: userid, + appid: appid + }; + + return this._u2f_meta_collection.updateAsync(filter, newDocument, { upsert: true }); + } + + get_u2f_meta(userid: string, appid: string): Promise { + const filter = { + userid: userid, + appid: appid + }; + return this._u2f_meta_collection.findOneAsync(filter); + } + + save_authentication_trace(userid: string, type: string, is_success: boolean) { + const newDocument = { + userid: userid, + date: new Date(), + is_success: is_success, + type: type + }; + + return this._authentication_traces_collection.insertAsync(newDocument); + } + + get_last_authentication_traces(userid: string, type: string, is_success: boolean, count: number): Promise { + const q = { + userid: userid, + type: type, + is_success: is_success + }; + + const query = this._authentication_traces_collection.find(q) + .sort({ date: -1 }).limit(count); + const query_promisified = Promise.promisify(query.exec, { context: query }); + return query_promisified(); + } + + issue_identity_check_token(userid: string, token: string, data: string | object, max_age: number): Promise { + const newDocument = { + userid: userid, + token: token, + content: { + userid: userid, + data: data + }, + max_date: new Date(new Date().getTime() + max_age) + }; + + return this._identity_check_tokens_collection.insertAsync(newDocument); + } + + consume_identity_check_token(token: string): Promise { + const query = { + token: token + }; + + return this._identity_check_tokens_collection.findOneAsync(query) + .then(function (doc) { + if (!doc) { + return Promise.reject("Registration token does not exist"); + } + + const max_date = doc.max_date; + const current_date = new Date(); + if (current_date > max_date) { + return Promise.reject("Registration token is not valid anymore"); + } + return Promise.resolve(doc.content); + }) + .then((content) => { + return Promise.join(this._identity_check_tokens_collection.removeAsync(query), + Promise.resolve(content)); + }) + .then((v) => { + return Promise.resolve(v[1]); + }); + } + + set_totp_secret(userid: string, secret: TOTPSecret): Promise { + const doc = { + userid: userid, + secret: secret + }; + + const query = { + userid: userid + }; + return this._totp_secret_collection.updateAsync(query, doc, { upsert: true }); + } + + get_totp_secret(userid: string): Promise { + const query = { + userid: userid + }; + return this._totp_secret_collection.findOneAsync(query); + } +} + +function create_collection(name: string, options: any): NedbAsync { + const datastore_options = { + inMemoryOnly: options.inMemoryOnly || false, + autoload: true, + filename: "" + }; + + if (options.directory) + datastore_options.filename = path.resolve(options.directory, name); + + return Promise.promisifyAll(new Nedb(datastore_options)) as NedbAsync; +} diff --git a/src/lib/config_adapter.ts b/src/lib/config_adapter.ts index 6c9721e3..5503f1c8 100644 --- a/src/lib/config_adapter.ts +++ b/src/lib/config_adapter.ts @@ -1,6 +1,6 @@ import * as ObjectPath from "object-path"; -import { authelia } from "../types/authelia"; +import { AppConfiguration, UserConfiguration, NotifiersConfiguration, ACLConfiguration, LdapConfiguration } from "./Configuration"; function get_optional(config: object, path: string, default_value: T): T { @@ -17,7 +17,7 @@ function ensure_key_existence(config: object, path: string): void { } } -export = function(yaml_config: object): authelia.Configuration { +export = function(yaml_config: UserConfiguration): AppConfiguration { ensure_key_existence(yaml_config, "ldap"); ensure_key_existence(yaml_config, "session.secret"); @@ -25,14 +25,16 @@ export = function(yaml_config: object): authelia.Configuration { return { port: port, - ldap: ObjectPath.get(yaml_config, "ldap"), - session_domain: ObjectPath.get(yaml_config, "session.domain"), - session_secret: ObjectPath.get(yaml_config, "session.secret"), - session_max_age: get_optional(yaml_config, "session.expiration", 3600000), // in ms + ldap: ObjectPath.get(yaml_config, "ldap"), + session: { + domain: ObjectPath.get(yaml_config, "session.domain"), + secret: ObjectPath.get(yaml_config, "session.secret"), + expiration: get_optional(yaml_config, "session.expiration", 3600000), // in ms + }, store_directory: get_optional(yaml_config, "store_directory", undefined), logs_level: get_optional(yaml_config, "logs_level", "info"), - notifier: ObjectPath.get(yaml_config, "notifier"), - access_control: ObjectPath.get(yaml_config, "access_control") + notifier: ObjectPath.get(yaml_config, "notifier"), + access_control: ObjectPath.get(yaml_config, "access_control") }; }; diff --git a/src/lib/server.ts b/src/lib/server.ts deleted file mode 100644 index 8e6ca8a8..00000000 --- a/src/lib/server.ts +++ /dev/null @@ -1,70 +0,0 @@ - -import { authelia } from "../types/authelia"; -import * as Express from "express"; -import * as BodyParser from "body-parser"; -import * as Path from "path"; -import { AuthenticationRegulator } from "./AuthenticationRegulator"; - -const UserDataStore = require("./user_data_store"); -const Notifier = require("./notifier"); -const setup_endpoints = require("./setup_endpoints"); -const config_adapter = require("./config_adapter"); -const Ldap = require("./ldap"); -const AccessControl = require("./access_control"); - -export function run(yaml_configuration: authelia.Configuration, deps: authelia.GlobalDependencies, fn?: () => undefined) { - const config = config_adapter(yaml_configuration); - - const view_directory = Path.resolve(__dirname, "../views"); - const public_html_directory = Path.resolve(__dirname, "../public_html"); - const datastore_options = { - directory: config.store_directory, - inMemory: config.store_in_memory - }; - - const app = Express(); - app.use(Express.static(public_html_directory)); - app.use(BodyParser.urlencoded({ extended: false })); - app.use(BodyParser.json()); - app.set("trust proxy", 1); // trust first proxy - - app.use(deps.session({ - secret: config.session_secret, - resave: false, - saveUninitialized: true, - cookie: { - secure: false, - maxAge: config.session_max_age, - domain: config.session_domain - }, - })); - - app.set("views", view_directory); - app.set("view engine", "ejs"); - - // by default the level of logs is info - deps.winston.level = config.logs_level || "info"; - - const five_minutes = 5 * 60; - const data_store = new UserDataStore(deps.nedb, datastore_options); - const regulator = new AuthenticationRegulator(data_store, five_minutes); - const notifier = new Notifier(config.notifier, deps); - const ldap = new Ldap(deps, config.ldap); - const access_control = AccessControl(deps.winston, config.access_control); - - app.set("logger", deps.winston); - app.set("ldap", ldap); - app.set("totp engine", deps.speakeasy); - app.set("u2f", deps.u2f); - app.set("user data store", data_store); - app.set("notifier", notifier); - app.set("authentication regulator", regulator); - app.set("config", config); - app.set("access control", access_control); - setup_endpoints(app); - - return app.listen(config.port, function(err: string) { - console.log("Listening on %d...", config.port); - if (fn) fn(); - }); -} diff --git a/src/lib/user_data_store.js b/src/lib/user_data_store.js deleted file mode 100644 index 8789e745..00000000 --- a/src/lib/user_data_store.js +++ /dev/null @@ -1,124 +0,0 @@ - -module.exports = UserDataStore; - -var Promise = require('bluebird'); -var path = require('path'); - -function UserDataStore(DataStore, options) { - this._u2f_meta_collection = create_collection('u2f_meta', options, DataStore); - this._identity_check_tokens_collection = - create_collection('identity_check_tokens', options, DataStore); - this._authentication_traces_collection = - create_collection('authentication_traces', options, DataStore); - this._totp_secret_collection = - create_collection('totp_secrets', options, DataStore); -} - -function create_collection(name, options, DataStore) { - var datastore_options = {}; - - if(options.directory) - datastore_options.filename = path.resolve(options.directory, name); - - datastore_options.inMemoryOnly = options.inMemoryOnly || false; - datastore_options.autoload = true; - return Promise.promisifyAll(new DataStore(datastore_options)); -} - -UserDataStore.prototype.set_u2f_meta = function(userid, app_id, meta) { - var newDocument = {}; - newDocument.userid = userid; - newDocument.appid = app_id; - newDocument.meta = meta; - - var filter = {}; - filter.userid = userid; - filter.appid = app_id; - - return this._u2f_meta_collection.updateAsync(filter, newDocument, { upsert: true }); -} - -UserDataStore.prototype.get_u2f_meta = function(userid, app_id) { - var filter = {}; - filter.userid = userid; - filter.appid = app_id; - - return this._u2f_meta_collection.findOneAsync(filter); -} - -UserDataStore.prototype.save_authentication_trace = function(userid, type, is_success) { - var newDocument = {}; - newDocument.userid = userid; - newDocument.date = new Date(); - newDocument.is_success = is_success; - newDocument.type = type; - - return this._authentication_traces_collection.insertAsync(newDocument); -} - -UserDataStore.prototype.get_last_authentication_traces = function(userid, type, is_success, count) { - var query = {}; - query.userid = userid; - query.type = type; - query.is_success = is_success; - - var query = this._authentication_traces_collection.find(query) - .sort({ date: -1 }).limit(count); - var query_promisified = Promise.promisify(query.exec, { context: query }); - return query_promisified(); -} - -UserDataStore.prototype.issue_identity_check_token = function(userid, token, data, max_age) { - var newDocument = {}; - newDocument.userid = userid; - newDocument.token = token; - newDocument.content = { userid: userid, data: data }; - newDocument.max_date = new Date(new Date().getTime() + max_age); - - return this._identity_check_tokens_collection.insertAsync(newDocument); -} - -UserDataStore.prototype.consume_identity_check_token = function(token) { - var query = {}; - query.token = token; - var that = this; - var doc_content; - - return this._identity_check_tokens_collection.findOneAsync(query) - .then(function(doc) { - if(!doc) { - return Promise.reject('Registration token does not exist'); - } - - var max_date = doc.max_date; - var current_date = new Date(); - if(current_date > max_date) { - return Promise.reject('Registration token is not valid anymore'); - } - - doc_content = doc.content; - return Promise.resolve(); - }) - .then(function() { - return that._identity_check_tokens_collection.removeAsync(query); - }) - .then(function() { - return Promise.resolve(doc_content); - }) -} - -UserDataStore.prototype.set_totp_secret = function(userid, secret) { - var doc = {} - doc.userid = userid; - doc.secret = secret; - - var query = {}; - query.userid = userid; - return this._totp_secret_collection.updateAsync(query, doc, { upsert: true }); -} - -UserDataStore.prototype.get_totp_secret = function(userid) { - var query = {}; - query.userid = userid; - return this._totp_secret_collection.findOneAsync(query); -} diff --git a/src/types/authdog.d.ts b/src/types/authdog.d.ts new file mode 100644 index 00000000..9cb1121f --- /dev/null +++ b/src/types/authdog.d.ts @@ -0,0 +1,67 @@ + +declare module "authdog" { + interface RegisterRequest { + challenge: string; + } + + interface RegisteredKey { + version: number; + keyHandle: string; + } + + type RegisteredKeys = Array; + type RegisterRequests = Array; + type AppId = string; + + interface RegistrationRequest { + appId: AppId; + type: string; + registerRequests: RegisterRequests; + registeredKeys: RegisteredKeys; + } + + interface Registration { + publicKey: string; + keyHandle: string; + certificate: string; + } + + interface ClientData { + challenge: string; + } + + interface RegistrationResponse { + clientData: ClientData; + registrationData: string; + } + + interface Options { + timeoutSeconds: number; + requestId: string; + } + + interface AuthenticationRequest { + appId: AppId; + type: string; + challenge: string; + registeredKeys: RegisteredKeys; + timeoutSeconds: number; + requestId: string; + } + + interface AuthenticationResponse { + keyHandle: string; + clientData: ClientData; + signatureData: string; + } + + interface Authentication { + userPresence: Uint8Array, + counter: Uint32Array + } + + export function startRegistration(appId: AppId, registeredKeys: RegisteredKeys, options?: Options): Promise; + export function finishRegistration(registrationRequest: RegistrationRequest, registrationResponse: RegistrationResponse): Promise; + export function startAuthentication(appId: AppId, registeredKeys: RegisteredKeys, options: Options): Promise; + export function finishAuthentication(challenge: string, deviceResponse: AuthenticationResponse, registeredKeys: RegisteredKeys): Promise; +} \ No newline at end of file diff --git a/src/types/authelia.d.ts b/src/types/authelia.d.ts deleted file mode 100644 index 94950cbb..00000000 --- a/src/types/authelia.d.ts +++ /dev/null @@ -1,61 +0,0 @@ - -import * as winston from "winston"; -import * as nedb from "nedb"; - -declare namespace authelia { - interface LdapConfiguration { - url: string; - base_dn: string; - additional_user_dn?: string; - user_name_attribute?: string; // cn by default - additional_group_dn?: string; - group_name_attribute?: string; // cn by default - user: string; // admin username - password: string; // admin password - } - - type UserName = string; - type GroupName = string; - type DomainPattern = string; - - type ACLDefaultRules = Array; - type ACLGroupsRules = Map; - type ACLUsersRules = Map; - - export interface ACLConfiguration { - default: ACLDefaultRules; - groups: ACLGroupsRules; - users: ACLUsersRules; - } - - interface SessionCookieConfiguration { - secret: string; - expiration: number; - domain: string - } - - type NotifierType = string; - export type NotifiersConfiguration = Map; - - export interface Configuration { - port: number; - logs_level: string; - ldap: LdapConfiguration | {}; - session_domain?: string; - session_secret: string; - session_max_age: number; - store_directory?: string; - notifier: NotifiersConfiguration; - access_control: ACLConfiguration; - } - - export interface GlobalDependencies { - u2f: object; - nodemailer: any; - ldapjs: object; - session: any; - winston: winston.Winston; - speakeasy: object; - nedb: object; - } -} \ No newline at end of file diff --git a/src/types/nedb-async.d.ts b/src/types/nedb-async.d.ts new file mode 100644 index 00000000..e5dc9926 --- /dev/null +++ b/src/types/nedb-async.d.ts @@ -0,0 +1,12 @@ +import Nedb = require("nedb"); +import * as Promise from "bluebird"; + +declare module "nedb" { + export class NedbAsync extends Nedb { + constructor(pathOrOptions?: string | Nedb.DataStoreOptions); + updateAsync(query: any, updateQuery: any, options?: Nedb.UpdateOptions): Promise; + findOneAsync(query: any): Promise; + insertAsync(newDoc: T): Promise; + removeAsync(query: any): Promise; + } +} \ No newline at end of file diff --git a/src/types/request-async.d.ts b/src/types/request-async.d.ts new file mode 100644 index 00000000..38a36822 --- /dev/null +++ b/src/types/request-async.d.ts @@ -0,0 +1,14 @@ +import * as Promise from "bluebird"; +import * as request from "request"; + +declare module "request" { + export interface RequestAsync extends RequestAPI { + getAsync(uri: string, options?: RequiredUriUrl): Promise; + getAsync(uri: string): Promise; + getAsync(options: RequiredUriUrl & CoreOptions): Promise; + + postAsync(uri: string, options?: CoreOptions): Promise; + postAsync(uri: string): Promise; + postAsync(options: RequiredUriUrl & CoreOptions): Promise; + } +} \ No newline at end of file diff --git a/test/unitary/AuthenticationRegulator.test.js b/test/unitary/AuthenticationRegulator.test.js deleted file mode 100644 index c06e91e2..00000000 --- a/test/unitary/AuthenticationRegulator.test.js +++ /dev/null @@ -1,71 +0,0 @@ - -import { AuthenticationRegulator } from "../../src/lib/AuthenticationRegulator"; -import * as UserDataStore from "../../src/lib/user_data_store"; -import * as DataStore from "nedb"; -import * as MockDate from "mockdate"; - -var exceptions = require('../../src/lib/exceptions'); - -describe.only('test authentication regulator', function() { - it('should mark 2 authentication and regulate (resolve)', function() { - var options = {}; - options.inMemoryOnly = true; - var data_store = new UserDataStore(DataStore, options); - var regulator = new AuthenticationRegulator(data_store, 10); - var user = 'user'; - - return regulator.mark(user, false) - .then(function() { - return regulator.mark(user, true); - }) - .then(function() { - return regulator.regulate(user); - }); - }); - - it('should mark 3 authentications and regulate (reject)', function(done) { - var options = {}; - options.inMemoryOnly = true; - var data_store = new UserDataStore(DataStore, options); - var regulator = new AuthenticationRegulator(data_store, 10); - var user = 'user'; - - regulator.mark(user, false) - .then(function() { - return regulator.mark(user, false); - }) - .then(function() { - return regulator.mark(user, false); - }) - .then(function() { - return regulator.regulate(user); - }) - .catch(exceptions.AuthenticationRegulationError, function() { - done(); - }) - }); - - it('should mark 3 authentications and regulate (resolve)', function(done) { - var options = {}; - options.inMemoryOnly = true; - var data_store = new UserDataStore(DataStore, options); - var regulator = new AuthenticationRegulator(data_store, 10); - var user = 'user'; - - MockDate.set('1/2/2000 00:00:00'); - regulator.mark(user, false) - .then(function() { - MockDate.set('1/2/2000 00:00:15'); - return regulator.mark(user, false); - }) - .then(function() { - return regulator.mark(user, false); - }) - .then(function() { - return regulator.regulate(user); - }) - .then(function() { - done(); - }) - }); -}); diff --git a/test/unitary/AuthenticationRegulator.test.ts b/test/unitary/AuthenticationRegulator.test.ts new file mode 100644 index 00000000..3ee9e1c2 --- /dev/null +++ b/test/unitary/AuthenticationRegulator.test.ts @@ -0,0 +1,73 @@ + +import { AuthenticationRegulator } from "../../src/lib/AuthenticationRegulator"; +import UserDataStore from "../../src/lib/UserDataStore"; +import * as MockDate from "mockdate"; + +const exceptions = require("../../src/lib/exceptions"); + +describe("test authentication regulator", function() { + it("should mark 2 authentication and regulate (resolve)", function() { + const options = { + inMemoryOnly: true + }; + const data_store = new UserDataStore(options); + const regulator = new AuthenticationRegulator(data_store, 10); + const user = "user"; + + return regulator.mark(user, false) + .then(function() { + return regulator.mark(user, true); + }) + .then(function() { + return regulator.regulate(user); + }); + }); + + it("should mark 3 authentications and regulate (reject)", function(done) { + const options = { + inMemoryOnly: true + }; + const data_store = new UserDataStore(options); + const regulator = new AuthenticationRegulator(data_store, 10); + const user = "user"; + + regulator.mark(user, false) + .then(function() { + return regulator.mark(user, false); + }) + .then(function() { + return regulator.mark(user, false); + }) + .then(function() { + return regulator.regulate(user); + }) + .catch(exceptions.AuthenticationRegulationError, function() { + done(); + }); + }); + + it("should mark 3 authentications and regulate (resolve)", function(done) { + const options = { + inMemoryOnly: true + }; + const data_store = new UserDataStore(options); + const regulator = new AuthenticationRegulator(data_store, 10); + const user = "user"; + + MockDate.set("1/2/2000 00:00:00"); + regulator.mark(user, false) + .then(function() { + MockDate.set("1/2/2000 00:00:15"); + return regulator.mark(user, false); + }) + .then(function() { + return regulator.mark(user, false); + }) + .then(function() { + return regulator.regulate(user); + }) + .then(function() { + done(); + }); + }); +}); diff --git a/test/unitary/Server.test.ts b/test/unitary/Server.test.ts new file mode 100644 index 00000000..cd9d0630 --- /dev/null +++ b/test/unitary/Server.test.ts @@ -0,0 +1,393 @@ + +import Server from "../../src/lib/Server"; +import Ldap = require("../../src/lib/ldap"); + +import * as Promise from "bluebird"; +import * as speakeasy from "speakeasy"; +import * as request from "request"; +import * as nedb from "nedb"; +import { TOTPSecret } from "../../src/lib/TOTPSecret"; + + +const requestp = Promise.promisifyAll(request) as request.RequestAsync; +const assert = require("assert"); +const sinon = require("sinon"); +const MockDate = require("mockdate"); +const session = require("express-session"); +const winston = require("winston"); +const ldapjs = require("ldapjs"); + +const PORT = 8090; +const BASE_URL = "http://localhost:" + PORT; +const requests = require("./requests")(PORT); + +describe("test the server", function () { + let server: Server; + let transporter: object; + let u2f: any; + + beforeEach(function () { + const config = { + port: PORT, + totp_secret: "totp_secret", + ldap: { + url: "ldap://127.0.0.1:389", + base_dn: "ou=users,dc=example,dc=com", + user_name_attribute: "cn", + user: "cn=admin,dc=example,dc=com", + password: "password", + }, + session: { + secret: "session_secret", + expiration: 50000, + }, + store_in_memory: true, + notifier: { + gmail: { + user: "user@example.com", + pass: "password" + } + } + }; + + const ldap_client = { + bind: sinon.stub(), + search: sinon.stub(), + modify: sinon.stub(), + on: sinon.spy() + }; + const ldap = { + Change: sinon.spy(), + createClient: sinon.spy(function () { + return ldap_client; + }) + }; + + u2f = { + startRegistration: sinon.stub(), + finishRegistration: sinon.stub(), + startAuthentication: sinon.stub(), + finishAuthentication: sinon.stub() + }; + + transporter = { + sendMail: sinon.stub().yields() + }; + + const nodemailer = { + createTransport: sinon.spy(function () { + return transporter; + }) + }; + + const ldap_document = { + object: { + mail: "test_ok@example.com", + } + }; + + const search_res = { + on: sinon.spy(function (event: string, fn: (s: any) => void) { + if (event != "error") fn(ldap_document); + }) + }; + + ldap_client.bind.withArgs("cn=test_ok,ou=users,dc=example,dc=com", + "password").yields(undefined); + ldap_client.bind.withArgs("cn=admin,dc=example,dc=com", + "password").yields(undefined); + + ldap_client.bind.withArgs("cn=test_nok,ou=users,dc=example,dc=com", + "password").yields("error"); + + ldap_client.modify.yields(undefined); + ldap_client.search.yields(undefined, search_res); + + const deps = { + u2f: u2f, + nedb: nedb, + nodemailer: nodemailer, + ldapjs: ldap, + session: session, + winston: winston, + speakeasy: speakeasy + }; + + server = new Server(); + return server.start(config, deps); + }); + + afterEach(function () { + server.stop(); + }); + + describe("test GET /login", function () { + test_login(); + }); + + describe("test GET /logout", function () { + test_logout(); + }); + + describe("test GET /reset-password-form", function () { + test_reset_password_form(); + }); + + describe("test endpoints locks", function () { + function should_post_and_reply_with(url: string, status_code: number) { + return requestp.postAsync(url).then(function (response: request.RequestResponse) { + assert.equal(response.statusCode, status_code); + return Promise.resolve(); + }); + } + + function should_get_and_reply_with(url: string, status_code: number) { + return requestp.getAsync(url).then(function (response: request.RequestResponse) { + assert.equal(response.statusCode, status_code); + return Promise.resolve(); + }); + } + + function should_post_and_reply_with_403(url: string) { + return should_post_and_reply_with(url, 403); + } + function should_get_and_reply_with_403(url: string) { + return should_get_and_reply_with(url, 403); + } + + function should_post_and_reply_with_401(url: string) { + return should_post_and_reply_with(url, 401); + } + function should_get_and_reply_with_401(url: string) { + return should_get_and_reply_with(url, 401); + } + + function should_get_and_post_reply_with_403(url: string) { + const p1 = should_post_and_reply_with_403(url); + const p2 = should_get_and_reply_with_403(url); + return Promise.all([p1, p2]); + } + + it("should block /new-password", function () { + return should_post_and_reply_with_403(BASE_URL + "/new-password"); + }); + + it("should block /u2f-register", function () { + return should_get_and_post_reply_with_403(BASE_URL + "/u2f-register"); + }); + + it("should block /reset-password", function () { + return should_get_and_post_reply_with_403(BASE_URL + "/reset-password"); + }); + + it("should block /2ndfactor/u2f/register_request", function () { + return should_get_and_reply_with_403(BASE_URL + "/2ndfactor/u2f/register_request"); + }); + + it("should block /2ndfactor/u2f/register", function () { + return should_post_and_reply_with_403(BASE_URL + "/2ndfactor/u2f/register"); + }); + + it("should block /2ndfactor/u2f/sign_request", function () { + return should_get_and_reply_with_403(BASE_URL + "/2ndfactor/u2f/sign_request"); + }); + + it("should block /2ndfactor/u2f/sign", function () { + return should_post_and_reply_with_403(BASE_URL + "/2ndfactor/u2f/sign"); + }); + }); + + describe("test authentication and verification", function () { + test_authentication(); + test_reset_password(); + test_regulation(); + }); + + function test_reset_password_form() { + it("should serve the reset password form page", function (done) { + requestp.getAsync(BASE_URL + "/reset-password-form") + .then(function (response: request.RequestResponse) { + assert.equal(response.statusCode, 200); + done(); + }); + }); + } + + function test_login() { + it("should serve the login page", function (done) { + requestp.getAsync(BASE_URL + "/login") + .then(function (response: request.RequestResponse) { + assert.equal(response.statusCode, 200); + done(); + }); + }); + } + + function test_logout() { + it("should logout and redirect to /", function (done) { + requestp.getAsync(BASE_URL + "/logout") + .then(function (response: any) { + assert.equal(response.req.path, "/"); + done(); + }); + }); + } + + function test_authentication() { + it("should return status code 401 when user is not authenticated", function () { + return requestp.getAsync({ url: BASE_URL + "/verify" }) + .then(function (response: request.RequestResponse) { + assert.equal(response.statusCode, 401); + return Promise.resolve(); + }); + }); + + it("should return status code 204 when user is authenticated using totp", function () { + const j = requestp.jar(); + return requests.login(j) + .then(function (res: request.RequestResponse) { + assert.equal(res.statusCode, 200, "get login page failed"); + return requests.first_factor(j); + }) + .then(function (res: request.RequestResponse) { + assert.equal(res.statusCode, 204, "first factor failed"); + return requests.register_totp(j, transporter); + }) + .then(function (secret: string) { + const sec = JSON.parse(secret) as TOTPSecret; + const real_token = speakeasy.totp({ + secret: sec.base32, + encoding: "base32" + }); + return requests.totp(j, real_token); + }) + .then(function (res: request.RequestResponse) { + assert.equal(res.statusCode, 204, "second factor failed"); + return requests.verify(j); + }) + .then(function (res: request.RequestResponse) { + assert.equal(res.statusCode, 204, "verify failed"); + return Promise.resolve(); + }); + }); + + it("should keep session variables when login page is reloaded", function () { + const real_token = speakeasy.totp({ + secret: "totp_secret", + encoding: "base32" + }); + const j = requestp.jar(); + return requests.login(j) + .then(function (res: request.RequestResponse) { + assert.equal(res.statusCode, 200, "get login page failed"); + return requests.first_factor(j); + }) + .then(function (res: request.RequestResponse) { + assert.equal(res.statusCode, 204, "first factor failed"); + return requests.totp(j, real_token); + }) + .then(function (res: request.RequestResponse) { + assert.equal(res.statusCode, 204, "second factor failed"); + return requests.login(j); + }) + .then(function (res: request.RequestResponse) { + assert.equal(res.statusCode, 200, "login page loading failed"); + return requests.verify(j); + }) + .then(function (res: request.RequestResponse) { + assert.equal(res.statusCode, 204, "verify failed"); + return Promise.resolve(); + }) + .catch(function (err: Error) { + console.error(err); + }); + }); + + it("should return status code 204 when user is authenticated using u2f", function () { + const sign_request = {}; + const sign_status = {}; + const registration_request = {}; + const registration_status = {}; + u2f.startRegistration.returns(Promise.resolve(sign_request)); + u2f.finishRegistration.returns(Promise.resolve(sign_status)); + u2f.startAuthentication.returns(Promise.resolve(registration_request)); + u2f.finishAuthentication.returns(Promise.resolve(registration_status)); + + const j = requestp.jar(); + return requests.login(j) + .then(function (res: request.RequestResponse) { + assert.equal(res.statusCode, 200, "get login page failed"); + return requests.first_factor(j); + }) + .then(function (res: request.RequestResponse) { + assert.equal(res.statusCode, 204, "first factor failed"); + return requests.u2f_registration(j, transporter); + }) + .then(function (res: request.RequestResponse) { + assert.equal(res.statusCode, 204, "second factor, finish register failed"); + return requests.u2f_authentication(j); + }) + .then(function (res: request.RequestResponse) { + assert.equal(res.statusCode, 204, "second factor, finish sign failed"); + return requests.verify(j); + }) + .then(function (res: request.RequestResponse) { + assert.equal(res.statusCode, 204, "verify failed"); + return Promise.resolve(); + }); + }); + } + + function test_reset_password() { + it("should reset the password", function () { + const j = requestp.jar(); + return requests.login(j) + .then(function (res: request.RequestResponse) { + assert.equal(res.statusCode, 200, "get login page failed"); + return requests.first_factor(j); + }) + .then(function (res: request.RequestResponse) { + assert.equal(res.statusCode, 204, "first factor failed"); + return requests.reset_password(j, transporter, "user", "new-password"); + }) + .then(function (res: request.RequestResponse) { + assert.equal(res.statusCode, 204, "second factor, finish register failed"); + return Promise.resolve(); + }); + }); + } + + function test_regulation() { + it("should regulate authentication", function () { + const j = requestp.jar(); + MockDate.set("1/2/2017 00:00:00"); + return requests.login(j) + .then(function (res: request.RequestResponse) { + assert.equal(res.statusCode, 200, "get login page failed"); + return requests.failing_first_factor(j); + }) + .then(function (res: request.RequestResponse) { + assert.equal(res.statusCode, 401, "first factor failed"); + return requests.failing_first_factor(j); + }) + .then(function (res: request.RequestResponse) { + assert.equal(res.statusCode, 401, "first factor failed"); + return requests.failing_first_factor(j); + }) + .then(function (res: request.RequestResponse) { + assert.equal(res.statusCode, 401, "first factor failed"); + return requests.failing_first_factor(j); + }) + .then(function (res: request.RequestResponse) { + assert.equal(res.statusCode, 403, "first factor failed"); + MockDate.set("1/2/2017 00:30:00"); + return requests.failing_first_factor(j); + }) + .then(function (res: request.RequestResponse) { + assert.equal(res.statusCode, 401, "first factor failed"); + return Promise.resolve(); + }); + }); + } +}); + diff --git a/test/unitary/UserDataStore.test.ts b/test/unitary/UserDataStore.test.ts new file mode 100644 index 00000000..a631a946 --- /dev/null +++ b/test/unitary/UserDataStore.test.ts @@ -0,0 +1,206 @@ + +import UserDataStore from "../../src/lib/UserDataStore"; +import { U2FMetaDocument, Options } from "../../src/lib/UserDataStore"; + +import DataStore = require("nedb"); +import assert = require("assert"); +import Promise = require("bluebird"); +import sinon = require("sinon"); +import MockDate = require("mockdate"); + +describe("test user data store", () => { + let options: Options; + + beforeEach(function () { + options = { + inMemoryOnly: true + }; + }); + + + describe("test u2f meta", () => { + it("should save a u2f meta", function () { + const data_store = new UserDataStore(options); + + const userid = "user"; + const app_id = "https://localhost"; + const meta = { + publicKey: "pbk" + }; + + return data_store.set_u2f_meta(userid, app_id, meta) + .then(function (numUpdated) { + assert.equal(1, numUpdated); + return Promise.resolve(); + }); + }); + + it("should retrieve no u2f meta", function () { + const options = { + inMemoryOnly: true + }; + + const data_store = new UserDataStore(options); + + const userid = "user"; + const app_id = "https://localhost"; + const meta = { + publicKey: "pbk" + }; + + return data_store.get_u2f_meta(userid, app_id) + .then(function (doc) { + assert.equal(undefined, doc); + return Promise.resolve(); + }); + }); + + it("should insert and retrieve a u2f meta", function () { + const options = { + inMemoryOnly: true + }; + + const data_store = new UserDataStore(options); + + const userid = "user"; + const app_id = "https://localhost"; + const meta = { + publicKey: "pbk" + }; + + return data_store.set_u2f_meta(userid, app_id, meta) + .then(function (numUpdated: number) { + assert.equal(1, numUpdated); + return data_store.get_u2f_meta(userid, app_id); + }) + .then(function (doc: U2FMetaDocument) { + assert.deepEqual(meta, doc.meta); + assert.deepEqual(userid, doc.userid); + assert.deepEqual(app_id, doc.appid); + assert("_id" in doc); + return Promise.resolve(); + }); + }); + }); + + + describe("test u2f registration token", () => { + it("should save u2f registration token", function () { + const data_store = new UserDataStore(options); + + const userid = "user"; + const token = "token"; + const max_age = 60; + const content = "abc"; + + return data_store.issue_identity_check_token(userid, token, content, max_age) + .then(function (document) { + assert.equal(document.userid, userid); + assert.equal(document.token, token); + assert.deepEqual(document.content, { userid: "user", data: content }); + assert("max_date" in document); + assert("_id" in document); + return Promise.resolve(); + }) + .catch(function (err) { + console.error(err); + return Promise.reject(err); + }); + }); + + it("should save u2f registration token and consume it", function (done) { + const data_store = new UserDataStore(options); + + const userid = "user"; + const token = "token"; + const max_age = 50; + + data_store.issue_identity_check_token(userid, token, {}, max_age) + .then(function (document) { + return data_store.consume_identity_check_token(token); + }) + .then(function () { + done(); + }) + .catch(function (err) { + console.error(err); + }); + }); + + it("should not be able to consume registration token twice", function (done) { + const data_store = new UserDataStore(options); + + const userid = "user"; + const token = "token"; + const max_age = 50; + + data_store.issue_identity_check_token(userid, token, {}, max_age) + .then(function (document) { + return data_store.consume_identity_check_token(token); + }) + .then(function (document) { + return data_store.consume_identity_check_token(token); + }) + .catch(function (err) { + console.error(err); + done(); + }); + }); + + it("should fail when token does not exist", function () { + const data_store = new UserDataStore(options); + + const token = "token"; + + return data_store.consume_identity_check_token(token) + .then(function (document) { + return Promise.reject("Error while checking token"); + }) + .catch(function (err) { + return Promise.resolve(err); + }); + }); + + it("should fail when token expired", function (done) { + const data_store = new UserDataStore(options); + + const userid = "user"; + const token = "token"; + const max_age = 60; + MockDate.set("1/1/2000"); + + data_store.issue_identity_check_token(userid, token, {}, max_age) + .then(function () { + MockDate.set("1/2/2000"); + return data_store.consume_identity_check_token(token); + }) + .catch(function (err) { + MockDate.reset(); + done(); + }); + }); + + it("should save the userid and some data with the token", function (done) { + const data_store = new UserDataStore(options); + + const userid = "user"; + const token = "token"; + const max_age = 60; + MockDate.set("1/1/2000"); + const data = "abc"; + + data_store.issue_identity_check_token(userid, token, data, max_age) + .then(function () { + return data_store.consume_identity_check_token(token); + }) + .then(function (content) { + const expected_content = { + userid: "user", + data: "abc" + }; + assert.deepEqual(content, expected_content); + done(); + }); + }); + }); +}); diff --git a/test/unitary/config_adapter.test.ts b/test/unitary/config_adapter.test.ts index 51a4eef9..6f55ac2b 100644 --- a/test/unitary/config_adapter.test.ts +++ b/test/unitary/config_adapter.test.ts @@ -1,19 +1,31 @@ import * as Assert from "assert"; +import { UserConfiguration } from "../../src/lib/Configuration"; +import config_adapter = require("../../src/lib/config_adapter"); -const config_adapter = require("../../src/lib/config_adapter"); describe("test config adapter", function() { - function build_yaml_config(): any { + function build_yaml_config(): UserConfiguration { const yaml_config = { port: 8080, - ldap: {}, + ldap: { + url: "http://ldap", + base_dn: "cn=test,dc=example,dc=com", + user: "user", + password: "pass" + }, session: { domain: "example.com", secret: "secret", max_age: 40000 }, store_directory: "/mydirectory", - logs_level: "debug" + logs_level: "debug", + notifier: { + gmail: { + user: "user", + pass: "password" + } + } }; return yaml_config; } @@ -36,8 +48,9 @@ describe("test config adapter", function() { const yaml_config = build_yaml_config(); yaml_config.ldap = { url: "http://ldap", - user_search_base: "ou=groups,dc=example,dc=com", - user_search_filter: "uid", + base_dn: "cn=test,dc=example,dc=com", + additional_user_dn: "ou=users", + user_name_attribute: "uid", user: "admin", password: "pass" }; @@ -45,8 +58,8 @@ describe("test config adapter", function() { const config = config_adapter(yaml_config); Assert.equal(config.ldap.url, "http://ldap"); - Assert.equal(config.ldap.user_search_base, "ou=groups,dc=example,dc=com"); - Assert.equal(config.ldap.user_search_filter, "uid"); + Assert.equal(config.ldap.additional_user_dn, "ou=users"); + Assert.equal(config.ldap.user_name_attribute, "uid"); Assert.equal(config.ldap.user, "admin"); Assert.equal(config.ldap.password, "pass"); }); @@ -59,9 +72,9 @@ describe("test config adapter", function() { expiration: 3600 }; const config = config_adapter(yaml_config); - Assert.equal(config.session_domain, "example.com"); - Assert.equal(config.session_secret, "secret"); - Assert.equal(config.session_max_age, 3600); + Assert.equal(config.session.domain, "example.com"); + Assert.equal(config.session.secret, "secret"); + Assert.equal(config.session.expiration, 3600); }); it("should get the log level", function() { @@ -73,15 +86,33 @@ describe("test config adapter", function() { it("should get the notifier config", function() { const yaml_config = build_yaml_config(); - yaml_config.notifier = "notifier"; + yaml_config.notifier = { + gmail: { + user: "user", + pass: "pass" + } + }; const config = config_adapter(yaml_config); - Assert.equal(config.notifier, "notifier"); + Assert.deepEqual(config.notifier, { + gmail: { + user: "user", + pass: "pass" + } + }); }); it("should get the access_control config", function() { const yaml_config = build_yaml_config(); - yaml_config.access_control = "access_control"; + yaml_config.access_control = { + default: [], + users: {}, + groups: {} + }; const config = config_adapter(yaml_config); - Assert.equal(config.access_control, "access_control"); + Assert.deepEqual(config.access_control, { + default: [], + users: {}, + groups: {} + }); }); }); diff --git a/test/unitary/data_persistence.test.ts b/test/unitary/data_persistence.test.ts new file mode 100644 index 00000000..4f2154f5 --- /dev/null +++ b/test/unitary/data_persistence.test.ts @@ -0,0 +1,179 @@ + +import * as Promise from "bluebird"; +import * as request from "request"; + +import Server from "../../src/lib/Server"; +import { UserConfiguration } from "../../src/lib/Configuration"; +import { GlobalDependencies } from "../../src/lib/GlobalDependencies"; +import * as tmp from "tmp"; + + +const requestp = Promise.promisifyAll(request) as request.Request; +const assert = require("assert"); +const speakeasy = require("speakeasy"); +const sinon = require("sinon"); +const nedb = require("nedb"); +const session = require("express-session"); +const winston = require("winston"); + +const PORT = 8050; +const requests = require("./requests")(PORT); + +describe("test data persistence", function () { + let u2f: any; + let tmpDir: tmp.SynchrounousResult; + const ldap_client = { + bind: sinon.stub(), + search: sinon.stub(), + on: sinon.spy() + }; + const ldap = { + createClient: sinon.spy(function () { + return ldap_client; + }) + }; + + let config: UserConfiguration; + + before(function () { + u2f = { + startRegistration: sinon.stub(), + finishRegistration: sinon.stub(), + startAuthentication: sinon.stub(), + finishAuthentication: sinon.stub() + }; + + const search_doc = { + object: { + mail: "test_ok@example.com" + } + }; + + const search_res = { + on: sinon.spy(function (event: string, fn: (s: object) => void) { + if (event != "error") fn(search_doc); + }) + }; + + ldap_client.bind.withArgs("cn=test_ok,ou=users,dc=example,dc=com", + "password").yields(undefined); + ldap_client.bind.withArgs("cn=test_nok,ou=users,dc=example,dc=com", + "password").yields("error"); + ldap_client.search.yields(undefined, search_res); + + tmpDir = tmp.dirSync({ unsafeCleanup: true }); + config = { + port: PORT, + ldap: { + url: "ldap://127.0.0.1:389", + base_dn: "ou=users,dc=example,dc=com", + user: "user", + password: "password" + }, + session: { + secret: "session_secret", + expiration: 50000, + }, + store_directory: tmpDir.name, + notifier: { + gmail: { + user: "user@example.com", + pass: "password" + } + } + }; + }); + + after(function () { + tmpDir.removeCallback(); + }); + + it("should save a u2f meta and reload it after a restart of the server", function () { + let server: Server; + const sign_request = {}; + const sign_status = {}; + const registration_request = {}; + const registration_status = {}; + u2f.startRegistration.returns(Promise.resolve(sign_request)); + u2f.finishRegistration.returns(Promise.resolve(sign_status)); + u2f.startAuthentication.returns(Promise.resolve(registration_request)); + u2f.finishAuthentication.returns(Promise.resolve(registration_status)); + + const nodemailer = { + createTransport: sinon.spy(function () { + return transporter; + }) + }; + const transporter = { + sendMail: sinon.stub().yields() + }; + + const deps = { + u2f: u2f, + nedb: nedb, + nodemailer: nodemailer, + session: session, + winston: winston, + ldapjs: ldap, + speakeasy: speakeasy + } as GlobalDependencies; + + const j1 = request.jar(); + const j2 = request.jar(); + + return start_server(config, deps) + .then(function (s) { + server = s; + return requests.login(j1); + }) + .then(function (res) { + return requests.first_factor(j1); + }) + .then(function () { + return requests.u2f_registration(j1, transporter); + }) + .then(function () { + return requests.u2f_authentication(j1); + }) + .then(function () { + return stop_server(server); + }) + .then(function () { + return start_server(config, deps); + }) + .then(function (s) { + server = s; + return requests.login(j2); + }) + .then(function () { + return requests.first_factor(j2); + }) + .then(function () { + return requests.u2f_authentication(j2); + }) + .then(function (res) { + assert.equal(204, res.statusCode); + server.stop(); + return Promise.resolve(); + }) + .catch(function (err) { + console.error(err); + return Promise.reject(err); + }); + }); + + function start_server(config: UserConfiguration, deps: GlobalDependencies): Promise { + return new Promise(function (resolve, reject) { + const s = new Server(); + s.start(config, deps); + resolve(s); + }); + } + + function stop_server(s: Server) { + return new Promise(function (resolve, reject) { + s.stop(); + resolve(); + }); + } +}); diff --git a/test/unitary/server_config.test.ts b/test/unitary/server_config.test.ts new file mode 100644 index 00000000..6e4bf17f --- /dev/null +++ b/test/unitary/server_config.test.ts @@ -0,0 +1,72 @@ + +import * as assert from "assert"; +import * as sinon from "sinon"; +import nedb = require("nedb"); +import * as express from "express"; +import * as winston from "winston"; +import * as speakeasy from "speakeasy"; +import * as u2f from "authdog"; + +import { AppConfiguration, UserConfiguration } from "../../src/lib/Configuration"; +import { GlobalDependencies } from "../../src/lib/GlobalDependencies"; +import Server from "../../src/lib/Server"; + + +describe("test server configuration", function () { + let deps: GlobalDependencies; + + before(function () { + const transporter = { + sendMail: sinon.stub().yields() + }; + + const nodemailer = { + createTransport: sinon.spy(function () { + return transporter; + }) + }; + + deps = { + speakeasy: speakeasy, + u2f: u2f, + nedb: nedb, + winston: winston, + nodemailer: nodemailer, + ldapjs: { + createClient: sinon.spy(function () { + return { on: sinon.spy() }; + }) + }, + session: sinon.spy(function () { + return function (req: express.Request, res: express.Response, next: express.NextFunction) { next(); }; + }) + }; + }); + + + it("should set cookie scope to domain set in the config", function () { + const config = { + session: { + domain: "example.com", + secret: "secret" + }, + ldap: { + url: "http://ldap", + user: "user", + password: "password" + }, + notifier: { + gmail: { + user: "user@example.com", + pass: "password" + } + } + } as UserConfiguration; + + const server = new Server(); + server.start(config, deps); + + assert(deps.session.calledOnce); + assert.equal(deps.session.getCall(0).args[0].cookie.domain, "example.com"); + }); +}); diff --git a/test/unitary/test_data_persistence.js b/test/unitary/test_data_persistence.js deleted file mode 100644 index ec6efbdb..00000000 --- a/test/unitary/test_data_persistence.js +++ /dev/null @@ -1,162 +0,0 @@ - -var server = require('../../src/lib/server'); - -var Promise = require('bluebird'); -var request = Promise.promisifyAll(require('request')); -var assert = require('assert'); -var speakeasy = require('speakeasy'); -var sinon = require('sinon'); -var tmp = require('tmp'); -var nedb = require('nedb'); -var session = require('express-session'); -var winston = require('winston'); - -var PORT = 8050; -var requests = require('./requests')(PORT); - - -describe('test data persistence', function() { - var u2f; - var tmpDir; - var ldap_client = { - bind: sinon.stub(), - search: sinon.stub(), - on: sinon.spy() - }; - var ldap = { - createClient: sinon.spy(function() { - return ldap_client; - }) - } - var config; - - before(function() { - u2f = {}; - u2f.startRegistration = sinon.stub(); - u2f.finishRegistration = sinon.stub(); - u2f.startAuthentication = sinon.stub(); - u2f.finishAuthentication = sinon.stub(); - - var search_doc = { - object: { - mail: 'test_ok@example.com' - } - }; - - var search_res = {}; - search_res.on = sinon.spy(function(event, fn) { - if(event != 'error') fn(search_doc); - }); - - ldap_client.bind.withArgs('cn=test_ok,ou=users,dc=example,dc=com', - 'password').yields(undefined); - ldap_client.bind.withArgs('cn=test_nok,ou=users,dc=example,dc=com', - 'password').yields('error'); - ldap_client.search.yields(undefined, search_res); - - tmpDir = tmp.dirSync({ unsafeCleanup: true }); - config = { - port: PORT, - totp_secret: 'totp_secret', - ldap: { - url: 'ldap://127.0.0.1:389', - base_dn: 'ou=users,dc=example,dc=com', - }, - session: { - secret: 'session_secret', - expiration: 50000, - }, - store_directory: tmpDir.name, - notifier: { gmail: { user: 'user@example.com', pass: 'password' } } - }; - }); - - after(function() { - tmpDir.removeCallback(); - }); - - it('should save a u2f meta and reload it after a restart of the server', function() { - var server; - var sign_request = {}; - var sign_status = {}; - var registration_request = {}; - var registration_status = {}; - u2f.startRegistration.returns(Promise.resolve(sign_request)); - u2f.finishRegistration.returns(Promise.resolve(sign_status)); - u2f.startAuthentication.returns(Promise.resolve(registration_request)); - u2f.finishAuthentication.returns(Promise.resolve(registration_status)); - - var nodemailer = {}; - var transporter = { - sendMail: sinon.stub().yields() - }; - nodemailer.createTransport = sinon.spy(function() { - return transporter; - }); - - var deps = {}; - deps.u2f = u2f; - deps.nedb = nedb; - deps.nodemailer = nodemailer; - deps.session = session; - deps.winston = winston; - deps.ldapjs = ldap; - - var j1 = request.jar(); - var j2 = request.jar(); - - return start_server(config, deps) - .then(function(s) { - server = s; - return requests.login(j1); - }) - .then(function(res) { - return requests.first_factor(j1); - }) - .then(function() { - return requests.u2f_registration(j1, transporter); - }) - .then(function() { - return requests.u2f_authentication(j1); - }) - .then(function() { - return stop_server(server); - }) - .then(function() { - return start_server(config, deps) - }) - .then(function(s) { - server = s; - return requests.login(j2); - }) - .then(function() { - return requests.first_factor(j2); - }) - .then(function() { - return requests.u2f_authentication(j2); - }) - .then(function(res) { - assert.equal(204, res.statusCode); - server.close(); - return Promise.resolve(); - }) - .catch(function(err) { - console.error(err); - return Promise.reject(err); - }); - }); - - function start_server(config, deps) { - return new Promise(function(resolve, reject) { - var s = server.run(config, deps); - resolve(s); - }); - } - - function stop_server(s) { - return new Promise(function(resolve, reject) { - s.close(); - resolve(); - }); - } -}); diff --git a/test/unitary/test_server.js b/test/unitary/test_server.js deleted file mode 100644 index 3a94d7aa..00000000 --- a/test/unitary/test_server.js +++ /dev/null @@ -1,389 +0,0 @@ - -var server = require('../../src/lib/server'); -var Ldap = require('../../src/lib/ldap'); - -var Promise = require('bluebird'); -var request = Promise.promisifyAll(require('request')); -var assert = require('assert'); -var speakeasy = require('speakeasy'); -var sinon = require('sinon'); -var MockDate = require('mockdate'); -var session = require('express-session'); -var winston = require('winston'); -var speakeasy = require('speakeasy'); -var ldapjs = require('ldapjs'); - -var PORT = 8090; -var BASE_URL = 'http://localhost:' + PORT; -var requests = require('./requests')(PORT); - -describe('test the server', function() { - var _server - var deps; - var u2f, nedb; - var transporter; - var collection; - - beforeEach(function(done) { - var config = { - port: PORT, - totp_secret: 'totp_secret', - ldap: { - url: 'ldap://127.0.0.1:389', - base_dn: 'ou=users,dc=example,dc=com', - user_name_attribute: 'cn', - user: 'cn=admin,dc=example,dc=com', - password: 'password', - }, - session: { - secret: 'session_secret', - expiration: 50000, - }, - store_in_memory: true, - notifier: { - gmail: { - user: 'user@example.com', - pass: 'password' - } - } - }; - - var ldap_client = { - bind: sinon.stub(), - search: sinon.stub(), - modify: sinon.stub(), - on: sinon.spy() - }; - var ldap = { - Change: sinon.spy(), - createClient: sinon.spy(function() { - return ldap_client; - }) - }; - - u2f = {}; - u2f.startRegistration = sinon.stub(); - u2f.finishRegistration = sinon.stub(); - u2f.startAuthentication = sinon.stub(); - u2f.finishAuthentication = sinon.stub(); - - nedb = require('nedb'); - - transporter = {}; - transporter.sendMail = sinon.stub().yields(); - - var nodemailer = {}; - nodemailer.createTransport = sinon.spy(function() { - return transporter; -  }); - - ldap_document = { - object: { - mail: 'test_ok@example.com', - } - }; - - var search_res = {}; - search_res.on = sinon.spy(function(event, fn) { - if(event != 'error') fn(ldap_document); - }); - - ldap_client.bind.withArgs('cn=test_ok,ou=users,dc=example,dc=com', - 'password').yields(undefined); - ldap_client.bind.withArgs('cn=admin,dc=example,dc=com', - 'password').yields(undefined); - - ldap_client.bind.withArgs('cn=test_nok,ou=users,dc=example,dc=com', - 'password').yields('error'); - - ldap_client.modify.yields(undefined); - ldap_client.search.yields(undefined, search_res); - - var deps = {}; - deps.u2f = u2f; - deps.nedb = nedb; - deps.nodemailer = nodemailer; - deps.ldapjs = ldap; - deps.session = session; - deps.winston = winston; - deps.speakeasy = speakeasy; - - _server = server.run(config, deps, function() { - done(); - }); - }); - - afterEach(function() { - _server.close(); -  }); - - describe('test GET /login', function() { - test_login(); - }); - - describe('test GET /logout', function() { - test_logout(); - }); - - describe('test GET /reset-password-form', function() { - test_reset_password_form(); - }); - - describe('test endpoints locks', function() { - function should_post_and_reply_with(url, status_code) { - return request.postAsync(url).then(function(response) { - assert.equal(response.statusCode, status_code); - return Promise.resolve(); - }) - } - - function should_get_and_reply_with(url, status_code) { - return request.getAsync(url).then(function(response) { - assert.equal(response.statusCode, status_code); - return Promise.resolve(); - }) - } - - function should_post_and_reply_with_403(url) { - return should_post_and_reply_with(url, 403); -  } - function should_get_and_reply_with_403(url) { - return should_get_and_reply_with(url, 403); -  } - - function should_post_and_reply_with_401(url) { - return should_post_and_reply_with(url, 401); -  } - function should_get_and_reply_with_401(url) { - return should_get_and_reply_with(url, 401); -  } - - function should_get_and_post_reply_with_403(url) { - var p1 = should_post_and_reply_with_403(url); - var p2 = should_get_and_reply_with_403(url); - return Promise.all([p1, p2]); -  } - - it('should block /new-password', function() { - return should_post_and_reply_with_403(BASE_URL + '/new-password') - }); - - it('should block /u2f-register', function() { - return should_get_and_post_reply_with_403(BASE_URL + '/u2f-register'); - }); - - it('should block /reset-password', function() { - return should_get_and_post_reply_with_403(BASE_URL + '/reset-password'); - }); - - it('should block /2ndfactor/u2f/register_request', function() { - return should_get_and_reply_with_403(BASE_URL + '/2ndfactor/u2f/register_request'); - }); - - it('should block /2ndfactor/u2f/register', function() { - return should_post_and_reply_with_403(BASE_URL + '/2ndfactor/u2f/register'); - }); - - it('should block /2ndfactor/u2f/sign_request', function() { - return should_get_and_reply_with_403(BASE_URL + '/2ndfactor/u2f/sign_request'); - }); - - it('should block /2ndfactor/u2f/sign', function() { - return should_post_and_reply_with_403(BASE_URL + '/2ndfactor/u2f/sign'); - }); - }); - - describe('test authentication and verification', function() { - test_authentication(); - test_reset_password(); - test_regulation(); - }); - - function test_reset_password_form() { - it('should serve the reset password form page', function(done) { - request.getAsync(BASE_URL + '/reset-password-form') - .then(function(response) { - assert.equal(response.statusCode, 200); - done(); - }); - }); - } - - function test_login() { - it('should serve the login page', function(done) { - request.getAsync(BASE_URL + '/login') - .then(function(response) { - assert.equal(response.statusCode, 200); - done(); - }); - }); - } - - function test_logout() { - it('should logout and redirect to /', function(done) { - request.getAsync(BASE_URL + '/logout') - .then(function(response) { - assert.equal(response.req.path, '/'); - done(); - }); - }); - } - - function test_authentication() { - it('should return status code 401 when user is not authenticated', function() { - return request.getAsync({ url: BASE_URL + '/verify' }) - .then(function(response) { - assert.equal(response.statusCode, 401); - return Promise.resolve(); - }); - }); - - it('should return status code 204 when user is authenticated using totp', function() { - var j = request.jar(); - return requests.login(j) - .then(function(res) { - assert.equal(res.statusCode, 200, 'get login page failed'); - return requests.first_factor(j); - }) - .then(function(res) { - assert.equal(res.statusCode, 204, 'first factor failed'); - return requests.register_totp(j, transporter); - }) - .then(function(secret) { - var sec = JSON.parse(secret); - var real_token = speakeasy.totp({ - secret: sec.base32, - encoding: 'base32' - }); - return requests.totp(j, real_token); - }) - .then(function(res) { - assert.equal(res.statusCode, 204, 'second factor failed'); - return requests.verify(j); - }) - .then(function(res) { - assert.equal(res.statusCode, 204, 'verify failed'); - return Promise.resolve(); - }); - }); - - it('should keep session variables when login page is reloaded', function() { - var real_token = speakeasy.totp({ - secret: 'totp_secret', - encoding: 'base32' - }); - var j = request.jar(); - return requests.login(j) - .then(function(res) { - assert.equal(res.statusCode, 200, 'get login page failed'); - return requests.first_factor(j); - }) - .then(function(res) { - assert.equal(res.statusCode, 204, 'first factor failed'); - return requests.totp(j, real_token); - }) - .then(function(res) { - assert.equal(res.statusCode, 204, 'second factor failed'); - return requests.login(j); - }) - .then(function(res) { - assert.equal(res.statusCode, 200, 'login page loading failed'); - return requests.verify(j); - }) - .then(function(res) { - assert.equal(res.statusCode, 204, 'verify failed'); - return Promise.resolve(); - }) - .catch(function(err) { - console.error(err); -  }); - }); - - it('should return status code 204 when user is authenticated using u2f', function() { - var sign_request = {}; - var sign_status = {}; - var registration_request = {}; - var registration_status = {}; - u2f.startRegistration.returns(Promise.resolve(sign_request)); - u2f.finishRegistration.returns(Promise.resolve(sign_status)); - u2f.startAuthentication.returns(Promise.resolve(registration_request)); - u2f.finishAuthentication.returns(Promise.resolve(registration_status)); - - var j = request.jar(); - return requests.login(j) - .then(function(res) { - assert.equal(res.statusCode, 200, 'get login page failed'); - return requests.first_factor(j); - }) - .then(function(res) { - assert.equal(res.statusCode, 204, 'first factor failed'); - return requests.u2f_registration(j, transporter); - }) - .then(function(res) { - assert.equal(res.statusCode, 204, 'second factor, finish register failed'); - return requests.u2f_authentication(j); - }) - .then(function(res) { - assert.equal(res.statusCode, 204, 'second factor, finish sign failed'); - return requests.verify(j); - }) - .then(function(res) { - assert.equal(res.statusCode, 204, 'verify failed'); - return Promise.resolve(); - }); - }); - } - - function test_reset_password() { - it('should reset the password', function() { - var j = request.jar(); - return requests.login(j) - .then(function(res) { - assert.equal(res.statusCode, 200, 'get login page failed'); - return requests.first_factor(j); - }) - .then(function(res) { - assert.equal(res.statusCode, 204, 'first factor failed'); - return requests.reset_password(j, transporter, 'user', 'new-password'); - }) - .then(function(res) { - assert.equal(res.statusCode, 204, 'second factor, finish register failed'); - return Promise.resolve(); - }); - }); - } - - function test_regulation() { - it('should regulate authentication', function() { - var j = request.jar(); - MockDate.set('1/2/2017 00:00:00'); - return requests.login(j) - .then(function(res) { - assert.equal(res.statusCode, 200, 'get login page failed'); - return requests.failing_first_factor(j); - }) - .then(function(res) { - assert.equal(res.statusCode, 401, 'first factor failed'); - return requests.failing_first_factor(j); - }) - .then(function(res) { - assert.equal(res.statusCode, 401, 'first factor failed'); - return requests.failing_first_factor(j); - }) - .then(function(res) { - assert.equal(res.statusCode, 401, 'first factor failed'); - return requests.failing_first_factor(j); - }) - .then(function(res) { - assert.equal(res.statusCode, 403, 'first factor failed'); - MockDate.set('1/2/2017 00:30:00'); - return requests.failing_first_factor(j); - }) - .then(function(res) { - assert.equal(res.statusCode, 401, 'first factor failed'); - return Promise.resolve(); - }) - }); - } -}); - diff --git a/test/unitary/test_server_config.js b/test/unitary/test_server_config.js deleted file mode 100644 index aadca125..00000000 --- a/test/unitary/test_server_config.js +++ /dev/null @@ -1,52 +0,0 @@ - -var sinon = require('sinon'); -var server = require('../../src/lib/server'); -var assert = require('assert'); - -describe('test server configuration', function() { - var deps; - var config; - - before(function() { - config = {}; - config.notifier = { - gmail: { - user: 'user@example.com', - pass: 'password' - } - } - - transporter = {}; - transporter.sendMail = sinon.stub().yields(); - - var nodemailer = {}; - nodemailer.createTransport = sinon.spy(function() { - return transporter; -  }); - - deps = {}; - deps.nedb = require('nedb'); - deps.winston = sinon.spy(); - deps.nodemailer = nodemailer; - deps.ldapjs = {}; - deps.ldapjs.createClient = sinon.spy(function() { - return { on: sinon.spy() }; - }); - deps.session = sinon.spy(function() { - return function(req, res, next) { next(); }; - }); - }); - - - it('should set cookie scope to domain set in the config', function() { - config.session = {}; - config.session.domain = 'example.com'; - config.session.secret = 'secret'; - config.ldap = {}; - config.ldap.url = 'http://ldap'; - server.run(config, deps); - - assert(deps.session.calledOnce); - assert.equal(deps.session.getCall(0).args[0].cookie.domain, 'example.com'); - }); -}); diff --git a/test/unitary/test_totp.js b/test/unitary/test_totp.js deleted file mode 100644 index c8648e7a..00000000 --- a/test/unitary/test_totp.js +++ /dev/null @@ -1,32 +0,0 @@ - -var totp = require('../../src/lib/totp'); -var sinon = require('sinon'); -var Promise = require('bluebird'); - -describe('test TOTP validation', function() { - it('should validate the TOTP token', function() { - var totp_secret = 'NBD2ZV64R9UV1O7K'; - var token = 'token'; - var totp_mock = sinon.mock(); - totp_mock.returns('token'); - var speakeasy_mock = { - totp: totp_mock - } - return totp.validate(speakeasy_mock, token, totp_secret); - }); - - it('should not validate a wrong TOTP token', function() { - var totp_secret = 'NBD2ZV64R9UV1O7K'; - var token = 'wrong token'; - var totp_mock = sinon.mock(); - totp_mock.returns('token'); - var speakeasy_mock = { - totp: totp_mock - } - return totp.validate(speakeasy_mock, token, totp_secret) - .catch(function() { - return Promise.resolve(); - }); - }); -}); - diff --git a/test/unitary/test_user_data_store.js b/test/unitary/test_user_data_store.js deleted file mode 100644 index a9a343ab..00000000 --- a/test/unitary/test_user_data_store.js +++ /dev/null @@ -1,212 +0,0 @@ - -var UserDataStore = require('../../src/lib/user_data_store'); -var DataStore = require('nedb'); -var assert = require('assert'); -var Promise = require('bluebird'); -var sinon = require('sinon'); -var MockDate = require('mockdate'); - -describe('test user data store', function() { - describe('test u2f meta', test_u2f_meta); - describe('test u2f registration token', test_u2f_registration_token); -}); - -function test_u2f_meta() { - it('should save a u2f meta', function() { - var options = {}; - options.inMemoryOnly = true; - - var data_store = new UserDataStore(DataStore, options); - - var userid = 'user'; - var app_id = 'https://localhost'; - var meta = {}; - meta.publicKey = 'pbk'; - - return data_store.set_u2f_meta(userid, app_id, meta) - .then(function(numUpdated) { - assert.equal(1, numUpdated); - return Promise.resolve(); - }); - }); - - it('should retrieve no u2f meta', function() { - var options = {}; - options.inMemoryOnly = true; - - var data_store = new UserDataStore(DataStore, options); - - var userid = 'user'; - var app_id = 'https://localhost'; - var meta = {}; - meta.publicKey = 'pbk'; - - return data_store.get_u2f_meta(userid, app_id) - .then(function(doc) { - assert.equal(undefined, doc); - return Promise.resolve(); - }); - }); - - it('should insert and retrieve a u2f meta', function() { - var options = {}; - options.inMemoryOnly = true; - - var data_store = new UserDataStore(DataStore, options); - - var userid = 'user'; - var app_id = 'https://localhost'; - var meta = {}; - meta.publicKey = 'pbk'; - - return data_store.set_u2f_meta(userid, app_id, meta) - .then(function(numUpdated, data) { - assert.equal(1, numUpdated); - return data_store.get_u2f_meta(userid, app_id) - }) - .then(function(doc) { - assert.deepEqual(meta, doc.meta); - assert.deepEqual(userid, doc.userid); - assert.deepEqual(app_id, doc.appid); - assert('_id' in doc); - return Promise.resolve(); - }); - }); -} - -function test_u2f_registration_token() { - it('should save u2f registration token', function() { - var options = {}; - options.inMemoryOnly = true; - - var data_store = new UserDataStore(DataStore, options); - - var userid = 'user'; - var token = 'token'; - var max_age = 60; - var content = 'abc'; - - return data_store.issue_identity_check_token(userid, token, content, max_age) - .then(function(document) { - assert.equal(document.userid, userid); - assert.equal(document.token, token); - assert.deepEqual(document.content, { userid: 'user', data: content }); - assert('max_date' in document); - assert('_id' in document); - return Promise.resolve(); - }) - .catch(function(err) { - console.error(err); - return Promise.reject(err); - }); - }); - - it('should save u2f registration token and consume it', function(done) { - var options = {}; - options.inMemoryOnly = true; - - var data_store = new UserDataStore(DataStore, options); - - var userid = 'user'; - var token = 'token'; - var max_age = 50; - - data_store.issue_identity_check_token(userid, token, {}, max_age) - .then(function(document) { - return data_store.consume_identity_check_token(token); - }) - .then(function() { - done(); - }) - .catch(function(err) { - console.error(err); - }); - }); - - it('should not be able to consume registration token twice', function(done) { - var options = {}; - options.inMemoryOnly = true; - - var data_store = new UserDataStore(DataStore, options); - - var userid = 'user'; - var token = 'token'; - var max_age = 50; - - data_store.issue_identity_check_token(userid, token, {}, max_age) - .then(function(document) { - return data_store.consume_identity_check_token(token); - }) - .then(function(document) { - return data_store.consume_identity_check_token(token); - }) - .catch(function(err) { - console.error(err); - done(); - }); - }); - - it('should fail when token does not exist', function() { - var options = {}; - options.inMemoryOnly = true; - - var data_store = new UserDataStore(DataStore, options); - - var token = 'token'; - - return data_store.consume_identity_check_token(token) - .then(function(document) { - return Promise.reject(); - }) - .catch(function(err) { - return Promise.resolve(err); - }); - }); - - it('should fail when token expired', function(done) { - var options = {}; - options.inMemoryOnly = true; - - var data_store = new UserDataStore(DataStore, options); - - var userid = 'user'; - var token = 'token'; - var max_age = 60; - MockDate.set('1/1/2000'); - - data_store.issue_identity_check_token(userid, token, {}, max_age) - .then(function() { - MockDate.set('1/2/2000'); - return data_store.consume_identity_check_token(token); - }) - .catch(function(err) { - MockDate.reset(); - done(); - }); - }); - - it('should save the userid and some data with the token', function(done) { - var options = {}; - options.inMemoryOnly = true; - - var data_store = new UserDataStore(DataStore, options); - - var userid = 'user'; - var token = 'token'; - var max_age = 60; - MockDate.set('1/1/2000'); - var data = 'abc'; - - data_store.issue_identity_check_token(userid, token, data, max_age) - .then(function() { - return data_store.consume_identity_check_token(token); - }) - .then(function(content) { - var expected_content = {}; - expected_content.userid = 'user'; - expected_content.data = 'abc'; - assert.deepEqual(content, expected_content); - done(); - }) - }); -} diff --git a/test/unitary/totp.test.ts b/test/unitary/totp.test.ts new file mode 100644 index 00000000..f797587d --- /dev/null +++ b/test/unitary/totp.test.ts @@ -0,0 +1,32 @@ + +const totp = require("../../src/lib/totp"); +const sinon = require("sinon"); +import Promise = require("bluebird"); + +describe("test TOTP validation", function() { + it("should validate the TOTP token", function() { + const totp_secret = "NBD2ZV64R9UV1O7K"; + const token = "token"; + const totp_mock = sinon.mock(); + totp_mock.returns("token"); + const speakeasy_mock = { + totp: totp_mock + }; + return totp.validate(speakeasy_mock, token, totp_secret); + }); + + it("should not validate a wrong TOTP token", function() { + const totp_secret = "NBD2ZV64R9UV1O7K"; + const token = "wrong token"; + const totp_mock = sinon.mock(); + totp_mock.returns("token"); + const speakeasy_mock = { + totp: totp_mock + }; + return totp.validate(speakeasy_mock, token, totp_secret) + .catch(function() { + return Promise.resolve(); + }); + }); +}); + diff --git a/test/unitary/user_data_store/authentication_audit.test.ts b/test/unitary/user_data_store/authentication_audit.test.ts new file mode 100644 index 00000000..2dc5c930 --- /dev/null +++ b/test/unitary/user_data_store/authentication_audit.test.ts @@ -0,0 +1,70 @@ + +import * as assert from "assert"; +import * as Promise from "bluebird"; +import * as sinon from "sinon"; +import * as MockDate from "mockdate"; +import UserDataStore from "../../../src/lib/UserDataStore"; + +describe("test user data store", function() { + describe("test authentication traces", test_authentication_traces); +}); + +function test_authentication_traces() { + it("should save an authentication trace in db", function() { + const options = { + inMemoryOnly: true + }; + + const data_store = new UserDataStore(options); + const userid = "user"; + const type = "1stfactor"; + const is_success = false; + return data_store.save_authentication_trace(userid, type, is_success) + .then(function(doc) { + assert("_id" in doc); + assert.equal(doc.userid, "user"); + assert.equal(doc.is_success, false); + assert.equal(doc.type, "1stfactor"); + return Promise.resolve(); + }); + }); + + it("should return 3 last authentication traces", function() { + const options = { + inMemoryOnly: true + }; + + const data_store = new UserDataStore(options); + const userid = "user"; + const type = "1stfactor"; + const is_success = false; + MockDate.set("2/1/2000"); + return data_store.save_authentication_trace(userid, type, false) + .then(function(doc) { + MockDate.set("1/2/2000"); + return data_store.save_authentication_trace(userid, type, true); + }) + .then(function(doc) { + MockDate.set("1/7/2000"); + return data_store.save_authentication_trace(userid, type, false); + }) + .then(function(doc) { + MockDate.set("1/2/2000"); + return data_store.save_authentication_trace(userid, type, false); + }) + .then(function(doc) { + MockDate.set("1/5/2000"); + return data_store.save_authentication_trace(userid, type, false); + }) + .then(function(doc) { + return data_store.get_last_authentication_traces(userid, type, false, 3); + }) + .then(function(docs) { + assert.equal(docs.length, 3); + assert.deepEqual(docs[0].date, new Date("2/1/2000")); + assert.deepEqual(docs[1].date, new Date("1/7/2000")); + assert.deepEqual(docs[2].date, new Date("1/5/2000")); + return Promise.resolve(); + }); + }); +} diff --git a/test/unitary/user_data_store/test_authentication_audit.js b/test/unitary/user_data_store/test_authentication_audit.js deleted file mode 100644 index d317b480..00000000 --- a/test/unitary/user_data_store/test_authentication_audit.js +++ /dev/null @@ -1,69 +0,0 @@ - -var assert = require('assert'); -var Promise = require('bluebird'); -var sinon = require('sinon'); -var MockDate = require('mockdate'); -var UserDataStore = require('../../../src/lib/user_data_store'); -var DataStore = require('nedb'); - -describe('test user data store', function() { - describe('test authentication traces', test_authentication_traces); -}); - -function test_authentication_traces() { - it('should save an authentication trace in db', function() { - var options = {}; - options.inMemoryOnly = true; - - var data_store = new UserDataStore(DataStore, options); - var userid = 'user'; - var type = '1stfactor'; - var is_success = false; - return data_store.save_authentication_trace(userid, type, is_success) - .then(function(doc) { - assert('_id' in doc); - assert.equal(doc.userid, 'user'); - assert.equal(doc.is_success, false); - assert.equal(doc.type, '1stfactor'); - return Promise.resolve(); - }); - }); - - it('should return 3 last authentication traces', function() { - var options = {}; - options.inMemoryOnly = true; - - var data_store = new UserDataStore(DataStore, options); - var userid = 'user'; - var type = '1stfactor'; - var is_success = false; - MockDate.set('2/1/2000'); - return data_store.save_authentication_trace(userid, type, false) - .then(function(doc) { - MockDate.set('1/2/2000'); - return data_store.save_authentication_trace(userid, type, true); - }) - .then(function(doc) { - MockDate.set('1/7/2000'); - return data_store.save_authentication_trace(userid, type, false); - }) - .then(function(doc) { - MockDate.set('1/2/2000'); - return data_store.save_authentication_trace(userid, type, false); - }) - .then(function(doc) { - MockDate.set('1/5/2000'); - return data_store.save_authentication_trace(userid, type, false); - }) - .then(function(doc) { - return data_store.get_last_authentication_traces(userid, type, false, 3); - }) - .then(function(docs) { - assert.equal(docs.length, 3); - assert.deepEqual(docs[0].date, new Date('2/1/2000')); - assert.deepEqual(docs[1].date, new Date('1/7/2000')); - assert.deepEqual(docs[2].date, new Date('1/5/2000')); - return Promise.resolve(); - }) - }); -} diff --git a/test/unitary/user_data_store/test_totp_secret.js b/test/unitary/user_data_store/test_totp_secret.js deleted file mode 100644 index f08e4fff..00000000 --- a/test/unitary/user_data_store/test_totp_secret.js +++ /dev/null @@ -1,65 +0,0 @@ - -var assert = require('assert'); -var Promise = require('bluebird'); -var sinon = require('sinon'); -var MockDate = require('mockdate'); -var UserDataStore = require('../../../src/lib/user_data_store'); -var DataStore = require('nedb'); - -describe('test user data store', function() { - describe('test totp secrets store', test_totp_secrets); -}); - -function test_totp_secrets() { - it('should save and reload a totp secret', function() { - var options = {}; - options.inMemoryOnly = true; - - var data_store = new UserDataStore(DataStore, options); - var userid = 'user'; - var secret = {}; - secret.ascii = 'abc'; - secret.base32 = 'ABCDKZLEFZGREJK'; - - return data_store.set_totp_secret(userid, secret) - .then(function() { - return data_store.get_totp_secret(userid); - }) - .then(function(doc) { - assert('_id' in doc); - assert.equal(doc.userid, 'user'); - assert.equal(doc.secret.ascii, 'abc'); - assert.equal(doc.secret.base32, 'ABCDKZLEFZGREJK'); - return Promise.resolve(); - }); - }); - - it('should only remember last secret', function() { - var options = {}; - options.inMemoryOnly = true; - - var data_store = new UserDataStore(DataStore, options); - var userid = 'user'; - var secret1 = {}; - secret1.ascii = 'abc'; - secret1.base32 = 'ABCDKZLEFZGREJK'; - var secret2 = {}; - secret2.ascii = 'def'; - secret2.base32 = 'XYZABC'; - - return data_store.set_totp_secret(userid, secret1) - .then(function() { - return data_store.set_totp_secret(userid, secret2) - }) - .then(function() { - return data_store.get_totp_secret(userid); - }) - .then(function(doc) { - assert('_id' in doc); - assert.equal(doc.userid, 'user'); - assert.equal(doc.secret.ascii, 'def'); - assert.equal(doc.secret.base32, 'XYZABC'); - return Promise.resolve(); - }); - }); -} diff --git a/test/unitary/user_data_store/totp_secret.test.ts b/test/unitary/user_data_store/totp_secret.test.ts new file mode 100644 index 00000000..cddaa227 --- /dev/null +++ b/test/unitary/user_data_store/totp_secret.test.ts @@ -0,0 +1,72 @@ + +import * as assert from "assert"; +import * as Promise from "bluebird"; +import * as sinon from "sinon"; +import * as MockDate from "mockdate"; +import UserDataStore from "../../../src/lib/UserDataStore"; + +describe("test user data store", function() { + describe("test totp secrets store", test_totp_secrets); +}); + +function test_totp_secrets() { + it("should save and reload a totp secret", function() { + const options = { + inMemoryOnly: true + }; + + const data_store = new UserDataStore(options); + const userid = "user"; + const secret = { + ascii: "abc", + base32: "ABCDKZLEFZGREJK", + otpauth_url: "totp://test" + }; + + return data_store.set_totp_secret(userid, secret) + .then(function() { + return data_store.get_totp_secret(userid); + }) + .then(function(doc) { + assert("_id" in doc); + assert.equal(doc.userid, "user"); + assert.equal(doc.secret.ascii, "abc"); + assert.equal(doc.secret.base32, "ABCDKZLEFZGREJK"); + return Promise.resolve(); + }); + }); + + it("should only remember last secret", function() { + const options = { + inMemoryOnly: true + }; + + const data_store = new UserDataStore(options); + const userid = "user"; + const secret1 = { + ascii: "abc", + base32: "ABCDKZLEFZGREJK", + otpauth_url: "totp://test" + }; + const secret2 = { + ascii: "def", + base32: "XYZABC", + otpauth_url: "totp://test" + }; + + return data_store.set_totp_secret(userid, secret1) + .then(function() { + return data_store.set_totp_secret(userid, secret2); + }) + .then(function() { + return data_store.get_totp_secret(userid); + }) + .then(function(doc) { + assert("_id" in doc); + assert.equal(doc.userid, "user"); + assert.equal(doc.secret.ascii, "def"); + assert.equal(doc.secret.base32, "XYZABC"); + return Promise.resolve(); + }); + }); +} diff --git a/tsconfig.json b/tsconfig.json index 40cc3822..4d4d2aa5 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -10,12 +10,13 @@ "allowJs": true, "paths": { "*": [ - "node_modules/*", + "node_modules/@types/*", "src/types/*" ] } }, "include": [ - "src/**/*" + "src/**/*", + "test/**/*" ] }