Merge pull request #47 from clems4ever/redis-sessions

Use Redis as a store of user sessions for resilience
This commit is contained in:
Clément Michaud 2017-07-14 17:01:50 +02:00 committed by GitHub
commit 1e1653d81f
27 changed files with 700 additions and 204 deletions

View File

@ -13,16 +13,20 @@ module.exports = function (grunt) {
args: ['-c', 'tslint.json', '-p', 'tsconfig.json'] args: ['-c', 'tslint.json', '-p', 'tsconfig.json']
}, },
"test": { "test": {
cmd: "npm", cmd: "./node_modules/.bin/mocha",
args: ['run', 'test'] args: ['--compilers', 'ts:ts-node/register', '--recursive', 'test/client', 'test/server']
},
"test-int": {
cmd: "./node_modules/.bin/mocha",
args: ['--compilers', 'ts:ts-node/register', '--recursive', 'test/integration']
}, },
"docker-build": { "docker-build": {
cmd: "docker", cmd: "docker",
args: ['build', '-t', 'clems4ever/authelia', '.'] args: ['build', '-t', 'clems4ever/authelia', '.']
}, },
"docker-restart": { "docker-restart": {
cmd: "docker-compose", cmd: "./scripts/dc-example.sh",
args: ['-f', 'docker-compose.yml', '-f', 'docker-compose.dev.yml', 'restart', 'auth'] args: ['up', '-d']
}, },
"minify": { "minify": {
cmd: "./node_modules/.bin/uglifyjs", cmd: "./node_modules/.bin/uglifyjs",
@ -109,7 +113,7 @@ module.exports = function (grunt) {
}, },
client: { client: {
files: ['src/client/**/*.ts', 'test/client/**/*.ts'], files: ['src/client/**/*.ts', 'test/client/**/*.ts'],
tasks: ['build'], tasks: ['build-dev'],
options: { options: {
interrupt: true, interrupt: true,
atBegin: true atBegin: true
@ -117,9 +121,10 @@ module.exports = function (grunt) {
}, },
server: { server: {
files: ['src/server/**/*.ts', 'test/server/**/*.ts'], files: ['src/server/**/*.ts', 'test/server/**/*.ts'],
tasks: ['build', 'run:docker-restart', 'run:make-dev-views' ], tasks: ['build-dev', 'run:docker-restart', 'run:make-dev-views' ],
options: { options: {
interrupt: true, interrupt: true,
atBegin: true
} }
} }
}, },

View File

@ -7,13 +7,37 @@
nginx. It has been made to work with nginx [auth_request] module and is currently nginx. It has been made to work with nginx [auth_request] module and is currently
used in production to secure internal services in a small docker swarm cluster. used in production to secure internal services in a small docker swarm cluster.
## Features # Table of Contents
1. [Features summary](#features-summary)
2. [Deployment](#deployment)
1. [With NPM](#with-npm)
2. [With Docker](#with-docker)
3. [Getting started](#getting-started)
1. [Pre-requisites](#pre-requisites)
2. [Run it!](#run-it)
4. [Features in details](#features-in-details)
1. [First factor with LDAP and ACL](#first-factor-with-ldap-and-acl)
2. [Second factor with TOTP](#second-factor-with-totp)
3. [Second factor with U2F security keys](#second-factor-with-u2f-security-keys)
4. [Password reset](#password-reset)
5. [Access control](#access-control)
6. [Session management with Redis](#session-management-with-redis)
4. [Documentation](#documentation)
1. [Authelia configuration](#authelia-configuration)
1. [API documentation](#api-documentation)
5. [Contributing to Authelia](#contributing-to-authelia)
6. [License](#license)
---
## Features summary
* Two-factor authentication using either * Two-factor authentication using either
**[TOTP] - Time-Base One Time password -** or **[U2F] - Universal 2-Factor -** **[TOTP] - Time-Base One Time password -** or **[U2F] - Universal 2-Factor -**
as 2nd factor. as 2nd factor.
* Password reset with identity verification by sending links to user email * Password reset with identity verification by sending links to user email
address. address.
* Access restriction after too many authentication attempts. * Access restriction after too many authentication attempts.
* Session management using Redis key/value store.
## Deployment ## Deployment
@ -73,7 +97,7 @@ Add the following lines to your **/etc/hosts** to alias multiple subdomains so t
127.0.0.1 mx2.mail.test.local 127.0.0.1 mx2.mail.test.local
127.0.0.1 auth.test.local 127.0.0.1 auth.test.local
### Deployment ### Run it!
Deploy **Authelia** example with the following command: Deploy **Authelia** example with the following command:
@ -93,7 +117,9 @@ Below is what the login page looks like:
<img src="https://raw.githubusercontent.com/clems4ever/authelia/master/images/first_factor.png" width="400"> <img src="https://raw.githubusercontent.com/clems4ever/authelia/master/images/first_factor.png" width="400">
### First factor: LDAP and ACL ## Features in details
### First factor with LDAP and ACL
An LDAP server has been deployed for you with the following credentials and An LDAP server has been deployed for you with the following credentials and
access control list: access control list:
@ -117,8 +143,8 @@ your credentials are wrong.
<img src="https://raw.githubusercontent.com/clems4ever/authelia/master/images/second_factor.png" width="400"> <img src="https://raw.githubusercontent.com/clems4ever/authelia/master/images/second_factor.png" width="400">
### Second factor: TOTP (Time-Base One Time Password) ### Second factor with TOTP
In **Authelia**, you need to register a per user TOTP secret before In **Authelia**, you need to register a per user TOTP (Time-Based One Time Password) secret before
authenticating. To do that, you need to click on the register button. It will authenticating. To do that, you need to click on the register button. It will
send a link to the user email address. Since this is an example, no email will send a link to the user email address. Since this is an example, no email will
be sent, the link is rather delivered in the file be sent, the link is rather delivered in the file
@ -129,8 +155,8 @@ to store them and get the generated tokens with the app.
<img src="https://raw.githubusercontent.com/clems4ever/authelia/master/images/totp.png" width="400"> <img src="https://raw.githubusercontent.com/clems4ever/authelia/master/images/totp.png" width="400">
### 2nd factor: U2F (Universal 2-Factor) with security keys ### Second factor with U2F security keys
**Authelia** also offers authentication using U2F devices like [Yubikey](Yubikey) **Authelia** also offers authentication using U2F (Universal 2-Factor) devices like [Yubikey](Yubikey)
USB security keys. U2F is one of the most secure authentication protocol and is USB security keys. U2F is one of the most secure authentication protocol and is
already available for Google, Facebook, Github accounts and more. already available for Google, Facebook, Github accounts and more.
@ -160,8 +186,11 @@ the user access to some subdomains. Those rules are defined in the
configuration file and can be set either for everyone, per-user or per-group policies. configuration file and can be set either for everyone, per-user or per-group policies.
Check out the *config.template.yml* to see how they are defined. Check out the *config.template.yml* to see how they are defined.
### Session management with Redis
When your users authenticate against Authelia, sessions are stored in a Redis key/value store. You can specify your own Redis instance in the [configuration file](#authelia-configuration).
## Documentation ## Documentation
### Configuration ### Authelia configuration
The configuration of the server is defined in the file The configuration of the server is defined in the file
**configuration.template.yml**. All the details are documented there. **configuration.template.yml**. All the details are documented there.
You can specify another configuration file by giving it as first argument of You can specify another configuration file by giving it as first argument of

View File

@ -73,7 +73,9 @@ session:
secret: unsecure_secret secret: unsecure_secret
expiration: 3600000 expiration: 3600000
domain: test.local domain: test.local
redis:
host: redis
port: 6379
# The directory where the DB files will be saved # The directory where the DB files will be saved
store_directory: /var/lib/authelia/store store_directory: /var/lib/authelia/store

View File

@ -6,6 +6,7 @@ services:
volumes: volumes:
- ./config.template.yml:/etc/authelia/config.yml:ro - ./config.template.yml:/etc/authelia/config.yml:ro
- ./notifications:/var/lib/authelia/notifications - ./notifications:/var/lib/authelia/notifications
depends_on:
- redis
networks: networks:
- example-network - example-network

View File

@ -0,0 +1,6 @@
version: '2'
services:
redis:
image: redis
networks:
- example-network

View File

@ -7,7 +7,7 @@
"authelia": "dist/src/server/index.js" "authelia": "dist/src/server/index.js"
}, },
"scripts": { "scripts": {
"test": "./node_modules/.bin/mocha --compilers ts:ts-node/register --recursive test/client test/server", "test": "./node_modules/.bin/grunt test",
"cover": "NODE_ENV=test nyc npm t", "cover": "NODE_ENV=test nyc npm t",
"serve": "node dist/server/index.js" "serve": "node dist/server/index.js"
}, },
@ -27,6 +27,7 @@
"@types/cors": "^2.8.1", "@types/cors": "^2.8.1",
"bluebird": "^3.4.7", "bluebird": "^3.4.7",
"body-parser": "^1.15.2", "body-parser": "^1.15.2",
"connect-redis": "^3.3.0",
"dovehash": "0.0.5", "dovehash": "0.0.5",
"ejs": "^2.5.5", "ejs": "^2.5.5",
"express": "^4.14.0", "express": "^4.14.0",
@ -45,6 +46,7 @@
"devDependencies": { "devDependencies": {
"@types/bluebird": "^3.5.4", "@types/bluebird": "^3.5.4",
"@types/body-parser": "^1.16.3", "@types/body-parser": "^1.16.3",
"@types/connect-redis": "0.0.6",
"@types/ejs": "^2.3.33", "@types/ejs": "^2.3.33",
"@types/express": "^4.0.35", "@types/express": "^4.0.35",
"@types/express-session": "0.0.32", "@types/express-session": "0.0.32",
@ -59,7 +61,7 @@
"@types/proxyquire": "^1.3.27", "@types/proxyquire": "^1.3.27",
"@types/query-string": "^4.3.1", "@types/query-string": "^4.3.1",
"@types/randomstring": "^1.1.5", "@types/randomstring": "^1.1.5",
"@types/request": "0.0.45", "@types/request": "0.0.46",
"@types/sinon": "^2.2.1", "@types/sinon": "^2.2.1",
"@types/speakeasy": "^2.0.1", "@types/speakeasy": "^2.0.1",
"@types/tmp": "0.0.33", "@types/tmp": "0.0.33",

View File

@ -2,7 +2,7 @@
service_count=`docker ps -a | grep "Up " | wc -l` service_count=`docker ps -a | grep "Up " | wc -l`
if [ "${service_count}" -eq "3" ] if [ "${service_count}" -eq "4" ]
then then
echo "Service are up and running." echo "Service are up and running."
exit 0 exit 0

View File

@ -2,4 +2,4 @@
set -e set -e
docker-compose -f docker-compose.base.yml -f docker-compose.yml -f example/nginx/docker-compose.yml -f example/ldap/docker-compose.yml $* docker-compose -f docker-compose.base.yml -f docker-compose.yml -f example/redis/docker-compose.yml -f example/nginx/docker-compose.yml -f example/ldap/docker-compose.yml $*

View File

@ -2,4 +2,4 @@
set -e set -e
docker-compose -f docker-compose.base.yml -f example/ldap/docker-compose.yml -f test/integration/docker-compose.yml $* docker-compose -f docker-compose.base.yml -f example/redis/docker-compose.yml -f example/ldap/docker-compose.yml -f test/integration/docker-compose.yml $*

View File

@ -6,7 +6,9 @@ echo "Build services images..."
./scripts/dc-test.sh build ./scripts/dc-test.sh build
echo "Start services..." echo "Start services..."
./scripts/dc-test.sh up -d authelia nginx openldap ./scripts/dc-test.sh up -d redis openldap
sleep 2
./scripts/dc-test.sh up -d authelia nginx
sleep 3 sleep 3
docker ps -a docker ps -a

View File

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

View File

@ -1,6 +1,6 @@
import * as ObjectPath from "object-path"; import * as ObjectPath from "object-path";
import { AppConfiguration, UserConfiguration, NotifierConfiguration, ACLConfiguration, LdapConfiguration } from "./../../types/Configuration"; import { AppConfiguration, UserConfiguration, NotifierConfiguration, ACLConfiguration, LdapConfiguration, SessionRedisOptions } from "./../../types/Configuration";
const LDAP_URL_ENV_VARIABLE = "LDAP_URL"; const LDAP_URL_ENV_VARIABLE = "LDAP_URL";
@ -32,6 +32,7 @@ function adaptFromUserConfiguration(userConfiguration: UserConfiguration): AppCo
domain: ObjectPath.get<object, string>(userConfiguration, "session.domain"), domain: ObjectPath.get<object, string>(userConfiguration, "session.domain"),
secret: ObjectPath.get<object, string>(userConfiguration, "session.secret"), secret: ObjectPath.get<object, string>(userConfiguration, "session.secret"),
expiration: get_optional<number>(userConfiguration, "session.expiration", 3600000), // in ms expiration: get_optional<number>(userConfiguration, "session.expiration", 3600000), // in ms
redis: ObjectPath.get<object, SessionRedisOptions>(userConfiguration, "session.redis")
}, },
store_directory: get_optional<string>(userConfiguration, "store_directory", undefined), store_directory: get_optional<string>(userConfiguration, "store_directory", undefined),
logs_level: get_optional<string>(userConfiguration, "logs_level", "info"), logs_level: get_optional<string>(userConfiguration, "logs_level", "info"),

View File

@ -5,12 +5,13 @@ import { GlobalDependencies } from "../../types/Dependencies";
import { AuthenticationRegulator } from "./AuthenticationRegulator"; import { AuthenticationRegulator } from "./AuthenticationRegulator";
import UserDataStore from "./UserDataStore"; import UserDataStore from "./UserDataStore";
import ConfigurationAdapter from "./ConfigurationAdapter"; import ConfigurationAdapter from "./ConfigurationAdapter";
import {  TOTPValidator } from "./TOTPValidator"; import { TOTPValidator } from "./TOTPValidator";
import { TOTPGenerator } from "./TOTPGenerator"; import { TOTPGenerator } from "./TOTPGenerator";
import RestApi from "./RestApi"; import RestApi from "./RestApi";
import { LdapClient } from "./LdapClient"; import { LdapClient } from "./LdapClient";
import BluebirdPromise = require("bluebird"); import BluebirdPromise = require("bluebird");
import ServerVariables = require("./ServerVariables"); import ServerVariables = require("./ServerVariables");
import SessionConfigurationBuilder from "./SessionConfigurationBuilder";
import * as Express from "express"; import * as Express from "express";
import * as BodyParser from "body-parser"; import * as BodyParser from "body-parser";
@ -20,40 +21,32 @@ import * as http from "http";
export default class Server { export default class Server {
private httpServer: http.Server; private httpServer: http.Server;
start(yaml_configuration: UserConfiguration, deps: GlobalDependencies): BluebirdPromise<void> { start(yamlConfiguration: UserConfiguration, deps: GlobalDependencies): BluebirdPromise<void> {
const config = ConfigurationAdapter.adapt(yaml_configuration); const config = ConfigurationAdapter.adapt(yamlConfiguration);
const view_directory = Path.resolve(__dirname, "../views"); const viewsDirectory = Path.resolve(__dirname, "../views");
const public_html_directory = Path.resolve(__dirname, "../public_html"); const publicHtmlDirectory = Path.resolve(__dirname, "../public_html");
const expressSessionOptions = SessionConfigurationBuilder.build(config, deps);
const app = Express(); const app = Express();
app.use(Express.static(public_html_directory)); app.use(Express.static(publicHtmlDirectory));
app.use(BodyParser.urlencoded({ extended: false })); app.use(BodyParser.urlencoded({ extended: false }));
app.use(BodyParser.json()); app.use(BodyParser.json());
app.use(deps.session(expressSessionOptions));
app.set("trust proxy", 1); // trust first proxy app.set("trust proxy", 1);
app.set("views", viewsDirectory);
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", "pug"); app.set("view engine", "pug");
// by default the level of logs is info // by default the level of logs is info
deps.winston.level = config.logs_level; deps.winston.level = config.logs_level;
console.log("Log level = ", deps.winston.level); console.log("Log level = ", deps.winston.level);
deps.winston.debug("Content of YAML configuration file is %s", JSON.stringify(yamlConfiguration, undefined, 2));
deps.winston.debug("Authelia configuration is %s", JSON.stringify(config, undefined, 2)); deps.winston.debug("Authelia configuration is %s", JSON.stringify(config, undefined, 2));
ServerVariables.fill(app, config, deps); ServerVariables.fill(app, config, deps);
RestApi.setup(app); RestApi.setup(app);
return new BluebirdPromise<void>((resolve, reject) => { return new BluebirdPromise<void>((resolve, reject) => {

View File

@ -0,0 +1,37 @@
import ExpressSession = require("express-session");
import { AppConfiguration } from "../../types/Configuration";
import { GlobalDependencies } from "../../types/Dependencies";
export default class SessionConfigurationBuilder {
static build(configuration: AppConfiguration, deps: GlobalDependencies): ExpressSession.SessionOptions {
const sessionOptions: ExpressSession.SessionOptions = {
secret: configuration.session.secret,
resave: false,
saveUninitialized: true,
cookie: {
secure: false,
maxAge: configuration.session.expiration,
domain: configuration.session.domain
},
};
if (configuration.session.redis) {
let redisOptions;
if (configuration.session.redis.host
&& configuration.session.redis.port) {
redisOptions = {
host: configuration.session.redis.host,
port: configuration.session.redis.port
};
}
if (redisOptions) {
const RedisStore = deps.ConnectRedis(deps.session);
sessionOptions.store = new RedisStore(redisOptions);
}
}
return sessionOptions;
}
}

View File

@ -24,10 +24,16 @@ export interface ACLConfiguration {
users: ACLUsersRules; users: ACLUsersRules;
} }
export interface SessionRedisOptions {
host: string;
port: number;
}
interface SessionCookieConfiguration { interface SessionCookieConfiguration {
secret: string; secret: string;
expiration?: number; expiration?: number;
domain?: string; domain?: string;
redis?: SessionRedisOptions;
} }
export interface GmailNotifierConfiguration { export interface GmailNotifierConfiguration {

View File

@ -5,6 +5,7 @@ import session = require("express-session");
import nedb = require("nedb"); import nedb = require("nedb");
import ldapjs = require("ldapjs"); import ldapjs = require("ldapjs");
import u2f = require("u2f"); import u2f = require("u2f");
import RedisSession = require("connect-redis");
export type Nodemailer = typeof nodemailer; export type Nodemailer = typeof nodemailer;
export type Speakeasy = typeof speakeasy; export type Speakeasy = typeof speakeasy;
@ -13,12 +14,14 @@ export type Session = typeof session;
export type Nedb = typeof nedb; export type Nedb = typeof nedb;
export type Ldapjs = typeof ldapjs; export type Ldapjs = typeof ldapjs;
export type U2f = typeof u2f; export type U2f = typeof u2f;
export type ConnectRedis = typeof RedisSession;
export interface GlobalDependencies { export interface GlobalDependencies {
u2f: U2f; u2f: U2f;
nodemailer: Nodemailer; nodemailer: Nodemailer;
ldapjs: Ldapjs; ldapjs: Ldapjs;
session: Session; session: Session;
ConnectRedis: ConnectRedis;
winston: Winston; winston: Winston;
speakeasy: Speakeasy; speakeasy: Speakeasy;
nedb: Nedb; nedb: Nedb;

View File

@ -2,4 +2,3 @@ FROM node:7-alpine
WORKDIR /usr/src WORKDIR /usr/src
CMD ["./node_modules/.bin/mocha", "--compilers", "ts:ts-node/register", "--recursive", "test/integration"]

View File

@ -73,6 +73,9 @@ session:
secret: unsecure_secret secret: unsecure_secret
expiration: 3600000 expiration: 3600000
domain: test.local domain: test.local
redis:
host: redis
port: 6379
# The directory where the DB files will be saved # The directory where the DB files will be saved

View File

@ -11,6 +11,7 @@ services:
int-test: int-test:
build: ./test/integration build: ./test/integration
command: ./node_modules/.bin/mocha --compilers ts:ts-node/register --recursive test/integration
volumes: volumes:
- ./:/usr/src - ./:/usr/src
networks: networks:

View File

@ -0,0 +1,23 @@
import Redis = require("redis");
import Assert = require("assert");
const redisOptions = {
host: "redis",
port: 6379
};
describe("test redis is correctly used", function () {
let redisClient: Redis.RedisClient;
before(function () {
redisClient = Redis.createClient(redisOptions);
});
it("should have registered at least one session", function (done) {
redisClient.dbsize(function (err: Error, count: number) {
Assert.equal(1, count);
done();
});
});
});

View File

@ -100,15 +100,16 @@ describe("test data persistence", function () {
sendMail: sinon.stub().yields() sendMail: sinon.stub().yields()
}; };
const deps = { const deps: GlobalDependencies = {
u2f: u2f, u2f: u2f,
nedb: nedb, nedb: nedb,
nodemailer: nodemailer, nodemailer: nodemailer,
session: session, session: session,
winston: winston, winston: winston,
ldapjs: ldap, ldapjs: ldap,
speakeasy: speakeasy speakeasy: speakeasy,
} as GlobalDependencies; ConnectRedis: sinon.spy()
};
const j1 = request.jar(); const j1 = request.jar();
const j2 = request.jar(); const j2 = request.jar();

View File

@ -38,11 +38,12 @@ describe("test server configuration", function () {
createClient: sinon.spy(function () { createClient: sinon.spy(function () {
return { return {
on: sinon.spy(), on: sinon.spy(),
bind: sinon.spy() bind: sinon.spy(),
}; };
}) })
}, },
session: sessionMock as any session: sessionMock as any,
ConnectRedis: sinon.spy()
}; };
}); });

View File

@ -0,0 +1,131 @@
import SessionConfigurationBuilder from "../../src/server/lib/SessionConfigurationBuilder";
import { AppConfiguration } from "../../src/types/Configuration";
import { GlobalDependencies } from "../../src/types/Dependencies";
import ExpressSession = require("express-session");
import ConnectRedis = require("connect-redis");
import sinon = require("sinon");
import Assert = require("assert");
describe("test session configuration builder", function () {
it("should return session options without redis options", function () {
const configuration: AppConfiguration = {
access_control: {
default: [],
users: {},
groups: {}
},
ldap: {
url: "ldap://ldap",
base_dn: "dc=example,dc=com",
user: "user",
password: "password"
},
logs_level: "debug",
notifier: {
filesystem: {
filename: "/test"
}
},
port: 8080,
session: {
domain: "example.com",
expiration: 3600,
secret: "secret"
},
store_in_memory: true
};
const deps: GlobalDependencies = {
ConnectRedis: sinon.spy() as any,
ldapjs: sinon.spy() as any,
nedb: sinon.spy() as any,
nodemailer: sinon.spy() as any,
session: sinon.spy() as any,
speakeasy: sinon.spy() as any,
u2f: sinon.spy() as any,
winston: sinon.spy() as any
};
const options = SessionConfigurationBuilder.build(configuration, deps);
const expectedOptions = {
secret: "secret",
resave: false,
saveUninitialized: true,
cookie: {
secure: false,
maxAge: 3600,
domain: "example.com"
}
};
Assert.deepEqual(expectedOptions, options);
});
it("should return session options with redis options", function () {
const configuration: AppConfiguration = {
access_control: {
default: [],
users: {},
groups: {}
},
ldap: {
url: "ldap://ldap",
base_dn: "dc=example,dc=com",
user: "user",
password: "password"
},
logs_level: "debug",
notifier: {
filesystem: {
filename: "/test"
}
},
port: 8080,
session: {
domain: "example.com",
expiration: 3600,
secret: "secret",
redis: {
host: "redis.example.com",
port: 6379
}
},
store_in_memory: true
};
const RedisStoreMock = sinon.spy();
const deps: GlobalDependencies = {
ConnectRedis: sinon.stub().returns(RedisStoreMock) as any,
ldapjs: sinon.spy() as any,
nedb: sinon.spy() as any,
nodemailer: sinon.spy() as any,
session: sinon.spy() as any,
speakeasy: sinon.spy() as any,
u2f: sinon.spy() as any,
winston: sinon.spy() as any
};
const options = SessionConfigurationBuilder.build(configuration, deps);
const expectedOptions: ExpressSession.SessionOptions = {
secret: "secret",
resave: false,
saveUninitialized: true,
cookie: {
secure: false,
maxAge: 3600,
domain: "example.com"
},
store: sinon.match.object as any
};
Assert((deps.ConnectRedis as sinon.SinonStub).calledWith(deps.session));
Assert.equal(options.secret, expectedOptions.secret);
Assert.equal(options.resave, expectedOptions.resave);
Assert.equal(options.saveUninitialized, expectedOptions.saveUninitialized);
Assert.deepEqual(options.cookie, expectedOptions.cookie);
Assert(options.store != undefined);
});
});

View File

@ -0,0 +1,177 @@
import Server from "../../../src/server/lib/Server";
import LdapClient = require("../../../src/server/lib/LdapClient");
import BluebirdPromise = require("bluebird");
import speakeasy = require("speakeasy");
import request = require("request");
import nedb = require("nedb");
import { GlobalDependencies } from "../../../src/types/Dependencies";
import { TOTPSecret } from "../../../src/types/TOTPSecret";
import U2FMock = require("./../mocks/u2f");
import Endpoints = require("../../../src/server/endpoints");
import Requests = require("../requests");
import Assert = require("assert");
import Sinon = require("sinon");
import Winston = require("winston");
import MockDate = require("mockdate");
import ExpressSession = require("express-session");
import ldapjs = require("ldapjs");
const requestp = BluebirdPromise.promisifyAll(request) as typeof request;
const PORT = 8090;
const BASE_URL = "http://localhost:" + PORT;
const requests = Requests(PORT);
describe("Private pages of the server must not be accessible without session", function () {
let server: Server;
let transporter: any;
let u2f: U2FMock.U2FMock;
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 = U2FMock.U2FMock();
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: GlobalDependencies = {
u2f: u2f,
nedb: nedb,
nodemailer: nodemailer,
ldapjs: ldap,
session: ExpressSession,
winston: Winston,
speakeasy: speakeasy,
ConnectRedis: Sinon.spy()
};
server = new Server();
return server.start(config, deps);
});
afterEach(function () {
server.stop();
});
describe("Second factor endpoints must be protected if first factor is not validated", function () {
function should_post_and_reply_with(url: string, status_code: number): BluebirdPromise<void> {
return requestp.postAsync(url).then(function (response: request.RequestResponse) {
Assert.equal(response.statusCode, status_code);
return BluebirdPromise.resolve();
});
}
function should_get_and_reply_with(url: string, status_code: number): BluebirdPromise<void> {
return requestp.getAsync(url).then(function (response: request.RequestResponse) {
Assert.equal(response.statusCode, status_code);
return BluebirdPromise.resolve();
});
}
function should_post_and_reply_with_401(url: string): BluebirdPromise<void> {
return should_post_and_reply_with(url, 401);
}
function should_get_and_reply_with_401(url: string): BluebirdPromise<void> {
return should_get_and_reply_with(url, 401);
}
it("should block " + Endpoints.SECOND_FACTOR_GET, function () {
return should_get_and_reply_with_401(BASE_URL + Endpoints.SECOND_FACTOR_GET);
});
it("should block " + Endpoints.SECOND_FACTOR_U2F_IDENTITY_START_GET, function () {
return should_get_and_reply_with_401(BASE_URL + Endpoints.SECOND_FACTOR_U2F_IDENTITY_START_GET);
});
it("should block " + Endpoints.SECOND_FACTOR_U2F_IDENTITY_FINISH_GET, function () {
return should_get_and_reply_with_401(BASE_URL + Endpoints.SECOND_FACTOR_U2F_IDENTITY_FINISH_GET + "?identity_token=dummy");
});
it("should block " + Endpoints.SECOND_FACTOR_U2F_REGISTER_REQUEST_GET, function () {
return should_get_and_reply_with_401(BASE_URL + Endpoints.SECOND_FACTOR_U2F_REGISTER_REQUEST_GET);
});
it("should block " + Endpoints.SECOND_FACTOR_U2F_REGISTER_POST, function () {
return should_post_and_reply_with_401(BASE_URL + Endpoints.SECOND_FACTOR_U2F_REGISTER_POST);
});
it("should block " + Endpoints.SECOND_FACTOR_U2F_SIGN_REQUEST_GET, function () {
return should_get_and_reply_with_401(BASE_URL + Endpoints.SECOND_FACTOR_U2F_SIGN_REQUEST_GET);
});
it("should block " + Endpoints.SECOND_FACTOR_U2F_SIGN_POST, function () {
return should_post_and_reply_with_401(BASE_URL + Endpoints.SECOND_FACTOR_U2F_SIGN_POST);
});
it("should block " + Endpoints.SECOND_FACTOR_TOTP_POST, function () {
return should_post_and_reply_with_401(BASE_URL + Endpoints.SECOND_FACTOR_TOTP_POST);
});
});
});

View File

@ -0,0 +1,165 @@
import Server from "../../../src/server/lib/Server";
import LdapClient = require("../../../src/server/lib/LdapClient");
import BluebirdPromise = require("bluebird");
import speakeasy = require("speakeasy");
import Request = require("request");
import nedb = require("nedb");
import { GlobalDependencies } from "../../../src/types/Dependencies";
import { TOTPSecret } from "../../../src/types/TOTPSecret";
import U2FMock = require("./../mocks/u2f");
import Endpoints = require("../../../src/server/endpoints");
import Requests = require("../requests");
import Assert = require("assert");
import Sinon = require("sinon");
import Winston = require("winston");
import MockDate = require("mockdate");
import ExpressSession = require("express-session");
import ldapjs = require("ldapjs");
const requestp = BluebirdPromise.promisifyAll(Request) as typeof Request;
const PORT = 8090;
const BASE_URL = "http://localhost:" + PORT;
const requests = Requests(PORT);
describe("Public pages of the server must be accessible without session", function () {
let server: Server;
let transporter: object;
let u2f: U2FMock.U2FMock;
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 = U2FMock.U2FMock();
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: GlobalDependencies = {
u2f: u2f,
nedb: nedb,
nodemailer: nodemailer,
ldapjs: ldap,
session: ExpressSession,
winston: Winston,
speakeasy: speakeasy,
ConnectRedis: Sinon.spy()
};
server = new Server();
return server.start(config, deps);
});
afterEach(function () {
server.stop();
});
describe("test GET " + Endpoints.FIRST_FACTOR_GET, function () {
test_login();
});
describe("test GET " + Endpoints.LOGOUT_GET, function () {
test_logout();
});
describe("test GET" + Endpoints.RESET_PASSWORD_REQUEST_GET, function () {
test_reset_password_form();
});
function test_reset_password_form() {
it("should serve the reset password form page", function (done) {
requestp.getAsync(BASE_URL + Endpoints.RESET_PASSWORD_REQUEST_GET)
.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 + Endpoints.FIRST_FACTOR_GET)
.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 + Endpoints.LOGOUT_GET)
.then(function (response: any) {
Assert.equal(response.req.path, "/");
done();
});
});
}
});

View File

@ -1,32 +1,34 @@
import Server from "../../src/server/lib/Server";
import LdapClient = require("../../src/server/lib/LdapClient"); import Server from "../../../src/server/lib/Server";
import { LdapjsClientMock } from "./mocks/ldapjs"; import LdapClient = require("../../../src/server/lib/LdapClient");
import { LdapjsClientMock } from "./../mocks/ldapjs";
import BluebirdPromise = require("bluebird"); import BluebirdPromise = require("bluebird");
import speakeasy = require("speakeasy"); import speakeasy = require("speakeasy");
import request = require("request"); import request = require("request");
import nedb = require("nedb"); import nedb = require("nedb");
import { TOTPSecret } from "../../src/types/TOTPSecret"; import { GlobalDependencies } from "../../../src/types/Dependencies";
import U2FMock = require("./mocks/u2f"); import { TOTPSecret } from "../../../src/types/TOTPSecret";
import Endpoints = require("../../src/server/endpoints"); import U2FMock = require("./../mocks/u2f");
import Endpoints = require("../../../src/server/endpoints");
import Requests = require("../requests");
import Assert = require("assert");
import Sinon = require("sinon");
import Winston = require("winston");
import MockDate = require("mockdate");
import ExpressSession = require("express-session");
import ldapjs = require("ldapjs");
const requestp = BluebirdPromise.promisifyAll(request) as typeof request; const requestp = BluebirdPromise.promisifyAll(request) as typeof request;
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 PORT = 8090;
const BASE_URL = "http://localhost:" + PORT; const BASE_URL = "http://localhost:" + PORT;
const requests = require("./requests")(PORT); const requests = Requests(PORT);
describe("test the server", function () { describe("test the server", function () {
let server: Server; let server: Server;
let transporter: object; let transporter: any;
let u2f: U2FMock.U2FMock; let u2f: U2FMock.U2FMock;
beforeEach(function () { beforeEach(function () {
@ -54,8 +56,8 @@ describe("test the server", function () {
const ldapClient = LdapjsClientMock(); const ldapClient = LdapjsClientMock();
const ldap = { const ldap = {
Change: sinon.spy(), Change: Sinon.spy(),
createClient: sinon.spy(function () { createClient: Sinon.spy(function () {
return ldapClient; return ldapClient;
}) })
}; };
@ -63,11 +65,11 @@ describe("test the server", function () {
u2f = U2FMock.U2FMock(); u2f = U2FMock.U2FMock();
transporter = { transporter = {
sendMail: sinon.stub().yields() sendMail: Sinon.stub().yields()
}; };
const nodemailer = { const nodemailer = {
createTransport: sinon.spy(function () { createTransport: Sinon.spy(function () {
return transporter; return transporter;
}) })
}; };
@ -79,7 +81,7 @@ describe("test the server", function () {
}; };
const search_res = { const search_res = {
on: sinon.spy(function (event: string, fn: (s: any) => void) { on: Sinon.spy(function (event: string, fn: (s: any) => void) {
if (event != "error") fn(ldapDocument); if (event != "error") fn(ldapDocument);
}) })
}; };
@ -96,14 +98,15 @@ describe("test the server", function () {
ldapClient.modify.yields(); ldapClient.modify.yields();
ldapClient.search.yields(undefined, search_res); ldapClient.search.yields(undefined, search_res);
const deps = { const deps: GlobalDependencies = {
u2f: u2f, u2f: u2f,
nedb: nedb, nedb: nedb,
nodemailer: nodemailer, nodemailer: nodemailer,
ldapjs: ldap, ldapjs: ldap,
session: session, session: ExpressSession,
winston: winston, winston: Winston,
speakeasy: speakeasy speakeasy: speakeasy,
ConnectRedis: Sinon.spy()
}; };
server = new Server(); server = new Server();
@ -114,114 +117,17 @@ describe("test the server", function () {
server.stop(); server.stop();
}); });
describe("test GET " + Endpoints.FIRST_FACTOR_GET, function () {
test_login();
});
describe("test GET " + Endpoints.LOGOUT_GET, function () {
test_logout();
});
describe("test GET" + Endpoints.RESET_PASSWORD_REQUEST_GET, function () {
test_reset_password_form();
});
describe("Second factor endpoints must be protected if first factor is not validated", function () {
function should_post_and_reply_with(url: string, status_code: number): BluebirdPromise<void> {
return requestp.postAsync(url).then(function (response: request.RequestResponse) {
assert.equal(response.statusCode, status_code);
return BluebirdPromise.resolve();
});
}
function should_get_and_reply_with(url: string, status_code: number): BluebirdPromise<void> {
return requestp.getAsync(url).then(function (response: request.RequestResponse) {
assert.equal(response.statusCode, status_code);
return BluebirdPromise.resolve();
});
}
function should_post_and_reply_with_401(url: string): BluebirdPromise<void> {
return should_post_and_reply_with(url, 401);
}
function should_get_and_reply_with_401(url: string): BluebirdPromise<void> {
return should_get_and_reply_with(url, 401);
}
it("should block " + Endpoints.SECOND_FACTOR_GET, function () {
return should_get_and_reply_with_401(BASE_URL + Endpoints.SECOND_FACTOR_GET);
});
it("should block " + Endpoints.SECOND_FACTOR_U2F_IDENTITY_START_GET, function () {
return should_get_and_reply_with_401(BASE_URL + Endpoints.SECOND_FACTOR_U2F_IDENTITY_START_GET);
});
it("should block " + Endpoints.SECOND_FACTOR_U2F_IDENTITY_FINISH_GET, function () {
return should_get_and_reply_with_401(BASE_URL + Endpoints.SECOND_FACTOR_U2F_IDENTITY_FINISH_GET + "?identity_token=dummy");
});
it("should block " + Endpoints.SECOND_FACTOR_U2F_REGISTER_REQUEST_GET, function () {
return should_get_and_reply_with_401(BASE_URL + Endpoints.SECOND_FACTOR_U2F_REGISTER_REQUEST_GET);
});
it("should block " + Endpoints.SECOND_FACTOR_U2F_REGISTER_POST, function () {
return should_post_and_reply_with_401(BASE_URL + Endpoints.SECOND_FACTOR_U2F_REGISTER_POST);
});
it("should block " + Endpoints.SECOND_FACTOR_U2F_SIGN_REQUEST_GET, function () {
return should_get_and_reply_with_401(BASE_URL + Endpoints.SECOND_FACTOR_U2F_SIGN_REQUEST_GET);
});
it("should block " + Endpoints.SECOND_FACTOR_U2F_SIGN_POST, function () {
return should_post_and_reply_with_401(BASE_URL + Endpoints.SECOND_FACTOR_U2F_SIGN_POST);
});
it("should block " + Endpoints.SECOND_FACTOR_TOTP_POST, function () {
return should_post_and_reply_with_401(BASE_URL + Endpoints.SECOND_FACTOR_TOTP_POST);
});
});
describe("test authentication and verification", function () { describe("test authentication and verification", function () {
test_authentication(); test_authentication();
test_reset_password(); test_reset_password();
test_regulation(); test_regulation();
}); });
function test_reset_password_form() {
it("should serve the reset password form page", function (done) {
requestp.getAsync(BASE_URL + Endpoints.RESET_PASSWORD_REQUEST_GET)
.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 + Endpoints.FIRST_FACTOR_GET)
.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 + Endpoints.LOGOUT_GET)
.then(function (response: any) {
assert.equal(response.req.path, "/");
done();
});
});
}
function test_authentication() { function test_authentication() {
it("should return status code 401 when user is not authenticated", function () { it("should return status code 401 when user is not authenticated", function () {
return requestp.getAsync({ url: BASE_URL + Endpoints.VERIFY_GET }) return requestp.getAsync({ url: BASE_URL + Endpoints.VERIFY_GET })
.then(function (response: request.RequestResponse) { .then(function (response: request.RequestResponse) {
assert.equal(response.statusCode, 401); Assert.equal(response.statusCode, 401);
return BluebirdPromise.resolve(); return BluebirdPromise.resolve();
}); });
}); });
@ -230,11 +136,11 @@ describe("test the server", function () {
const j = requestp.jar(); const j = requestp.jar();
return requests.login(j) return requests.login(j)
.then(function (res: request.RequestResponse) { .then(function (res: request.RequestResponse) {
assert.equal(res.statusCode, 200, "get login page failed"); Assert.equal(res.statusCode, 200, "get login page failed");
return requests.first_factor(j); return requests.first_factor(j);
}) })
.then(function (res: request.RequestResponse) { .then(function (res: request.RequestResponse) {
assert.equal(res.statusCode, 302, "first factor failed"); Assert.equal(res.statusCode, 302, "first factor failed");
return requests.register_totp(j, transporter); return requests.register_totp(j, transporter);
}) })
.then(function (base32_secret: string) { .then(function (base32_secret: string) {
@ -245,11 +151,11 @@ describe("test the server", function () {
return requests.totp(j, realToken); return requests.totp(j, realToken);
}) })
.then(function (res: request.RequestResponse) { .then(function (res: request.RequestResponse) {
assert.equal(res.statusCode, 200, "second factor failed"); Assert.equal(res.statusCode, 200, "second factor failed");
return requests.verify(j); return requests.verify(j);
}) })
.then(function (res: request.RequestResponse) { .then(function (res: request.RequestResponse) {
assert.equal(res.statusCode, 204, "verify failed"); Assert.equal(res.statusCode, 204, "verify failed");
return BluebirdPromise.resolve(); return BluebirdPromise.resolve();
}) })
.catch(function (err: Error) { return BluebirdPromise.reject(err); }); .catch(function (err: Error) { return BluebirdPromise.reject(err); });
@ -259,11 +165,11 @@ describe("test the server", function () {
const j = requestp.jar(); const j = requestp.jar();
return requests.login(j) return requests.login(j)
.then(function (res: request.RequestResponse) { .then(function (res: request.RequestResponse) {
assert.equal(res.statusCode, 200, "get login page failed"); Assert.equal(res.statusCode, 200, "get login page failed");
return requests.first_factor(j); return requests.first_factor(j);
}) })
.then(function (res: request.RequestResponse) { .then(function (res: request.RequestResponse) {
assert.equal(res.statusCode, 302, "first factor failed"); Assert.equal(res.statusCode, 302, "first factor failed");
return requests.register_totp(j, transporter); return requests.register_totp(j, transporter);
}) })
.then(function (base32_secret: string) { .then(function (base32_secret: string) {
@ -274,15 +180,15 @@ describe("test the server", function () {
return requests.totp(j, realToken); return requests.totp(j, realToken);
}) })
.then(function (res: request.RequestResponse) { .then(function (res: request.RequestResponse) {
assert.equal(res.statusCode, 200, "second factor failed"); Assert.equal(res.statusCode, 200, "second factor failed");
return requests.login(j); return requests.login(j);
}) })
.then(function (res: request.RequestResponse) { .then(function (res: request.RequestResponse) {
assert.equal(res.statusCode, 200, "login page loading failed"); Assert.equal(res.statusCode, 200, "login page loading failed");
return requests.verify(j); return requests.verify(j);
}) })
.then(function (res: request.RequestResponse) { .then(function (res: request.RequestResponse) {
assert.equal(res.statusCode, 204, "verify failed"); Assert.equal(res.statusCode, 204, "verify failed");
return BluebirdPromise.resolve(); return BluebirdPromise.resolve();
}) })
.catch(function (err: Error) { return BluebirdPromise.reject(err); }); .catch(function (err: Error) { return BluebirdPromise.reject(err); });
@ -300,25 +206,25 @@ describe("test the server", function () {
const j = requestp.jar(); const j = requestp.jar();
return requests.login(j) return requests.login(j)
.then(function (res: request.RequestResponse) { .then(function (res: request.RequestResponse) {
assert.equal(res.statusCode, 200, "get login page failed"); Assert.equal(res.statusCode, 200, "get login page failed");
return requests.first_factor(j); return requests.first_factor(j);
}) })
.then(function (res: request.RequestResponse) { .then(function (res: request.RequestResponse) {
// console.log(res); // console.log(res);
assert.equal(res.headers.location, Endpoints.SECOND_FACTOR_GET); Assert.equal(res.headers.location, Endpoints.SECOND_FACTOR_GET);
assert.equal(res.statusCode, 302, "first factor failed"); Assert.equal(res.statusCode, 302, "first factor failed");
return requests.u2f_registration(j, transporter); return requests.u2f_registration(j, transporter);
}) })
.then(function (res: request.RequestResponse) { .then(function (res: request.RequestResponse) {
assert.equal(res.statusCode, 200, "second factor, finish register failed"); Assert.equal(res.statusCode, 200, "second factor, finish register failed");
return requests.u2f_authentication(j); return requests.u2f_authentication(j);
}) })
.then(function (res: request.RequestResponse) { .then(function (res: request.RequestResponse) {
assert.equal(res.statusCode, 200, "second factor, finish sign failed"); Assert.equal(res.statusCode, 200, "second factor, finish sign failed");
return requests.verify(j); return requests.verify(j);
}) })
.then(function (res: request.RequestResponse) { .then(function (res: request.RequestResponse) {
assert.equal(res.statusCode, 204, "verify failed"); Assert.equal(res.statusCode, 204, "verify failed");
return BluebirdPromise.resolve(); return BluebirdPromise.resolve();
}); });
}); });
@ -329,16 +235,16 @@ describe("test the server", function () {
const j = requestp.jar(); const j = requestp.jar();
return requests.login(j) return requests.login(j)
.then(function (res: request.RequestResponse) { .then(function (res: request.RequestResponse) {
assert.equal(res.statusCode, 200, "get login page failed"); Assert.equal(res.statusCode, 200, "get login page failed");
return requests.first_factor(j); return requests.first_factor(j);
}) })
.then(function (res: request.RequestResponse) { .then(function (res: request.RequestResponse) {
assert.equal(res.headers.location, Endpoints.SECOND_FACTOR_GET); Assert.equal(res.headers.location, Endpoints.SECOND_FACTOR_GET);
assert.equal(res.statusCode, 302, "first factor failed"); Assert.equal(res.statusCode, 302, "first factor failed");
return requests.reset_password(j, transporter, "user", "new-password"); return requests.reset_password(j, transporter, "user", "new-password");
}) })
.then(function (res: request.RequestResponse) { .then(function (res: request.RequestResponse) {
assert.equal(res.statusCode, 204, "second factor, finish register failed"); Assert.equal(res.statusCode, 204, "second factor, finish register failed");
return BluebirdPromise.resolve(); return BluebirdPromise.resolve();
}); });
}); });
@ -350,28 +256,28 @@ describe("test the server", function () {
MockDate.set("1/2/2017 00:00:00"); MockDate.set("1/2/2017 00:00:00");
return requests.login(j) return requests.login(j)
.then(function (res: request.RequestResponse) { .then(function (res: request.RequestResponse) {
assert.equal(res.statusCode, 200, "get login page failed"); Assert.equal(res.statusCode, 200, "get login page failed");
return requests.failing_first_factor(j); return requests.failing_first_factor(j);
}) })
.then(function (res: request.RequestResponse) { .then(function (res: request.RequestResponse) {
assert.equal(res.statusCode, 401, "first factor failed"); Assert.equal(res.statusCode, 401, "first factor failed");
return requests.failing_first_factor(j); return requests.failing_first_factor(j);
}) })
.then(function (res: request.RequestResponse) { .then(function (res: request.RequestResponse) {
assert.equal(res.statusCode, 401, "first factor failed"); Assert.equal(res.statusCode, 401, "first factor failed");
return requests.failing_first_factor(j); return requests.failing_first_factor(j);
}) })
.then(function (res: request.RequestResponse) { .then(function (res: request.RequestResponse) {
assert.equal(res.statusCode, 401, "first factor failed"); Assert.equal(res.statusCode, 401, "first factor failed");
return requests.failing_first_factor(j); return requests.failing_first_factor(j);
}) })
.then(function (res: request.RequestResponse) { .then(function (res: request.RequestResponse) {
assert.equal(res.statusCode, 403, "first factor failed"); Assert.equal(res.statusCode, 403, "first factor failed");
MockDate.set("1/2/2017 00:30:00"); MockDate.set("1/2/2017 00:30:00");
return requests.failing_first_factor(j); return requests.failing_first_factor(j);
}) })
.then(function (res: request.RequestResponse) { .then(function (res: request.RequestResponse) {
assert.equal(res.statusCode, 401, "first factor failed"); Assert.equal(res.statusCode, 401, "first factor failed");
return BluebirdPromise.resolve(); return BluebirdPromise.resolve();
}); });
}); });