Merge pull request #33 from clems4ever/typescript

Move project to typescript and grunt
pull/36/head
Clément Michaud 2017-05-22 00:39:41 +02:00 committed by GitHub
commit 3890c5b24d
136 changed files with 5937 additions and 4955 deletions

10
.gitignore vendored
View File

@ -1,8 +1,13 @@
# NodeJs modules
node_modules/
# Coverage reports
coverage/
src/.baseDir.ts
.vscode/
*.swp
*.sh
@ -11,6 +16,11 @@ config.yml
npm-debug.log
# Directory used by example
notifications/
# VSCode user configuration
.vscode/
# Generated by TypeScript compiler
dist/

View File

@ -19,8 +19,9 @@ addons:
before_install: npm install -g npm@'>=2.13.5'
script:
- npm test
- docker build -t clems4ever/authelia .
- grunt test
- grunt build
- grunt docker-build
- docker-compose build
- docker-compose up -d
- sleep 5

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

55
Gruntfile.js 100644
View File

@ -0,0 +1,55 @@
module.exports = function(grunt) {
grunt.initConfig({
run: {
options: {},
"build-ts": {
cmd: "npm",
args: ['run', 'build-ts']
},
"tslint": {
cmd: "npm",
args: ['run', 'tslint']
},
"test": {
cmd: "npm",
args: ['run', 'test']
},
"docker-build": {
cmd: "docker",
args: ['build', '-t', 'clems4ever/authelia', '.']
}
},
copy: {
resources: {
expand: true,
cwd: 'src/resources/',
src: '**',
dest: 'dist/src/resources/'
},
views: {
expand: true,
cwd: 'src/views/',
src: '**',
dest: 'dist/src/views/'
},
public_html: {
expand: true,
cwd: 'src/public_html/',
src: '**',
dest: 'dist/src/public_html/'
}
}
});
grunt.loadNpmTasks('grunt-run');
grunt.loadNpmTasks('grunt-contrib-copy');
grunt.registerTask('default', ['build']);
grunt.registerTask('res', ['copy:resources', 'copy:views', 'copy:public_html']);
grunt.registerTask('build', ['run:tslint', 'run:build-ts', 'res']);
grunt.registerTask('docker-build', ['run:docker-build']);
grunt.registerTask('test', ['run:test']);
};

View File

@ -7,11 +7,14 @@
"authelia": "src/index.js"
},
"scripts": {
"test": "./node_modules/.bin/mocha --recursive test/unitary",
"unit-test": "./node_modules/.bin/mocha --recursive test/unitary",
"test": "./node_modules/.bin/mocha --compilers ts:ts-node/register --recursive test/unitary",
"test-dbg": "./node_modules/.bin/mocha --debug-brk --compilers ts:ts-node/register --recursive test/unitary",
"int-test": "./node_modules/.bin/mocha --recursive test/integration",
"all-test": "./node_modules/.bin/mocha --recursive test",
"coverage": "./node_modules/.bin/istanbul cover _mocha -- -R spec --recursive test"
"coverage": "./node_modules/.bin/istanbul cover _mocha -- -R spec --recursive test",
"build-ts": "tsc",
"watch-ts": "tsc -w",
"tslint": "tslint -c tslint.json -p tsconfig.json",
"serve": "node dist/src/index.js"
},
"repository": {
"type": "git",
@ -43,12 +46,39 @@
"yamljs": "^0.2.8"
},
"devDependencies": {
"@types/assert": "0.0.31",
"@types/bluebird": "^3.5.4",
"@types/body-parser": "^1.16.3",
"@types/ejs": "^2.3.33",
"@types/express": "^4.0.35",
"@types/express-session": "0.0.32",
"@types/ldapjs": "^1.0.0",
"@types/mocha": "^2.2.41",
"@types/mockdate": "^2.0.0",
"@types/nedb": "^1.8.3",
"@types/nodemailer": "^1.3.32",
"@types/object-path": "^0.9.28",
"@types/proxyquire": "^1.3.27",
"@types/randomstring": "^1.1.5",
"@types/request": "0.0.43",
"@types/sinon": "^2.2.1",
"@types/speakeasy": "^2.0.1",
"@types/tmp": "0.0.33",
"@types/winston": "^2.3.2",
"@types/yamljs": "^0.2.30",
"grunt": "^1.0.1",
"grunt-contrib-copy": "^1.0.0",
"grunt-run": "^0.6.0",
"mocha": "^3.2.0",
"mockdate": "^2.0.1",
"proxyquire": "^1.8.0",
"request": "^2.79.0",
"should": "^11.1.1",
"sinon": "^1.17.6",
"sinon-promise": "^0.1.3",
"tmp": "0.0.31"
"tmp": "0.0.31",
"ts-node": "^3.0.4",
"tslint": "^5.2.0",
"typescript": "^2.3.2"
}
}

View File

@ -1,36 +0,0 @@
#! /usr/bin/env node
process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
var server = require('./lib/server');
var ldapjs = require('ldapjs');
var u2f = require('authdog');
var nodemailer = require('nodemailer');
var nedb = require('nedb');
var YAML = require('yamljs');
var session = require('express-session');
var winston = require('winston');
var speakeasy = require('speakeasy');
var config_path = process.argv[2];
if(!config_path) {
console.log('No config file has been provided.');
console.log('Usage: authelia <config>');
process.exit(0);
}
console.log('Parse configuration file: %s', config_path);
var yaml_config = YAML.load(config_path);
var deps = {};
deps.u2f = u2f;
deps.nedb = nedb;
deps.nodemailer = nodemailer;
deps.ldapjs = ldapjs;
deps.session = session;
deps.winston = winston;
deps.speakeasy = speakeasy;
server.run(yaml_config, deps);

33
src/index.ts 100755
View File

@ -0,0 +1,33 @@
#! /usr/bin/env node
process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
import Server from "./lib/Server";
const YAML = require("yamljs");
const config_path = process.argv[2];
if (!config_path) {
console.log("No config file has been provided.");
console.log("Usage: authelia <config>");
process.exit(0);
}
console.log("Parse configuration file: %s", config_path);
const yaml_config = YAML.load(config_path);
const deps = {
u2f: require("authdog"),
nodemailer: require("nodemailer"),
ldapjs: require("ldapjs"),
session: require("express-session"),
winston: require("winston"),
speakeasy: require("speakeasy"),
nedb: require("nedb")
};
const server = new Server();
server.start(yaml_config, deps)
.then(() => {
console.log("The server is started!");
});

View File

@ -0,0 +1,43 @@
import * as BluebirdPromise from "bluebird";
import exceptions = require("./Exceptions");
const REGULATION_TRACE_TYPE = "regulation";
const MAX_AUTHENTICATION_COUNT_IN_TIME_RANGE = 3;
interface DatedDocument {
date: Date;
}
export default class AuthenticationRegulator {
private _user_data_store: any;
private _lock_time_in_seconds: number;
constructor(user_data_store: any, lock_time_in_seconds: number) {
this._user_data_store = user_data_store;
this._lock_time_in_seconds = lock_time_in_seconds;
}
// Mark authentication
mark(userid: string, is_success: boolean): BluebirdPromise<void> {
return this._user_data_store.save_authentication_trace(userid, REGULATION_TRACE_TYPE, is_success);
}
regulate(userid: string): BluebirdPromise<void> {
return this._user_data_store.get_last_authentication_traces(userid, REGULATION_TRACE_TYPE, false, 3)
.then((docs: Array<DatedDocument>) => {
if (docs.length < MAX_AUTHENTICATION_COUNT_IN_TIME_RANGE) {
// less than the max authorized number of authentication in time range, thus authorizing access
return BluebirdPromise.resolve();
}
const oldest_doc = docs[MAX_AUTHENTICATION_COUNT_IN_TIME_RANGE - 1];
const no_lock_min_date = new Date(new Date().getTime() - this._lock_time_in_seconds * 1000);
if (oldest_doc.date > no_lock_min_date) {
throw new exceptions.AuthenticationRegulationError("Max number of authentication. Please retry in few minutes.");
}
return BluebirdPromise.resolve();
});
}
}

View File

@ -0,0 +1,66 @@
export interface LdapConfiguration {
url: string;
base_dn: string;
additional_user_dn?: string;
user_name_attribute?: string; // cn by default
additional_group_dn?: string;
group_name_attribute?: string; // cn by default
user: string; // admin username
password: string; // admin password
}
type UserName = string;
type GroupName = string;
type DomainPattern = string;
export type ACLDefaultRules = DomainPattern[];
export type ACLGroupsRules = { [group: string]: string[]; };
export type ACLUsersRules = { [user: string]: string[]; };
export interface ACLConfiguration {
default: ACLDefaultRules;
groups: ACLGroupsRules;
users: ACLUsersRules;
}
interface SessionCookieConfiguration {
secret: string;
expiration?: number;
domain?: string;
}
export interface GmailNotifierConfiguration {
username: string;
password: string;
}
export interface FileSystemNotifierConfiguration {
filename: string;
}
export interface NotifierConfiguration {
gmail?: GmailNotifierConfiguration;
filesystem?: FileSystemNotifierConfiguration;
}
export interface UserConfiguration {
port?: number;
logs_level?: string;
ldap: LdapConfiguration;
session: SessionCookieConfiguration;
store_directory?: string;
notifier: NotifierConfiguration;
access_control?: ACLConfiguration;
}
export interface AppConfiguration {
port: number;
logs_level: string;
ldap: LdapConfiguration;
session: SessionCookieConfiguration;
store_in_memory?: boolean;
store_directory?: string;
notifier: NotifierConfiguration;
access_control?: ACLConfiguration;
}

View File

@ -0,0 +1,42 @@
import * as ObjectPath from "object-path";
import { AppConfiguration, UserConfiguration, NotifierConfiguration, ACLConfiguration, LdapConfiguration } from "./Configuration";
function get_optional<T>(config: object, path: string, default_value: T): T {
let entry = default_value;
if (ObjectPath.has(config, path)) {
entry = ObjectPath.get<object, T>(config, path);
}
return entry;
}
function ensure_key_existence(config: object, path: string): void {
if (!ObjectPath.has(config, path)) {
throw new Error(`Configuration error: key '${path}' is missing in configuration file`);
}
}
export default class ConfigurationAdapter {
static adapt(yaml_config: UserConfiguration): AppConfiguration {
ensure_key_existence(yaml_config, "ldap");
ensure_key_existence(yaml_config, "session.secret");
const port = ObjectPath.get(yaml_config, "port", 8080);
return {
port: port,
ldap: ObjectPath.get<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, NotifierConfiguration>(yaml_config, "notifier"),
access_control: ObjectPath.get<object, ACLConfiguration>(yaml_config, "access_control")
};
}
}

View File

@ -0,0 +1,56 @@
export class LdapSeachError extends Error {
constructor(message?: string) {
super(message);
this.name = "LdapSeachError";
Object.setPrototypeOf(this, LdapSeachError.prototype);
}
}
export class LdapBindError extends Error {
constructor(message?: string) {
super(message);
this.name = "LdapBindError";
Object.setPrototypeOf(this, LdapBindError.prototype);
}
}
export class IdentityError extends Error {
constructor(message?: string) {
super(message);
this.name = "IdentityError";
Object.setPrototypeOf(this, IdentityError.prototype);
}
}
export class AccessDeniedError extends Error {
constructor(message?: string) {
super(message);
this.name = "AccessDeniedError";
Object.setPrototypeOf(this, AccessDeniedError.prototype);
}
}
export class AuthenticationRegulationError extends Error {
constructor(message?: string) {
super(message);
this.name = "AuthenticationRegulationError";
Object.setPrototypeOf(this, AuthenticationRegulationError.prototype);
}
}
export class InvalidTOTPError extends Error {
constructor(message?: string) {
super(message);
this.name = "InvalidTOTPError";
Object.setPrototypeOf(this, InvalidTOTPError.prototype);
}
}
export class DomainAccessDenied extends Error {
constructor(message?: string) {
super(message);
this.name = "DomainAccessDenied";
Object.setPrototypeOf(this, DomainAccessDenied.prototype);
}
}

View File

@ -0,0 +1,155 @@
import objectPath = require("object-path");
import randomstring = require("randomstring");
import BluebirdPromise = require("bluebird");
import util = require("util");
import exceptions = require("./Exceptions");
import fs = require("fs");
import ejs = require("ejs");
import UserDataStore from "./UserDataStore";
import { ILogger } from "../types/ILogger";
import express = require("express");
import Identity = require("../types/Identity");
import { IdentityValidationRequestContent } from "./UserDataStore";
const filePath = __dirname + "/../resources/email-template.ejs";
const email_template = fs.readFileSync(filePath, "utf8");
// IdentityValidator allows user to go through a identity validation process in two steps:
// - Request an operation to be performed (password reset, registration).
// - Confirm operation with email.
export interface IdentityValidable {
challenge(): string;
templateName(): string;
preValidation(req: express.Request): BluebirdPromise<Identity.Identity>;
mailSubject(): string;
}
export class IdentityValidator {
private userDataStore: UserDataStore;
private logger: ILogger;
constructor(userDataStore: UserDataStore, logger: ILogger) {
this.userDataStore = userDataStore;
this.logger = logger;
}
static setup(app: express.Application, endpoint: string, handler: IdentityValidable, userDataStore: UserDataStore, logger: ILogger) {
const identityValidator = new IdentityValidator(userDataStore, logger);
app.get(endpoint, identityValidator.identity_check_get(endpoint, handler));
app.post(endpoint, identityValidator.identity_check_post(endpoint, handler));
}
private issue_token(userid: string, content: Object): BluebirdPromise<string> {
const five_minutes = 4 * 60 * 1000;
const token = randomstring.generate({ length: 64 });
const that = this;
this.logger.debug("identity_check: issue identity token %s for 5 minutes", token);
return this.userDataStore.issue_identity_check_token(userid, token, content, five_minutes)
.then(function () {
return BluebirdPromise.resolve(token);
});
}
private consume_token(token: string): BluebirdPromise<IdentityValidationRequestContent> {
this.logger.debug("identity_check: consume token %s", token);
return this.userDataStore.consume_identity_check_token(token);
}
private identity_check_get(endpoint: string, handler: IdentityValidable): express.RequestHandler {
const that = this;
return function (req: express.Request, res: express.Response) {
const logger = req.app.get("logger");
const identity_token = objectPath.get<express.Request, string>(req, "query.identity_token");
logger.info("GET identity_check: identity token provided is %s", identity_token);
if (!identity_token) {
res.status(403);
res.send();
return;
}
that.consume_token(identity_token)
.then(function (content: IdentityValidationRequestContent) {
objectPath.set(req, "session.auth_session.identity_check", {});
req.session.auth_session.identity_check.challenge = handler.challenge();
req.session.auth_session.identity_check.userid = content.userid;
res.render(handler.templateName());
}, function (err: Error) {
logger.error("GET identity_check: Error while consuming token %s", err);
throw new exceptions.AccessDeniedError("Access denied");
})
.catch(exceptions.AccessDeniedError, function (err: Error) {
logger.error("GET identity_check: Access Denied %s", err);
res.status(403);
res.send();
})
.catch(function (err: Error) {
logger.error("GET identity_check: Internal error %s", err);
res.status(500);
res.send();
});
};
}
private identity_check_post(endpoint: string, handler: IdentityValidable): express.RequestHandler {
const that = this;
return function (req: express.Request, res: express.Response) {
const logger = req.app.get("logger");
const notifier = req.app.get("notifier");
let identity: Identity.Identity;
handler.preValidation(req)
.then(function (id: Identity.Identity) {
identity = id;
const email_address = objectPath.get<Identity.Identity, string>(identity, "email");
const userid = objectPath.get<Identity.Identity, string>(identity, "userid");
if (!(email_address && userid)) {
throw new exceptions.IdentityError("Missing user id or email address");
}
return that.issue_token(userid, undefined);
}, function (err: Error) {
throw new exceptions.AccessDeniedError(err.message);
})
.then(function (token: string) {
const redirect_url = objectPath.get<express.Request, string>(req, "body.redirect");
const original_url = util.format("https://%s%s", req.headers.host, req.headers["x-original-uri"]);
let link_url = util.format("%s?identity_token=%s", original_url, token);
if (redirect_url) {
link_url = util.format("%s&redirect=%s", link_url, redirect_url);
}
logger.info("POST identity_check: notify to %s", identity.userid);
return notifier.notify(identity, handler.mailSubject(), link_url);
})
.then(function () {
res.status(204);
res.send();
})
.catch(exceptions.IdentityError, function (err: Error) {
logger.error("POST identity_check: %s", err);
res.status(400);
res.send();
})
.catch(exceptions.AccessDeniedError, function (err: Error) {
logger.error("POST identity_check: %s", err);
res.status(403);
res.send();
})
.catch(function (err: Error) {
logger.error("POST identity_check: Error %s", err);
res.status(500);
res.send();
});
};
}
}

View File

@ -0,0 +1,169 @@
import util = require("util");
import BluebirdPromise = require("bluebird");
import exceptions = require("./Exceptions");
import Dovehash = require("dovehash");
import ldapjs = require("ldapjs");
import { EventEmitter } from "events";
import { LdapConfiguration } from "./Configuration";
import { Ldapjs } from "../types/Dependencies";
import { ILogger } from "../types/ILogger";
interface SearchEntry {
object: any;
}
export class LdapClient {
options: LdapConfiguration;
ldapjs: Ldapjs;
logger: ILogger;
client: ldapjs.ClientAsync;
constructor(options: LdapConfiguration, ldapjs: Ldapjs, logger: ILogger) {
this.options = options;
this.ldapjs = ldapjs;
this.logger = logger;
this.connect();
}
connect(): void {
const ldap_client = this.ldapjs.createClient({
url: this.options.url,
reconnect: true
});
ldap_client.on("error", function (err: Error) {
console.error("LDAP Error:", err.message);
});
this.client = BluebirdPromise.promisifyAll(ldap_client) as ldapjs.ClientAsync;
}
private build_user_dn(username: string): string {
let user_name_attr = this.options.user_name_attribute;
// if not provided, default to cn
if (!user_name_attr) user_name_attr = "cn";
const additional_user_dn = this.options.additional_user_dn;
const base_dn = this.options.base_dn;
let user_dn = util.format("%s=%s", user_name_attr, username);
if (additional_user_dn) user_dn += util.format(",%s", additional_user_dn);
user_dn += util.format(",%s", base_dn);
return user_dn;
}
bind(username: string, password: string): BluebirdPromise<void> {
const user_dn = this.build_user_dn(username);
this.logger.debug("LDAP: Bind user %s", user_dn);
return this.client.bindAsync(user_dn, password)
.error(function (err) {
throw new exceptions.LdapBindError(err.message);
});
}
private search_in_ldap(base: string, query: ldapjs.SearchOptions): BluebirdPromise<any> {
this.logger.debug("LDAP: Search for %s in %s", JSON.stringify(query), base);
return new BluebirdPromise((resolve, reject) => {
this.client.searchAsync(base, query)
.then(function (res: EventEmitter) {
const doc: SearchEntry[] = [];
res.on("searchEntry", function (entry: SearchEntry) {
doc.push(entry.object);
});
res.on("error", function (err: Error) {
reject(err);
});
res.on("end", function () {
resolve(doc);
});
})
.catch(function (err) {
reject(err);
});
});
}
get_groups(username: string): BluebirdPromise<string[]> {
const user_dn = this.build_user_dn(username);
let group_name_attr = this.options.group_name_attribute;
if (!group_name_attr) group_name_attr = "cn";
const additional_group_dn = this.options.additional_group_dn;
const base_dn = this.options.base_dn;
let group_dn = base_dn;
if (additional_group_dn)
group_dn = util.format("%s,", additional_group_dn) + group_dn;
const query = {
scope: "sub",
attributes: [group_name_attr],
filter: "member=" + user_dn
};
const that = this;
this.logger.debug("LDAP: get groups of user %s", username);
return this.search_in_ldap(group_dn, query)
.then(function (docs) {
const groups = [];
for (let i = 0; i < docs.length; ++i) {
groups.push(docs[i].cn);
}
that.logger.debug("LDAP: got groups %s", groups);
return BluebirdPromise.resolve(groups);
});
}
get_emails(username: string): BluebirdPromise<string[]> {
const that = this;
const user_dn = this.build_user_dn(username);
const query = {
scope: "base",
sizeLimit: 1,
attributes: ["mail"]
};
this.logger.debug("LDAP: get emails of user %s", username);
return this.search_in_ldap(user_dn, query)
.then(function (docs) {
const emails = [];
for (let i = 0; i < docs.length; ++i) {
if (typeof docs[i].mail === "string")
emails.push(docs[i].mail);
else {
emails.concat(docs[i].mail);
}
}
that.logger.debug("LDAP: got emails %s", emails);
return BluebirdPromise.resolve(emails);
});
}
update_password(username: string, new_password: string): BluebirdPromise<void> {
const user_dn = this.build_user_dn(username);
const encoded_password = Dovehash.encode("SSHA", new_password);
const change = {
operation: "replace",
modification: {
userPassword: encoded_password
}
};
const that = this;
this.logger.debug("LDAP: update password of user %s", username);
this.logger.debug("LDAP: bind admin");
return this.client.bindAsync(this.options.user, this.options.password)
.then(function () {
that.logger.debug("LDAP: modify password");
return that.client.modifyAsync(user_dn, change);
});
}
}

