mirror of
https://github.com/0rangebananaspy/authelia.git
synced 2024-09-14 22:47:21 +07:00
Merge pull request #33 from clems4ever/typescript
Move project to typescript and grunt
This commit is contained in:
commit
3890c5b24d
10
.gitignore
vendored
10
.gitignore
vendored
|
@ -1,8 +1,13 @@
|
||||||
|
|
||||||
|
# NodeJs modules
|
||||||
node_modules/
|
node_modules/
|
||||||
|
|
||||||
|
# Coverage reports
|
||||||
coverage/
|
coverage/
|
||||||
|
|
||||||
|
src/.baseDir.ts
|
||||||
|
.vscode/
|
||||||
|
|
||||||
*.swp
|
*.swp
|
||||||
|
|
||||||
*.sh
|
*.sh
|
||||||
|
@ -11,6 +16,11 @@ config.yml
|
||||||
|
|
||||||
npm-debug.log
|
npm-debug.log
|
||||||
|
|
||||||
|
# Directory used by example
|
||||||
notifications/
|
notifications/
|
||||||
|
|
||||||
|
# VSCode user configuration
|
||||||
.vscode/
|
.vscode/
|
||||||
|
|
||||||
|
# Generated by TypeScript compiler
|
||||||
|
dist/
|
||||||
|
|
|
@ -19,8 +19,9 @@ addons:
|
||||||
|
|
||||||
before_install: npm install -g npm@'>=2.13.5'
|
before_install: npm install -g npm@'>=2.13.5'
|
||||||
script:
|
script:
|
||||||
- npm test
|
- grunt test
|
||||||
- docker build -t clems4ever/authelia .
|
- grunt build
|
||||||
|
- grunt docker-build
|
||||||
- docker-compose build
|
- docker-compose build
|
||||||
- docker-compose up -d
|
- docker-compose up -d
|
||||||
- sleep 5
|
- sleep 5
|
||||||
|
|
|
@ -5,7 +5,7 @@ WORKDIR /usr/src
|
||||||
COPY package.json /usr/src/package.json
|
COPY package.json /usr/src/package.json
|
||||||
RUN npm install --production
|
RUN npm install --production
|
||||||
|
|
||||||
COPY src /usr/src
|
COPY dist/src /usr/src
|
||||||
|
|
||||||
ENV PORT=80
|
ENV PORT=80
|
||||||
EXPOSE 80
|
EXPOSE 80
|
||||||
|
|
55
Gruntfile.js
Normal file
55
Gruntfile.js
Normal 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']);
|
||||||
|
};
|
40
package.json
40
package.json
|
@ -7,11 +7,14 @@
|
||||||
"authelia": "src/index.js"
|
"authelia": "src/index.js"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "./node_modules/.bin/mocha --recursive test/unitary",
|
"test": "./node_modules/.bin/mocha --compilers ts:ts-node/register --recursive test/unitary",
|
||||||
"unit-test": "./node_modules/.bin/mocha --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",
|
"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": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
@ -43,12 +46,39 @@
|
||||||
"yamljs": "^0.2.8"
|
"yamljs": "^0.2.8"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"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",
|
"mocha": "^3.2.0",
|
||||||
"mockdate": "^2.0.1",
|
"mockdate": "^2.0.1",
|
||||||
|
"proxyquire": "^1.8.0",
|
||||||
"request": "^2.79.0",
|
"request": "^2.79.0",
|
||||||
"should": "^11.1.1",
|
"should": "^11.1.1",
|
||||||
"sinon": "^1.17.6",
|
"sinon": "^1.17.6",
|
||||||
"sinon-promise": "^0.1.3",
|
"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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
36
src/index.js
36
src/index.js
|
@ -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
Executable file
33
src/index.ts
Executable 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!");
|
||||||
|
});
|
43
src/lib/AuthenticationRegulator.ts
Normal file
43
src/lib/AuthenticationRegulator.ts
Normal 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();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
66
src/lib/Configuration.ts
Normal file
66
src/lib/Configuration.ts
Normal 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;
|
||||||
|
}
|
42
src/lib/ConfigurationAdapter.ts
Normal file
42
src/lib/ConfigurationAdapter.ts
Normal 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")
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
56
src/lib/Exceptions.ts
Normal file
56
src/lib/Exceptions.ts
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
155
src/lib/IdentityValidator.ts
Normal file
155
src/lib/IdentityValidator.ts
Normal 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();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
169
src/lib/LdapClient.ts
Normal file
169
src/lib/LdapClient.ts
Normal 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
Normal file
282
src/lib/RestApi.ts
Normal 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
Normal file
94
src/lib/Server.ts
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
16
src/lib/TOTPGenerator.ts
Normal file
16
src/lib/TOTPGenerator.ts
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
23
src/lib/TOTPValidator.ts
Normal file
23
src/lib/TOTPValidator.ts
Normal 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"));
|
||||||
|
}
|
||||||
|
}
|
182
src/lib/UserDataStore.ts
Normal file
182
src/lib/UserDataStore.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 ['*'];
|
|
||||||
}
|
|
35
src/lib/access_control/AccessController.ts
Normal file
35
src/lib/access_control/AccessController.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
61
src/lib/access_control/PatternBuilder.ts
Normal file
61
src/lib/access_control/PatternBuilder.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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();
|
|
||||||
});
|
|
||||||
}
|
|
|
@ -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')
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
|
@ -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);
|
|
|
@ -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();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
154
src/lib/ldap.js
154
src/lib/ldap.js
|
@ -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);
|
|
||||||
});
|
|
||||||
}
|
|
|
@ -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);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
25
src/lib/notifiers/FileSystemNotifier.ts
Normal file
25
src/lib/notifiers/FileSystemNotifier.ts
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
44
src/lib/notifiers/GMailNotifier.ts
Normal file
44
src/lib/notifiers/GMailNotifier.ts
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
7
src/lib/notifiers/INotifier.ts
Normal file
7
src/lib/notifiers/INotifier.ts
Normal 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>;
|
||||||
|
}
|
22
src/lib/notifiers/NotifierFactory.ts
Normal file
22
src/lib/notifiers/NotifierFactory.ts
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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);
|
|
||||||
}
|
|
||||||
|
|
|
@ -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);
|
|
||||||
}
|
|
||||||
|
|
|
@ -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
Normal file
41
src/lib/routes.ts
Normal 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);
|
||||||
|
}
|
||||||
|
|
53
src/lib/routes/AuthenticationValidator.ts
Normal file
53
src/lib/routes/AuthenticationValidator.ts
Normal 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();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
19
src/lib/routes/DenyNotLogged.ts
Normal file
19
src/lib/routes/DenyNotLogged.ts
Normal 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);
|
||||||
|
};
|
||||||
|
};
|
82
src/lib/routes/FirstFactor.ts
Normal file
82
src/lib/routes/FirstFactor.ts
Normal 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");
|
||||||
|
});
|
||||||
|
};
|
81
src/lib/routes/PasswordReset.ts
Normal file
81
src/lib/routes/PasswordReset.ts
Normal 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)
|
||||||
|
};
|
28
src/lib/routes/SecondFactorRoutes.ts
Normal file
28
src/lib/routes/SecondFactorRoutes.ts
Normal 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;
|
||||||
|
|
49
src/lib/routes/TOTPAuthenticator.ts
Normal file
49
src/lib/routes/TOTPAuthenticator.ts
Normal 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");
|
||||||
|
});
|
||||||
|
};
|
86
src/lib/routes/TOTPRegistration.ts
Normal file
86
src/lib/routes/TOTPRegistration.ts
Normal 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,
|
||||||
|
};
|
84
src/lib/routes/U2FAuthenticationProcess.ts
Normal file
84
src/lib/routes/U2FAuthenticationProcess.ts
Normal 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
|
||||||
|
};
|
51
src/lib/routes/U2FRegistration.ts
Normal file
51
src/lib/routes/U2FRegistration.ts
Normal 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(),
|
||||||
|
};
|
||||||
|
|
89
src/lib/routes/U2FRegistrationProcess.ts
Normal file
89
src/lib/routes/U2FRegistrationProcess.ts
Normal 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
|
||||||
|
};
|
19
src/lib/routes/U2FRoutes.ts
Normal file
19
src/lib/routes/U2FRoutes.ts
Normal 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;
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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');
|
|
||||||
});
|
|
||||||
}
|
|
|
@ -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();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
|
@ -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),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -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');
|
|
||||||
});
|
|
||||||
}
|
|
|
@ -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();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
|
@ -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();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
|
@ -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();
|
|
||||||
}
|
|
39
src/lib/routes/u2f_common.ts
Normal file
39
src/lib/routes/u2f_common.ts
Normal 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
|
||||||
|
};
|
|
@ -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');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
|
@ -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);
|
|
||||||
}
|
|
||||||
|
|
|
@ -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();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
|
@ -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();
|
|
||||||
});
|
|
||||||
}
|
|
|
@ -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);
|
|
||||||
}
|
|
||||||
|
|
|
@ -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');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
|
@ -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);
|
|
||||||
}
|
|
|
@ -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;
|
|
||||||
}
|
|
23
src/types/Dependencies.ts
Normal file
23
src/types/Dependencies.ts
Normal 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;
|
||||||
|
}
|
7
src/types/ILogger.ts
Normal file
7
src/types/ILogger.ts
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
|
||||||
|
import * as winston from "winston";
|
||||||
|
|
||||||
|
export interface ILogger {
|
||||||
|
debug: winston.LeveledLogMethod;
|
||||||
|
}
|
||||||
|
|
6
src/types/Identity.ts
Normal file
6
src/types/Identity.ts
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
|
||||||
|
|
||||||
|
export interface Identity {
|
||||||
|
userid: string;
|
||||||
|
email: string;
|
||||||
|
}
|
6
src/types/TOTPSecret.ts
Normal file
6
src/types/TOTPSecret.ts
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
|
||||||
|
export interface TOTPSecret {
|
||||||
|
base32: string;
|
||||||
|
ascii: string;
|
||||||
|
otpauth_url: string;
|
||||||
|
}
|
69
src/types/authdog.d.ts
vendored
Normal file
69
src/types/authdog.d.ts
vendored
Normal 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
Normal file
4
src/types/dovehash.d.ts
vendored
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
|
||||||
|
declare module "dovehash" {
|
||||||
|
function encode(algo: string, text: string): string;
|
||||||
|
}
|
11
src/types/ldapjs-async.d.ts
vendored
Normal file
11
src/types/ldapjs-async.d.ts
vendored
Normal 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
Normal file
12
src/types/nedb-async.d.ts
vendored
Normal 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
Normal file
14
src/types/request-async.d.ts
vendored
Normal 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>;
|
||||||
|
}
|
||||||
|
}
|
73
test/unitary/AuthenticationRegulator.test.ts
Normal file
73
test/unitary/AuthenticationRegulator.test.ts
Normal 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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
213
test/unitary/IdentityValidator.test.ts
Normal file
213
test/unitary/IdentityValidator.test.ts
Normal 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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
243
test/unitary/LdapClient.test.ts
Normal file
243
test/unitary/LdapClient.test.ts
Normal 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");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
392
test/unitary/Server.test.ts
Normal file
392
test/unitary/Server.test.ts
Normal 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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
30
test/unitary/TOTPValidator.test.ts
Normal file
30
test/unitary/TOTPValidator.test.ts
Normal 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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
206
test/unitary/UserDataStore.test.ts
Normal file
206
test/unitary/UserDataStore.test.ts
Normal 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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
53
test/unitary/access_control/AccessController.test.ts
Normal file
53
test/unitary/access_control/AccessController.test.ts
Normal 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", []));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
120
test/unitary/access_control/PatternBuilder.test.ts
Normal file
120
test/unitary/access_control/PatternBuilder.test.ts
Normal 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",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
117
test/unitary/config_adapter.test.ts
Normal file
117
test/unitary/config_adapter.test.ts
Normal 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: {}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
179
test/unitary/data_persistence.test.ts
Normal file
179
test/unitary/data_persistence.test.ts
Normal 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();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
12
test/unitary/mocks/AccessController.ts
Normal file
12
test/unitary/mocks/AccessController.ts
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
|
||||||
|
import sinon = require("sinon");
|
||||||
|
|
||||||
|
export interface AccessControllerMock {
|
||||||
|
isDomainAllowedForUser: sinon.SinonStub;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AccessControllerMock() {
|
||||||
|
return {
|
||||||
|
isDomainAllowedForUser: sinon.stub()
|
||||||
|
};
|
||||||
|
}
|
15
test/unitary/mocks/AuthenticationRegulator.ts
Normal file
15
test/unitary/mocks/AuthenticationRegulator.ts
Normal 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()
|
||||||
|
};
|
||||||
|
}
|
35
test/unitary/mocks/IdentityValidator.ts
Normal file
35
test/unitary/mocks/IdentityValidator.ts
Normal 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()
|
||||||
|
};
|
||||||
|
}
|
20
test/unitary/mocks/LdapClient.ts
Normal file
20
test/unitary/mocks/LdapClient.ts
Normal 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()
|
||||||
|
};
|
||||||
|
}
|
12
test/unitary/mocks/Notifier.ts
Normal file
12
test/unitary/mocks/Notifier.ts
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
|
||||||
|
import sinon = require("sinon");
|
||||||
|
|
||||||
|
export interface NotifierMock {
|
||||||
|
notify: sinon.SinonStub;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function NotifierMock(): NotifierMock {
|
||||||
|
return {
|
||||||
|
notify: sinon.stub()
|
||||||
|
};
|
||||||
|
}
|
12
test/unitary/mocks/TOTPValidator.ts
Normal file
12
test/unitary/mocks/TOTPValidator.ts
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
|
||||||
|
import sinon = require("sinon");
|
||||||
|
|
||||||
|
export interface TOTPValidatorMock {
|
||||||
|
validate: sinon.SinonStub;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function TOTPValidatorMock(): TOTPValidatorMock {
|
||||||
|
return {
|
||||||
|
validate: sinon.stub()
|
||||||
|
};
|
||||||
|
}
|
22
test/unitary/mocks/UserDataStore.ts
Normal file
22
test/unitary/mocks/UserDataStore.ts
Normal 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()
|
||||||
|
};
|
||||||
|
}
|
19
test/unitary/mocks/authdog.ts
Normal file
19
test/unitary/mocks/authdog.ts
Normal 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()
|
||||||
|
};
|
||||||
|
}
|
97
test/unitary/mocks/express.ts
Normal file
97
test/unitary/mocks/express.ts
Normal 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()
|
||||||
|
};
|
||||||
|
}
|
28
test/unitary/mocks/ldapjs.ts
Normal file
28
test/unitary/mocks/ldapjs.ts
Normal 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()
|
||||||
|
};
|
||||||
|
}
|
22
test/unitary/mocks/nodemailer.ts
Normal file
22
test/unitary/mocks/nodemailer.ts
Normal 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()
|
||||||
|
};
|
||||||
|
}
|
7
test/unitary/mocks/speakeasy.ts
Normal file
7
test/unitary/mocks/speakeasy.ts
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
|
||||||
|
import sinon = require("sinon");
|
||||||
|
|
||||||
|
export = {
|
||||||
|
totp: sinon.stub(),
|
||||||
|
generateSecret: sinon.stub()
|
||||||
|
};
|
42
test/unitary/notifiers/FileSystemNotifier.test.ts
Normal file
42
test/unitary/notifiers/FileSystemNotifier.test.ts
Normal 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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
40
test/unitary/notifiers/GMailNotifier.test.ts
Normal file
40
test/unitary/notifiers/GMailNotifier.test.ts
Normal 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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
36
test/unitary/notifiers/NotifierFactory.test.ts
Normal file
36
test/unitary/notifiers/NotifierFactory.test.ts
Normal 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);
|
||||||
|
});
|
||||||
|
});
|
|
@ -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();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -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();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -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
Loading…
Reference in New Issue
Block a user