Migrate server to typescript

pull/33/head
Clement Michaud 2017-05-16 23:17:46 +02:00
parent 923886667d
commit b0c6c61df5
34 changed files with 1596 additions and 1340 deletions

View File

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

View File

@ -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",

View File

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

View File

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

View File

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

View File

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

84
src/lib/Server.ts 100644
View File

@ -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<void> {
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<void>((resolve, reject) => {
this.httpServer = app.listen(config.port, function (err: string) {
console.log("Listening on %d...", config.port);
resolve();
});
});
}
stop() {
this.httpServer.close();
}
}

View File

@ -0,0 +1,6 @@
export interface TOTPSecret {
base32: string;
ascii: string;
otpauth_url: string;
}

View File

@ -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<any> {
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<U2FMetaDocument> {
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<any> {
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<any> {
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<any> {
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<any> {
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<TOTPSecretDocument> {
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;
}

View File

@ -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<T>(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<object, string>(yaml_config, "session.domain"),
session_secret: ObjectPath.get<object, string>(yaml_config, "session.secret"),
session_max_age: get_optional<number>(yaml_config, "session.expiration", 3600000), // in ms
ldap: ObjectPath.get<object, LdapConfiguration>(yaml_config, "ldap"),
session: {
domain: ObjectPath.get<object, string>(yaml_config, "session.domain"),
secret: ObjectPath.get<object, string>(yaml_config, "session.secret"),
expiration: get_optional<number>(yaml_config, "session.expiration", 3600000), // in ms
},
store_directory: get_optional<string>(yaml_config, "store_directory", undefined),
logs_level: get_optional<string>(yaml_config, "logs_level", "info"),
notifier: ObjectPath.get<object, authelia.NotifiersConfiguration>(yaml_config, "notifier"),
access_control: ObjectPath.get<object, authelia.ACLConfiguration>(yaml_config, "access_control")
notifier: ObjectPath.get<object, NotifiersConfiguration>(yaml_config, "notifier"),
access_control: ObjectPath.get<object, ACLConfiguration>(yaml_config, "access_control")
};
};

View File

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

View File

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

67
src/types/authdog.d.ts vendored 100644
View File

@ -0,0 +1,67 @@
declare module "authdog" {
interface RegisterRequest {
challenge: string;
}
interface RegisteredKey {
version: number;
keyHandle: string;
}
type RegisteredKeys = Array<RegisteredKey>;
type RegisterRequests = Array<RegisterRequest>;
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<RegistrationRequest>;
export function finishRegistration(registrationRequest: RegistrationRequest, registrationResponse: RegistrationResponse): Promise<Registration>;
export function startAuthentication(appId: AppId, registeredKeys: RegisteredKeys, options: Options): Promise<AuthenticationRequest>;
export function finishAuthentication(challenge: string, deviceResponse: AuthenticationResponse, registeredKeys: RegisteredKeys): Promise<Authentication>;
}

View File

@ -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<DomainPattern>;
type ACLGroupsRules = Map<GroupName, DomainPattern>;
type ACLUsersRules = Map<UserName, DomainPattern>;
export interface ACLConfiguration {
default: ACLDefaultRules;
groups: ACLGroupsRules;
users: ACLUsersRules;
}
interface SessionCookieConfiguration {
secret: string;
expiration: number;
domain: string
}
type NotifierType = string;
export type NotifiersConfiguration = Map<NotifierType, any>;
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;
}
}

12
src/types/nedb-async.d.ts vendored 100644
View File

@ -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<any>;
findOneAsync(query: any): Promise<any>;
insertAsync<T>(newDoc: T): Promise<any>;
removeAsync(query: any): Promise<any>;
}
}

14
src/types/request-async.d.ts vendored 100644
View File

@ -0,0 +1,14 @@
import * as Promise from "bluebird";
import * as request from "request";
declare module "request" {
export interface RequestAsync extends RequestAPI<Request, CoreOptions, RequiredUriUrl> {
getAsync(uri: string, options?: RequiredUriUrl): Promise<RequestResponse>;
getAsync(uri: string): Promise<RequestResponse>;
getAsync(options: RequiredUriUrl & CoreOptions): Promise<RequestResponse>;
postAsync(uri: string, options?: CoreOptions): Promise<RequestResponse>;
postAsync(uri: string): Promise<RequestResponse>;
postAsync(options: RequiredUriUrl & CoreOptions): Promise<RequestResponse>;
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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<Server> {
return new Promise<Server>(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();
});
}
});

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -10,12 +10,13 @@
"allowJs": true,
"paths": {
"*": [
"node_modules/*",
"node_modules/@types/*",
"src/types/*"
]
}
},
"include": [
"src/**/*"
"src/**/*",
"test/**/*"
]
}