282
src/lib/RestApi.ts 100644
View File

@ -0,0 +1,282 @@
import express = require("express");
import routes = require("./routes");
import IdentityValidator = require("./IdentityValidator");
import UserDataStore from "./UserDataStore";
import { ILogger } from "../types/ILogger";
export default class RestApi {
static setup(app: express.Application, userDataStore: UserDataStore, logger: ILogger): void {
/**
* @apiDefine UserSession
* @apiHeader {String} Cookie Cookie containing "connect.sid", the user
* session token.
*/
/**
* @apiDefine InternalError
* @apiError (Error 500) {String} error Internal error message.
*/
/**
* @apiDefine IdentityValidationPost
*
* @apiSuccess (Success 204) status Identity validation has been initiated.
* @apiError (Error 403) AccessDenied Access is denied.
* @apiError (Error 400) InvalidIdentity User identity is invalid.
* @apiError (Error 500) {String} error Internal error message.
*
* @apiDescription This request issue an identity validation token for the user
* bound to the session. It sends a challenge to the email address set in the user
* LDAP entry. The user must visit the sent URL to complete the validation and
* continue the registration process.
*/
/**
* @apiDefine IdentityValidationGet
* @apiParam {String} identity_token The one-time identity validation token provided in the email.
* @apiSuccess (Success 200) {String} content The content of the page.
* @apiError (Error 403) AccessDenied Access is denied.
* @apiError (Error 500) {String} error Internal error message.
*/
/**
* @api {get} /login Serve login page
* @apiName Login
* @apiGroup Pages
* @apiVersion 1.0.0
*
* @apiParam {String} redirect Redirect to this URL when user is authenticated.
* @apiSuccess (Success 200) {String} Content The content of the login page.
*
* @apiDescription Create a user session and serve the login page along with
* a cookie.
*/
app.get("/login", routes.login);
/**
* @api {get} /logout Server logout page
* @apiName Logout
* @apiGroup Pages
* @apiVersion 1.0.0
*
* @apiParam {String} redirect Redirect to this URL when user is deauthenticated.
* @apiSuccess (Success 301) redirect Redirect to the URL.
*
* @apiDescription Deauthenticate the user and redirect him.
*/
app.get("/logout", routes.logout);
/**
* @api {post} /totp-register Request TOTP registration
* @apiName RequestTOTPRegistration
* @apiGroup Registration
* @apiVersion 1.0.0
* @apiUse UserSession
* @apiUse IdentityValidationPost
*/
/**
* @api {get} /totp-register Serve TOTP registration page
* @apiName ServeTOTPRegistrationPage
* @apiGroup Registration
* @apiVersion 1.0.0
* @apiUse UserSession
* @apiUse IdentityValidationGet
*
*
* @apiDescription Serves the TOTP registration page that displays the secret.
* The secret is a QRCode and a base32 secret.
*/
IdentityValidator.IdentityValidator.setup(app, "/totp-register", routes.totp_register.icheck_interface, userDataStore, logger);
/**
* @api {post} /u2f-register Request U2F registration
* @apiName RequestU2FRegistration
* @apiGroup Registration
* @apiVersion 1.0.0
* @apiUse UserSession
* @apiUse IdentityValidationPost
*/
/**
* @api {get} /u2f-register Serve U2F registration page
* @apiName ServeU2FRegistrationPage
* @apiGroup Pages
* @apiVersion 1.0.0
* @apiUse UserSession
* @apiUse IdentityValidationGet
*
* @apiDescription Serves the U2F registration page that asks the user to
* touch the token of the U2F device.
*/
IdentityValidator.IdentityValidator.setup(app, "/u2f-register", routes.u2f_register.icheck_interface, userDataStore, logger);
/**
* @api {post} /reset-password Request for password reset
* @apiName RequestPasswordReset
* @apiGroup Registration
* @apiVersion 1.0.0
* @apiUse UserSession
* @apiUse IdentityValidationPost
*/
/**
* @api {get} /reset-password Serve password reset form.
* @apiName ServePasswordResetForm
* @apiGroup Pages
* @apiVersion 1.0.0
* @apiUse UserSession
* @apiUse IdentityValidationGet
*
* @apiDescription Serves password reset form that allow the user to provide
* the new password.
*/
IdentityValidator.IdentityValidator.setup(app, "/reset-password", routes.reset_password.icheck_interface, userDataStore, logger);
app.get("/reset-password-form", function (req, res) { res.render("reset-password-form"); });
/**
* @api {post} /new-password Set LDAP password
* @apiName SetLDAPPassword
* @apiGroup Registration
* @apiVersion 1.0.0
* @apiUse UserSession
*
* @apiParam {String} password New password
*
* @apiDescription Set a new password for the user.
*/
app.post("/new-password", routes.reset_password.post);
/**
* @api {post} /new-totp-secret Generate TOTP secret
* @apiName GenerateTOTPSecret
* @apiGroup Registration
* @apiVersion 1.0.0
* @apiUse UserSession
*
* @apiSuccess (Success 200) {String} base32 The base32 representation of the secret.
* @apiSuccess (Success 200) {String} ascii The ASCII representation of the secret.
* @apiSuccess (Success 200) {String} qrcode The QRCode of the secret in URI format.
*
* @apiError (Error 403) {String} error No user provided in the session or
* unexpected identity validation challenge in the session.
* @apiError (Error 500) {String} error Internal error message
*
* @apiDescription Generate a new TOTP secret and returns it.
*/
app.post("/new-totp-secret", routes.totp_register.post);
/**
* @api {get} /verify Verify user authentication
* @apiName VerifyAuthentication
* @apiGroup Verification
* @apiVersion 1.0.0
* @apiUse UserSession
*
* @apiSuccess (Success 204) status The user is authenticated.
* @apiError (Error 401) status The user is not authenticated.
*
* @apiDescription Verify that the user is authenticated, i.e., the two
* factors have been validated
*/
app.get("/verify", routes.verify);
/**
* @api {post} /1stfactor LDAP authentication
* @apiName ValidateFirstFactor
* @apiGroup Authentication
* @apiVersion 1.0.0
* @apiUse UserSession
* @apiUse InternalError
*
* @apiParam {String} username User username.
* @apiParam {String} password User password.
*
* @apiSuccess (Success 204) status 1st factor is validated.
* @apiError (Error 401) {none} error 1st factor is not validated.
* @apiError (Error 403) {none} error Access has been restricted after too
* many authentication attempts
*
* @apiDescription Verify credentials against the LDAP.
*/
app.post("/1stfactor", routes.first_factor);
/**
* @api {post} /2ndfactor/totp TOTP authentication
* @apiName ValidateTOTPSecondFactor
* @apiGroup Authentication
* @apiVersion 1.0.0
* @apiUse UserSession
* @apiUse InternalError
*
* @apiParam {String} token TOTP token.
*
* @apiSuccess (Success 204) status TOTP token is valid.
* @apiError (Error 401) {none} error TOTP token is invalid.
*
* @apiDescription Verify TOTP token. The user is authenticated upon success.
*/
app.post("/2ndfactor/totp", routes.second_factor.totp);
/**
* @api {get} /2ndfactor/u2f/sign_request U2F Start authentication
* @apiName StartU2FAuthentication
* @apiGroup Authentication
* @apiVersion 1.0.0
* @apiUse UserSession
* @apiUse InternalError
*
* @apiSuccess (Success 200) authentication_request The U2F authentication request.
* @apiError (Error 401) {none} error There is no key registered for user in session.
*
* @apiDescription Initiate an authentication request using a U2F device.
*/
app.get("/2ndfactor/u2f/sign_request", routes.second_factor.u2f.sign_request);
/**
* @api {post} /2ndfactor/u2f/sign U2F Complete authentication
* @apiName CompleteU2FAuthentication
* @apiGroup Authentication
* @apiVersion 1.0.0
* @apiUse UserSession
* @apiUse InternalError
*
* @apiSuccess (Success 204) status The U2F authentication succeeded.
* @apiError (Error 403) {none} error No authentication request has been provided.
*
* @apiDescription Complete authentication request of the U2F device.
*/
app.post("/2ndfactor/u2f/sign", routes.second_factor.u2f.sign);
/**
* @api {get} /2ndfactor/u2f/register_request U2F Start device registration
* @apiName StartU2FRegistration
* @apiGroup Registration
* @apiVersion 1.0.0
* @apiUse UserSession
* @apiUse InternalError
*
* @apiSuccess (Success 200) authentication_request The U2F registration request.
* @apiError (Error 403) {none} error Unexpected identity validation challenge.
*
* @apiDescription Initiate a U2F device registration request.
*/
app.get("/2ndfactor/u2f/register_request", routes.second_factor.u2f.register_request);
/**
* @api {post} /2ndfactor/u2f/register U2F Complete device registration
* @apiName CompleteU2FRegistration
* @apiGroup Registration
* @apiVersion 1.0.0
* @apiUse UserSession
* @apiUse InternalError
*
* @apiSuccess (Success 204) status The U2F registration succeeded.
* @apiError (Error 403) {none} error Unexpected identity validation challenge.
* @apiError (Error 403) {none} error No registration request has been provided.
*
* @apiDescription Complete U2F registration request.
*/
app.post("/2ndfactor/u2f/register", routes.second_factor.u2f.register);
}
}

94
src/lib/Server.ts 100644
View File

@ -0,0 +1,94 @@
import { UserConfiguration } from "./Configuration";
import { GlobalDependencies } from "../types/Dependencies";
import AuthenticationRegulator from "./AuthenticationRegulator";
import UserDataStore from "./UserDataStore";
import ConfigurationAdapter from "./ConfigurationAdapter";
import { NotifierFactory } from "./notifiers/NotifierFactory";
import TOTPValidator from "./TOTPValidator";
import TOTPGenerator from "./TOTPGenerator";
import RestApi from "./RestApi";
import { LdapClient } from "./LdapClient";
import BluebirdPromise = require("bluebird");
import { IdentityValidator } from "./IdentityValidator";
import * as Express from "express";
import * as BodyParser from "body-parser";
import * as Path from "path";
import * as http from "http";
import AccessController from "./access_control/AccessController";
export default class Server {
private httpServer: http.Server;
start(yaml_configuration: UserConfiguration, deps: GlobalDependencies): BluebirdPromise<void> {
const config = ConfigurationAdapter.adapt(yaml_configuration);
const view_directory = Path.resolve(__dirname, "../views");
const public_html_directory = Path.resolve(__dirname, "../public_html");
const datastore_options = {
directory: config.store_directory,
inMemory: config.store_in_memory
};
const app = Express();
app.use(Express.static(public_html_directory));
app.use(BodyParser.urlencoded({ extended: false }));
app.use(BodyParser.json());
app.set("trust proxy", 1); // trust first proxy
app.use(deps.session({
secret: config.session.secret,
resave: false,
saveUninitialized: true,
cookie: {
secure: false,
maxAge: config.session.expiration,
domain: config.session.domain
},
}));
app.set("views", view_directory);
app.set("view engine", "ejs");
// by default the level of logs is info
deps.winston.level = config.logs_level || "info";
const five_minutes = 5 * 60;
const userDataStore = new UserDataStore(datastore_options, deps.nedb);
const regulator = new AuthenticationRegulator(userDataStore, five_minutes);
const notifier = NotifierFactory.build(config.notifier, deps.nodemailer);
const ldap = new LdapClient(config.ldap, deps.ldapjs, deps.winston);
const accessController = new AccessController(config.access_control, deps.winston);
const totpValidator = new TOTPValidator(deps.speakeasy);
const totpGenerator = new TOTPGenerator(deps.speakeasy);
const identityValidator = new IdentityValidator(userDataStore, deps.winston);
app.set("logger", deps.winston);
app.set("ldap", ldap);
app.set("totp validator", totpValidator);
app.set("totp generator", totpGenerator);
app.set("u2f", deps.u2f);
app.set("user data store", userDataStore);
app.set("notifier", notifier);
app.set("authentication regulator", regulator);
app.set("config", config);
app.set("access controller", accessController);
app.set("identity validator", identityValidator);
RestApi.setup(app, userDataStore, deps.winston);
return new BluebirdPromise<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,16 @@
import * as speakeasy from "speakeasy";
import { Speakeasy } from "../types/Dependencies";
import BluebirdPromise = require("bluebird");
export default class TOTPGenerator {
private speakeasy: Speakeasy;
constructor(speakeasy: Speakeasy) {
this.speakeasy = speakeasy;
}
generate(options: speakeasy.GenerateOptions): speakeasy.Key {
return this.speakeasy.generateSecret(options);
}
}

View File

@ -0,0 +1,23 @@
import { Speakeasy } from "../types/Dependencies";
import BluebirdPromise = require("bluebird");
const TOTP_ENCODING = "base32";
export default class TOTPValidator {
private speakeasy: Speakeasy;
constructor(speakeasy: Speakeasy) {
this.speakeasy = speakeasy;
}
validate(token: string, secret: string): BluebirdPromise<void> {
const real_token = this.speakeasy.totp({
secret: secret,
encoding: TOTP_ENCODING
});
if (token == real_token) return BluebirdPromise.resolve();
return BluebirdPromise.reject(new Error("Wrong challenge"));
}
}

View File

@ -0,0 +1,182 @@
import * as BluebirdPromise from "bluebird";
import * as path from "path";
import { NedbAsync } from "nedb";
import { TOTPSecret } from "../types/TOTPSecret";
import { Nedb } from "../types/Dependencies";
// Constants
const U2F_META_COLLECTION_NAME = "u2f_meta";
const IDENTITY_CHECK_TOKENS_COLLECTION_NAME = "identity_check_tokens";
const AUTHENTICATION_TRACES_COLLECTION_NAME = "authentication_traces";
const TOTP_SECRETS_COLLECTION_NAME = "totp_secrets";
export interface TOTPSecretDocument {
userid: string;
secret: TOTPSecret;
}
export interface U2FMetaDocument {
meta: object;
userid: string;
appid: string;
}
export interface Options {
inMemoryOnly?: boolean;
directory?: string;
}
export interface IdentityValidationRequestContent {
userid: string;
data: string;
}
export interface IdentityValidationRequestDocument {
userid: string;
token: string;
content: IdentityValidationRequestContent;
max_date: Date;
}
// Source
export default class UserDataStore {
private _u2f_meta_collection: NedbAsync;
private _identity_check_tokens_collection: NedbAsync;
private _authentication_traces_collection: NedbAsync;
private _totp_secret_collection: NedbAsync;
private nedb: Nedb;
constructor(options: Options, nedb: Nedb) {
this.nedb = nedb;
this._u2f_meta_collection = this.create_collection(U2F_META_COLLECTION_NAME, options);
this._identity_check_tokens_collection =
this.create_collection(IDENTITY_CHECK_TOKENS_COLLECTION_NAME, options);
this._authentication_traces_collection =
this.create_collection(AUTHENTICATION_TRACES_COLLECTION_NAME, options);
this._totp_secret_collection =
this.create_collection(TOTP_SECRETS_COLLECTION_NAME, options);
}
set_u2f_meta(userid: string, appid: string, meta: Object): BluebirdPromise<any> {
const newDocument = {
userid: userid,
appid: appid,
meta: meta
} as U2FMetaDocument;
const filter = {
userid: userid,
appid: appid
};
return this._u2f_meta_collection.updateAsync(filter, newDocument, { upsert: true });
}
get_u2f_meta(userid: string, appid: string): BluebirdPromise<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): BluebirdPromise<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 = BluebirdPromise.promisify(query.exec, { context: query });
return query_promisified();
}
issue_identity_check_token(userid: string, token: string, data: string | object, max_age: number): BluebirdPromise<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): BluebirdPromise<IdentityValidationRequestContent> {
const query = {
token: token
};
return this._identity_check_tokens_collection.findOneAsync(query)
.then(function (doc) {
if (!doc) {
return BluebirdPromise.reject("Registration token does not exist");
}
const max_date = doc.max_date;
const current_date = new Date();
if (current_date > max_date) {
return BluebirdPromise.reject("Registration token is not valid anymore");
}
return BluebirdPromise.resolve(doc.content);
})
.then((content) => {
return BluebirdPromise.join(this._identity_check_tokens_collection.removeAsync(query),
BluebirdPromise.resolve(content));
})
.then((v) => {
return BluebirdPromise.resolve(v[1]);
});
}
set_totp_secret(userid: string, secret: TOTPSecret): BluebirdPromise<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): BluebirdPromise<TOTPSecretDocument> {
const query = {
userid: userid
};
return this._totp_secret_collection.findOneAsync(query);
}
private create_collection(name: string, options: any): NedbAsync {
const datastore_options = {
inMemoryOnly: options.inMemoryOnly || false,
autoload: true,
filename: ""
};
if (options.directory)
datastore_options.filename = path.resolve(options.directory, name);
return BluebirdPromise.promisifyAll(new this.nedb(datastore_options)) as NedbAsync;
}
}

View File

@ -1,84 +0,0 @@
module.exports = function(logger, acl_config) {
return {
builder: new AccessControlBuilder(logger, acl_config),
matcher: new AccessControlMatcher(logger)
};
}
var objectPath = require('object-path');
// *************** PER DOMAIN MATCHER ***************
function AccessControlMatcher(logger) {
this.logger = logger;
}
AccessControlMatcher.prototype.is_domain_allowed = function(domain, allowed_domains) {
// Allow all matcher
if(allowed_domains.length == 1 && allowed_domains[0] == '*') return true;
this.logger.debug('ACL: trying to match %s with %s', domain,
JSON.stringify(allowed_domains));
for(var i = 0; i < allowed_domains.length; ++i) {
var allowed_domain = allowed_domains[i];
if(allowed_domain.startsWith('*') &&
domain.endsWith(allowed_domain.substr(1))) {
return true;
}
else if(domain == allowed_domain) {
return true;
}
}
return false;
}
// *************** MATCHER BUILDER ***************
function AccessControlBuilder(logger, acl_config) {
this.logger = logger;
this.config = acl_config;
}
AccessControlBuilder.prototype.extract_per_group = function(groups) {
var allowed_domains = [];
var groups_policy = objectPath.get(this.config, 'groups');
if(groups_policy) {
for(var i=0; i<groups.length; ++i) {
var group = groups[i];
if(group in groups_policy) {
allowed_domains = allowed_domains.concat(groups_policy[group]);
}
}
  }
return allowed_domains;
}
AccessControlBuilder.prototype.extract_per_user = function(user) {
var allowed_domains = [];
var users_policy = objectPath.get(this.config, 'users');
if(users_policy) {
if(user in users_policy) {
allowed_domains = allowed_domains.concat(users_policy[user]);
}
  }
return allowed_domains;
}
AccessControlBuilder.prototype.get_allowed_domains = function(user, groups) {
var allowed_domains = [];
var default_policy = objectPath.get(this.config, 'default');
if(default_policy) {
allowed_domains = allowed_domains.concat(default_policy);
}
allowed_domains = allowed_domains.concat(this.extract_per_group(groups));
allowed_domains = allowed_domains.concat(this.extract_per_user(user));
this.logger.debug('ACL: user \'%s\' is allowed access to %s', user,
JSON.stringify(allowed_domains));
return allowed_domains;
}
AccessControlBuilder.prototype.get_any_domain = function() {
return ['*'];
}

View File

@ -0,0 +1,35 @@
import { ACLConfiguration } from "../Configuration";
import PatternBuilder from "./PatternBuilder";
import { ILogger } from "../../types/ILogger";
export default class AccessController {
private logger: ILogger;
private patternBuilder: PatternBuilder;
constructor(configuration: ACLConfiguration, logger_: ILogger) {
this.logger = logger_;
this.patternBuilder = new PatternBuilder(configuration, logger_);
}
isDomainAllowedForUser(domain: string, user: string, groups: string[]): boolean {
const allowed_domains = this.patternBuilder.getAllowedDomains(user, groups);
// Allow all matcher
if (allowed_domains.length == 1 && allowed_domains[0] == "*") return true;
this.logger.debug("ACL: trying to match %s with %s", domain,
JSON.stringify(allowed_domains));
for (let i = 0; i < allowed_domains.length; ++i) {
const allowed_domain = allowed_domains[i];
if (allowed_domain.startsWith("*") &&
domain.endsWith(allowed_domain.substr(1))) {
return true;
}
else if (domain == allowed_domain) {
return true;
}
}
return false;
}
}

View File

@ -0,0 +1,61 @@
import { ILogger } from "../../types/ILogger";
import { ACLConfiguration, ACLGroupsRules, ACLUsersRules, ACLDefaultRules } from "../Configuration";
import objectPath = require("object-path");
export default class AccessControlPatternBuilder {
logger: ILogger;
configuration: ACLConfiguration;
constructor(configuration: ACLConfiguration | undefined, logger_: ILogger) {
this.configuration = configuration;
this.logger = logger_;
}
private buildFromGroups(groups: string[]): string[] {
let allowed_domains: string[] = [];
const groups_policy = objectPath.get<ACLConfiguration, ACLGroupsRules>(this.configuration, "groups");
if (groups_policy) {
for (let i = 0; i < groups.length; ++i) {
const group = groups[i];
if (group in groups_policy) {
const group_policy: string[] = groups_policy[group];
allowed_domains = allowed_domains.concat(groups_policy[group]);
}
}
}
return allowed_domains;
}
private buildFromUser(user: string): string[] {
let allowed_domains: string[] = [];
const users_policy = objectPath.get<ACLConfiguration, ACLUsersRules>(this.configuration, "users");
if (users_policy) {
if (user in users_policy) {
allowed_domains = allowed_domains.concat(users_policy[user]);
}
}
return allowed_domains;
}
getAllowedDomains(user: string, groups: string[]): string[] {
if (!this.configuration) {
this.logger.debug("No access control rules found." +
"Default policy to allow all.");
return ["*"]; // No configuration means, no restrictions.
}
let allowed_domains: string[] = [];
const default_policy = objectPath.get<ACLConfiguration, ACLDefaultRules>(this.configuration, "default");
if (default_policy) {
allowed_domains = allowed_domains.concat(default_policy);
}
allowed_domains = allowed_domains.concat(this.buildFromGroups(groups));
allowed_domains = allowed_domains.concat(this.buildFromUser(user));
this.logger.debug("ACL: user \'%s\' is allowed access to %s", user,
JSON.stringify(allowed_domains));
return allowed_domains;
}
}

View File

@ -1,35 +0,0 @@
module.exports = AuthenticationRegulator;
var exceptions = require('./exceptions');
var Promise = require('bluebird');
function AuthenticationRegulator(user_data_store, lock_time_in_seconds) {
this._user_data_store = user_data_store;
this._lock_time_in_seconds = lock_time_in_seconds;
}
// Mark authentication
AuthenticationRegulator.prototype.mark = function(userid, is_success) {
return this._user_data_store.save_authentication_trace(userid, '1stfactor', is_success);
}
AuthenticationRegulator.prototype.regulate = function(userid) {
var that = this;
return this._user_data_store.get_last_authentication_traces(userid, '1stfactor', false, 3)
.then(function(docs) {
if(docs.length < 3) {
return Promise.resolve();
}
var oldest_doc = docs[2];
var no_lock_min_date = new Date(new Date().getTime() -
that._lock_time_in_seconds * 1000);
if(oldest_doc.date > no_lock_min_date) {
throw new exceptions.AuthenticationRegulationError();
}
return Promise.resolve();
});
}

View File

@ -1,17 +0,0 @@
var objectPath = require('object-path');
module.exports = function(yaml_config) {
return {
port: objectPath.get(yaml_config, 'port', 8080),
ldap: objectPath.get(yaml_config, 'ldap', 'ldap://127.0.0.1:389'),
session_domain: objectPath.get(yaml_config, 'session.domain'),
session_secret: objectPath.get(yaml_config, 'session.secret'),
session_max_age: objectPath.get(yaml_config, 'session.expiration', 3600000), // in ms
store_directory: objectPath.get(yaml_config, 'store_directory'),
logs_level: objectPath.get(yaml_config, 'logs_level'),
notifier: objectPath.get(yaml_config, 'notifier'),
access_control: objectPath.get(yaml_config, 'access_control')
}
};

View File

@ -1,45 +0,0 @@
module.exports = {
LdapSearchError: LdapSearchError,
LdapBindError: LdapBindError,
IdentityError: IdentityError,
AccessDeniedError: AccessDeniedError,
AuthenticationRegulationError: AuthenticationRegulationError,
InvalidTOTPError: InvalidTOTPError,
}
function LdapSearchError(message) {
this.name = "LdapSearchError";
this.message = (message || "");
}
LdapSearchError.prototype = Object.create(Error.prototype);
function LdapBindError(message) {
this.name = "LdapBindError";
this.message = (message || "");
}
LdapBindError.prototype = Object.create(Error.prototype);
function IdentityError(message) {
this.name = "IdentityError";
this.message = (message || "");
}
IdentityError.prototype = Object.create(Error.prototype);
function AccessDeniedError(message) {
this.name = "AccessDeniedError";
this.message = (message || "");
}
AccessDeniedError.prototype = Object.create(Error.prototype);
function AuthenticationRegulationError(message) {
this.name = "AuthenticationRegulationError";
this.message = (message || "");
}
AuthenticationRegulationError.prototype = Object.create(Error.prototype);
function InvalidTOTPError(message) {
this.name = "InvalidTOTPError";
this.message = (message || "");
}
InvalidTOTPError.prototype = Object.create(Error.prototype);

View File

@ -1,144 +0,0 @@
var objectPath = require('object-path');
var randomstring = require('randomstring');
var Promise = require('bluebird');
var util = require('util');
var exceptions = require('./exceptions');
var fs = require('fs');
var ejs = require('ejs');
module.exports = identity_check;
var filePath = __dirname + '/../resources/email-template.ejs';
var email_template = fs.readFileSync(filePath, 'utf8');
// IdentityCheck class
function IdentityCheck(user_data_store, logger) {
this._user_data_store = user_data_store;
this._logger = logger;
}
IdentityCheck.prototype.issue_token = function(userid, content, logger) {
var five_minutes = 4 * 60 * 1000;
var token = randomstring.generate({ length: 64 });
var that = this;
this._logger.debug('identity_check: issue identity token %s for 5 minutes', token);
return this._user_data_store.issue_identity_check_token(userid, token, content, five_minutes)
.then(function() {
return Promise.resolve(token);
});
}
IdentityCheck.prototype.consume_token = function(token, logger) {
this._logger.debug('identity_check: consume token %s', token);
return this._user_data_store.consume_identity_check_token(token)
}
// The identity_check middleware that allows the user two perform a two step validation
// using the user email
function identity_check(app, endpoint, icheck_interface) {
app.get(endpoint, identity_check_get(endpoint, icheck_interface));
app.post(endpoint, identity_check_post(endpoint, icheck_interface));
}
function identity_check_get(endpoint, icheck_interface) {
return function(req, res) {
var logger = req.app.get('logger');
var identity_token = objectPath.get(req, 'query.identity_token');
logger.info('GET identity_check: identity token provided is %s', identity_token);
if(!identity_token) {
res.status(403);
res.send();
return;
}
var email_sender = req.app.get('email sender');
var user_data_store = req.app.get('user data store');
var identity_check = new IdentityCheck(user_data_store, logger);
identity_check.consume_token(identity_token, logger)
.then(function(content) {
objectPath.set(req, 'session.auth_session.identity_check', {});
req.session.auth_session.identity_check.challenge = icheck_interface.challenge;
req.session.auth_session.identity_check.userid = content.userid;
res.render(icheck_interface.render_template);
}, function(err) {
logger.error('GET identity_check: Error while consuming token %s', err);
throw new exceptions.AccessDeniedError('Access denied');
})
.catch(exceptions.AccessDeniedError, function(err) {
logger.error('GET identity_check: Access Denied %s', err);
res.status(403);
res.send();
  })
.catch(function(err) {
logger.error('GET identity_check: Internal error %s', err);
res.status(500);
res.send();
});
}
}
function identity_check_post(endpoint, icheck_interface) {
return function(req, res) {
var logger = req.app.get('logger');
var notifier = req.app.get('notifier');
var user_data_store = req.app.get('user data store');
var identity_check = new IdentityCheck(user_data_store, logger);
var identity;
icheck_interface.pre_check_callback(req)
.then(function(id) {
identity = id;
var email_address = objectPath.get(identity, 'email');
var userid = objectPath.get(identity, 'userid');
if(!(email_address && userid)) {
throw new exceptions.IdentityError('Missing user id or email address');
}
return identity_check.issue_token(userid, undefined, logger);
}, function(err) {
throw new exceptions.AccessDeniedError();
})
.then(function(token) {
var redirect_url = objectPath.get(req, 'body.redirect');
var original_url = util.format('https://%s%s', req.headers.host, req.headers['x-original-uri']);
var link_url = util.format('%s?identity_token=%s', original_url, token);
if(redirect_url) {
link_url = util.format('%s&redirect=%s', link_url, redirect_url);
}
logger.info('POST identity_check: notify to %s', identity.userid);
return notifier.notify(identity, icheck_interface.email_subject, link_url);
})
.then(function() {
res.status(204);
res.send();
})
.catch(exceptions.IdentityError, function(err) {
logger.error('POST identity_check: IdentityError %s', err);
res.status(400);
res.send();
})
.catch(exceptions.AccessDeniedError, function(err) {
logger.error('POST identity_check: AccessDeniedError %s', err);
res.status(403);
res.send();
})
.catch(function(err) {
logger.error('POST identity_check: Error %s', err);
res.status(500);
res.send();
});
}
}

View File

@ -1,154 +0,0 @@
module.exports = Ldap;
var util = require('util');
var Promise = require('bluebird');
var exceptions = require('./exceptions');
var Dovehash = require('dovehash');
function Ldap(deps, ldap_config) {
this.ldap_config = ldap_config;
this.ldapjs = deps.ldapjs;
this.logger = deps.winston;
this.connect();
}
Ldap.prototype.connect = function() {
var ldap_client = this.ldapjs.createClient({
url: this.ldap_config.url,
reconnect: true
});
ldap_client.on('error', function(err) {
console.error('LDAP Error:', err.message)
});
this.ldap_client = Promise.promisifyAll(ldap_client);
}
Ldap.prototype._build_user_dn = function(username) {
var user_name_attr = this.ldap_config.user_name_attribute;
// if not provided, default to cn
if(!user_name_attr) user_name_attr = 'cn';
var additional_user_dn = this.ldap_config.additional_user_dn;
var base_dn = this.ldap_config.base_dn;
var user_dn = util.format("%s=%s", user_name_attr, username);
if(additional_user_dn) user_dn += util.format(",%s", additional_user_dn);
user_dn += util.format(',%s', base_dn);
return user_dn;
}
Ldap.prototype.bind = function(username, password) {
var user_dn = this._build_user_dn(username);
this.logger.debug('LDAP: Bind user %s', user_dn);
return this.ldap_client.bindAsync(user_dn, password)
.error(function(err) {
throw new exceptions.LdapBindError(err.message);
});
}
Ldap.prototype._search_in_ldap = function(base, query) {
var that = this;
this.logger.debug('LDAP: Search for %s in %s', JSON.stringify(query), base);
return new Promise(function(resolve, reject) {
that.ldap_client.searchAsync(base, query)
.then(function(res) {
var doc = [];
res.on('searchEntry', function(entry) {
doc.push(entry.object);
});
res.on('error', function(err) {
reject(new exceptions.LdapSearchError(err));
});
res.on('end', function(result) {
resolve(doc);
});
})
.catch(function(err) {
reject(new exceptions.LdapSearchError(err));
});
});
}
Ldap.prototype.get_groups = function(username) {
var user_dn = this._build_user_dn(username);
var group_name_attr = this.ldap_config.group_name_attribute;
if(!group_name_attr) group_name_attr = 'cn';
var additional_group_dn = this.ldap_config.additional_group_dn;
var base_dn = this.ldap_config.base_dn;
var group_dn = base_dn;
if(additional_group_dn)
group_dn = util.format('%s,', additional_group_dn) + group_dn;
var query = {};
query.scope = 'sub';
query.attributes = [group_name_attr];
query.filter = 'member=' + user_dn ;
var that = this;
this.logger.debug('LDAP: get groups of user %s', username);
return this._search_in_ldap(group_dn, query)
.then(function(docs) {
var groups = [];
for(var i = 0; i<docs.length; ++i) {
groups.push(docs[i].cn);
}
that.logger.debug('LDAP: got groups %s', groups);
return Promise.resolve(groups);
});
}
Ldap.prototype.get_emails = function(username) {
var that = this;
var user_dn = this._build_user_dn(username);
var query = {};
query.scope = 'base';
query.sizeLimit = 1;
query.attributes = ['mail'];
this.logger.debug('LDAP: get emails of user %s', username);
return this._search_in_ldap(user_dn, query)
.then(function(docs) {
var emails = [];
for(var i = 0; i<docs.length; ++i) {
if(typeof docs[i].mail === 'string')
emails.push(docs[i].mail);
else {
emails.concat(docs[i].mail);
}
}
that.logger.debug('LDAP: got emails %s', emails);
return Promise.resolve(emails);
});
}
Ldap.prototype.update_password = function(username, new_password) {
var user_dn = this._build_user_dn(username);
var encoded_password = Dovehash.encode('SSHA', new_password);
var change = new this.ldapjs.Change({
operation: 'replace',
modification: {
userPassword: encoded_password
}
});
var that = this;
this.logger.debug('LDAP: update password of user %s', username);
this.logger.debug('LDAP: bind admin');
return this.ldap_client.bindAsync(this.ldap_config.user, this.ldap_config.password)
.then(function() {
that.logger.debug('LDAP: modify password');
return that.ldap_client.modifyAsync(user_dn, change);
});
}

View File

@ -1,24 +0,0 @@
module.exports = Notifier;
var GmailNotifier = require('./notifiers/gmail.js');
var FSNotifier = require('./notifiers/filesystem.js');
function notifier_factory(options, deps) {
if('gmail' in options) {
return new GmailNotifier(options.gmail, deps);
}
else if('filesystem' in options) {
return new FSNotifier(options.filesystem);
}
}
function Notifier(options, deps) {
this._notifier = notifier_factory(options, deps);
}
Notifier.prototype.notify = function(identity, subject, link) {
return this._notifier.notify(identity, subject, link);
}

View File

@ -0,0 +1,25 @@
import * as BluebirdPromise from "bluebird";
import * as util from "util";
import * as fs from "fs";
import { INotifier } from "./INotifier";
import { Identity } from "../../types/Identity";
import { FileSystemNotifierConfiguration } from "../Configuration";
export class FileSystemNotifier extends INotifier {
private filename: string;
constructor(options: FileSystemNotifierConfiguration) {
super();
this.filename = options.filename;
}
notify(identity: Identity, subject: string, link: string): BluebirdPromise<void> {
const content = util.format("User: %s\nSubject: %s\nLink: %s", identity.userid,
subject, link);
const writeFilePromised = BluebirdPromise.promisify<void, string, string>(fs.writeFile);
return writeFilePromised(this.filename, content);
}
}

View File

@ -0,0 +1,44 @@
import * as BluebirdPromise from "bluebird";
import * as fs from "fs";
import * as ejs from "ejs";
import nodemailer = require("nodemailer");
import { Nodemailer } from "../../types/Dependencies";
import { Identity } from "../../types/Identity";
import { INotifier } from "../notifiers/INotifier";
import { GmailNotifierConfiguration } from "../Configuration";
const email_template = fs.readFileSync(__dirname + "/../../resources/email-template.ejs", "UTF-8");
export class GMailNotifier extends INotifier {
private transporter: any;
constructor(options: GmailNotifierConfiguration, nodemailer: Nodemailer) {
super();
const transporter = nodemailer.createTransport({
service: "gmail",
auth: {
user: options.username,
pass: options.password
}
});
this.transporter = BluebirdPromise.promisifyAll(transporter);
}
notify(identity: Identity, subject: string, link: string): BluebirdPromise<void> {
const d = {
url: link,
button_title: "Continue",
title: subject
};
const mailOptions = {
from: "auth-server@open-intent.io",
to: identity.email,
subject: subject,
html: ejs.render(email_template, d)
};
return this.transporter.sendMailAsync(mailOptions);
}
}

View File

@ -0,0 +1,7 @@
import * as BluebirdPromise from "bluebird";
import { Identity } from "../../types/Identity";
export abstract class INotifier {
abstract notify(identity: Identity, subject: string, link: string): BluebirdPromise<void>;
}

View File

@ -0,0 +1,22 @@
import { NotifierConfiguration } from "..//Configuration";
import { Nodemailer } from "../../types/Dependencies";
import { INotifier } from "./INotifier";
import { GMailNotifier } from "./GMailNotifier";
import { FileSystemNotifier } from "./FileSystemNotifier";
export class NotifierFactory {
static build(options: NotifierConfiguration, nodemailer: Nodemailer): INotifier {
if ("gmail" in options) {
return new GMailNotifier(options.gmail, nodemailer);
}
else if ("filesystem" in options) {
return new FileSystemNotifier(options.filesystem);
}
}
}

View File

@ -1,16 +0,0 @@
module.exports = FSNotifier;
var Promise = require('bluebird');
var fs = Promise.promisifyAll(require('fs'));
var util = require('util');
function FSNotifier(options) {
this._filename = options.filename;
}
FSNotifier.prototype.notify = function(identity, subject, link) {
var content = util.format('User: %s\nSubject: %s\nLink: %s', identity.userid,
subject, link);
return fs.writeFileAsync(this._filename, content);
}

View File

@ -1,33 +0,0 @@
module.exports = GmailNotifier;
var Promise = require('bluebird');
var fs = require('fs');
var ejs = require('ejs');
var email_template = fs.readFileSync(__dirname + '/../../resources/email-template.ejs', 'UTF-8');
function GmailNotifier(options, deps) {
var transporter = deps.nodemailer.createTransport({
service: 'gmail',
auth: {
user: options.username,
pass: options.password
}
});
this.transporter = Promise.promisifyAll(transporter);
}
GmailNotifier.prototype.notify = function(identity, subject, link) {
var d = {};
d.url = link;
d.button_title = 'Continue';
d.title = subject;
var mailOptions = {};
mailOptions.from = 'auth-server@open-intent.io';
mailOptions.to = identity.email;
mailOptions.subject = subject;
mailOptions.html = ejs.render(email_template, d);
return this.transporter.sendMailAsync(mailOptions);
}

View File

@ -1,39 +0,0 @@
var first_factor = require('./routes/first_factor');
var second_factor = require('./routes/second_factor');
var reset_password = require('./routes/reset_password');
var verify = require('./routes/verify');
var u2f_register_handler = require('./routes/u2f_register_handler');
var totp_register = require('./routes/totp_register');
var objectPath = require('object-path');
module.exports = {
login: serveLogin,
logout: serveLogout,
verify: verify,
first_factor: first_factor,
second_factor: second_factor,
reset_password: reset_password,
u2f_register: u2f_register_handler,
totp_register: totp_register,
}
function serveLogin(req, res) {
if(!(objectPath.has(req, 'session.auth_session'))) {
req.session.auth_session = {};
req.session.auth_session.first_factor = false;
req.session.auth_session.second_factor = false;
}
res.render('login');
}
function serveLogout(req, res) {
var redirect_param = req.query.redirect;
var redirect_url = redirect_param || '/';
req.session.auth_session = {
first_factor: false,
second_factor: false
}
res.redirect(redirect_url);
}

41
src/lib/routes.ts 100644
View File

@ -0,0 +1,41 @@
import FirstFactor = require("./routes/FirstFactor");
import SecondFactorRoutes = require("./routes/SecondFactorRoutes");
import PasswordReset = require("./routes/PasswordReset");
import AuthenticationValidator = require("./routes/AuthenticationValidator");
import U2FRegistration = require("./routes/U2FRegistration");
import TOTPRegistration = require("./routes/TOTPRegistration");
import objectPath = require("object-path");
import express = require("express");
export = {
login: serveLogin,
logout: serveLogout,
verify: AuthenticationValidator,
first_factor: FirstFactor,
second_factor: SecondFactorRoutes,
reset_password: PasswordReset,
u2f_register: U2FRegistration,
totp_register: TOTPRegistration,
};
function serveLogin(req: express.Request, res: express.Response) {
if (!(objectPath.has(req, "session.auth_session"))) {
req.session.auth_session = {};
req.session.auth_session.first_factor = false;
req.session.auth_session.second_factor = false;
}
res.render("login");
}
function serveLogout(req: express.Request, res: express.Response) {
const redirect_param = req.query.redirect;
const redirect_url = redirect_param || "/";
req.session.auth_session = {
first_factor: false,
second_factor: false
};
res.redirect(redirect_url);
}

View File

@ -0,0 +1,53 @@
import objectPath = require("object-path");
import BluebirdPromise = require("bluebird");
import express = require("express");
import AccessController from "../access_control/AccessController";
import exceptions = require("../Exceptions");
function verify_filter(req: express.Request, res: express.Response) {
const logger = req.app.get("logger");
const accessController: AccessController = req.app.get("access controller");
if (!objectPath.has(req, "session.auth_session"))
return BluebirdPromise.reject("No auth_session variable");
if (!objectPath.has(req, "session.auth_session.first_factor"))
return BluebirdPromise.reject("No first factor variable");
if (!objectPath.has(req, "session.auth_session.second_factor"))
return BluebirdPromise.reject("No second factor variable");
if (!objectPath.has(req, "session.auth_session.userid"))
return BluebirdPromise.reject("No userid variable");
const username = objectPath.get<express.Request, string>(req, "session.auth_session.userid");
const groups = objectPath.get<express.Request, string[]>(req, "session.auth_session.groups");
const host = objectPath.get<express.Request, string>(req, "headers.host");
const domain = host.split(":")[0];
const isAllowed = accessController.isDomainAllowedForUser(domain, username, groups);
if (!isAllowed) return BluebirdPromise.reject(
new exceptions.DomainAccessDenied("User '" + username + "' does not have access to " + domain));
if (!req.session.auth_session.first_factor ||
!req.session.auth_session.second_factor)
return BluebirdPromise.reject(new exceptions.AccessDeniedError("First or second factor not validated"));
return BluebirdPromise.resolve();
}
export = function (req: express.Request, res: express.Response) {
verify_filter(req, res)
.then(function () {
res.status(204);
res.send();
})
.catch(function (err) {
req.app.get("logger").error(err);
res.status(401);
res.send();
});
};

View File

@ -0,0 +1,19 @@
import objectPath = require("object-path");
import express = require("express");
type ExpressRequest = (req: express.Request, res: express.Response, next?: express.NextFunction) => void;
export = function(callback: ExpressRequest): ExpressRequest {
return function (req: express.Request, res: express.Response, next: express.NextFunction) {
const auth_session = req.session.auth_session;
const first_factor = objectPath.has(req, "session.auth_session.first_factor")
&& req.session.auth_session.first_factor;
if (!first_factor) {
res.status(403);
res.send();
return;
}
callback(req, res, next);
};
};

View File

@ -0,0 +1,82 @@
import exceptions = require("../Exceptions");
import objectPath = require("object-path");
import BluebirdPromise = require("bluebird");
import express = require("express");
import AccessController from "../access_control/AccessController";
import AuthenticationRegulator from "../AuthenticationRegulator";
import { LdapClient } from "../LdapClient";
export = function (req: express.Request, res: express.Response) {
const username: string = req.body.username;
const password: string = req.body.password;
if (!username || !password) {
res.status(401);
res.send();
return;
}
const logger = req.app.get("logger");
const ldap: LdapClient = req.app.get("ldap");
const config = req.app.get("config");
const regulator: AuthenticationRegulator = req.app.get("authentication regulator");
const accessController: AccessController = req.app.get("access controller");
logger.info("1st factor: Starting authentication of user \"%s\"", username);
logger.debug("1st factor: Start bind operation against LDAP");
logger.debug("1st factor: username=%s", username);
regulator.regulate(username)
.then(function () {
return ldap.bind(username, password);
})
.then(function () {
objectPath.set(req, "session.auth_session.userid", username);
objectPath.set(req, "session.auth_session.first_factor", true);
logger.info("1st factor: LDAP binding successful");
logger.debug("1st factor: Retrieve email from LDAP");
return BluebirdPromise.join(ldap.get_emails(username), ldap.get_groups(username));
})
.then(function (data: [string[], string[]]) {
const emails: string[] = data[0];
const groups: string[] = data[1];
if (!emails && emails.length <= 0) throw new Error("No email found");
logger.debug("1st factor: Retrieved email are %s", emails);
objectPath.set(req, "session.auth_session.email", emails[0]);
objectPath.set(req, "session.auth_session.groups", groups);
regulator.mark(username, true);
res.status(204);
res.send();
})
.catch(exceptions.LdapSeachError, function (err: Error) {
logger.error("1st factor: Unable to retrieve email from LDAP", err);
res.status(500);
res.send();
})
.catch(exceptions.LdapBindError, function (err: Error) {
logger.error("1st factor: LDAP binding failed");
logger.debug("1st factor: LDAP binding failed due to ", err);
regulator.mark(username, false);
res.status(401);
res.send("Bad credentials");
})
.catch(exceptions.AuthenticationRegulationError, function (err: Error) {
logger.error("1st factor: the regulator rejected the authentication of user %s", username);
logger.debug("1st factor: authentication rejected due to %s", err);
res.status(403);
res.send("Access has been restricted for a few minutes...");
})
.catch(exceptions.DomainAccessDenied, (err: Error) => {
logger.error("1st factor: ", err);
res.status(401);
res.send("Access denied...");
})
.catch(function (err: Error) {
console.log(err.stack);
logger.error("1st factor: Unhandled error %s", err);
res.status(500);
res.send("Internal error");
});
};

View File

@ -0,0 +1,81 @@
import BluebirdPromise = require("bluebird");
import objectPath = require("object-path");
import exceptions = require("../Exceptions");
import express = require("express");
import { Identity } from "../../types/Identity";
import { IdentityValidable } from "../IdentityValidator";
const CHALLENGE = "reset-password";
class PasswordResetHandler implements IdentityValidable {
challenge(): string {
return CHALLENGE;
}
templateName(): string {
return "reset-password";
}
preValidation(req: express.Request): BluebirdPromise<Identity> {
const userid = objectPath.get(req, "body.userid");
if (!userid) {
return BluebirdPromise.reject(new exceptions.AccessDeniedError("No user id provided"));
}
const ldap = req.app.get("ldap");
return ldap.get_emails(userid)
.then(function (emails: string[]) {
if (!emails && emails.length <= 0) throw new Error("No email found");
const identity = {
email: emails[0],
userid: userid
};
return BluebirdPromise.resolve(identity);
});
}
mailSubject(): string {
return "Reset your password";
}
}
function protect(fn: express.RequestHandler) {
return function (req: express.Request, res: express.Response) {
const challenge = objectPath.get(req, "session.auth_session.identity_check.challenge");
if (challenge != CHALLENGE) {
res.status(403);
res.send();
return;
}
fn(req, res, undefined);
};
}
function post(req: express.Request, res: express.Response) {
const logger = req.app.get("logger");
const ldap = req.app.get("ldap");
const new_password = objectPath.get(req, "body.password");
const userid = objectPath.get(req, "session.auth_session.identity_check.userid");
logger.info("POST reset-password: User %s wants to reset his/her password", userid);
ldap.update_password(userid, new_password)
.then(function () {
logger.info("POST reset-password: Password reset for user %s", userid);
objectPath.set(req, "session.auth_session", undefined);
res.status(204);
res.send();
})
.catch(function (err: Error) {
logger.error("POST reset-password: Error while resetting the password of user %s. %s", userid, err);
res.status(500);
res.send();
});
}
export = {
icheck_interface: new PasswordResetHandler(),
post: protect(post)
};

View File

@ -0,0 +1,28 @@
import DenyNotLogged = require("./DenyNotLogged");
import U2FRoutes = require("./U2FRoutes");
import TOTPAuthenticator = require("./TOTPAuthenticator");
import express = require("express");
interface SecondFactorRoutes {
totp: express.RequestHandler;
u2f: {
register_request: express.RequestHandler;
register: express.RequestHandler;
sign_request: express.RequestHandler;
sign: express.RequestHandler;
};
}
export = {
totp: DenyNotLogged(TOTPAuthenticator),
u2f: {
register_request: U2FRoutes.register_request,
register: U2FRoutes.register,
sign_request: DenyNotLogged(U2FRoutes.sign_request),
sign: DenyNotLogged(U2FRoutes.sign),
}
} as SecondFactorRoutes;

View File

@ -0,0 +1,49 @@
import exceptions = require("../Exceptions");
import objectPath = require("object-path");
import express = require("express");
import { TOTPSecretDocument } from "../UserDataStore";
import BluebirdPromise = require("bluebird");
const UNAUTHORIZED_MESSAGE = "Unauthorized access";
export = function(req: express.Request, res: express.Response) {
const logger = req.app.get("logger");
const userid = objectPath.get(req, "session.auth_session.userid");
logger.info("POST 2ndfactor totp: Initiate TOTP validation for user %s", userid);
if (!userid) {
logger.error("POST 2ndfactor totp: No user id in the session");
res.status(403);
res.send();
return;
}
const token = req.body.token;
const totpValidator = req.app.get("totp validator");
const userDataStore = req.app.get("user data store");
logger.debug("POST 2ndfactor totp: Fetching secret for user %s", userid);
userDataStore.get_totp_secret(userid)
.then(function (doc: TOTPSecretDocument) {
logger.debug("POST 2ndfactor totp: TOTP secret is %s", JSON.stringify(doc));
return totpValidator.validate(token, doc.secret.base32);
})
.then(function () {
logger.debug("POST 2ndfactor totp: TOTP validation succeeded");
objectPath.set(req, "session.auth_session.second_factor", true);
res.status(204);
res.send();
})
.catch(exceptions.InvalidTOTPError, function (err: Error) {
logger.error("POST 2ndfactor totp: Invalid TOTP token %s", err.message);
res.status(401);
res.send("Invalid TOTP token");
})
.catch(function (err: Error) {
console.log(err.stack);
logger.error("POST 2ndfactor totp: Internal error %s", err.message);
res.status(500);
res.send("Internal error");
});
};

View File

@ -0,0 +1,86 @@
import objectPath = require("object-path");
import BluebirdPromise = require("bluebird");
import express = require("express");
import exceptions = require("../Exceptions");
import { Identity } from "../../types/Identity";
import { IdentityValidable } from "../IdentityValidator";
const CHALLENGE = "totp-register";
const TEMPLATE_NAME = "totp-register";
class TOTPRegistrationHandler implements IdentityValidable {
challenge(): string {
return CHALLENGE;
}
templateName(): string {
return TEMPLATE_NAME;
}
preValidation(req: express.Request): BluebirdPromise<Identity> {
const first_factor_passed = objectPath.get(req, "session.auth_session.first_factor");
if (!first_factor_passed) {
return BluebirdPromise.reject("Authentication required before registering TOTP secret key");
}
const userid = objectPath.get<express.Request, string>(req, "session.auth_session.userid");
const email = objectPath.get<express.Request, string>(req, "session.auth_session.email");
if (!(userid && email)) {
return BluebirdPromise.reject("User ID or email is missing");
}
const identity = {
email: email,
userid: userid
};
return BluebirdPromise.resolve(identity);
}
mailSubject(): string {
return "Register your TOTP secret key";
}
}
// Generate a secret and send it to the user
function post(req: express.Request, res: express.Response) {
const logger = req.app.get("logger");
const userid = objectPath.get(req, "session.auth_session.identity_check.userid");
const challenge = objectPath.get(req, "session.auth_session.identity_check.challenge");
if (challenge != CHALLENGE || !userid) {
res.status(403);
res.send();
return;
}
const user_data_store = req.app.get("user data store");
const totpGenerator = req.app.get("totp generator");
const secret = totpGenerator.generate();
logger.debug("POST new-totp-secret: save the TOTP secret in DB");
user_data_store.set_totp_secret(userid, secret)
.then(function () {
const doc = {
otpauth_url: secret.otpauth_url,
base32: secret.base32,
ascii: secret.ascii
};
objectPath.set(req, "session", undefined);
res.status(200);
res.json(doc);
})
.catch(function (err: Error) {
logger.error("POST new-totp-secret: Internal error %s", err);
res.status(500);
res.send();
});
}
export = {
icheck_interface: new TOTPRegistrationHandler(),
post: post,
};

View File

@ -0,0 +1,84 @@
import u2f_register_handler = require("./U2FRegistration");
import objectPath = require("object-path");
import u2f_common = require("./u2f_common");
import BluebirdPromise = require("bluebird");
import express = require("express");
import authdog = require("../../types/authdog");
import UserDataStore, { U2FMetaDocument } from "../UserDataStore";
function retrieve_u2f_meta(req: express.Request, userDataStore: UserDataStore) {
const userid = req.session.auth_session.userid;
const appid = u2f_common.extract_app_id(req);
return userDataStore.get_u2f_meta(userid, appid);
}
function sign_request(req: express.Request, res: express.Response) {
const logger = req.app.get("logger");
const userDataStore = req.app.get("user data store");
retrieve_u2f_meta(req, userDataStore)
.then(function (doc: U2FMetaDocument) {
if (!doc) {
u2f_common.reply_with_missing_registration(res);
return;
}
const u2f = req.app.get("u2f");
const meta = doc.meta;
const appid = u2f_common.extract_app_id(req);
logger.info("U2F sign_request: Start authentication to app %s", appid);
return u2f.startAuthentication(appid, [meta]);
})
.then(function (authRequest: authdog.AuthenticationRequest) {
logger.info("U2F sign_request: Store authentication request and reply");
req.session.auth_session.sign_request = authRequest;
res.status(200);
res.json(authRequest);
})
.catch(function (err: Error) {
logger.info("U2F sign_request: %s", err);
res.status(500);
res.send();
});
}
function sign(req: express.Request, res: express.Response) {
if (!objectPath.has(req, "session.auth_session.sign_request")) {
u2f_common.reply_with_unauthorized(res);
return;
}
const logger = req.app.get("logger");
const userDataStore = req.app.get("user data store");
retrieve_u2f_meta(req, userDataStore)
.then(function (doc: U2FMetaDocument) {
const appid = u2f_common.extract_app_id(req);
const u2f = req.app.get("u2f");
const authRequest = req.session.auth_session.sign_request;
const meta = doc.meta;
logger.info("U2F sign: Finish authentication");
return u2f.finishAuthentication(authRequest, req.body, [meta]);
})
.then(function (authenticationStatus: authdog.Authentication) {
logger.info("U2F sign: Authentication successful");
req.session.auth_session.second_factor = true;
res.status(204);
res.send();
})
.catch(function (err: Error) {
logger.error("U2F sign: %s", err);
res.status(500);
res.send();
});
}
export = {
sign_request: sign_request,
sign: sign
};

View File

@ -0,0 +1,51 @@
import objectPath = require("object-path");
import BluebirdPromise = require("bluebird");
import express = require("express");
import { IdentityValidable } from "../IdentityValidator";
import { Identity } from "../../types/Identity";
const CHALLENGE = "u2f-register";
const TEMPLATE_NAME = "u2f-register";
const MAIL_SUBJECT = "Register your U2F device";
class U2FRegistrationHandler implements IdentityValidable {
challenge(): string {
return CHALLENGE;
}
templateName(): string {
return TEMPLATE_NAME;
}
preValidation(req: express.Request): BluebirdPromise<Identity> {
const first_factor_passed = objectPath.get(req, "session.auth_session.first_factor");
if (!first_factor_passed) {
return BluebirdPromise.reject("Authentication required before issuing a u2f registration request");
}
const userid = objectPath.get<express.Request, string>(req, "session.auth_session.userid");
const email = objectPath.get<express.Request, string>(req, "session.auth_session.email");
if (!(userid && email)) {
return BluebirdPromise.reject("User ID or email is missing");
}
const identity = {
email: email,
userid: userid
};
return BluebirdPromise.resolve(identity);
}
mailSubject(): string {
return MAIL_SUBJECT;
}
}
export = {
icheck_interface: new U2FRegistrationHandler(),
};

View File

@ -0,0 +1,89 @@
import u2f_register_handler = require("./U2FRegistration");
import objectPath = require("object-path");
import u2f_common = require("./u2f_common");
import BluebirdPromise = require("bluebird");
import express = require("express");
import authdog = require("../../types/authdog");
function register_request(req: express.Request, res: express.Response) {
const logger = req.app.get("logger");
const challenge = objectPath.get(req, "session.auth_session.identity_check.challenge");
if (challenge != "u2f-register") {
res.status(403);
res.send();
return;
}
const u2f = req.app.get("u2f");
const appid = u2f_common.extract_app_id(req);
logger.debug("U2F register_request: headers=%s", JSON.stringify(req.headers));
logger.info("U2F register_request: Starting registration of app %s", appid);
u2f.startRegistration(appid, [])
.then(function (registrationRequest: authdog.AuthenticationRequest) {
logger.info("U2F register_request: Sending back registration request");
req.session.auth_session.register_request = registrationRequest;
res.status(200);
res.json(registrationRequest);
})
.catch(function (err: Error) {
logger.error("U2F register_request: %s", err);
res.status(500);
res.send("Unable to start registration request");
});
}
function register(req: express.Request, res: express.Response) {
const registrationRequest = objectPath.get(req, "session.auth_session.register_request");
const challenge = objectPath.get(req, "session.auth_session.identity_check.challenge");
if (!registrationRequest) {
res.status(403);
res.send();
return;
}
if (!(registrationRequest && challenge == "u2f-register")) {
res.status(403);
res.send();
return;
}
const user_data_storage = req.app.get("user data store");
const u2f = req.app.get("u2f");
const userid = req.session.auth_session.userid;
const appid = u2f_common.extract_app_id(req);
const logger = req.app.get("logger");
logger.info("U2F register: Finishing registration");
logger.debug("U2F register: register_request=%s", JSON.stringify(registrationRequest));
logger.debug("U2F register: body=%s", JSON.stringify(req.body));
u2f.finishRegistration(registrationRequest, req.body)
.then(function (registrationStatus: authdog.Registration) {
logger.info("U2F register: Store registration and reply");
const meta = {
keyHandle: registrationStatus.keyHandle,
publicKey: registrationStatus.publicKey,
certificate: registrationStatus.certificate
};
return user_data_storage.set_u2f_meta(userid, appid, meta);
})
.then(function () {
objectPath.set(req, "session.auth_session.identity_check", undefined);
res.status(204);
res.send();
})
.catch(function (err: Error) {
logger.error("U2F register: %s", err);
res.status(500);
res.send("Unable to register");
});
}
export = {
register_request: register_request,
register: register
};

View File

@ -0,0 +1,19 @@
import U2FRegistrationProcess = require("./U2FRegistrationProcess");
import U2FAuthenticationProcess = require("./U2FAuthenticationProcess");
import express = require("express");
interface U2FRoutes {
register_request: express.RequestHandler;
register: express.RequestHandler;
sign_request: express.RequestHandler;
sign: express.RequestHandler;
}
export = {
register_request: U2FRegistrationProcess.register_request,
register: U2FRegistrationProcess.register,
sign_request: U2FAuthenticationProcess.sign_request,
sign: U2FAuthenticationProcess.sign,
} as U2FRoutes;

View File

@ -1,19 +0,0 @@
module.exports = denyNotLogged;
var objectPath = require('object-path');
function denyNotLogged(next) {
return function(req, res) {
var auth_session = req.session.auth_session;
var first_factor = objectPath.has(req, 'session.auth_session.first_factor')
&& req.session.auth_session.first_factor;
if(!first_factor) {
res.status(403);
res.send();
return;
}
next(req, res);
}
}

View File

@ -1,103 +0,0 @@
module.exports = first_factor;
var exceptions = require('../exceptions');
var objectPath = require('object-path');
var Promise = require('bluebird');
function get_allowed_domains(access_control, username, groups) {
var allowed_domains = [];
for(var i = 0; i<access_control.length; ++i) {
var rule = access_control[i];
if('allowed_domains' in rule) {
if('group' in rule && groups.indexOf(rule['group']) >= 0) {
var domains = rule.allowed_domains;
allowed_domains = allowed_domains.concat(domains);
}
else if('user' in rule && username == rule['user']) {
var domains = rule.allowed_domains;
allowed_domains = allowed_domains.concat(domains);
}
}
}
return allowed_domains;
}
function first_factor(req, res) {
var username = req.body.username;
var password = req.body.password;
if(!username || !password) {
res.status(401);
res.send();
return;
}
var logger = req.app.get('logger');
var ldap = req.app.get('ldap');
var config = req.app.get('config');
var regulator = req.app.get('authentication regulator');
var acl_builder = req.app.get('access control').builder;
logger.info('1st factor: Starting authentication of user "%s"', username);
logger.debug('1st factor: Start bind operation against LDAP');
logger.debug('1st factor: username=%s', username);
regulator.regulate(username)
.then(function() {
return ldap.bind(username, password);
})
.then(function() {
objectPath.set(req, 'session.auth_session.userid', username);
objectPath.set(req, 'session.auth_session.first_factor', true);
logger.info('1st factor: LDAP binding successful');
logger.debug('1st factor: Retrieve email from LDAP');
return Promise.join(ldap.get_emails(username), ldap.get_groups(username));
})
.then(function(data) {
var emails = data[0];
var groups = data[1];
var allowed_domains;
if(!emails && emails.length <= 0) throw new Error('No email found');
logger.debug('1st factor: Retrieved email are %s', emails);
objectPath.set(req, 'session.auth_session.email', emails[0]);
if(config.access_control) {
allowed_domains = acl_builder.get_allowed_domains(username, groups);
}
else {
allowed_domains = acl_builder.get_any_domain();
logger.debug('1st factor: no access control rules found.' +
'Default policy to allow all.');
}
objectPath.set(req, 'session.auth_session.allowed_domains', allowed_domains);
regulator.mark(username, true);
res.status(204);
res.send();
})
.catch(exceptions.LdapSearchError, function(err) {
logger.error('1st factor: Unable to retrieve email from LDAP', err);
res.status(500);
res.send();
})
.catch(exceptions.LdapBindError, function(err) {
logger.error('1st factor: LDAP binding failed');
logger.debug('1st factor: LDAP binding failed due to ', err);
regulator.mark(username, false);
res.status(401);
res.send('Bad credentials');
})
.catch(exceptions.AuthenticationRegulationError, function(err) {
logger.error('1st factor: the regulator rejected the authentication of user %s', username);
logger.debug('1st factor: authentication rejected due to %s', err);
res.status(403);
res.send('Access has been restricted for a few minutes...');
})
.catch(function(err) {
logger.error('1st factor: Unhandled error %s', err);
res.status(500);
res.send('Internal error');
});
}

View File

@ -1,72 +0,0 @@
var Promise = require('bluebird');
var objectPath = require('object-path');
var exceptions = require('../exceptions');
var CHALLENGE = 'reset-password';
var icheck_interface = {
challenge: CHALLENGE,
render_template: 'reset-password',
pre_check_callback: pre_check,
email_subject: 'Reset your password',
}
module.exports = {
icheck_interface: icheck_interface,
post: protect(post)
}
function pre_check(req) {
var userid = objectPath.get(req, 'body.userid');
if(!userid) {
var err = new exceptions.AccessDeniedError();
return Promise.reject(err);
}
var ldap = req.app.get('ldap');
return ldap.get_emails(userid)
.then(function(emails) {
if(!emails && emails.length <= 0) throw new Error('No email found');
var identity = {}
identity.email = emails[0];
identity.userid = userid;
return Promise.resolve(identity);
});
}
function protect(fn) {
return function(req, res) {
var challenge = objectPath.get(req, 'session.auth_session.identity_check.challenge');
if(challenge != CHALLENGE) {
res.status(403);
res.send();
return;
}
fn(req, res);
  }
}
function post(req, res) {
var logger = req.app.get('logger');
var ldap = req.app.get('ldap');
var new_password = objectPath.get(req, 'body.password');
var userid = objectPath.get(req, 'session.auth_session.identity_check.userid');
logger.info('POST reset-password: User %s wants to reset his/her password', userid);
ldap.update_password(userid, new_password)
.then(function() {
logger.info('POST reset-password: Password reset for user %s', userid);
objectPath.set(req, 'session.auth_session', undefined);
res.status(204);
res.send();
})
.catch(function(err) {
logger.error('POST reset-password: Error while resetting the password of user %s. %s', userid, err);
res.status(500);
res.send();
});
}

View File

@ -1,17 +0,0 @@
var denyNotLogged = require('./deny_not_logged');
var u2f = require('./u2f');
module.exports = {
totp: denyNotLogged(require('./totp')),
u2f: {
register_request: u2f.register_request,
register: u2f.register,
register_handler_get: u2f.register_handler_get,
register_handler_post: u2f.register_handler_post,
sign_request: denyNotLogged(u2f.sign_request),
sign: denyNotLogged(u2f.sign),
}
}

View File

@ -1,50 +0,0 @@
module.exports = totp;
var totp = require('../totp');
var objectPath = require('object-path');
var exceptions = require('../../../src/lib/exceptions');
var UNAUTHORIZED_MESSAGE = 'Unauthorized access';
function totp(req, res) {
var logger = req.app.get('logger');
var userid = objectPath.get(req, 'session.auth_session.userid');
logger.info('POST 2ndfactor totp: Initiate TOTP validation for user %s', userid);
if(!userid) {
logger.error('POST 2ndfactor totp: No user id in the session');
res.status(403);
res.send();
return;
}
var token = req.body.token;
var totp_engine = req.app.get('totp engine');
var data_store = req.app.get('user data store');
logger.debug('POST 2ndfactor totp: Fetching secret for user %s', userid);
data_store.get_totp_secret(userid)
.then(function(doc) {
logger.debug('POST 2ndfactor totp: TOTP secret is %s', JSON.stringify(doc));
return totp.validate(totp_engine, token, doc.secret.base32)
})
.then(function() {
logger.debug('POST 2ndfactor totp: TOTP validation succeeded');
objectPath.set(req, 'session.auth_session.second_factor', true);
res.status(204);
res.send();
}, function(err) {
throw new exceptions.InvalidTOTPError();
})
.catch(exceptions.InvalidTOTPError, function(err) {
logger.error('POST 2ndfactor totp: Invalid TOTP token %s', err);
res.status(401);
res.send('Invalid TOTP token');
})
.catch(function(err) {
logger.error('POST 2ndfactor totp: Internal error %s', err);
res.status(500);
res.send('Internal error');
});
}

View File

@ -1,72 +0,0 @@
var objectPath = require('object-path');
var Promise = require('bluebird');
var CHALLENGE = 'totp-register';
var icheck_interface = {
challenge: CHALLENGE,
render_template: 'totp-register',
pre_check_callback: pre_check,
email_subject: 'Register your TOTP secret key',
}
module.exports = {
icheck_interface: icheck_interface,
post: post,
}
function pre_check(req) {
var first_factor_passed = objectPath.get(req, 'session.auth_session.first_factor');
if(!first_factor_passed) {
return Promise.reject('Authentication required before registering TOTP secret key');
}
var userid = objectPath.get(req, 'session.auth_session.userid');
var email = objectPath.get(req, 'session.auth_session.email');
if(!(userid && email)) {
return Promise.reject('User ID or email is missing');
}
var identity = {};
identity.email = email;
identity.userid = userid;
return Promise.resolve(identity);
}
// Generate a secret and send it to the user
function post(req, res) {
var logger = req.app.get('logger');
var userid = objectPath.get(req, 'session.auth_session.identity_check.userid');
var challenge = objectPath.get(req, 'session.auth_session.identity_check.challenge');
if(challenge != CHALLENGE || !userid) {
res.status(403);
res.send();
return;
}
var user_data_store = req.app.get('user data store');
var totp = req.app.get('totp engine');
var secret = totp.generateSecret();
logger.debug('POST new-totp-secret: save the TOTP secret in DB');
user_data_store.set_totp_secret(userid, secret)
.then(function() {
var doc = {};
doc.otpauth_url = secret.otpauth_url;
doc.base32 = secret.base32;
doc.ascii = secret.ascii;
objectPath.set(req, 'session', undefined);
res.status(200);
res.json(doc);
})
.catch(function(err) {
logger.error('POST new-totp-secret: Internal error %s', err);
res.status(500);
res.send();
});
}

View File

@ -1,85 +0,0 @@
var u2f_register = require('./u2f_register');
var u2f_common = require('./u2f_common');
var objectPath = require('object-path');
module.exports = {
register_request: u2f_register.register_request,
register: u2f_register.register,
register_handler_get: u2f_register.register_handler_get,
register_handler_post: u2f_register.register_handler_post,
sign_request: sign_request,
sign: sign,
}
function retrieve_u2f_meta(req, user_data_storage) {
var userid = req.session.auth_session.userid;
var appid = u2f_common.extract_app_id(req);
return user_data_storage.get_u2f_meta(userid, appid);
}
function sign_request(req, res) {
var logger = req.app.get('logger');
var user_data_storage = req.app.get('user data store');
retrieve_u2f_meta(req, user_data_storage)
.then(function(doc) {
if(!doc) {
u2f_common.reply_with_missing_registration(res);
return;
}
var u2f = req.app.get('u2f');
var meta = doc.meta;
var appid = u2f_common.extract_app_id(req);
logger.info('U2F sign_request: Start authentication to app %s', appid);
return u2f.startAuthentication(appid, [meta])
})
.then(function(authRequest) {
logger.info('U2F sign_request: Store authentication request and reply');
req.session.auth_session.sign_request = authRequest;
res.status(200);
res.json(authRequest);
})
.catch(function(err) {
logger.info('U2F sign_request: %s', err);
res.status(500);
res.send();
});
}
function sign(req, res) {
if(!objectPath.has(req, 'session.auth_session.sign_request')) {
u2f_common.reply_with_unauthorized(res);
return;
}
var logger = req.app.get('logger');
var user_data_storage = req.app.get('user data store');
retrieve_u2f_meta(req, user_data_storage)
.then(function(doc) {
var appid = u2f_common.extract_app_id(req);
var u2f = req.app.get('u2f');
var authRequest = req.session.auth_session.sign_request;
var meta = doc.meta;
logger.info('U2F sign: Finish authentication');
return u2f.finishAuthentication(authRequest, req.body, [meta])
})
.then(function(authenticationStatus) {
logger.info('U2F sign: Authentication successful');
req.session.auth_session.second_factor = true;
res.status(204);
res.send();
})
.catch(function(err) {
logger.error('U2F sign: %s', err);
res.status(500);
res.send();
});
}

View File

@ -1,38 +0,0 @@
module.exports = {
extract_app_id: extract_app_id,
extract_original_url: extract_original_url,
extract_referrer: extract_referrer,
reply_with_internal_error: reply_with_internal_error,
reply_with_missing_registration: reply_with_missing_registration,
reply_with_unauthorized: reply_with_unauthorized
}
var util = require('util');
function extract_app_id(req) {
return util.format('https://%s', req.headers.host);
}
function extract_original_url(req) {
return util.format('https://%s%s', req.headers.host, req.headers['x-original-uri']);
}
function extract_referrer(req) {
return req.headers.referrer;
}
function reply_with_internal_error(res, msg) {
res.status(500);
res.send(msg)
}
function reply_with_missing_registration(res) {
res.status(401);
res.send('Please register before authenticate');
}
function reply_with_unauthorized(res) {
res.status(401);
res.send();
}

View File

@ -0,0 +1,39 @@
import util = require("util");
import express = require("express");
function extract_app_id(req: express.Request) {
return util.format("https://%s", req.headers.host);
}
function extract_original_url(req: express.Request) {
return util.format("https://%s%s", req.headers.host, req.headers["x-original-uri"]);
}
function extract_referrer(req: express.Request) {
return req.headers.referrer;
}
function reply_with_internal_error(res: express.Response, msg: string) {
res.status(500);
res.send(msg);
}
function reply_with_missing_registration(res: express.Response) {
res.status(401);
res.send("Please register before authenticate");
}
function reply_with_unauthorized(res: express.Response) {
res.status(401);
res.send();
}
export = {
extract_app_id: extract_app_id,
extract_original_url: extract_original_url,
extract_referrer: extract_referrer,
reply_with_internal_error: reply_with_internal_error,
reply_with_missing_registration: reply_with_missing_registration,
reply_with_unauthorized: reply_with_unauthorized
};

View File

@ -1,91 +0,0 @@
var u2f_register_handler = require('./u2f_register_handler');
module.exports = {
register_request: register_request,
register: register,
register_handler_get: u2f_register_handler.get,
register_handler_post: u2f_register_handler.post
}
var objectPath = require('object-path');
var u2f_common = require('./u2f_common');
var Promise = require('bluebird');
function register_request(req, res) {
var logger = req.app.get('logger');
var challenge = objectPath.get(req, 'session.auth_session.identity_check.challenge');
if(challenge != 'u2f-register') {
res.status(403);
res.send();
return;
}
var u2f = req.app.get('u2f');
var appid = u2f_common.extract_app_id(req);
logger.debug('U2F register_request: headers=%s', JSON.stringify(req.headers));
logger.info('U2F register_request: Starting registration of app %s', appid);
u2f.startRegistration(appid, [])
.then(function(registrationRequest) {
logger.info('U2F register_request: Sending back registration request');
req.session.auth_session.register_request = registrationRequest;
res.status(200);
res.json(registrationRequest);
})
.catch(function(err) {
logger.error('U2F register_request: %s', err);
res.status(500);
res.send('Unable to start registration request');
});
}
function register(req, res) {
var registrationRequest = objectPath.get(req, 'session.auth_session.register_request');
var challenge = objectPath.get(req, 'session.auth_session.identity_check.challenge');
if(!registrationRequest) {
res.status(403);
res.send();
return;
}
if(!(registrationRequest && challenge == 'u2f-register')) {
res.status(403);
res.send();
return;
}
var user_data_storage = req.app.get('user data store');
var u2f = req.app.get('u2f');
var userid = req.session.auth_session.userid;
var appid = u2f_common.extract_app_id(req);
var logger = req.app.get('logger');
logger.info('U2F register: Finishing registration');
logger.debug('U2F register: register_request=%s', JSON.stringify(registrationRequest));
logger.debug('U2F register: body=%s', JSON.stringify(req.body));
u2f.finishRegistration(registrationRequest, req.body)
.then(function(registrationStatus) {
logger.info('U2F register: Store registration and reply');
var meta = {
keyHandle: registrationStatus.keyHandle,
publicKey: registrationStatus.publicKey,
certificate: registrationStatus.certificate
}
return user_data_storage.set_u2f_meta(userid, appid, meta);
})
.then(function() {
objectPath.set(req, 'session.auth_session.identity_check', undefined);
res.status(204);
res.send();
})
.catch(function(err) {
logger.error('U2F register: %s', err);
res.status(500);
res.send('Unable to register');
});
}

View File

@ -1,37 +0,0 @@
var objectPath = require('object-path');
var Promise = require('bluebird');
var CHALLENGE = 'u2f-register';
var icheck_interface = {
challenge: CHALLENGE,
render_template: 'u2f-register',
pre_check_callback: pre_check,
email_subject: 'Register your U2F device',
}
module.exports = {
icheck_interface: icheck_interface,
}
function pre_check(req) {
var first_factor_passed = objectPath.get(req, 'session.auth_session.first_factor');
if(!first_factor_passed) {
return Promise.reject('Authentication required before issuing a u2f registration request');
}
var userid = objectPath.get(req, 'session.auth_session.userid');
var email = objectPath.get(req, 'session.auth_session.email');
if(!(userid && email)) {
return Promise.reject('User ID or email is missing');
}
var identity = {};
identity.email = email;
identity.userid = userid;
return Promise.resolve(identity);
}

View File

@ -1,53 +0,0 @@
module.exports = verify;
var objectPath = require('object-path');
var Promise = require('bluebird');
function verify_filter(req, res) {
var logger = req.app.get('logger');
if(!objectPath.has(req, 'session.auth_session'))
return Promise.reject('No auth_session variable');
if(!objectPath.has(req, 'session.auth_session.first_factor'))
return Promise.reject('No first factor variable');
if(!objectPath.has(req, 'session.auth_session.second_factor'))
return Promise.reject('No second factor variable');
if(!objectPath.has(req, 'session.auth_session.userid'))
return Promise.reject('No userid variable');
if(!objectPath.has(req, 'session.auth_session.allowed_domains'))
return Promise.reject('No allowed_domains variable');
// Get the session ACL matcher
var allowed_domains = objectPath.get(req, 'session.auth_session.allowed_domains');
var host = objectPath.get(req, 'headers.host');
var domain = host.split(':')[0];
var acl_matcher = req.app.get('access control').matcher;
if(!acl_matcher.is_domain_allowed(domain, allowed_domains))
return Promise.reject('Access restricted by ACL rules');
if(!req.session.auth_session.first_factor ||
!req.session.auth_session.second_factor)
return Promise.reject('First or second factor not validated');
return Promise.resolve();
}
function verify(req, res) {
verify_filter(req, res)
.then(function() {
res.status(204);
res.send();
})
.catch(function(err) {
req.app.get('logger').error(err);
res.status(401);
res.send();
});
}

View File

@ -1,73 +0,0 @@
module.exports = {
run: run
}
var express = require('express');
var bodyParser = require('body-parser');
var path = require('path');
var UserDataStore = require('./user_data_store');
var Notifier = require('./notifier');
var AuthenticationRegulator = require('./authentication_regulator');
var setup_endpoints = require('./setup_endpoints');
var config_adapter = require('./config_adapter');
var Ldap = require('./ldap');
var AccessControl = require('./access_control');
function run(yaml_config, deps, fn) {
var config = config_adapter(yaml_config);
var view_directory = path.resolve(__dirname, '../views');
var public_html_directory = path.resolve(__dirname, '../public_html');
var datastore_options = {};
datastore_options.directory = config.store_directory;
if(config.store_in_memory)
datastore_options.inMemory = true;
var app = express();
app.use(express.static(public_html_directory));
app.use(bodyParser.urlencoded({ extended: false }));
app.use(bodyParser.json());
app.set('trust proxy', 1); // trust first proxy
app.use(deps.session({
secret: config.session_secret,
resave: false,
saveUninitialized: true,
cookie: {
secure: false,
maxAge: config.session_max_age,
domain: config.session_domain
},
}));
app.set('views', view_directory);
app.set('view engine', 'ejs');
// by default the level of logs is info
deps.winston.level = config.logs_level || 'info';
var five_minutes = 5 * 60;
var data_store = new UserDataStore(deps.nedb, datastore_options);
var regulator = new AuthenticationRegulator(data_store, five_minutes);
var notifier = new Notifier(config.notifier, deps);
var ldap = new Ldap(deps, config.ldap);
var access_control = AccessControl(deps.winston, config.access_control);
app.set('logger', deps.winston);
app.set('ldap', ldap);
app.set('totp engine', deps.speakeasy);
app.set('u2f', deps.u2f);
app.set('user data store', data_store);
app.set('notifier', notifier);
app.set('authentication regulator', regulator);
app.set('config', config);
app.set('access control', access_control);
setup_endpoints(app);
return app.listen(config.port, function(err) {
console.log('Listening on %d...', config.port);
if(fn) fn();
});
}

View File

@ -1,280 +0,0 @@
module.exports = setup_endpoints;
var routes = require('./routes');
var identity_check = require('./identity_check');
function setup_endpoints(app) {
/**
* @apiDefine UserSession
* @apiHeader {String} Cookie Cookie containing 'connect.sid', the user
* session token.
*/
/**
* @apiDefine InternalError
* @apiError (Error 500) {String} error Internal error message.
*/
/**
* @apiDefine IdentityValidationPost
*
* @apiSuccess (Success 204) status Identity validation has been initiated.
* @apiError (Error 403) AccessDenied Access is denied.
* @apiError (Error 400) InvalidIdentity User identity is invalid.
* @apiError (Error 500) {String} error Internal error message.
*
* @apiDescription This request issue an identity validation token for the user
* bound to the session. It sends a challenge to the email address set in the user
* LDAP entry. The user must visit the sent URL to complete the validation and
* continue the registration process.
*/
/**
* @apiDefine IdentityValidationGet
* @apiParam {String} identity_token The one-time identity validation token provided in the email.
* @apiSuccess (Success 200) {String} content The content of the page.
* @apiError (Error 403) AccessDenied Access is denied.
* @apiError (Error 500) {String} error Internal error message.
*/
/**
* @api {get} /login Serve login page
* @apiName Login
* @apiGroup Pages
* @apiVersion 1.0.0
*
* @apiParam {String} redirect Redirect to this URL when user is authenticated.
* @apiSuccess (Success 200) {String} Content The content of the login page.
*
* @apiDescription Create a user session and serve the login page along with
* a cookie.
*/
app.get ('/login', routes.login);
/**
* @api {get} /logout Server logout page
* @apiName Logout
* @apiGroup Pages
* @apiVersion 1.0.0
*
* @apiParam {String} redirect Redirect to this URL when user is deauthenticated.
* @apiSuccess (Success 301) redirect Redirect to the URL.
*
* @apiDescription Deauthenticate the user and redirect him.
*/
app.get ('/logout', routes.logout);
/**
* @api {post} /totp-register Request TOTP registration
* @apiName RequestTOTPRegistration
* @apiGroup Registration
* @apiVersion 1.0.0
* @apiUse UserSession
* @apiUse IdentityValidationPost
*/
/**
* @api {get} /totp-register Serve TOTP registration page
* @apiName ServeTOTPRegistrationPage
* @apiGroup Registration
* @apiVersion 1.0.0
* @apiUse UserSession
* @apiUse IdentityValidationGet
*
*
* @apiDescription Serves the TOTP registration page that displays the secret.
* The secret is a QRCode and a base32 secret.
*/
identity_check(app, '/totp-register', routes.totp_register.icheck_interface);
/**
* @api {post} /u2f-register Request U2F registration
* @apiName RequestU2FRegistration
* @apiGroup Registration
* @apiVersion 1.0.0
* @apiUse UserSession
* @apiUse IdentityValidationPost
*/
/**
* @api {get} /u2f-register Serve U2F registration page
* @apiName ServeU2FRegistrationPage
* @apiGroup Pages
* @apiVersion 1.0.0
* @apiUse UserSession
* @apiUse IdentityValidationGet
*
* @apiDescription Serves the U2F registration page that asks the user to
* touch the token of the U2F device.
*/
identity_check(app, '/u2f-register', routes.u2f_register.icheck_interface);
/**
* @api {post} /reset-password Request for password reset
* @apiName RequestPasswordReset
* @apiGroup Registration
* @apiVersion 1.0.0
* @apiUse UserSession
* @apiUse IdentityValidationPost
*/
/**
* @api {get} /reset-password Serve password reset form.
* @apiName ServePasswordResetForm
* @apiGroup Pages
* @apiVersion 1.0.0
* @apiUse UserSession
* @apiUse IdentityValidationGet
*
* @apiDescription Serves password reset form that allow the user to provide
* the new password.
*/
identity_check(app, '/reset-password', routes.reset_password.icheck_interface);
app.get ('/reset-password-form', function(req, res) { res.render('reset-password-form'); });
/**
* @api {post} /new-password Set LDAP password
* @apiName SetLDAPPassword
* @apiGroup Registration
* @apiVersion 1.0.0
* @apiUse UserSession
*
* @apiParam {String} password New password
*
* @apiDescription Set a new password for the user.
*/
app.post ('/new-password', routes.reset_password.post);
/**
* @api {post} /new-totp-secret Generate TOTP secret
* @apiName GenerateTOTPSecret
* @apiGroup Registration
* @apiVersion 1.0.0
* @apiUse UserSession
*
* @apiSuccess (Success 200) {String} base32 The base32 representation of the secret.
* @apiSuccess (Success 200) {String} ascii The ASCII representation of the secret.
* @apiSuccess (Success 200) {String} qrcode The QRCode of the secret in URI format.
*
* @apiError (Error 403) {String} error No user provided in the session or
* unexpected identity validation challenge in the session.
* @apiError (Error 500) {String} error Internal error message
*
* @apiDescription Generate a new TOTP secret and returns it.
*/
app.post ('/new-totp-secret', routes.totp_register.post);
/**
* @api {get} /verify Verify user authentication
* @apiName VerifyAuthentication
* @apiGroup Verification
* @apiVersion 1.0.0
* @apiUse UserSession
*
* @apiSuccess (Success 204) status The user is authenticated.
* @apiError (Error 401) status The user is not authenticated.
*
* @apiDescription Verify that the user is authenticated, i.e., the two
* factors have been validated
*/
app.get ('/verify', routes.verify);
/**
* @api {post} /1stfactor LDAP authentication
* @apiName ValidateFirstFactor
* @apiGroup Authentication
* @apiVersion 1.0.0
* @apiUse UserSession
* @apiUse InternalError
*
* @apiParam {String} username User username.
* @apiParam {String} password User password.
*
* @apiSuccess (Success 204) status 1st factor is validated.
* @apiError (Error 401) {none} error 1st factor is not validated.
* @apiError (Error 403) {none} error Access has been restricted after too
* many authentication attempts
*
* @apiDescription Verify credentials against the LDAP.
*/
app.post ('/1stfactor', routes.first_factor);
/**
* @api {post} /2ndfactor/totp TOTP authentication
* @apiName ValidateTOTPSecondFactor
* @apiGroup Authentication
* @apiVersion 1.0.0
* @apiUse UserSession
* @apiUse InternalError
*
* @apiParam {String} token TOTP token.
*
* @apiSuccess (Success 204) status TOTP token is valid.
* @apiError (Error 401) {none} error TOTP token is invalid.
*
* @apiDescription Verify TOTP token. The user is authenticated upon success.
*/
app.post ('/2ndfactor/totp', routes.second_factor.totp);
/**
* @api {get} /2ndfactor/u2f/sign_request U2F Start authentication
* @apiName StartU2FAuthentication
* @apiGroup Authentication
* @apiVersion 1.0.0
* @apiUse UserSession
* @apiUse InternalError
*
* @apiSuccess (Success 200) authentication_request The U2F authentication request.
* @apiError (Error 401) {none} error There is no key registered for user in session.
*
* @apiDescription Initiate an authentication request using a U2F device.
*/
app.get ('/2ndfactor/u2f/sign_request', routes.second_factor.u2f.sign_request);
/**
* @api {post} /2ndfactor/u2f/sign U2F Complete authentication
* @apiName CompleteU2FAuthentication
* @apiGroup Authentication
* @apiVersion 1.0.0
* @apiUse UserSession
* @apiUse InternalError
*
* @apiSuccess (Success 204) status The U2F authentication succeeded.
* @apiError (Error 403) {none} error No authentication request has been provided.
*
* @apiDescription Complete authentication request of the U2F device.
*/
app.post ('/2ndfactor/u2f/sign', routes.second_factor.u2f.sign);
/**
* @api {get} /2ndfactor/u2f/register_request U2F Start device registration
* @apiName StartU2FRegistration
* @apiGroup Registration
* @apiVersion 1.0.0
* @apiUse UserSession
* @apiUse InternalError
*
* @apiSuccess (Success 200) authentication_request The U2F registration request.
* @apiError (Error 403) {none} error Unexpected identity validation challenge.
*
* @apiDescription Initiate a U2F device registration request.
*/
app.get ('/2ndfactor/u2f/register_request', routes.second_factor.u2f.register_request);
/**
* @api {post} /2ndfactor/u2f/register U2F Complete device registration
* @apiName CompleteU2FRegistration
* @apiGroup Registration
* @apiVersion 1.0.0
* @apiUse UserSession
* @apiUse InternalError
*
* @apiSuccess (Success 204) status The U2F registration succeeded.
* @apiError (Error 403) {none} error Unexpected identity validation challenge.
* @apiError (Error 403) {none} error No registration request has been provided.
*
* @apiDescription Complete U2F registration request.
*/
app.post ('/2ndfactor/u2f/register', routes.second_factor.u2f.register);
}

View File

@ -1,22 +0,0 @@
module.exports = {
'validate': validate
}
var Promise = require('bluebird');
function validate(totp_engine, token, totp_secret) {
return new Promise(function(resolve, reject) {
var real_token = totp_engine.totp({
secret: totp_secret,
encoding: 'base32'
});
if(token == real_token) {
resolve();
}
else {
reject('Wrong challenge');
}
});
}

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

View File

@ -1,35 +0,0 @@
module.exports = {
'promisify': promisify,
'resolve': resolve,
'reject': reject
}
var Q = require('q');
function promisify(fn, context) {
return function() {
var defer = Q.defer();
var args = Array.prototype.slice.call(arguments);
args.push(function(err, val) {
if (err !== null && err !== undefined) {
return defer.reject(err);
}
return defer.resolve(val);
});
fn.apply(context || {}, args);
return defer.promise;
};
}
function resolve(data) {
var defer = Q.defer();
defer.resolve(data);
return defer.promise;
}
function reject(err) {
var defer = Q.defer();
defer.reject(err);
return defer.promise;
}

View File

@ -0,0 +1,23 @@
import winston = require("winston");
import speakeasy = require("speakeasy");
import nodemailer = require("nodemailer");
import session = require("express-session");
import nedb = require("nedb");
import ldapjs = require("ldapjs");
export type Nodemailer = typeof nodemailer;
export type Speakeasy = typeof speakeasy;
export type Winston = typeof winston;
export type Session = typeof session;
export type Nedb = typeof nedb;
export type Ldapjs = typeof ldapjs;
export interface GlobalDependencies {
u2f: object;
nodemailer: Nodemailer;
ldapjs: Ldapjs;
session: Session;
winston: Winston;
speakeasy: Speakeasy;
nedb: Nedb;
}

View File

@ -0,0 +1,7 @@
import * as winston from "winston";
export interface ILogger {
debug: winston.LeveledLogMethod;
}

View File

@ -0,0 +1,6 @@
export interface Identity {
userid: string;
email: string;
}

View File

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

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

@ -0,0 +1,69 @@
import BluebirdPromise = require("bluebird");
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): BluebirdPromise<RegistrationRequest>;
export function finishRegistration(registrationRequest: RegistrationRequest, registrationResponse: RegistrationResponse): BluebirdPromise<Registration>;
export function startAuthentication(appId: AppId, registeredKeys: RegisteredKeys, options: Options): BluebirdPromise<AuthenticationRequest>;
export function finishAuthentication(challenge: string, deviceResponse: AuthenticationResponse, registeredKeys: RegisteredKeys): BluebirdPromise<Authentication>;
}

4
src/types/dovehash.d.ts vendored 100644
View File

@ -0,0 +1,4 @@
declare module "dovehash" {
function encode(algo: string, text: string): string;
}

11
src/types/ldapjs-async.d.ts vendored 100644
View File

@ -0,0 +1,11 @@
import ldapjs = require("ldapjs");
import * as BluebirdPromise from "bluebird";
import { EventEmitter } from "events";
declare module "ldapjs" {
export interface ClientAsync {
bindAsync(username: string, password: string): BluebirdPromise<void>;
searchAsync(base: string, query: ldapjs.SearchOptions): BluebirdPromise<EventEmitter>;
modifyAsync(userdn: string, change: ldapjs.Change): BluebirdPromise<void>;
}
}

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

@ -0,0 +1,12 @@
import Nedb = require("nedb");
import BluebirdPromise = require("bluebird");
declare module "nedb" {
export class NedbAsync extends Nedb {
constructor(pathOrOptions?: string | Nedb.DataStoreOptions);
updateAsync(query: any, updateQuery: any, options?: Nedb.UpdateOptions): BluebirdPromise<any>;
findOneAsync(query: any): BluebirdPromise<any>;
insertAsync<T>(newDoc: T): BluebirdPromise<any>;
removeAsync(query: any): BluebirdPromise<any>;
}
}

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

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

View File

@ -0,0 +1,73 @@
import AuthenticationRegulator from "../../src/lib/AuthenticationRegulator";
import UserDataStore from "../../src/lib/UserDataStore";
import MockDate = require("mockdate");
import exceptions = require("../../src/lib/Exceptions");
import nedb = require("nedb");
describe("test authentication regulator", function() {
it("should mark 2 authentication and regulate (resolve)", function() {
const options = {
inMemoryOnly: true
};
const data_store = new UserDataStore(options, nedb);
const regulator = new AuthenticationRegulator(data_store, 10);
const user = "user";
return regulator.mark(user, false)
.then(function() {
return regulator.mark(user, true);
})
.then(function() {
return regulator.regulate(user);
});
});
it("should mark 3 authentications and regulate (reject)", function(done) {
const options = {
inMemoryOnly: true
};
const data_store = new UserDataStore(options, nedb);
const regulator = new AuthenticationRegulator(data_store, 10);
const user = "user";
regulator.mark(user, false)
.then(function() {
return regulator.mark(user, false);
})
.then(function() {
return regulator.mark(user, false);
})
.then(function() {
return regulator.regulate(user);
})
.catch(exceptions.AuthenticationRegulationError, function() {
done();
});
});
it("should mark 3 authentications and regulate (resolve)", function(done) {
const options = {
inMemoryOnly: true
};
const data_store = new UserDataStore(options, nedb);
const regulator = new AuthenticationRegulator(data_store, 10);
const user = "user";
MockDate.set("1/2/2000 00:00:00");
regulator.mark(user, false)
.then(function() {
MockDate.set("1/2/2000 00:00:15");
return regulator.mark(user, false);
})
.then(function() {
return regulator.mark(user, false);
})
.then(function() {
return regulator.regulate(user);
})
.then(function() {
done();
});
});
});

View File

@ -0,0 +1,213 @@
import sinon = require("sinon");
import IdentityValidator = require("../../src/lib/IdentityValidator");
import exceptions = require("../../src/lib/Exceptions");
import assert = require("assert");
import winston = require("winston");
import Promise = require("bluebird");
import express = require("express");
import BluebirdPromise = require("bluebird");
import ExpressMock = require("./mocks/express");
import UserDataStoreMock = require("./mocks/UserDataStore");
import NotifierMock = require("./mocks/Notifier");
import IdentityValidatorMock = require("./mocks/IdentityValidator");
describe("test identity check process", function() {
let req: ExpressMock.RequestMock;
let res: ExpressMock.ResponseMock;
let userDataStore: UserDataStoreMock.UserDataStore;
let notifier: NotifierMock.NotifierMock;
let app: express.Application;
let app_get: sinon.SinonStub;
let app_post: sinon.SinonStub;
let identityValidable: IdentityValidatorMock.IdentityValidableMock;
beforeEach(function() {
req = ExpressMock.RequestMock();
res = ExpressMock.ResponseMock();
userDataStore = UserDataStoreMock.UserDataStore();
userDataStore.issue_identity_check_token = sinon.stub();
userDataStore.issue_identity_check_token.returns(Promise.resolve());
userDataStore.consume_identity_check_token = sinon.stub();
userDataStore.consume_identity_check_token.returns(Promise.resolve({ userid: "user" }));
notifier = NotifierMock.NotifierMock();
notifier.notify = sinon.stub().returns(Promise.resolve());
req.headers = {};
req.session = {};
req.session.auth_session = {};
req.query = {};
req.app = {};
req.app.get = sinon.stub();
req.app.get.withArgs("logger").returns(winston);
req.app.get.withArgs("user data store").returns(userDataStore);
req.app.get.withArgs("notifier").returns(notifier);
app = express();
app_get = sinon.stub(app, "get");
app_post = sinon.stub(app, "post");
identityValidable = IdentityValidatorMock.IdentityValidableMock();
});
afterEach(function() {
app_get.restore();
app_post.restore();
});
it("should register a POST and GET endpoint", function() {
const endpoint = "/test";
const icheck_interface = {};
IdentityValidator.IdentityValidator.setup(app, endpoint, identityValidable, userDataStore as any, winston);
assert(app_get.calledOnce);
assert(app_get.calledWith(endpoint));
assert(app_post.calledOnce);
assert(app_post.calledWith(endpoint));
});
describe("test POST", test_post_handler);
describe("test GET", test_get_handler);
function test_post_handler() {
it("should send 403 if pre check rejects", function(done) {
const endpoint = "/protected";
identityValidable.preValidation.returns(Promise.reject("No access"));
IdentityValidator.IdentityValidator.setup(app, endpoint, identityValidable, userDataStore as any, winston);
res.send = sinon.spy(function() {
assert.equal(res.status.getCall(0).args[0], 403);
done();
});
const handler = app_post.getCall(0).args[1];
handler(req, res);
});
it("should send 400 if email is missing in provided identity", function(done) {
const endpoint = "/protected";
const identity = { userid: "abc" };
identityValidable.preValidation.returns(Promise.resolve(identity));
IdentityValidator.IdentityValidator.setup(app, endpoint, identityValidable, userDataStore as any, winston);
res.send = sinon.spy(function() {
assert.equal(res.status.getCall(0).args[0], 400);
done();
});
const handler = app_post.getCall(0).args[1];
handler(req, res);
});
it("should send 400 if userid is missing in provided identity", function(done) {
const endpoint = "/protected";
const identity = { email: "abc@example.com" };
identityValidable.preValidation.returns(Promise.resolve(identity));
IdentityValidator.IdentityValidator.setup(app, endpoint, identityValidable, userDataStore as any, winston);
res.send = sinon.spy(function() {
assert.equal(res.status.getCall(0).args[0], 400);
done();
});
const handler = app_post.getCall(0).args[1];
handler(req, res);
});
it("should issue a token, send an email and return 204", function(done) {
const endpoint = "/protected";
const identity = { userid: "user", email: "abc@example.com" };
req.headers.host = "localhost";
req.headers["x-original-uri"] = "/auth/test";
identityValidable.preValidation.returns(Promise.resolve(identity));
IdentityValidator.IdentityValidator.setup(app, endpoint, identityValidable, userDataStore as any, winston);
res.send = sinon.spy(function() {
assert.equal(res.status.getCall(0).args[0], 204);
assert(notifier.notify.calledOnce);
assert(userDataStore.issue_identity_check_token.calledOnce);
assert.equal(userDataStore.issue_identity_check_token.getCall(0).args[0], "user");
assert.equal(userDataStore.issue_identity_check_token.getCall(0).args[3], 240000);
done();
});
const handler = app_post.getCall(0).args[1];
handler(req, res);
});
}
function test_get_handler() {
it("should send 403 if no identity_token is provided", function(done) {
const endpoint = "/protected";
IdentityValidator.IdentityValidator.setup(app, endpoint, identityValidable, userDataStore as any, winston);
res.send = sinon.spy(function() {
assert.equal(res.status.getCall(0).args[0], 403);
done();
});
const handler = app_get.getCall(0).args[1];
handler(req, res);
});
it("should render template if identity_token is provided and still valid", function(done) {
req.query.identity_token = "token";
const endpoint = "/protected";
identityValidable.templateName.returns("template");
IdentityValidator.IdentityValidator.setup(app, endpoint, identityValidable, userDataStore as any, winston);
res.render = sinon.spy(function(template: string) {
assert.equal(template, "template");
done();
});
const handler = app_get.getCall(0).args[1];
handler(req, res);
});
it("should return 403 if identity_token is provided but invalid", function(done) {
req.query.identity_token = "token";
const endpoint = "/protected";
identityValidable.templateName.returns("template");
userDataStore.consume_identity_check_token
.returns(Promise.reject("Invalid token"));
IdentityValidator.IdentityValidator.setup(app, endpoint, identityValidable, userDataStore as any, winston);
res.send = sinon.spy(function(template: string) {
assert.equal(res.status.getCall(0).args[0], 403);
done();
});
const handler = app_get.getCall(0).args[1];
handler(req, res);
});
it("should set the identity_check session object even if session does not exist yet", function(done) {
req.query.identity_token = "token";
const endpoint = "/protected";
req.session = {};
identityValidable.templateName.returns("template");
IdentityValidator.IdentityValidator.setup(app, endpoint, identityValidable, userDataStore as any, winston);
res.render = sinon.spy(function(template: string) {
assert.equal(req.session.auth_session.identity_check.userid, "user");
assert.equal(template, "template");
done();
});
const handler = app_get.getCall(0).args[1];
handler(req, res);
});
}
});

View File

@ -0,0 +1,243 @@
import LdapClient = require("../../src/lib/LdapClient");
import { LdapConfiguration } from "../../src/lib/Configuration";
import sinon = require("sinon");
import BluebirdPromise = require("bluebird");
import assert = require("assert");
import ldapjs = require("ldapjs");
import winston = require("winston");
import { EventEmitter } from "events";
import { LdapjsMock, LdapjsClientMock } from "./mocks/ldapjs";
describe("test ldap validation", function () {
let ldap: LdapClient.LdapClient;
let ldap_client: LdapjsClientMock;
let ldapjs: LdapjsMock;
let ldap_config: LdapConfiguration;
beforeEach(function () {
ldap_client = {
bind: sinon.stub(),
search: sinon.stub(),
modify: sinon.stub(),
on: sinon.stub()
} as any;
ldapjs = LdapjsMock();
ldapjs.createClient.returns(ldap_client);
ldap_config = {
url: "http://localhost:324",
user: "admin",
password: "password",
base_dn: "dc=example,dc=com",
additional_user_dn: "ou=users"
};
ldap = new LdapClient.LdapClient(ldap_config, ldapjs, winston);
return ldap.connect();
});
describe("test binding", test_binding);
describe("test get emails from username", test_get_emails);
describe("test get groups from username", test_get_groups);
describe("test update password", test_update_password);
function test_binding() {
function test_bind() {
const username = "username";
const password = "password";
return ldap.bind(username, password);
}
it("should bind the user if good credentials provided", function () {
ldap_client.bind.yields();
return test_bind();
});
it("should bind the user with correct DN", function () {
ldap_config.user_name_attribute = "uid";
const username = "user";
const password = "password";
ldap_client.bind.withArgs("uid=user,ou=users,dc=example,dc=com").yields();
return ldap.bind(username, password);
});
it("should default to cn user search filter if no filter provided", function () {
const username = "user";
const password = "password";
ldap_client.bind.withArgs("cn=user,ou=users,dc=example,dc=com").yields();
return ldap.bind(username, password);
});
it("should not bind the user if wrong credentials provided", function () {
ldap_client.bind.yields("wrong credentials");
const promise = test_bind();
return promise.catch(function () {
return Promise.resolve();
});
});
}
function test_get_emails() {
let res_emitter: any;
let expected_doc: any;
beforeEach(function () {
expected_doc = {
object: {
mail: "user@example.com"
}
};
res_emitter = {
on: sinon.spy(function (event: string, fn: (doc: any) => void) {
if (event != "error") fn(expected_doc);
})
};
});
it("should retrieve the email of an existing user", function () {
ldap_client.search.yields(undefined, res_emitter);
return ldap.get_emails("user")
.then(function (emails) {
assert.deepEqual(emails, [expected_doc.object.mail]);
return Promise.resolve();
});
});
it("should retrieve email for user with uid name attribute", function () {
ldap_config.user_name_attribute = "uid";
ldap_client.search.withArgs("uid=username,ou=users,dc=example,dc=com").yields(undefined, res_emitter);
return ldap.get_emails("username")
.then(function (emails) {
assert.deepEqual(emails, ["user@example.com"]);
return Promise.resolve();
});
});
it("should fail on error with search method", function () {
const expected_doc = {
mail: ["user@example.com"]
};
ldap_client.search.yields("Error while searching mails");
return ldap.get_emails("user")
.catch(function () {
return Promise.resolve();
});
});
}
function test_get_groups() {
let res_emitter: any;
let expected_doc1: any, expected_doc2: any;
beforeEach(function () {
expected_doc1 = {
object: {
cn: "group1"
}
};
expected_doc2 = {
object: {
cn: "group2"
}
};
res_emitter = {
on: sinon.spy(function (event: string, fn: (doc: any) => void) {
if (event != "error") fn(expected_doc1);
if (event != "error") fn(expected_doc2);
})
};
});
it("should retrieve the groups of an existing user", function () {
ldap_client.search.yields(undefined, res_emitter);
return ldap.get_groups("user")
.then(function (groups) {
assert.deepEqual(groups, ["group1", "group2"]);
return Promise.resolve();
});
});
it("should reduce the scope to additional_group_dn", function (done) {
ldap_config.additional_group_dn = "ou=groups";
ldap_client.search.yields(undefined, res_emitter);
ldap.get_groups("user")
.then(function() {
assert.equal(ldap_client.search.getCall(0).args[0], "ou=groups,dc=example,dc=com");
done();
});
});
it("should use default group_name_attr if not provided", function (done) {
ldap_client.search.yields(undefined, res_emitter);
ldap.get_groups("user")
.then(function() {
assert.equal(ldap_client.search.getCall(0).args[0], "dc=example,dc=com");
assert.equal(ldap_client.search.getCall(0).args[1].filter, "member=cn=user,ou=users,dc=example,dc=com");
assert.deepEqual(ldap_client.search.getCall(0).args[1].attributes, ["cn"]);
done();
});
});
it("should fail on error with search method", function () {
ldap_client.search.yields("error");
return ldap.get_groups("user")
.catch(function () {
return Promise.resolve();
});
});
}
function test_update_password() {
it("should update the password successfully", function () {
const change = {
operation: "replace",
modification: {
userPassword: "new-password"
}
};
const userdn = "cn=user,ou=users,dc=example,dc=com";
ldap_client.bind.yields(undefined);
ldap_client.modify.yields(undefined);
return ldap.update_password("user", "new-password")
.then(function () {
assert.deepEqual(ldap_client.modify.getCall(0).args[0], userdn);
assert.deepEqual(ldap_client.modify.getCall(0).args[1].operation, change.operation);
const userPassword = ldap_client.modify.getCall(0).args[1].modification.userPassword;
assert(/{SSHA}/.test(userPassword));
return Promise.resolve();
});
});
it("should fail when ldap throws an error", function () {
ldap_client.bind.yields(undefined);
ldap_client.modify.yields("Error");
return ldap.update_password("user", "new-password")
.catch(function () {
return Promise.resolve();
});
});
it("should update password of user using particular user name attribute", function () {
ldap_config.user_name_attribute = "uid";
ldap_client.bind.yields(undefined);
ldap_client.modify.withArgs("uid=username,ou=users,dc=example,dc=com").yields();
return ldap.update_password("username", "newpass");
});
}
});

View File

@ -0,0 +1,392 @@
import Server from "../../src/lib/Server";
import LdapClient = require("../../src/lib/LdapClient");
import Promise = require("bluebird");
import speakeasy = require("speakeasy");
import request = require("request");
import nedb = require("nedb");
import { TOTPSecret } from "../../src/types/TOTPSecret";
const requestp = Promise.promisifyAll(request) as request.RequestAsync;
const assert = require("assert");
const sinon = require("sinon");
const MockDate = require("mockdate");
const session = require("express-session");
const winston = require("winston");
const ldapjs = require("ldapjs");
const PORT = 8090;
const BASE_URL = "http://localhost:" + PORT;
const requests = require("./requests")(PORT);
describe("test the server", function () {
let server: Server;
let transporter: object;
let u2f: any;
beforeEach(function () {
const config = {
port: PORT,
ldap: {
url: "ldap://127.0.0.1:389",
base_dn: "ou=users,dc=example,dc=com",
user_name_attribute: "cn",
user: "cn=admin,dc=example,dc=com",
password: "password",
},
session: {
secret: "session_secret",
expiration: 50000,
},
store_in_memory: true,
notifier: {
gmail: {
username: "user@example.com",
password: "password"
}
}
};
const ldap_client = {
bind: sinon.stub(),
search: sinon.stub(),
modify: sinon.stub(),
on: sinon.spy()
};
const ldap = {
Change: sinon.spy(),
createClient: sinon.spy(function () {
return ldap_client;
})
};
u2f = {
startRegistration: sinon.stub(),
finishRegistration: sinon.stub(),
startAuthentication: sinon.stub(),
finishAuthentication: sinon.stub()
};
transporter = {
sendMail: sinon.stub().yields()
};
const nodemailer = {
createTransport: sinon.spy(function () {
return transporter;
})
};
const ldap_document = {
object: {
mail: "test_ok@example.com",
}
};
const search_res = {
on: sinon.spy(function (event: string, fn: (s: any) => void) {
if (event != "error") fn(ldap_document);
})
};
ldap_client.bind.withArgs("cn=test_ok,ou=users,dc=example,dc=com",
"password").yields(undefined);
ldap_client.bind.withArgs("cn=admin,dc=example,dc=com",
"password").yields(undefined);
ldap_client.bind.withArgs("cn=test_nok,ou=users,dc=example,dc=com",
"password").yields("error");
ldap_client.modify.yields(undefined);
ldap_client.search.yields(undefined, search_res);
const deps = {
u2f: u2f,
nedb: nedb,
nodemailer: nodemailer,
ldapjs: ldap,
session: session,
winston: winston,
speakeasy: speakeasy
};
server = new Server();
return server.start(config, deps);
});
afterEach(function () {
server.stop();
});
describe("test GET /login", function () {
test_login();
});
describe("test GET /logout", function () {
test_logout();
});
describe("test GET /reset-password-form", function () {
test_reset_password_form();
});
describe("test endpoints locks", function () {
function should_post_and_reply_with(url: string, status_code: number) {
return requestp.postAsync(url).then(function (response: request.RequestResponse) {
assert.equal(response.statusCode, status_code);
return Promise.resolve();
});
}
function should_get_and_reply_with(url: string, status_code: number) {
return requestp.getAsync(url).then(function (response: request.RequestResponse) {
assert.equal(response.statusCode, status_code);
return Promise.resolve();
});
}
function should_post_and_reply_with_403(url: string) {
return should_post_and_reply_with(url, 403);
}
function should_get_and_reply_with_403(url: string) {
return should_get_and_reply_with(url, 403);
}
function should_post_and_reply_with_401(url: string) {
return should_post_and_reply_with(url, 401);
}
function should_get_and_reply_with_401(url: string) {
return should_get_and_reply_with(url, 401);
}
function should_get_and_post_reply_with_403(url: string) {
const p1 = should_post_and_reply_with_403(url);
const p2 = should_get_and_reply_with_403(url);
return Promise.all([p1, p2]);
}
it("should block /new-password", function () {
return should_post_and_reply_with_403(BASE_URL + "/new-password");
});
it("should block /u2f-register", function () {
return should_get_and_post_reply_with_403(BASE_URL + "/u2f-register");
});
it("should block /reset-password", function () {
return should_get_and_post_reply_with_403(BASE_URL + "/reset-password");
});
it("should block /2ndfactor/u2f/register_request", function () {
return should_get_and_reply_with_403(BASE_URL + "/2ndfactor/u2f/register_request");
});
it("should block /2ndfactor/u2f/register", function () {
return should_post_and_reply_with_403(BASE_URL + "/2ndfactor/u2f/register");
});
it("should block /2ndfactor/u2f/sign_request", function () {
return should_get_and_reply_with_403(BASE_URL + "/2ndfactor/u2f/sign_request");
});
it("should block /2ndfactor/u2f/sign", function () {
return should_post_and_reply_with_403(BASE_URL + "/2ndfactor/u2f/sign");
});
});
describe("test authentication and verification", function () {
test_authentication();
test_reset_password();
test_regulation();
});
function test_reset_password_form() {
it("should serve the reset password form page", function (done) {
requestp.getAsync(BASE_URL + "/reset-password-form")
.then(function (response: request.RequestResponse) {
assert.equal(response.statusCode, 200);
done();
});
});
}
function test_login() {
it("should serve the login page", function (done) {
requestp.getAsync(BASE_URL + "/login")
.then(function (response: request.RequestResponse) {
assert.equal(response.statusCode, 200);
done();
});
});
}
function test_logout() {
it("should logout and redirect to /", function (done) {
requestp.getAsync(BASE_URL + "/logout")
.then(function (response: any) {
assert.equal(response.req.path, "/");
done();
});
});
}
function test_authentication() {
it("should return status code 401 when user is not authenticated", function () {
return requestp.getAsync({ url: BASE_URL + "/verify" })
.then(function (response: request.RequestResponse) {
assert.equal(response.statusCode, 401);
return Promise.resolve();
});
});
it("should return status code 204 when user is authenticated using totp", function () {
const j = requestp.jar();
return requests.login(j)
.then(function (res: request.RequestResponse) {
assert.equal(res.statusCode, 200, "get login page failed");
return requests.first_factor(j);
})
.then(function (res: request.RequestResponse) {
assert.equal(res.statusCode, 204, "first factor failed");
return requests.register_totp(j, transporter);
})
.then(function (secret: string) {
const sec = JSON.parse(secret) as TOTPSecret;
const real_token = speakeasy.totp({
secret: sec.base32,
encoding: "base32"
});
return requests.totp(j, real_token);
})
.then(function (res: request.RequestResponse) {
assert.equal(res.statusCode, 204, "second factor failed");
return requests.verify(j);
})
.then(function (res: request.RequestResponse) {
assert.equal(res.statusCode, 204, "verify failed");
return Promise.resolve();
});
});
it("should keep session variables when login page is reloaded", function () {
const real_token = speakeasy.totp({
secret: "totp_secret",
encoding: "base32"
});
const j = requestp.jar();
return requests.login(j)
.then(function (res: request.RequestResponse) {
assert.equal(res.statusCode, 200, "get login page failed");
return requests.first_factor(j);
})
.then(function (res: request.RequestResponse) {
assert.equal(res.statusCode, 204, "first factor failed");
return requests.totp(j, real_token);
})
.then(function (res: request.RequestResponse) {
assert.equal(res.statusCode, 204, "second factor failed");
return requests.login(j);
})
.then(function (res: request.RequestResponse) {
assert.equal(res.statusCode, 200, "login page loading failed");
return requests.verify(j);
})
.then(function (res: request.RequestResponse) {
assert.equal(res.statusCode, 204, "verify failed");
return Promise.resolve();
})
.catch(function (err: Error) {
console.error(err);
});
});
it("should return status code 204 when user is authenticated using u2f", function () {
const sign_request = {};
const sign_status = {};
const registration_request = {};
const registration_status = {};
u2f.startRegistration.returns(Promise.resolve(sign_request));
u2f.finishRegistration.returns(Promise.resolve(sign_status));
u2f.startAuthentication.returns(Promise.resolve(registration_request));
u2f.finishAuthentication.returns(Promise.resolve(registration_status));
const j = requestp.jar();
return requests.login(j)
.then(function (res: request.RequestResponse) {
assert.equal(res.statusCode, 200, "get login page failed");
return requests.first_factor(j);
})
.then(function (res: request.RequestResponse) {
assert.equal(res.statusCode, 204, "first factor failed");
return requests.u2f_registration(j, transporter);
})
.then(function (res: request.RequestResponse) {
assert.equal(res.statusCode, 204, "second factor, finish register failed");
return requests.u2f_authentication(j);
})
.then(function (res: request.RequestResponse) {
assert.equal(res.statusCode, 204, "second factor, finish sign failed");
return requests.verify(j);
})
.then(function (res: request.RequestResponse) {
assert.equal(res.statusCode, 204, "verify failed");
return Promise.resolve();
});
});
}
function test_reset_password() {
it("should reset the password", function () {
const j = requestp.jar();
return requests.login(j)
.then(function (res: request.RequestResponse) {
assert.equal(res.statusCode, 200, "get login page failed");
return requests.first_factor(j);
})
.then(function (res: request.RequestResponse) {
assert.equal(res.statusCode, 204, "first factor failed");
return requests.reset_password(j, transporter, "user", "new-password");
})
.then(function (res: request.RequestResponse) {
assert.equal(res.statusCode, 204, "second factor, finish register failed");
return Promise.resolve();
});
});
}
function test_regulation() {
it("should regulate authentication", function () {
const j = requestp.jar();
MockDate.set("1/2/2017 00:00:00");
return requests.login(j)
.then(function (res: request.RequestResponse) {
assert.equal(res.statusCode, 200, "get login page failed");
return requests.failing_first_factor(j);
})
.then(function (res: request.RequestResponse) {
assert.equal(res.statusCode, 401, "first factor failed");
return requests.failing_first_factor(j);
})
.then(function (res: request.RequestResponse) {
assert.equal(res.statusCode, 401, "first factor failed");
return requests.failing_first_factor(j);
})
.then(function (res: request.RequestResponse) {
assert.equal(res.statusCode, 401, "first factor failed");
return requests.failing_first_factor(j);
})
.then(function (res: request.RequestResponse) {
assert.equal(res.statusCode, 403, "first factor failed");
MockDate.set("1/2/2017 00:30:00");
return requests.failing_first_factor(j);
})
.then(function (res: request.RequestResponse) {
assert.equal(res.statusCode, 401, "first factor failed");
return Promise.resolve();
});
});
}
});

View File

@ -0,0 +1,30 @@
import TOTPValidator from "../../src/lib/TOTPValidator";
import sinon = require("sinon");
import Promise = require("bluebird");
import SpeakeasyMock = require("./mocks/speakeasy");
describe("test TOTP validation", function() {
let totpValidator: TOTPValidator;
beforeEach(() => {
SpeakeasyMock.totp.returns("token");
totpValidator = new TOTPValidator(SpeakeasyMock as any);
});
it("should validate the TOTP token", function() {
const totp_secret = "NBD2ZV64R9UV1O7K";
const token = "token";
return totpValidator.validate(token, totp_secret);
});
it("should not validate a wrong TOTP token", function(done) {
const totp_secret = "NBD2ZV64R9UV1O7K";
const token = "wrong token";
totpValidator.validate(token, totp_secret)
.catch(function() {
done();
});
});
});

View File

@ -0,0 +1,206 @@
import UserDataStore from "../../src/lib/UserDataStore";
import { U2FMetaDocument, Options } from "../../src/lib/UserDataStore";
import nedb = require("nedb");
import assert = require("assert");
import Promise = require("bluebird");
import sinon = require("sinon");
import MockDate = require("mockdate");
describe("test user data store", () => {
let options: Options;
beforeEach(function () {
options = {
inMemoryOnly: true
};
});
describe("test u2f meta", () => {
it("should save a u2f meta", function () {
const data_store = new UserDataStore(options, nedb);
const userid = "user";
const app_id = "https://localhost";
const meta = {
publicKey: "pbk"
};
return data_store.set_u2f_meta(userid, app_id, meta)
.then(function (numUpdated) {
assert.equal(1, numUpdated);
return Promise.resolve();
});
});
it("should retrieve no u2f meta", function () {
const options = {
inMemoryOnly: true
};
const data_store = new UserDataStore(options, nedb);
const userid = "user";
const app_id = "https://localhost";
const meta = {
publicKey: "pbk"
};
return data_store.get_u2f_meta(userid, app_id)
.then(function (doc) {
assert.equal(undefined, doc);
return Promise.resolve();
});
});
it("should insert and retrieve a u2f meta", function () {
const options = {
inMemoryOnly: true
};
const data_store = new UserDataStore(options, nedb);
const userid = "user";
const app_id = "https://localhost";
const meta = {
publicKey: "pbk"
};
return data_store.set_u2f_meta(userid, app_id, meta)
.then(function (numUpdated: number) {
assert.equal(1, numUpdated);
return data_store.get_u2f_meta(userid, app_id);
})
.then(function (doc: U2FMetaDocument) {
assert.deepEqual(meta, doc.meta);
assert.deepEqual(userid, doc.userid);
assert.deepEqual(app_id, doc.appid);
assert("_id" in doc);
return Promise.resolve();
});
});
});
describe("test u2f registration token", () => {
it("should save u2f registration token", function () {
const data_store = new UserDataStore(options, nedb);
const userid = "user";
const token = "token";
const max_age = 60;
const content = "abc";
return data_store.issue_identity_check_token(userid, token, content, max_age)
.then(function (document) {
assert.equal(document.userid, userid);
assert.equal(document.token, token);
assert.deepEqual(document.content, { userid: "user", data: content });
assert("max_date" in document);
assert("_id" in document);
return Promise.resolve();
})
.catch(function (err) {
console.error(err);
return Promise.reject(err);
});
});
it("should save u2f registration token and consume it", function (done) {
const data_store = new UserDataStore(options, nedb);
const userid = "user";
const token = "token";
const max_age = 50;
data_store.issue_identity_check_token(userid, token, {}, max_age)
.then(function (document) {
return data_store.consume_identity_check_token(token);
})
.then(function () {
done();
})
.catch(function (err) {
console.error(err);
});
});
it("should not be able to consume registration token twice", function (done) {
const data_store = new UserDataStore(options, nedb);
const userid = "user";
const token = "token";
const max_age = 50;
data_store.issue_identity_check_token(userid, token, {}, max_age)
.then(function (document) {
return data_store.consume_identity_check_token(token);
})
.then(function (document) {
return data_store.consume_identity_check_token(token);
})
.catch(function (err) {
console.error(err);
done();
});
});
it("should fail when token does not exist", function () {
const data_store = new UserDataStore(options, nedb);
const token = "token";
return data_store.consume_identity_check_token(token)
.then(function (document) {
return Promise.reject("Error while checking token");
})
.catch(function (err) {
return Promise.resolve(err);
});
});
it("should fail when token expired", function (done) {
const data_store = new UserDataStore(options, nedb);
const userid = "user";
const token = "token";
const max_age = 60;
MockDate.set("1/1/2000");
data_store.issue_identity_check_token(userid, token, {}, max_age)
.then(function () {
MockDate.set("1/2/2000");
return data_store.consume_identity_check_token(token);
})
.catch(function (err) {
MockDate.reset();
done();
});
});
it("should save the userid and some data with the token", function (done) {
const data_store = new UserDataStore(options, nedb);
const userid = "user";
const token = "token";
const max_age = 60;
MockDate.set("1/1/2000");
const data = "abc";
data_store.issue_identity_check_token(userid, token, data, max_age)
.then(function () {
return data_store.consume_identity_check_token(token);
})
.then(function (content) {
const expected_content = {
userid: "user",
data: "abc"
};
assert.deepEqual(content, expected_content);
done();
});
});
});
});

View File

@ -0,0 +1,53 @@
import assert = require("assert");
import winston = require("winston");
import AccessController from "../../../src/lib/access_control/AccessController";
import { ACLConfiguration } from "../../../src/lib/Configuration";
describe("test access control manager", function () {
let accessController: AccessController;
let configuration: ACLConfiguration;
beforeEach(function () {
configuration = {
default: [],
users: {},
groups: {}
};
accessController = new AccessController(configuration, winston);
});
describe("check access control matching", function () {
beforeEach(function () {
configuration.default = ["home.example.com", "*.public.example.com"];
configuration.users = {
user1: ["user1.example.com", "user1.mail.example.com"]
};
configuration.groups = {
group1: ["secret2.example.com"],
group2: ["secret.example.com", "secret1.example.com"]
};
});
it("should allow access to secret.example.com", function () {
assert(accessController.isDomainAllowedForUser("secret.example.com", "user", ["group1", "group2"]));
});
it("should deny access to secret3.example.com", function () {
assert(!accessController.isDomainAllowedForUser("secret3.example.com", "user", ["group1", "group2"]));
});
it("should allow access to home.example.com", function () {
assert(accessController.isDomainAllowedForUser("home.example.com", "user", ["group1", "group2"]));
});
it("should allow access to user1.example.com", function () {
assert(accessController.isDomainAllowedForUser("user1.example.com", "user1", ["group1", "group2"]));
});
it("should allow access *.public.example.com", function () {
assert(accessController.isDomainAllowedForUser("user.public.example.com", "nouser", []));
assert(accessController.isDomainAllowedForUser("test.public.example.com", "nouser", []));
});
});
});

View File

@ -0,0 +1,120 @@
import assert = require("assert");
import winston = require("winston");
import PatternBuilder from "../../../src/lib/access_control/PatternBuilder";
import { ACLConfiguration } from "../../../src/lib/Configuration";
describe("test access control manager", function () {
describe("test access control pattern builder when no configuration is provided", () => {
it("should allow access to the user", () => {
const patternBuilder = new PatternBuilder(undefined, winston);
const allowed_domains = patternBuilder.getAllowedDomains("user", ["group1"]);
assert.deepEqual(allowed_domains, ["*"]);
});
});
describe("test access control pattern builder", function () {
let patternBuilder: PatternBuilder;
let configuration: ACLConfiguration;
beforeEach(() => {
configuration = {
default: [],
users: {},
groups: {}
};
patternBuilder = new PatternBuilder(configuration, winston);
});
it("should deny all if nothing is defined in the config", function () {
const allowed_domains = patternBuilder.getAllowedDomains("user", ["group1", "group2"]);
assert.deepEqual(allowed_domains, []);
});
it("should allow domain test.example.com to all users if defined in" +
" default policy", function () {
configuration.default = ["test.example.com"];
const allowed_domains = patternBuilder.getAllowedDomains("user", ["group1", "group2"]);
assert.deepEqual(allowed_domains, ["test.example.com"]);
});
it("should allow domain test.example.com to all users in group mygroup", function () {
const allowed_domains0 = patternBuilder.getAllowedDomains("user", ["group1", "group1"]);
assert.deepEqual(allowed_domains0, []);
configuration.groups = {
mygroup: ["test.example.com"]
};
const allowed_domains1 = patternBuilder.getAllowedDomains("user", ["group1", "group2"]);
assert.deepEqual(allowed_domains1, []);
const allowed_domains2 = patternBuilder.getAllowedDomains("user", ["group1", "mygroup"]);
assert.deepEqual(allowed_domains2, ["test.example.com"]);
});
it("should allow domain test.example.com based on per user config", function () {
const allowed_domains0 = patternBuilder.getAllowedDomains("user", ["group1"]);
assert.deepEqual(allowed_domains0, []);
configuration.users = {
user1: ["test.example.com"]
};
const allowed_domains1 = patternBuilder.getAllowedDomains("user", ["group1", "mygroup"]);
assert.deepEqual(allowed_domains1, []);
const allowed_domains2 = patternBuilder.getAllowedDomains("user1", ["group1", "mygroup"]);
assert.deepEqual(allowed_domains2, ["test.example.com"]);
});
it("should allow domains from user and groups", function () {
configuration.groups = {
group2: ["secret.example.com", "secret1.example.com"]
};
configuration.users = {
user: ["test.example.com"]
};
const allowed_domains0 = patternBuilder.getAllowedDomains("user", ["group1", "group2"]);
assert.deepEqual(allowed_domains0, [
"secret.example.com",
"secret1.example.com",
"test.example.com",
]);
});
it("should allow domains from several groups", function () {
configuration.groups = {
group1: ["secret2.example.com"],
group2: ["secret.example.com", "secret1.example.com"]
};
const allowed_domains0 = patternBuilder.getAllowedDomains("user", ["group1", "group2"]);
assert.deepEqual(allowed_domains0, [
"secret2.example.com",
"secret.example.com",
"secret1.example.com",
]);
});
it("should allow domains from several groups and default policy", function () {
configuration.default = ["home.example.com"];
configuration.groups = {
group1: ["secret2.example.com"],
group2: ["secret.example.com", "secret1.example.com"]
};
const allowed_domains0 = patternBuilder.getAllowedDomains("user", ["group1", "group2"]);
assert.deepEqual(allowed_domains0, [
"home.example.com",
"secret2.example.com",
"secret.example.com",
"secret1.example.com",
]);
});
});
});

View File

@ -0,0 +1,117 @@
import * as Assert from "assert";
import { UserConfiguration } from "../../src/lib/Configuration";
import ConfigurationAdapter from "../../src/lib/ConfigurationAdapter";
describe("test config adapter", function() {
function build_yaml_config(): UserConfiguration {
const yaml_config = {
port: 8080,
ldap: {
url: "http://ldap",
base_dn: "cn=test,dc=example,dc=com",
user: "user",
password: "pass"
},
session: {
domain: "example.com",
secret: "secret",
max_age: 40000
},
store_directory: "/mydirectory",
logs_level: "debug",
notifier: {
gmail: {
username: "user",
password: "password"
}
}
};
return yaml_config;
}
it("should read the port from the yaml file", function() {
const yaml_config = build_yaml_config();
yaml_config.port = 7070;
const config = ConfigurationAdapter.adapt(yaml_config);
Assert.equal(config.port, 7070);
});
it("should default the port to 8080 if not provided", function() {
const yaml_config = build_yaml_config();
delete yaml_config.port;
const config = ConfigurationAdapter.adapt(yaml_config);
Assert.equal(config.port, 8080);
});
it("should get the ldap attributes", function() {
const yaml_config = build_yaml_config();
yaml_config.ldap = {
url: "http://ldap",
base_dn: "cn=test,dc=example,dc=com",
additional_user_dn: "ou=users",
user_name_attribute: "uid",
user: "admin",
password: "pass"
};
const config = ConfigurationAdapter.adapt(yaml_config);
Assert.equal(config.ldap.url, "http://ldap");
Assert.equal(config.ldap.additional_user_dn, "ou=users");
Assert.equal(config.ldap.user_name_attribute, "uid");
Assert.equal(config.ldap.user, "admin");
Assert.equal(config.ldap.password, "pass");
});
it("should get the session attributes", function() {
const yaml_config = build_yaml_config();
yaml_config.session = {
domain: "example.com",
secret: "secret",
expiration: 3600
};
const config = ConfigurationAdapter.adapt(yaml_config);
Assert.equal(config.session.domain, "example.com");
Assert.equal(config.session.secret, "secret");
Assert.equal(config.session.expiration, 3600);
});
it("should get the log level", function() {
const yaml_config = build_yaml_config();
yaml_config.logs_level = "debug";
const config = ConfigurationAdapter.adapt(yaml_config);
Assert.equal(config.logs_level, "debug");
});
it("should get the notifier config", function() {
const yaml_config = build_yaml_config();
yaml_config.notifier = {
gmail: {
username: "user",
password: "pass"
}
};
const config = ConfigurationAdapter.adapt(yaml_config);
Assert.deepEqual(config.notifier, {
gmail: {
username: "user",
password: "pass"
}
});
});
it("should get the access_control config", function() {
const yaml_config = build_yaml_config();
yaml_config.access_control = {
default: [],
users: {},
groups: {}
};
const config = ConfigurationAdapter.adapt(yaml_config);
Assert.deepEqual(config.access_control, {
default: [],
users: {},
groups: {}
});
});
});

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/types/Dependencies";
import * as tmp from "tmp";
const requestp = Promise.promisifyAll(request) as request.Request;
const assert = require("assert");
const speakeasy = require("speakeasy");
const sinon = require("sinon");
const nedb = require("nedb");
const session = require("express-session");
const winston = require("winston");
const PORT = 8050;
const requests = require("./requests")(PORT);
describe("test data persistence", function () {
let u2f: any;
let tmpDir: tmp.SynchrounousResult;
const ldap_client = {
bind: sinon.stub(),
search: sinon.stub(),
on: sinon.spy()
};
const ldap = {
createClient: sinon.spy(function () {
return ldap_client;
})
};
let config: UserConfiguration;
before(function () {
u2f = {
startRegistration: sinon.stub(),
finishRegistration: sinon.stub(),
startAuthentication: sinon.stub(),
finishAuthentication: sinon.stub()
};
const search_doc = {
object: {
mail: "test_ok@example.com"
}
};
const search_res = {
on: sinon.spy(function (event: string, fn: (s: object) => void) {
if (event != "error") fn(search_doc);
})
};
ldap_client.bind.withArgs("cn=test_ok,ou=users,dc=example,dc=com",
"password").yields(undefined);
ldap_client.bind.withArgs("cn=test_nok,ou=users,dc=example,dc=com",
"password").yields("error");
ldap_client.search.yields(undefined, search_res);
tmpDir = tmp.dirSync({ unsafeCleanup: true });
config = {
port: PORT,
ldap: {
url: "ldap://127.0.0.1:389",
base_dn: "ou=users,dc=example,dc=com",
user: "user",
password: "password"
},
session: {
secret: "session_secret",
expiration: 50000,
},
store_directory: tmpDir.name,
notifier: {
gmail: {
username: "user@example.com",
password: "password"
}
}
};
});
after(function () {
tmpDir.removeCallback();
});
it("should save a u2f meta and reload it after a restart of the server", function () {
let server: Server;
const sign_request = {};
const sign_status = {};
const registration_request = {};
const registration_status = {};
u2f.startRegistration.returns(Promise.resolve(sign_request));
u2f.finishRegistration.returns(Promise.resolve(sign_status));
u2f.startAuthentication.returns(Promise.resolve(registration_request));
u2f.finishAuthentication.returns(Promise.resolve(registration_status));
const nodemailer = {
createTransport: sinon.spy(function () {
return transporter;
})
};
const transporter = {
sendMail: sinon.stub().yields()
};
const deps = {
u2f: u2f,
nedb: nedb,
nodemailer: nodemailer,
session: session,
winston: winston,
ldapjs: ldap,
speakeasy: speakeasy
} as GlobalDependencies;
const j1 = request.jar();
const j2 = request.jar();
return start_server(config, deps)
.then(function (s) {
server = s;
return requests.login(j1);
})
.then(function (res) {
return requests.first_factor(j1);
})
.then(function () {
return requests.u2f_registration(j1, transporter);
})
.then(function () {
return requests.u2f_authentication(j1);
})
.then(function () {
return stop_server(server);
})
.then(function () {
return start_server(config, deps);
})
.then(function (s) {
server = s;
return requests.login(j2);
})
.then(function () {
return requests.first_factor(j2);
})
.then(function () {
return requests.u2f_authentication(j2);
})
.then(function (res) {
assert.equal(204, res.statusCode);
server.stop();
return Promise.resolve();
})
.catch(function (err) {
console.error(err);
return Promise.reject(err);
});
});
function start_server(config: UserConfiguration, deps: GlobalDependencies): Promise<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,12 @@
import sinon = require("sinon");
export interface AccessControllerMock {
isDomainAllowedForUser: sinon.SinonStub;
}
export function AccessControllerMock() {
return {
isDomainAllowedForUser: sinon.stub()
};
}

View File

@ -0,0 +1,15 @@
import sinon = require("sinon");
export interface AuthenticationRegulatorMock {
mark: sinon.SinonStub;
regulate: sinon.SinonStub;
}
export function AuthenticationRegulatorMock() {
return {
mark: sinon.stub(),
regulate: sinon.stub()
};
}

View File

@ -0,0 +1,35 @@
import sinon = require("sinon");
import { IdentityValidable } from "../../../src/lib/IdentityValidator";
import express = require("express");
import BluebirdPromise = require("bluebird");
import { Identity } from "../../../src/types/Identity";
export interface IdentityValidableMock {
challenge: sinon.SinonStub;
templateName: sinon.SinonStub;
preValidation: sinon.SinonStub;
mailSubject: sinon.SinonStub;
}
export function IdentityValidableMock() {
return {
challenge: sinon.stub(),
templateName: sinon.stub(),
preValidation: sinon.stub(),
mailSubject: sinon.stub()
};
}
export interface IdentityValidatorMock {
consume_token: sinon.SinonStub;
issue_token: sinon.SinonStub;
}
export function IdentityValidatorMock() {
return {
consume_token: sinon.stub(),
issue_token: sinon.stub()
};
}

View File

@ -0,0 +1,20 @@
import sinon = require("sinon");
export interface LdapClientMock {
bind: sinon.SinonStub;
get_emails: sinon.SinonStub;
get_groups: sinon.SinonStub;
search_in_ldap: sinon.SinonStub;
update_password: sinon.SinonStub;
}
export function LdapClientMock(): LdapClientMock {
return {
bind: sinon.stub(),
get_emails: sinon.stub(),
get_groups: sinon.stub(),
search_in_ldap: sinon.stub(),
update_password: sinon.stub()
};
}

View File

@ -0,0 +1,12 @@
import sinon = require("sinon");
export interface NotifierMock {
notify: sinon.SinonStub;
}
export function NotifierMock(): NotifierMock {
return {
notify: sinon.stub()
};
}

View File

@ -0,0 +1,12 @@
import sinon = require("sinon");
export interface TOTPValidatorMock {
validate: sinon.SinonStub;
}
export function TOTPValidatorMock(): TOTPValidatorMock {
return {
validate: sinon.stub()
};
}

View File

@ -0,0 +1,22 @@
import sinon = require("sinon");
export interface UserDataStore {
set_u2f_meta: sinon.SinonStub;
get_u2f_meta: sinon.SinonStub;
issue_identity_check_token: sinon.SinonStub;
consume_identity_check_token: sinon.SinonStub;
get_totp_secret: sinon.SinonStub;
set_totp_secret: sinon.SinonStub;
}
export function UserDataStore(): UserDataStore {
return {
set_u2f_meta: sinon.stub(),
get_u2f_meta: sinon.stub(),
issue_identity_check_token: sinon.stub(),
consume_identity_check_token: sinon.stub(),
get_totp_secret: sinon.stub(),
set_totp_secret: sinon.stub()
};
}

View File

@ -0,0 +1,19 @@
import sinon = require("sinon");
import authdog = require("authdog");
export interface AuthdogMock {
startRegistration: sinon.SinonStub;
finishRegistration: sinon.SinonStub;
startAuthentication: sinon.SinonStub;
finishAuthentication: sinon.SinonStub;
}
export function AuthdogMock(): AuthdogMock {
return {
startRegistration: sinon.stub(),
finishAuthentication: sinon.stub(),
startAuthentication: sinon.stub(),
finishRegistration: sinon.stub()
};
}

View File

@ -0,0 +1,97 @@
import sinon = require("sinon");
import express = require("express");
export interface RequestMock {
app?: any;
body?: any;
session?: any;
headers?: any;
get?: any;
query?: any;
}
export interface ResponseMock {
send: sinon.SinonStub | sinon.SinonSpy;
sendStatus: sinon.SinonStub;
sendFile: sinon.SinonStub;
sendfile: sinon.SinonStub;
status: sinon.SinonStub | sinon.SinonSpy;
json: sinon.SinonStub | sinon.SinonSpy;
links: sinon.SinonStub;
jsonp: sinon.SinonStub;
download: sinon.SinonStub;
contentType: sinon.SinonStub;
type: sinon.SinonStub;
format: sinon.SinonStub;
attachment: sinon.SinonStub;
set: sinon.SinonStub;
header: sinon.SinonStub;
headersSent: boolean;
get: sinon.SinonStub;
clearCookie: sinon.SinonStub;
cookie: sinon.SinonStub;
location: sinon.SinonStub;
redirect: sinon.SinonStub;
render: sinon.SinonStub | sinon.SinonSpy;
locals: sinon.SinonStub;
charset: string;
vary: sinon.SinonStub;
app: any;
write: sinon.SinonStub;
writeContinue: sinon.SinonStub;
writeHead: sinon.SinonStub;
statusCode: number;
statusMessage: string;
setHeader: sinon.SinonStub;
setTimeout: sinon.SinonStub;
sendDate: boolean;
getHeader: sinon.SinonStub;
}
export function RequestMock(): RequestMock {
return {
app: {
get: sinon.stub()
}
};
}
export function ResponseMock(): ResponseMock {
return {
send: sinon.stub(),
status: sinon.stub(),
json: sinon.stub(),
sendStatus: sinon.stub(),
links: sinon.stub(),
jsonp: sinon.stub(),
sendFile: sinon.stub(),
sendfile: sinon.stub(),
download: sinon.stub(),
contentType: sinon.stub(),
type: sinon.stub(),
format: sinon.stub(),
attachment: sinon.stub(),
set: sinon.stub(),
header: sinon.stub(),
headersSent: true,
get: sinon.stub(),
clearCookie: sinon.stub(),
cookie: sinon.stub(),
location: sinon.stub(),
redirect: sinon.stub(),
render: sinon.stub(),
locals: sinon.stub(),
charset: "utf-8",
vary: sinon.stub(),
app: sinon.stub(),
write: sinon.stub(),
writeContinue: sinon.stub(),
writeHead: sinon.stub(),
statusCode: 200,
statusMessage: "message",
setHeader: sinon.stub(),
setTimeout: sinon.stub(),
sendDate: true,
getHeader: sinon.stub()
};
}

View File

@ -0,0 +1,28 @@
import sinon = require("sinon");
export interface LdapjsMock {
createClient: sinon.SinonStub;
}
export interface LdapjsClientMock {
bind: sinon.SinonStub;
search: sinon.SinonStub;
modify: sinon.SinonStub;
on: sinon.SinonStub;
}
export function LdapjsMock(): LdapjsMock {
return {
createClient: sinon.stub()
};
}
export function LdapjsClientMock(): LdapjsClientMock {
return {
bind: sinon.stub(),
search: sinon.stub(),
modify: sinon.stub(),
on: sinon.stub()
};
}

View File

@ -0,0 +1,22 @@
import sinon = require("sinon");
export interface NodemailerMock {
createTransport: sinon.SinonStub;
}
export function NodemailerMock(): NodemailerMock {
return {
createTransport: sinon.stub()
};
}
export interface NodemailerTransporterMock {
sendMail: sinon.SinonStub;
}
export function NodemailerTransporterMock() {
return {
sendMail: sinon.stub()
};
}

View File

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

View File

@ -0,0 +1,42 @@
import * as sinon from "sinon";
import * as assert from "assert";
import { FileSystemNotifier } from "../../../src/lib/notifiers/FileSystemNotifier";
import * as tmp from "tmp";
import * as fs from "fs";
const NOTIFICATIONS_DIRECTORY = "notifications";
describe("test FS notifier", function() {
let tmpDir: tmp.SynchrounousResult;
before(function() {
tmpDir = tmp.dirSync({ unsafeCleanup: true });
});
after(function() {
tmpDir.removeCallback();
});
it("should write the notification in a file", function() {
const options = {
filename: tmpDir.name + "/" + NOTIFICATIONS_DIRECTORY
};
const sender = new FileSystemNotifier(options);
const subject = "subject";
const identity = {
userid: "user",
email: "user@example.com"
};
const url = "http://test.com";
return sender.notify(identity, subject, url)
.then(function() {
const content = fs.readFileSync(options.filename, "UTF-8");
assert(content.length > 0);
return Promise.resolve();
});
});
});

View File

@ -0,0 +1,40 @@
import * as sinon from "sinon";
import * as assert from "assert";
import NodemailerMock = require("../mocks/nodemailer");
import GMailNotifier = require("../../../src/lib/notifiers/GMailNotifier");
describe("test gmail notifier", function () {
it("should send an email", function () {
const transporter = {
sendMail: sinon.stub().yields()
};
const nodemailerMock = NodemailerMock.NodemailerMock();
nodemailerMock.createTransport.returns(transporter);
const options = {
username: "user_gmail",
password: "pass_gmail"
};
const sender = new GMailNotifier.GMailNotifier(options, nodemailerMock);
const subject = "subject";
const identity = {
userid: "user",
email: "user@example.com"
};
const url = "http://test.com";
return sender.notify(identity, subject, url)
.then(function () {
assert.equal(nodemailerMock.createTransport.getCall(0).args[0].auth.user, "user_gmail");
assert.equal(nodemailerMock.createTransport.getCall(0).args[0].auth.pass, "pass_gmail");
assert.equal(transporter.sendMail.getCall(0).args[0].to, "user@example.com");
assert.equal(transporter.sendMail.getCall(0).args[0].subject, "subject");
return Promise.resolve();
});
});
});

View File

@ -0,0 +1,36 @@
import * as sinon from "sinon";
import * as BluebirdPromise from "bluebird";
import * as assert from "assert";
import { NotifierFactory } from "../../../src/lib/notifiers/NotifierFactory";
import { GMailNotifier } from "../../../src/lib/notifiers/GMailNotifier";
import { FileSystemNotifier } from "../../../src/lib/notifiers/FileSystemNotifier";
import NodemailerMock = require("../mocks/nodemailer");
describe("test notifier factory", function() {
let nodemailerMock: NodemailerMock.NodemailerMock;
it("should build a Gmail Notifier", function() {
const options = {
gmail: {
username: "abc",
password: "password"
}
};
nodemailerMock = NodemailerMock.NodemailerMock();
nodemailerMock.createTransport.returns(sinon.spy());
assert(NotifierFactory.build(options, nodemailerMock) instanceof GMailNotifier);
});
it("should build a FS Notifier", function() {
const options = {
filesystem: {
filename: "abc"
}
};
assert(NotifierFactory.build(options, nodemailerMock) instanceof FileSystemNotifier);
});
});

View File

@ -1,37 +0,0 @@
var sinon = require('sinon');
var assert = require('assert');
var FSNotifier = require('../../../src/lib/notifiers/filesystem');
var tmp = require('tmp');
var fs = require('fs');
describe('test FS notifier', function() {
var tmpDir;
before(function() {
tmpDir = tmp.dirSync({ unsafeCleanup: true });
});
after(function() {
tmpDir.removeCallback();
});
it('should write the notification in a file', function() {
var options = {};
options.filename = tmpDir.name + '/notification';
var sender = new FSNotifier(options);
var subject = 'subject';
var identity = {};
identity.userid = 'user';
identity.email = 'user@example.com';
var url = 'http://test.com';
return sender.notify(identity, subject, url)
.then(function() {
var content = fs.readFileSync(options.filename, 'UTF-8');
assert(content.length > 0);
return Promise.resolve();
});
});
});

View File

@ -1,36 +0,0 @@
var sinon = require('sinon');
var assert = require('assert');
var GmailNotifier = require('../../../src/lib/notifiers/gmail');
describe('test gmail notifier', function() {
it('should send an email', function() {
var nodemailer = {};
var transporter = {};
nodemailer.createTransport = sinon.stub().returns(transporter);
transporter.sendMail = sinon.stub().yields();
var options = {};
options.username = 'user_gmail';
options.password = 'pass_gmail';
var deps = {};
deps.nodemailer = nodemailer;
var sender = new GmailNotifier(options, deps);
var subject = 'subject';
var identity = {};
identity.userid = 'user';
identity.email = 'user@example.com';
var url = 'http://test.com';
return sender.notify(identity, subject, url)
.then(function() {
assert.equal(nodemailer.createTransport.getCall(0).args[0].auth.user, 'user_gmail');
assert.equal(nodemailer.createTransport.getCall(0).args[0].auth.pass, 'pass_gmail');
assert.equal(transporter.sendMail.getCall(0).args[0].to, 'user@example.com');
assert.equal(transporter.sendMail.getCall(0).args[0].subject, 'subject');
return Promise.resolve();
});
});
});

View File

@ -1,35 +0,0 @@
var sinon = require('sinon');
var Promise = require('bluebird');
var assert = require('assert');
var Notifier = require('../../../src/lib/notifier');
var GmailNotifier = require('../../../src/lib/notifiers/gmail');
var FSNotifier = require('../../../src/lib/notifiers/filesystem');
describe('test notifier', function() {
it('should build a Gmail Notifier', function() {
var deps = {};
deps.nodemailer = {};
deps.nodemailer.createTransport = sinon.stub().returns({});
var options = {};
options.gmail = {};
options.gmail.user = 'abc';
options.gmail.pass = 'abcd';
var notifier = new Notifier(options, deps);
assert(notifier._notifier instanceof GmailNotifier);
});
it('should build a FS Notifier', function() {
var deps = {};
var options = {};
options.filesystem = {};
options.filesystem.filename = 'abc';
var notifier = new Notifier(options, deps);
assert(notifier._notifier instanceof FSNotifier);
});
});

Some files were not shown because too many files have changed in this diff Show More