Merge pull request #61 from clems4ever/resilient-db

Use a scalable and resilient database to improve Authelia QoS
pull/63/head
Clément Michaud 2017-08-01 00:04:04 +02:00 committed by GitHub
commit 785182236c
144 changed files with 2560 additions and 1517 deletions

View File

@ -1,23 +1,37 @@
dist: trusty
language: node_js
sudo: required
node_js:
- node
- "7"
services:
- docker
- ntp
- docker
- ntp
addons:
chrome: stable
apt:
sources:
- google-chrome
packages:
- libgif-dev
- google-chrome-stable
hosts:
- auth.test.local
- home.test.local
- public.test.local
- secret.test.local
- secret1.test.local
- secret2.test.local
- mx1.mail.test.local
- mx2.mail.test.local
before_install: npm install -g npm@'>=2.13.5'
before_install:
- npm install -g npm@'>=2.13.5'
before_script:
- export DISPLAY=:99.0
- sh -e /etc/init.d/xvfb start
- sleep 3
script:
- ./scripts/travis.sh

View File

@ -12,17 +12,13 @@ module.exports = function (grunt) {
cmd: "./node_modules/.bin/tslint",
args: ['-c', 'tslint.json', '-p', 'tsconfig.json']
},
"test": {
"unit-tests": {
cmd: "./node_modules/.bin/mocha",
args: ['--compilers', 'ts:ts-node/register', '--recursive', 'test/unit']
},
"test-int": {
cmd: "./node_modules/.bin/mocha",
args: ['--compilers', 'ts:ts-node/register', '--recursive', 'test/integration']
},
"test-system": {
cmd: "./node_modules/.bin/mocha",
args: ['--compilers', 'ts:ts-node/register', '--recursive', 'test/system']
"integration-tests": {
cmd: "./node_modules/.bin/cucumber-js",
args: ["--compiler", "ts:ts-node/register", "./test/features"]
},
"docker-build": {
cmd: "docker",
@ -165,5 +161,8 @@ module.exports = function (grunt) {
grunt.registerTask('docker-build', ['run:docker-build']);
grunt.registerTask('docker-restart', ['run:docker-restart']);
grunt.registerTask('test', ['run:test']);
grunt.registerTask('unit-tests', ['run:unit-tests']);
grunt.registerTask('integration-tests', ['run:unit-tests']);
grunt.registerTask('test', ['unit-tests']);
};

View File

@ -91,6 +91,7 @@ Make sure you don't have anything listening on port 8080.
Add the following lines to your **/etc/hosts** to alias multiple subdomains so that nginx can redirect request to the correct virtual host.
127.0.0.1 public.test.local
127.0.0.1 secret.test.local
127.0.0.1 secret1.test.local
127.0.0.1 secret2.test.local

View File

@ -48,7 +48,7 @@ ldap:
# beginning of the pattern.
access_control:
default:
- home.test.local
- public.test.local
groups:
admin:
- '*.test.local'
@ -77,9 +77,13 @@ session:
host: redis
port: 6379
# The directory where the DB files will be saved
store_directory: /var/lib/authelia/store
storage:
# The directory where the DB files will be saved
# local: /var/lib/authelia/store
# Settings to connect to mongo server
mongo:
url: mongodb://mongo/authelia
# Notifications are sent to users when they require a password reset, a u2f
# registration or a TOTP registration.

View File

@ -45,3 +45,10 @@ mail: bob.dylan@example.com
sn: Bob Dylan
userpassword: {SHA}W6ph5Mm5Pz8GgiULbPgzG37mj9g=
dn: cn=james,ou=users,dc=example,dc=com
cn: james
objectclass: inetOrgPerson
objectclass: top
mail: james.dean@example.com
sn: James Dean
userpassword: {SHA}W6ph5Mm5Pz8GgiULbPgzG37mj9g=

View File

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

View File

@ -9,6 +9,9 @@
You need to log in to access the secret!<br/><br/>
Try to access it via one of the following links.<br/>
<ul>
<li>
<a href="https://public.test.local:8080/secret.html">public.test.local</a>
</li>
<li>
<a href="https://secret.test.local:8080/secret.html">secret.test.local</a>
</li>
@ -18,9 +21,6 @@
<li>
<a href="https://secret2.test.local:8080/secret.html">secret2.test.local</a>
</li>
<li>
<a href="https://home.test.local:8080/secret.html">home.test.local</a>
</li>
<li>
<a href="https://mx1.mail.test.local:8080/secret.html">mx1.mail.test.local</a>
</li>
@ -45,7 +45,7 @@
<ul>
<li><strong>Default policy</strong>
<ul>
<li>home.test.local</li>
<li>public.test.local</li>
</ul>
</li>
<li><strong>Groups policy</strong>

View File

@ -53,7 +53,8 @@ http {
root /usr/share/nginx/html;
server_name secret1.test.local secret2.test.local secret.test.local
home.test.local mx1.mail.test.local mx2.mail.test.local;
home.test.local mx1.mail.test.local mx2.mail.test.local
public.test.local;
ssl on;
ssl_certificate /etc/ssl/server.crt;

View File

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

View File

@ -7,7 +7,7 @@
"authelia": "dist/src/server/index.js"
},
"scripts": {
"test": "./node_modules/.bin/grunt test",
"test": "./node_modules/.bin/grunt unit-tests",
"cover": "NODE_ENV=test nyc npm t",
"serve": "node dist/server/index.js"
},
@ -24,7 +24,6 @@
"title": "Authelia API documentation"
},
"dependencies": {
"@types/cors": "^2.8.1",
"bluebird": "^3.4.7",
"body-parser": "^1.15.2",
"connect-redis": "^3.3.0",
@ -33,6 +32,7 @@
"express": "^4.14.0",
"express-session": "^1.14.2",
"ldapjs": "^1.0.1",
"mongodb": "^2.2.30",
"nedb": "^1.8.0",
"nodemailer": "^4.0.1",
"object-path": "^0.11.3",
@ -47,6 +47,8 @@
"@types/bluebird": "^3.5.4",
"@types/body-parser": "^1.16.3",
"@types/connect-redis": "0.0.6",
"@types/cors": "^2.8.1",
"@types/cucumber": "^2.0.1",
"@types/ejs": "^2.3.33",
"@types/express": "^4.0.35",
"@types/express-session": "0.0.32",
@ -55,6 +57,7 @@
"@types/ldapjs": "^1.0.0",
"@types/mocha": "^2.2.41",
"@types/mockdate": "^2.0.0",
"@types/mongodb": "^2.2.7",
"@types/nedb": "^1.8.3",
"@types/nodemailer": "^1.3.32",
"@types/object-path": "^0.9.28",
@ -62,6 +65,7 @@
"@types/query-string": "^4.3.1",
"@types/randomstring": "^1.1.5",
"@types/request": "0.0.46",
"@types/selenium-webdriver": "^3.0.4",
"@types/sinon": "^2.2.1",
"@types/speakeasy": "^2.0.1",
"@types/tmp": "0.0.33",
@ -69,6 +73,8 @@
"@types/yamljs": "^0.2.30",
"apidoc": "^0.17.6",
"browserify": "^14.3.0",
"chromedriver": "^2.31.0",
"cucumber": "^2.3.1",
"grunt": "^1.0.1",
"grunt-browserify": "^5.0.0",
"grunt-contrib-concat": "^1.0.1",
@ -80,7 +86,7 @@
"jquery": "^3.2.1",
"js-logger": "^1.3.0",
"jsdom": "^11.0.0",
"mocha": "^3.2.0",
"mocha": "^3.4.2",
"mockdate": "^2.0.1",
"notifyjs-browser": "^0.4.2",
"nyc": "^10.3.2",
@ -88,11 +94,12 @@
"proxyquire": "^1.8.0",
"query-string": "^4.3.4",
"request": "^2.81.0",
"selenium-webdriver": "^3.5.0",
"should": "^11.1.1",
"sinon": "^1.17.6",
"sinon": "^2.3.8",
"sinon-promise": "^0.1.3",
"tmp": "0.0.31",
"ts-node": "^3.0.4",
"ts-node": "^3.3.0",
"tslint": "^5.2.0",
"typescript": "^2.3.2",
"u2f-api": "0.0.9",
@ -107,7 +114,8 @@
"doc",
"src/types",
"dist",
"test"
"test",
"src/**/*.d.ts"
],
"extension": [
".ts"

View File

@ -2,4 +2,11 @@
set -e
docker-compose -f docker-compose.base.yml -f docker-compose.yml -f docker-compose.dev.yml -f example/redis/docker-compose.yml -f example/nginx/docker-compose.yml -f example/ldap/docker-compose.yml -f test/integration/docker-compose.yml $*
docker-compose \
-f docker-compose.base.yml \
-f docker-compose.yml \
-f docker-compose.dev.yml \
-f example/mongo/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,10 @@
set -e
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 -f test/integration/docker-compose.yml $*
docker-compose \
-f docker-compose.base.yml \
-f docker-compose.yml \
-f example/mongo/docker-compose.yml \
-f example/redis/docker-compose.yml \
-f example/nginx/docker-compose.yml \
-f example/ldap/docker-compose.yml $*

View File

@ -3,4 +3,4 @@
DC_SCRIPT=./scripts/example/dc-example.sh
$DC_SCRIPT build
$DC_SCRIPT up -d redis openldap authelia nginx
$DC_SCRIPT up -d mongo redis openldap authelia nginx

View File

@ -1,9 +1,10 @@
#!/bin/bash
DC_SCRIPT=./scripts/example/dc-example.sh
EXPECTED_SERVICES_COUNT=5
start_services() {
$DC_SCRIPT up -d redis openldap authelia nginx nginx-tests
$DC_SCRIPT up -d mongo redis openldap authelia nginx
sleep 3
}
@ -26,39 +27,12 @@ expect_services_count() {
}
run_integration_tests() {
echo "Prepare nginx-test configuration"
cat example/nginx/nginx.conf | sed 's/listen 443 ssl/listen 8080 ssl/g' | dd of="test/integration/nginx.conf"
echo "Build services images..."
$DC_SCRIPT build
echo "Start services..."
start_services
docker ps -a
expect_services_count $EXPECTED_SERVICES_COUNT
echo "Display services logs..."
$DC_SCRIPT logs redis
$DC_SCRIPT logs openldap
$DC_SCRIPT logs nginx
$DC_SCRIPT logs nginx-tests
$DC_SCRIPT logs authelia
echo "Check number of services"
expect_services_count 5
echo "Run integration tests..."
$DC_SCRIPT run --rm integration-tests
echo "Shutdown services..."
shut_services
}
run_system_tests() {
echo "Start services..."
start_services
expect_services_count 5
./node_modules/.bin/mocha --compilers ts:ts-node/register --recursive test/system
sleep 5
./node_modules/.bin/grunt run:integration-tests
shut_services
}
@ -67,7 +41,7 @@ run_other_tests() {
npm install --only=dev
./node_modules/.bin/grunt build-dist
./scripts/example/deploy-example.sh
expect_services_count 4
expect_services_count 5
}
@ -79,11 +53,8 @@ set -e
echo "Make sure services are not already running"
shut_services
# Prepare & run integration tests
run_integration_tests
# Prepare & test example from end user perspective
run_system_tests
run_integration_tests
# Other tests like executing the deployment script
run_other_tests

View File

@ -13,7 +13,7 @@ export function validate(username: string, password: string, $: JQueryStatic): B
})
.fail(function (xhr: JQueryXHR, textStatus: string) {
if (xhr.status == 401)
reject(new Error("Authetication failed. Please check your credentials"));
reject(new Error("Authetication failed. Please check your credentials."));
reject(new Error(textStatus));
});
});

View File

@ -30,7 +30,7 @@ export default function(window: Window, $: JQueryStatic) {
requestPasswordReset(username)
.then(function () {
$.notify("An email has been sent. Click on the link to change your password", "success");
$.notify("An email has been sent. Click on the link to change your password.", "success");
setTimeout(function () {
window.location.replace(Endpoints.FIRST_FACTOR_GET);
}, 1000);

View File

@ -1,39 +1,36 @@
import * as BluebirdPromise from "bluebird";
import exceptions = require("./Exceptions");
import { UserDataStore } from "./storage/UserDataStore";
import { AuthenticationTraceDocument } from "./storage/AuthenticationTraceDocument";
const REGULATION_TRACE_TYPE = "regulation";
const MAX_AUTHENTICATION_COUNT_IN_TIME_RANGE = 3;
interface DatedDocument {
date: Date;
}
export class AuthenticationRegulator {
private _user_data_store: any;
private _lock_time_in_seconds: number;
private userDataStore: UserDataStore;
private lockTimeInSeconds: 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;
constructor(userDataStore: any, lockTimeInSeconds: number) {
this.userDataStore = userDataStore;
this.lockTimeInSeconds = lockTimeInSeconds;
}
// Mark authentication
mark(userid: string, is_success: boolean): BluebirdPromise<void> {
return this._user_data_store.save_authentication_trace(userid, REGULATION_TRACE_TYPE, is_success);
mark(userId: string, isAuthenticationSuccessful: boolean): BluebirdPromise<void> {
return this.userDataStore.saveAuthenticationTrace(userId, isAuthenticationSuccessful);
}
regulate(userid: string): BluebirdPromise<void> {
return this._user_data_store.get_last_authentication_traces(userid, REGULATION_TRACE_TYPE, false, 3)
.then((docs: Array<DatedDocument>) => {
regulate(userId: string): BluebirdPromise<void> {
return this.userDataStore.retrieveLatestAuthenticationTraces(userId, false, 3)
.then((docs: AuthenticationTraceDocument[]) => {
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) {
const oldestDocument = docs[MAX_AUTHENTICATION_COUNT_IN_TIME_RANGE - 1];
const noLockMinDate = new Date(new Date().getTime() - this.lockTimeInSeconds * 1000);
if (oldestDocument.date > noLockMinDate) {
throw new exceptions.AuthenticationRegulationError("Max number of authentication. Please retry in few minutes.");
}

View File

@ -6,15 +6,15 @@ import util = require("util");
import Exceptions = require("./Exceptions");
import fs = require("fs");
import ejs = require("ejs");
import UserDataStore from "./UserDataStore";
import { IUserDataStore } from "./storage/IUserDataStore";
import { Winston } from "../../types/Dependencies";
import express = require("express");
import ErrorReplies = require("./ErrorReplies");
import ServerVariables = require("./ServerVariables");
import { ServerVariablesHandler } from "./ServerVariablesHandler";
import AuthenticationSession = require("./AuthenticationSession");
import Identity = require("../../types/Identity");
import { IdentityValidationRequestContent } from "./UserDataStore";
import { IdentityValidationDocument } from "./storage/IdentityValidationDocument";
const filePath = __dirname + "/../resources/email-template.ejs";
const email_template = fs.readFileSync(filePath, "utf8");
@ -33,21 +33,21 @@ export interface IdentityValidable {
mailSubject(): string;
}
function issue_token(userid: string, content: Object, userDataStore: UserDataStore, logger: Winston): BluebirdPromise<string> {
function createAndSaveToken(userid: string, challenge: string, userDataStore: IUserDataStore, logger: Winston): BluebirdPromise<string> {
const five_minutes = 4 * 60 * 1000;
const token = randomstring.generate({ length: 64 });
const that = this;
logger.debug("identity_check: issue identity token %s for 5 minutes", token);
return userDataStore.issue_identity_check_token(userid, token, content, five_minutes)
return userDataStore.produceIdentityValidationToken(userid, token, challenge, five_minutes)
.then(function () {
return BluebirdPromise.resolve(token);
});
}
function consume_token(token: string, userDataStore: UserDataStore, logger: Winston): BluebirdPromise<IdentityValidationRequestContent> {
function consumeToken(token: string, challenge: string, userDataStore: IUserDataStore, logger: Winston): BluebirdPromise<IdentityValidationDocument> {
logger.debug("identity_check: consume token %s", token);
return userDataStore.consume_identity_check_token(token);
return userDataStore.consumeIdentityValidationToken(token, challenge);
}
export function register(app: express.Application, pre_validation_endpoint: string, post_validation_endpoint: string, handler: IdentityValidable) {
@ -63,8 +63,8 @@ function checkIdentityToken(req: express.Request, identityToken: string): Bluebi
export function get_finish_validation(handler: IdentityValidable): express.RequestHandler {
return function (req: express.Request, res: express.Response): BluebirdPromise<void> {
const logger = ServerVariables.getLogger(req.app);
const userDataStore = ServerVariables.getUserDataStore(req.app);
const logger = ServerVariablesHandler.getLogger(req.app);
const userDataStore = ServerVariablesHandler.getUserDataStore(req.app);
const authSession = AuthenticationSession.get(req);
const identityToken = objectPath.get<express.Request, string>(req, "query.identity_token");
@ -75,12 +75,12 @@ export function get_finish_validation(handler: IdentityValidable): express.Reque
return handler.postValidationInit(req);
})
.then(function () {
return consume_token(identityToken, userDataStore, logger);
return consumeToken(identityToken, handler.challenge(), userDataStore, logger);
})
.then(function (content: IdentityValidationRequestContent) {
.then(function (doc: IdentityValidationDocument) {
authSession.identity_check = {
challenge: handler.challenge(),
userid: content.userid
userid: doc.userId
};
handler.postValidationResponse(req, res);
return BluebirdPromise.resolve();
@ -94,9 +94,9 @@ export function get_finish_validation(handler: IdentityValidable): express.Reque
export function get_start_validation(handler: IdentityValidable, postValidationEndpoint: string): express.RequestHandler {
return function (req: express.Request, res: express.Response): BluebirdPromise<void> {
const logger = ServerVariables.getLogger(req.app);
const notifier = ServerVariables.getNotifier(req.app);
const userDataStore = ServerVariables.getUserDataStore(req.app);
const logger = ServerVariablesHandler.getLogger(req.app);
const notifier = ServerVariablesHandler.getNotifier(req.app);
const userDataStore = ServerVariablesHandler.getUserDataStore(req.app);
let identity: Identity.Identity;
logger.info("Identity Validation: Start identity validation");
@ -104,13 +104,13 @@ export function get_start_validation(handler: IdentityValidable, postValidationE
.then(function (id: Identity.Identity) {
logger.debug("Identity Validation: retrieved identity is %s", JSON.stringify(id));
identity = id;
const email_address = objectPath.get<Identity.Identity, string>(identity, "email");
const userid = objectPath.get<Identity.Identity, string>(identity, "userid");
const email = identity.email;
const userid = identity.userid;
if (!(email_address && userid))
if (!(email && userid))
return BluebirdPromise.reject(new Exceptions.IdentityError("Missing user id or email address"));
return issue_token(userid, undefined, userDataStore, logger);
return createAndSaveToken(userid, handler.challenge(), userDataStore, logger);
})
.then(function (token: string) {
const host = req.get("Host");

View File

@ -1,6 +1,6 @@
import express = require("express");
import UserDataStore from "./UserDataStore";
import { UserDataStore } from "./storage/UserDataStore";
import { Winston } from "../../types/Dependencies";
import FirstFactorGet = require("./routes/firstfactor/get");
@ -34,7 +34,7 @@ import Error404Get = require("./routes/error/404/get");
import Endpoints = require("../endpoints");
export default class RestApi {
export class RestApi {
static setup(app: express.Application): void {
app.get(Endpoints.FIRST_FACTOR_GET, FirstFactorGet.default);
app.get(Endpoints.SECOND_FACTOR_GET, SecondFactorGet.default);

View File

@ -1,43 +1,54 @@
import BluebirdPromise = require("bluebird");
import { AccessController } from "./access_control/AccessController";
import { UserConfiguration } from "./../../types/Configuration";
import { AppConfiguration, UserConfiguration } from "./configuration/Configuration";
import { GlobalDependencies } from "../../types/Dependencies";
import { AuthenticationRegulator } from "./AuthenticationRegulator";
import UserDataStore from "./UserDataStore";
import ConfigurationAdapter from "./ConfigurationAdapter";
import { UserDataStore } from "./storage/UserDataStore";
import { ConfigurationAdapter } from "./configuration/ConfigurationAdapter";
import { TOTPValidator } from "./TOTPValidator";
import { TOTPGenerator } from "./TOTPGenerator";
import RestApi from "./RestApi";
import { RestApi } from "./RestApi";
import { Client } from "./ldap/Client";
import BluebirdPromise = require("bluebird");
import ServerVariables = require("./ServerVariables");
import SessionConfigurationBuilder from "./SessionConfigurationBuilder";
import { ServerVariablesHandler } from "./ServerVariablesHandler";
import { SessionConfigurationBuilder } from "./configuration/SessionConfigurationBuilder";
import * as Express from "express";
import * as BodyParser from "body-parser";
import * as Path from "path";
import * as http from "http";
// Constants
const TRUST_PROXY = "trust proxy";
const VIEWS = "views";
const VIEW_ENGINE = "view engine";
const PUG = "pug";
export default class Server {
private httpServer: http.Server;
start(yamlConfiguration: UserConfiguration, deps: GlobalDependencies): BluebirdPromise<void> {
const config = ConfigurationAdapter.adapt(yamlConfiguration);
private setupExpressApplication(config: AppConfiguration, app: Express.Application, deps: GlobalDependencies): void {
const viewsDirectory = Path.resolve(__dirname, "../views");
const publicHtmlDirectory = Path.resolve(__dirname, "../public_html");
const expressSessionOptions = SessionConfigurationBuilder.build(config, deps);
const app = Express();
app.use(Express.static(publicHtmlDirectory));
app.use(BodyParser.urlencoded({ extended: false }));
app.use(BodyParser.json());
app.use(deps.session(expressSessionOptions));
app.set("trust proxy", 1);
app.set("views", viewsDirectory);
app.set("view engine", "pug");
app.set(TRUST_PROXY, 1);
app.set(VIEWS, viewsDirectory);
app.set(VIEW_ENGINE, PUG);
RestApi.setup(app);
}
private transformConfiguration(yamlConfiguration: UserConfiguration, deps: GlobalDependencies): AppConfiguration {
const config = ConfigurationAdapter.adapt(yamlConfiguration);
// by default the level of logs is info
deps.winston.level = config.logs_level;
@ -45,18 +56,33 @@ export default class Server {
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));
return config;
}
ServerVariables.fill(app, config, deps);
RestApi.setup(app);
private setup(config: AppConfiguration, app: Express.Application, deps: GlobalDependencies): BluebirdPromise<void> {
this.setupExpressApplication(config, app, deps);
return ServerVariablesHandler.initialize(app, config, deps);
}
private startServer(app: Express.Application, port: number) {
return new BluebirdPromise<void>((resolve, reject) => {
this.httpServer = app.listen(config.port, function (err: string) {
console.log("Listening on %d...", config.port);
this.httpServer = app.listen(port, function (err: string) {
console.log("Listening on %d...", port);
resolve();
});
});
}
start(yamlConfiguration: UserConfiguration, deps: GlobalDependencies): BluebirdPromise<void> {
const that = this;
const app = Express();
const config = this.transformConfiguration(yamlConfiguration, deps);
return this.setup(config, app, deps)
.then(function () {
return that.startServer(app, config.port);
});
}
stop() {
this.httpServer.close();
}

View File

@ -1,120 +0,0 @@
import winston = require("winston");
import { Authenticator } from "./ldap/Authenticator";
import { PasswordUpdater } from "./ldap/PasswordUpdater";
import { EmailsRetriever } from "./ldap/EmailsRetriever";
import { TOTPValidator } from "./TOTPValidator";
import { TOTPGenerator } from "./TOTPGenerator";
import U2F = require("u2f");
import UserDataStore from "./UserDataStore";
import { INotifier } from "./notifiers/INotifier";
import { AuthenticationRegulator } from "./AuthenticationRegulator";
import Configuration = require("../../types/Configuration");
import { AccessController } from "./access_control/AccessController";
import { NotifierFactory } from "./notifiers/NotifierFactory";
import { GlobalDependencies } from "../../types/Dependencies";
import express = require("express");
export const VARIABLES_KEY = "authelia-variables";
export interface ServerVariables {
logger: typeof winston;
ldapAuthenticator: Authenticator;
ldapPasswordUpdater: PasswordUpdater;
ldapEmailsRetriever: EmailsRetriever;
totpValidator: TOTPValidator;
totpGenerator: TOTPGenerator;
u2f: typeof U2F;
userDataStore: UserDataStore;
notifier: INotifier;
regulator: AuthenticationRegulator;
config: Configuration.AppConfiguration;
accessController: AccessController;
}
export function fill(app: express.Application, config: Configuration.AppConfiguration, deps: GlobalDependencies) {
const five_minutes = 5 * 60;
const datastore_options = {
directory: config.store_directory,
inMemory: config.store_in_memory
};
const userDataStore = new UserDataStore(datastore_options, deps.nedb);
const regulator = new AuthenticationRegulator(userDataStore, five_minutes);
const notifier = NotifierFactory.build(config.notifier, deps.nodemailer);
const ldapAuthenticator = new Authenticator(config.ldap, deps.ldapjs, deps.winston);
const ldapPasswordUpdater = new PasswordUpdater(config.ldap, deps.ldapjs, deps.dovehash, deps.winston);
const ldapEmailsRetriever = new EmailsRetriever(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 variables: ServerVariables = {
accessController: accessController,
config: config,
ldapAuthenticator: ldapAuthenticator,
ldapPasswordUpdater: ldapPasswordUpdater,
ldapEmailsRetriever: ldapEmailsRetriever,
logger: deps.winston,
notifier: notifier,
regulator: regulator,
totpGenerator: totpGenerator,
totpValidator: totpValidator,
u2f: deps.u2f,
userDataStore: userDataStore
};
app.set(VARIABLES_KEY, variables);
}
export function getLogger(app: express.Application): typeof winston {
return (app.get(VARIABLES_KEY) as ServerVariables).logger;
}
export function getUserDataStore(app: express.Application): UserDataStore {
return (app.get(VARIABLES_KEY) as ServerVariables).userDataStore;
}
export function getNotifier(app: express.Application): INotifier {
return (app.get(VARIABLES_KEY) as ServerVariables).notifier;
}
export function getLdapAuthenticator(app: express.Application): Authenticator {
return (app.get(VARIABLES_KEY) as ServerVariables).ldapAuthenticator;
}
export function getLdapPasswordUpdater(app: express.Application): PasswordUpdater {
return (app.get(VARIABLES_KEY) as ServerVariables).ldapPasswordUpdater;
}
export function getLdapEmailsRetriever(app: express.Application): EmailsRetriever {
return (app.get(VARIABLES_KEY) as ServerVariables).ldapEmailsRetriever;
}
export function getConfiguration(app: express.Application): Configuration.AppConfiguration {
return (app.get(VARIABLES_KEY) as ServerVariables).config;
}
export function getAuthenticationRegulator(app: express.Application): AuthenticationRegulator {
return (app.get(VARIABLES_KEY) as ServerVariables).regulator;
}
export function getAccessController(app: express.Application): AccessController {
return (app.get(VARIABLES_KEY) as ServerVariables).accessController;
}
export function getTOTPGenerator(app: express.Application): TOTPGenerator {
return (app.get(VARIABLES_KEY) as ServerVariables).totpGenerator;
}
export function getTOTPValidator(app: express.Application): TOTPValidator {
return (app.get(VARIABLES_KEY) as ServerVariables).totpValidator;
}
export function getU2F(app: express.Application): typeof U2F {
return (app.get(VARIABLES_KEY) as ServerVariables).u2f;
}

View File

@ -0,0 +1,151 @@
import winston = require("winston");
import BluebirdPromise = require("bluebird");
import { Authenticator } from "./ldap/Authenticator";
import { PasswordUpdater } from "./ldap/PasswordUpdater";
import { EmailsRetriever } from "./ldap/EmailsRetriever";
import { TOTPValidator } from "./TOTPValidator";
import { TOTPGenerator } from "./TOTPGenerator";
import U2F = require("u2f");
import { IUserDataStore } from "./storage/IUserDataStore";
import { UserDataStore } from "./storage/UserDataStore";
import { INotifier } from "./notifiers/INotifier";
import { AuthenticationRegulator } from "./AuthenticationRegulator";
import Configuration = require("./configuration/Configuration");
import { AccessController } from "./access_control/AccessController";
import { NotifierFactory } from "./notifiers/NotifierFactory";
import { CollectionFactoryFactory } from "./storage/CollectionFactoryFactory";
import { ICollectionFactory } from "./storage/ICollectionFactory";
import { MongoCollectionFactory } from "./storage/mongo/MongoCollectionFactory";
import { MongoConnectorFactory } from "./connectors/mongo/MongoConnectorFactory";
import { IMongoClient } from "./connectors/mongo/IMongoClient";
import { GlobalDependencies } from "../../types/Dependencies";
import express = require("express");
export const VARIABLES_KEY = "authelia-variables";
export interface ServerVariables {
logger: typeof winston;
ldapAuthenticator: Authenticator;
ldapPasswordUpdater: PasswordUpdater;
ldapEmailsRetriever: EmailsRetriever;
totpValidator: TOTPValidator;
totpGenerator: TOTPGenerator;
u2f: typeof U2F;
userDataStore: IUserDataStore;
notifier: INotifier;
regulator: AuthenticationRegulator;
config: Configuration.AppConfiguration;
accessController: AccessController;
}
class UserDataStoreFactory {
static create(config: Configuration.AppConfiguration): BluebirdPromise<UserDataStore> {
if (config.storage.local) {
const nedbOptions = {
directory: config.storage.local.path,
inMemory: config.storage.local.in_memory
};
const collectionFactory = CollectionFactoryFactory.createNedb(nedbOptions);
return BluebirdPromise.resolve(new UserDataStore(collectionFactory));
}
else if (config.storage.mongo) {
const mongoConnectorFactory = new MongoConnectorFactory();
const mongoConnector = mongoConnectorFactory.create(config.storage.mongo.url);
return mongoConnector.connect()
.then(function (client: IMongoClient) {
const collectionFactory = CollectionFactoryFactory.createMongo(client);
return BluebirdPromise.resolve(new UserDataStore(collectionFactory));
});
}
return BluebirdPromise.reject(new Error("Storage backend incorrectly configured."));
}
}
export class ServerVariablesHandler {
static initialize(app: express.Application, config: Configuration.AppConfiguration, deps: GlobalDependencies): BluebirdPromise<void> {
const five_minutes = 5 * 60;
const notifier = NotifierFactory.build(config.notifier, deps.nodemailer);
const ldapAuthenticator = new Authenticator(config.ldap, deps.ldapjs, deps.winston);
const ldapPasswordUpdater = new PasswordUpdater(config.ldap, deps.ldapjs, deps.dovehash, deps.winston);
const ldapEmailsRetriever = new EmailsRetriever(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);
return UserDataStoreFactory.create(config)
.then(function (userDataStore: UserDataStore) {
const regulator = new AuthenticationRegulator(userDataStore, five_minutes);
const variables: ServerVariables = {
accessController: accessController,
config: config,
ldapAuthenticator: ldapAuthenticator,
ldapPasswordUpdater: ldapPasswordUpdater,
ldapEmailsRetriever: ldapEmailsRetriever,
logger: deps.winston,
notifier: notifier,
regulator: regulator,
totpGenerator: totpGenerator,
totpValidator: totpValidator,
u2f: deps.u2f,
userDataStore: userDataStore
};
app.set(VARIABLES_KEY, variables);
});
}
static getLogger(app: express.Application): typeof winston {
return (app.get(VARIABLES_KEY) as ServerVariables).logger;
}
static getUserDataStore(app: express.Application): IUserDataStore {
return (app.get(VARIABLES_KEY) as ServerVariables).userDataStore;
}
static getNotifier(app: express.Application): INotifier {
return (app.get(VARIABLES_KEY) as ServerVariables).notifier;
}
static getLdapAuthenticator(app: express.Application): Authenticator {
return (app.get(VARIABLES_KEY) as ServerVariables).ldapAuthenticator;
}
static getLdapPasswordUpdater(app: express.Application): PasswordUpdater {
return (app.get(VARIABLES_KEY) as ServerVariables).ldapPasswordUpdater;
}
static getLdapEmailsRetriever(app: express.Application): EmailsRetriever {
return (app.get(VARIABLES_KEY) as ServerVariables).ldapEmailsRetriever;
}
static getConfiguration(app: express.Application): Configuration.AppConfiguration {
return (app.get(VARIABLES_KEY) as ServerVariables).config;
}
static getAuthenticationRegulator(app: express.Application): AuthenticationRegulator {
return (app.get(VARIABLES_KEY) as ServerVariables).regulator;
}
static getAccessController(app: express.Application): AccessController {
return (app.get(VARIABLES_KEY) as ServerVariables).accessController;
}
static getTOTPGenerator(app: express.Application): TOTPGenerator {
return (app.get(VARIABLES_KEY) as ServerVariables).totpGenerator;
}
static getTOTPValidator(app: express.Application): TOTPValidator {
return (app.get(VARIABLES_KEY) as ServerVariables).totpValidator;
}
static getU2F(app: express.Application): typeof U2F {
return (app.get(VARIABLES_KEY) as ServerVariables).u2f;
}
}

View File

@ -1,37 +0,0 @@
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

@ -1,190 +0,0 @@
import * as BluebirdPromise from "bluebird";
import * as path from "path";
import { NedbAsync } from "nedb";
import { TOTPSecret } from "../../types/TOTPSecret";
import { Nedb } from "../../types/Dependencies";
import u2f = require("u2f");
// 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 U2FRegistrationDocument {
keyHandle: string;
publicKey: string;
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;
}
interface U2FRegistrationFilter {
userId: string;
appId: string;
}
// Source
export default class UserDataStore {
private _u2f_meta_collection: NedbAsync;
private _identity_check_tokens_collection: NedbAsync;
private _authentication_traces_collection: NedbAsync;
private _totp_secret_collection: NedbAsync;
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, keyHandle: string, publicKey: string): BluebirdPromise<any> {
const newDocument: U2FRegistrationDocument = {
userId: userId,
appId: appId,
keyHandle: keyHandle,
publicKey: publicKey
};
const filter: U2FRegistrationFilter = {
userId: userId,
appId: appId
};
return this._u2f_meta_collection.updateAsync(filter, newDocument, { upsert: true });
}
get_u2f_meta(userId: string, appId: string): BluebirdPromise<U2FRegistrationDocument> {
const filter: U2FRegistrationFilter = {
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(new Error("Registration token does not exist"));
}
const max_date = doc.max_date;
const current_date = new Date();
if (current_date > max_date)
return BluebirdPromise.reject(new Error("Registration token is not valid anymore"));
return BluebirdPromise.resolve(doc.content);
})
.then((content) => {
return BluebirdPromise.join(this._identity_check_tokens_collection.removeAsync(query),
BluebirdPromise.resolve(content));
})
.then((v) => {
return BluebirdPromise.resolve(v[1]);
});
}
set_totp_secret(userid: string, secret: TOTPSecret): BluebirdPromise<any> {
const doc = {
userid: userid,
secret: secret
};
const query = {
userid: userid
};
return this._totp_secret_collection.updateAsync(query, doc, { upsert: true });
}
get_totp_secret(userid: string): BluebirdPromise<TOTPSecretDocument> {
const query = {
userid: userid
};
return this._totp_secret_collection.findOneAsync(query);
}
private create_collection(name: string, options: any): NedbAsync {
const datastore_options = {
inMemoryOnly: options.inMemoryOnly || false,
autoload: true,
filename: ""
};
if (options.directory)
datastore_options.filename = path.resolve(options.directory, name);
return BluebirdPromise.promisifyAll(new this.nedb(datastore_options)) as NedbAsync;
}
}

View File

@ -1,5 +1,5 @@
import { ACLConfiguration } from "../../../types/Configuration";
import { ACLConfiguration } from "../configuration/Configuration";
import PatternBuilder from "./PatternBuilder";
import { Winston } from "../../../types/Dependencies";

View File

@ -1,6 +1,6 @@
import { Winston } from "../../../types/Dependencies";
import { ACLConfiguration, ACLGroupsRules, ACLUsersRules, ACLDefaultRules } from "../../../types/Configuration";
import { ACLConfiguration, ACLGroupsRules, ACLUsersRules, ACLDefaultRules } from "../configuration/Configuration";
import objectPath = require("object-path");
export default class AccessControlPatternBuilder {

View File

@ -50,12 +50,26 @@ export interface NotifierConfiguration {
filesystem?: FileSystemNotifierConfiguration;
}
export interface MongoStorageConfiguration {
url: string;
}
export interface LocalStorageConfiguration {
path?: string;
in_memory?: boolean;
}
export interface StorageConfiguration {
local?: LocalStorageConfiguration;
mongo?: MongoStorageConfiguration;
}
export interface UserConfiguration {
port?: number;
logs_level?: string;
ldap: LdapConfiguration;
session: SessionCookieConfiguration;
store_directory?: string;
storage: StorageConfiguration;
notifier: NotifierConfiguration;
access_control?: ACLConfiguration;
}
@ -65,8 +79,7 @@ export interface AppConfiguration {
logs_level: string;
ldap: LdapConfiguration;
session: SessionCookieConfiguration;
store_in_memory?: boolean;
store_directory?: string;
storage: StorageConfiguration;
notifier: NotifierConfiguration;
access_control?: ACLConfiguration;
}

View File

@ -1,6 +1,10 @@
import * as ObjectPath from "object-path";
import { AppConfiguration, UserConfiguration, NotifierConfiguration, ACLConfiguration, LdapConfiguration, SessionRedisOptions } from "./../../types/Configuration";
import {
AppConfiguration, UserConfiguration, NotifierConfiguration,
ACLConfiguration, LdapConfiguration, SessionRedisOptions,
MongoStorageConfiguration, LocalStorageConfiguration
} from "./Configuration";
const LDAP_URL_ENV_VARIABLE = "LDAP_URL";
@ -34,14 +38,17 @@ function adaptFromUserConfiguration(userConfiguration: UserConfiguration): AppCo
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),
storage: {
local: get_optional<LocalStorageConfiguration>(userConfiguration, "storage.local", undefined),
mongo: get_optional<MongoStorageConfiguration>(userConfiguration, "storage.mongo", undefined)
},
logs_level: get_optional<string>(userConfiguration, "logs_level", "info"),
notifier: ObjectPath.get<object, NotifierConfiguration>(userConfiguration, "notifier"),
access_control: ObjectPath.get<object, ACLConfiguration>(userConfiguration, "access_control")
};
}
export default class ConfigurationAdapter {
export class ConfigurationAdapter {
static adapt(userConfiguration: UserConfiguration): AppConfiguration {
const appConfiguration = adaptFromUserConfiguration(userConfiguration);

View File

@ -0,0 +1,37 @@
import ExpressSession = require("express-session");
import { AppConfiguration } from "./Configuration";
import { GlobalDependencies } from "../../../types/Dependencies";
export 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

@ -0,0 +1,5 @@
import MongoDB = require("mongodb");
export interface IMongoClient {
collection(name: string): MongoDB.Collection;
}

View File

@ -0,0 +1,6 @@
import BluebirdPromise = require("bluebird");
import { IMongoClient } from "./IMongoClient";
export interface IMongoConnector {
connect(): BluebirdPromise<IMongoClient>;
}

View File

@ -0,0 +1,5 @@
import { IMongoConnector } from "./IMongoConnector";
export interface IMongoConnectorFactory {
create(url: string): IMongoConnector;
}

View File

@ -0,0 +1,15 @@
import MongoDB = require("mongodb");
import { IMongoClient } from "./IMongoClient";
export class MongoClient implements IMongoClient {
private db: MongoDB.Db;
constructor(db: MongoDB.Db) {
this.db = db;
}
collection(name: string): MongoDB.Collection {
return this.db.collection(name);
}
}

View File

@ -0,0 +1,22 @@
import MongoDB = require("mongodb");
import BluebirdPromise = require("bluebird");
import { IMongoClient } from "./IMongoClient";
import { IMongoConnector } from "./IMongoConnector";
import { MongoClient } from "./MongoClient";
export class MongoConnector implements IMongoConnector {
private url: string;
constructor(url: string) {
this.url = url;
}
connect(): BluebirdPromise<IMongoClient> {
const connectAsync = BluebirdPromise.promisify(MongoDB.MongoClient.connect);
return connectAsync(this.url)
.then(function (db: MongoDB.Db) {
return BluebirdPromise.resolve(new MongoClient(db));
});
}
}

View File

@ -0,0 +1,12 @@
import BluebirdPromise = require("bluebird");
import { IMongoConnectorFactory } from "./IMongoConnectorFactory";
import { IMongoConnector } from "./IMongoConnector";
import { MongoConnector } from "./MongoConnector";
import MongoDB = require("mongodb");
export class MongoConnectorFactory implements IMongoConnectorFactory {
create(url: string): IMongoConnector {
return new MongoConnector(url);
}
}

View File

@ -4,7 +4,7 @@ import ldapjs = require("ldapjs");
import { Client, Attributes } from "./Client";
import { buildUserDN } from "./common";
import { LdapConfiguration } from "./../../../types/Configuration";
import { LdapConfiguration } from "../configuration/Configuration";
import { Winston, Ldapjs, Dovehash } from "../../../types/Dependencies";

View File

@ -6,7 +6,7 @@ import ldapjs = require("ldapjs");
import { buildUserDN } from "./common";
import { EventEmitter } from "events";
import { LdapConfiguration } from "./../../../types/Configuration";
import { LdapConfiguration } from "../configuration/Configuration";
import { Winston, Ldapjs, Dovehash } from "../../../types/Dependencies";
interface SearchEntry {
@ -41,10 +41,10 @@ export class Client {
reconnect: true
});
const clientLogger = (ldapClient as any).log;
/*const clientLogger = (ldapClient as any).log;
if (clientLogger) {
clientLogger.level("trace");
}
}*/
this.client = BluebirdPromise.promisifyAll(ldapClient) as ldapjs.ClientAsync;
}

View File

@ -4,7 +4,7 @@ import ldapjs = require("ldapjs");
import { Client } from "./Client";
import { buildUserDN } from "./common";
import { LdapConfiguration } from "./../../../types/Configuration";
import { LdapConfiguration } from "../configuration/Configuration";
import { Winston, Ldapjs, Dovehash } from "../../../types/Dependencies";

View File

@ -4,7 +4,7 @@ import ldapjs = require("ldapjs");
import { Client } from "./Client";
import { buildUserDN } from "./common";
import { LdapConfiguration } from "./../../../types/Configuration";
import { LdapConfiguration } from "../configuration/Configuration";
import { Winston, Ldapjs, Dovehash } from "../../../types/Dependencies";

View File

@ -1,6 +1,6 @@
import util = require("util");
import { LdapConfiguration } from "./../../../types/Configuration";
import { LdapConfiguration } from "../configuration/Configuration";
export function buildUserDN(username: string, options: LdapConfiguration): string {

View File

@ -1,24 +1,23 @@
import * as BluebirdPromise from "bluebird";
import * as util from "util";
import * as fs from "fs";
import * as Fs from "fs";
import { INotifier } from "./INotifier";
import { Identity } from "../../../types/Identity";
import { FileSystemNotifierConfiguration } from "../../../types/Configuration";
import { FileSystemNotifierConfiguration } from "../configuration/Configuration";
export class FileSystemNotifier extends INotifier {
export class FileSystemNotifier implements 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("Date: %s\nUser: %s\nSubject: %s\nLink: %s", new Date().toString(), identity.userid,
subject, link);
const writeFilePromised = BluebirdPromise.promisify<void, string, string>(fs.writeFile);
const writeFilePromised: any = BluebirdPromise.promisify(Fs.writeFile);
return writeFilePromised(this.filename, content);
}
}

View File

@ -7,16 +7,15 @@ import nodemailer = require("nodemailer");
import { Nodemailer } from "../../../types/Dependencies";
import { Identity } from "../../../types/Identity";
import { INotifier } from "../notifiers/INotifier";
import { GmailNotifierConfiguration } from "../../../types/Configuration";
import { GmailNotifierConfiguration } from "../configuration/Configuration";
import path = require("path");
const email_template = fs.readFileSync(path.join(__dirname, "../../resources/email-template.ejs"), "UTF-8");
export class GMailNotifier extends INotifier {
export class GMailNotifier implements INotifier {
private transporter: any;
constructor(options: GmailNotifierConfiguration, nodemailer: Nodemailer) {
super();
const transporter = nodemailer.createTransport({
service: "gmail",
auth: {

View File

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

View File

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

View File

@ -1,5 +1,5 @@
import { NotifierConfiguration } from "../../../types/Configuration";
import { NotifierConfiguration } from "../configuration/Configuration";
import { Nodemailer } from "../../../types/Dependencies";
import { INotifier } from "./INotifier";

View File

@ -5,14 +5,14 @@ import FirstFactorValidator = require("../FirstFactorValidator");
import Exceptions = require("../Exceptions");
import ErrorReplies = require("../ErrorReplies");
import objectPath = require("object-path");
import ServerVariables = require("../ServerVariables");
import { ServerVariablesHandler } from "../ServerVariablesHandler";
import AuthenticationSession = require("../AuthenticationSession");
type Handler = (req: express.Request, res: express.Response) => BluebirdPromise<void>;
export default function (callback: Handler): Handler {
return function (req: express.Request, res: express.Response): BluebirdPromise<void> {
const logger = ServerVariables.getLogger(req.app);
const logger = ServerVariablesHandler.getLogger(req.app);
const authSession = AuthenticationSession.get(req);
logger.debug("AuthSession is %s", JSON.stringify(authSession));

View File

@ -1,7 +1,8 @@
import BluebirdPromise = require("bluebird");
import express = require("express");
export default function (req: express.Request, res: express.Response) {
export default function (req: express.Request, res: express.Response): BluebirdPromise<void> {
res.render("errors/401");
return BluebirdPromise.resolve();
}

View File

@ -1,7 +1,8 @@
import BluebirdPromise = require("bluebird");
import express = require("express");
export default function (req: express.Request, res: express.Response) {
export default function (req: express.Request, res: express.Response): BluebirdPromise<void> {
res.render("errors/403");
return BluebirdPromise.resolve();
}

View File

@ -1,7 +1,8 @@
import BluebirdPromise = require("bluebird");
import express = require("express");
export default function (req: express.Request, res: express.Response) {
export default function (req: express.Request, res: express.Response): BluebirdPromise<void> {
res.render("errors/404");
return BluebirdPromise.resolve();
}

View File

@ -4,10 +4,10 @@ import objectPath = require("object-path");
import winston = require("winston");
import Endpoints = require("../../../endpoints");
import AuthenticationValidator = require("../../AuthenticationValidator");
import ServerVariables = require("../../ServerVariables");
import { ServerVariablesHandler } from "../../ServerVariablesHandler";
export default function (req: express.Request, res: express.Response) {
const logger = ServerVariables.getLogger(req.app);
const logger = ServerVariablesHandler.getLogger(req.app);
logger.debug("First factor: headers are %s", JSON.stringify(req.headers));

View File

@ -8,16 +8,16 @@ import { AuthenticationRegulator } from "../../AuthenticationRegulator";
import { Client, Attributes } from "../../ldap/Client";
import Endpoint = require("../../../endpoints");
import ErrorReplies = require("../../ErrorReplies");
import ServerVariables = require("../../ServerVariables");
import { ServerVariablesHandler } from "../../ServerVariablesHandler";
import AuthenticationSession = require("../../AuthenticationSession");
export default function (req: express.Request, res: express.Response): BluebirdPromise<void> {
const username: string = req.body.username;
const password: string = req.body.password;
const logger = ServerVariables.getLogger(req.app);
const ldap = ServerVariables.getLdapAuthenticator(req.app);
const config = ServerVariables.getConfiguration(req.app);
const logger = ServerVariablesHandler.getLogger(req.app);
const ldap = ServerVariablesHandler.getLdapAuthenticator(req.app);
const config = ServerVariablesHandler.getConfiguration(req.app);
if (!username || !password) {
const err = new Error("No username or password");
@ -25,8 +25,8 @@ export default function (req: express.Request, res: express.Response): BluebirdP
return BluebirdPromise.reject(err);
}
const regulator = ServerVariables.getAuthenticationRegulator(req.app);
const accessController = ServerVariables.getAccessController(req.app);
const regulator = ServerVariablesHandler.getAuthenticationRegulator(req.app);
const accessController = ServerVariablesHandler.getAccessController(req.app);
const authSession = AuthenticationSession.get(req);
logger.info("1st factor: Starting authentication of user \"%s\"", username);

View File

@ -3,15 +3,15 @@ import express = require("express");
import BluebirdPromise = require("bluebird");
import objectPath = require("object-path");
import exceptions = require("../../../Exceptions");
import ServerVariables = require("../../../ServerVariables");
import { ServerVariablesHandler } from "../../../ServerVariablesHandler";
import AuthenticationSession = require("../../../AuthenticationSession");
import ErrorReplies = require("../../../ErrorReplies");
import Constants = require("./../constants");
export default function (req: express.Request, res: express.Response): BluebirdPromise<void> {
const logger = ServerVariables.getLogger(req.app);
const ldapPasswordUpdater = ServerVariables.getLdapPasswordUpdater(req.app);
const logger = ServerVariablesHandler.getLogger(req.app);
const ldapPasswordUpdater = ServerVariablesHandler.getLdapPasswordUpdater(req.app);
const authSession = AuthenticationSession.get(req);
const newPassword = objectPath.get<express.Request, string>(req, "body.password");

View File

@ -8,7 +8,7 @@ import { IdentityValidable } from "../../../IdentityCheckMiddleware";
import { PRE_VALIDATION_TEMPLATE } from "../../../IdentityCheckPreValidationTemplate";
import Constants = require("../constants");
import { Winston } from "winston";
import ServerVariables = require("../../../ServerVariables");
import { ServerVariablesHandler } from "../../../ServerVariablesHandler";
export const TEMPLATE_NAME = "password-reset-form";
@ -18,14 +18,14 @@ export default class PasswordResetHandler implements IdentityValidable {
}
preValidationInit(req: express.Request): BluebirdPromise<Identity> {
const logger = ServerVariables.getLogger(req.app);
const logger = ServerVariablesHandler.getLogger(req.app);
const userid: string = objectPath.get<express.Request, string>(req, "query.userid");
logger.debug("Reset Password: user '%s' requested a password reset", userid);
if (!userid)
return BluebirdPromise.reject(new exceptions.AccessDeniedError("No user id provided"));
const emailsRetriever = ServerVariables.getLdapEmailsRetriever(req.app);
const emailsRetriever = ServerVariablesHandler.getLdapEmailsRetriever(req.app);
return emailsRetriever.retrieve(userid)
.then(function (emails: string[]) {
if (!emails && emails.length <= 0) throw new Error("No email found");

View File

@ -3,7 +3,7 @@ import express = require("express");
import objectPath = require("object-path");
import winston = require("winston");
import Endpoints = require("../../../endpoints");
import ServerVariables = require("../../ServerVariables");
import { ServerVariablesHandler } from "../../ServerVariablesHandler";
import AuthenticationSession = require("../../AuthenticationSession");
export default function (req: express.Request, res: express.Response) {

View File

@ -9,7 +9,7 @@ import { PRE_VALIDATION_TEMPLATE } from "../../../../IdentityCheckPreValidationT
import Constants = require("../constants");
import Endpoints = require("../../../../../endpoints");
import ErrorReplies = require("../../../../ErrorReplies");
import ServerVariables = require("../../../../ServerVariables");
import { ServerVariablesHandler } from "../../../../ServerVariablesHandler";
import AuthenticationSession = require("../../../../AuthenticationSession");
import FirstFactorValidator = require("../../../../FirstFactorValidator");
@ -53,7 +53,7 @@ export default class RegistrationHandler implements IdentityValidable {
}
postValidationResponse(req: express.Request, res: express.Response) {
const logger = ServerVariables.getLogger(req.app);
const logger = ServerVariablesHandler.getLogger(req.app);
const authSession = AuthenticationSession.get(req);
const userid = authSession.identity_check.userid;
@ -65,12 +65,12 @@ export default class RegistrationHandler implements IdentityValidable {
return;
}
const userDataStore = ServerVariables.getUserDataStore(req.app);
const totpGenerator = ServerVariables.getTOTPGenerator(req.app);
const userDataStore = ServerVariablesHandler.getUserDataStore(req.app);
const totpGenerator = ServerVariablesHandler.getTOTPGenerator(req.app);
const secret = totpGenerator.generate();
logger.debug("POST new-totp-secret: save the TOTP secret in DB");
userDataStore.set_totp_secret(userid, secret)
userDataStore.saveTOTPSecret(userid, secret)
.then(function () {
objectPath.set(req, "session", undefined);

View File

@ -2,13 +2,13 @@
import exceptions = require("../../../../Exceptions");
import objectPath = require("object-path");
import express = require("express");
import { TOTPSecretDocument } from "../../../../UserDataStore";
import { TOTPSecretDocument } from "../../../../storage/TOTPSecretDocument";
import BluebirdPromise = require("bluebird");
import FirstFactorBlocker from "../../../FirstFactorBlocker";
import Endpoints = require("../../../../../endpoints");
import redirect from "../../redirect";
import ErrorReplies = require("../../../../ErrorReplies");
import ServerVariables = require("./../../../../ServerVariables");
import { ServerVariablesHandler } from "./../../../../ServerVariablesHandler";
import AuthenticationSession = require("../../../../AuthenticationSession");
const UNAUTHORIZED_MESSAGE = "Unauthorized access";
@ -16,17 +16,17 @@ const UNAUTHORIZED_MESSAGE = "Unauthorized access";
export default FirstFactorBlocker(handler);
export function handler(req: express.Request, res: express.Response): BluebirdPromise<void> {
const logger = ServerVariables.getLogger(req.app);
const logger = ServerVariablesHandler.getLogger(req.app);
const authSession = AuthenticationSession.get(req);
const userid = authSession.userid;
logger.info("POST 2ndfactor totp: Initiate TOTP validation for user %s", userid);
const token = req.body.token;
const totpValidator = ServerVariables.getTOTPValidator(req.app);
const userDataStore = ServerVariables.getUserDataStore(req.app);
const totpValidator = ServerVariablesHandler.getTOTPValidator(req.app);
const userDataStore = ServerVariablesHandler.getUserDataStore(req.app);
logger.debug("POST 2ndfactor totp: Fetching secret for user %s", userid);
return userDataStore.get_totp_secret(userid)
return userDataStore.retrieveTOTPSecret(userid)
.then(function (doc: TOTPSecretDocument) {
logger.debug("POST 2ndfactor totp: TOTP secret is %s", JSON.stringify(doc));
return totpValidator.validate(token, doc.secret.base32);

View File

@ -1,15 +1,16 @@
import UserDataStore from "../../../../UserDataStore";
import { UserDataStore } from "../../../../storage/UserDataStore";
import objectPath = require("object-path");
import u2f_common = require("../U2FCommon");
import BluebirdPromise = require("bluebird");
import express = require("express");
import U2f = require("u2f");
import { U2FRegistration } from "../../../../../../types/U2FRegistration";
import FirstFactorBlocker from "../../../FirstFactorBlocker";
import redirect from "../../redirect";
import ErrorReplies = require("../../../../ErrorReplies");
import ServerVariables = require("../../../../ServerVariables");
import { ServerVariablesHandler } from "../../../../ServerVariablesHandler";
import AuthenticationSession = require("../../../../AuthenticationSession");
@ -34,11 +35,11 @@ function handler(req: express.Request, res: express.Response): BluebirdPromise<v
}
const userDataStore = ServerVariables.getUserDataStore(req.app);
const u2f = ServerVariables.getU2F(req.app);
const userDataStore = ServerVariablesHandler.getUserDataStore(req.app);
const u2f = ServerVariablesHandler.getU2F(req.app);
const userid = authSession.userid;
const appid = u2f_common.extract_app_id(req);
const logger = ServerVariables.getLogger(req.app);
const logger = ServerVariablesHandler.getLogger(req.app);
const registrationResponse: U2f.RegistrationData = req.body;
@ -54,7 +55,11 @@ function handler(req: express.Request, res: express.Response): BluebirdPromise<v
const registrationResult: U2f.RegistrationResult = u2fResult as U2f.RegistrationResult;
logger.info("U2F register: Store regisutration and reply");
logger.debug("U2F register: registration = %s", JSON.stringify(registrationResult));
return userDataStore.set_u2f_meta(userid, appid, registrationResult.keyHandle, registrationResult.publicKey);
const registration: U2FRegistration = {
keyHandle: registrationResult.keyHandle,
publicKey: registrationResult.publicKey
};
return userDataStore.saveU2FRegistration(userid, appid, registration);
})
.then(function () {
authSession.identity_check = undefined;

View File

@ -1,5 +1,5 @@
import UserDataStore from "../../../../UserDataStore";
import { UserDataStore } from "../../../../storage/UserDataStore";
import objectPath = require("object-path");
import u2f_common = require("../U2FCommon");
@ -8,13 +8,13 @@ import express = require("express");
import U2f = require("u2f");
import FirstFactorBlocker from "../../../FirstFactorBlocker";
import ErrorReplies = require("../../../../ErrorReplies");
import ServerVariables = require("../../../../ServerVariables");
import { ServerVariablesHandler } from "../../../../ServerVariablesHandler";
import AuthenticationSession = require("../../../../AuthenticationSession");
export default FirstFactorBlocker(handler);
function handler(req: express.Request, res: express.Response): BluebirdPromise<void> {
const logger = ServerVariables.getLogger(req.app);
const logger = ServerVariablesHandler.getLogger(req.app);
const authSession = AuthenticationSession.get(req);
if (!authSession.identity_check
@ -24,7 +24,7 @@ function handler(req: express.Request, res: express.Response): BluebirdPromise<v
return;
}
const u2f = ServerVariables.getU2F(req.app);
const u2f = ServerVariablesHandler.getU2F(req.app);
const appid: string = u2f_common.extract_app_id(req);
logger.debug("U2F register_request: headers=%s", JSON.stringify(req.headers));

View File

@ -3,22 +3,22 @@ import objectPath = require("object-path");
import u2f_common = require("../U2FCommon");
import BluebirdPromise = require("bluebird");
import express = require("express");
import UserDataStore, { U2FRegistrationDocument } from "../../../../UserDataStore";
import { UserDataStore } from "../../../../storage/UserDataStore";
import { U2FRegistrationDocument } from "../../../../storage/U2FRegistrationDocument";
import { Winston } from "../../../../../../types/Dependencies";
import U2f = require("u2f");
import exceptions = require("../../../../Exceptions");
import FirstFactorBlocker from "../../../FirstFactorBlocker";
import redirect from "../../redirect";
import ErrorReplies = require("../../../../ErrorReplies");
import ServerVariables = require("../../../../ServerVariables");
import {  ServerVariablesHandler } from "../../../../ServerVariablesHandler";
import AuthenticationSession = require("../../../../AuthenticationSession");
export default FirstFactorBlocker(handler);
export function handler(req: express.Request, res: express.Response): BluebirdPromise<void> {
const logger = ServerVariables.getLogger(req.app);
const userDataStore = ServerVariables.getUserDataStore(req.app);
const logger = ServerVariablesHandler.getLogger(req.app);
const userDataStore = ServerVariablesHandler.getUserDataStore(req.app);
const authSession = AuthenticationSession.get(req);
if (!authSession.sign_request) {
@ -29,14 +29,14 @@ export function handler(req: express.Request, res: express.Response): BluebirdPr
const userid = authSession.userid;
const appid = u2f_common.extract_app_id(req);
return userDataStore.get_u2f_meta(userid, appid)
return userDataStore.retrieveU2FRegistration(userid, appid)
.then(function (doc: U2FRegistrationDocument): BluebirdPromise<U2f.SignatureResult | U2f.Error> {
const appid = u2f_common.extract_app_id(req);
const u2f = ServerVariables.getU2F(req.app);
const appId = u2f_common.extract_app_id(req);
const u2f = ServerVariablesHandler.getU2F(req.app);
const signRequest = authSession.sign_request;
const signData: U2f.SignatureData = req.body;
logger.info("U2F sign: Finish authentication");
return BluebirdPromise.resolve(u2f.checkSignature(signRequest, signData, doc.publicKey));
return BluebirdPromise.resolve(u2f.checkSignature(signRequest, signData, doc.registration.publicKey));
})
.then(function (result: U2f.SignatureResult | U2f.Error): BluebirdPromise<void> {
if (objectPath.has(result, "errorCode"))

View File

@ -4,39 +4,39 @@ import U2f = require("u2f");
import u2f_common = require("../../../secondfactor/u2f/U2FCommon");
import BluebirdPromise = require("bluebird");
import express = require("express");
import UserDataStore, { U2FRegistrationDocument } from "../../../../UserDataStore";
import { UserDataStore } from "../../../../storage/UserDataStore";
import { U2FRegistrationDocument } from "../../../../storage/U2FRegistrationDocument";
import { Winston } from "../../../../../../types/Dependencies";
import exceptions = require("../../../../Exceptions");
import { SignMessage } from "./SignMessage";
import FirstFactorBlocker from "../../../FirstFactorBlocker";
import ErrorReplies = require("../../../../ErrorReplies");
import ServerVariables = require("../../../../ServerVariables");
import { ServerVariablesHandler } from "../../../../ServerVariablesHandler";
import AuthenticationSession = require("../../../../AuthenticationSession");
export default FirstFactorBlocker(handler);
export function handler(req: express.Request, res: express.Response): BluebirdPromise<void> {
const logger = ServerVariables.getLogger(req.app);
const userDataStore = ServerVariables.getUserDataStore(req.app);
const logger = ServerVariablesHandler.getLogger(req.app);
const userDataStore = ServerVariablesHandler.getUserDataStore(req.app);
const authSession = AuthenticationSession.get(req);
const userid = authSession.userid;
const appid = u2f_common.extract_app_id(req);
return userDataStore.get_u2f_meta(userid, appid)
const userId = authSession.userid;
const appId = u2f_common.extract_app_id(req);
return userDataStore.retrieveU2FRegistration(userId, appId)
.then(function (doc: U2FRegistrationDocument): BluebirdPromise<SignMessage> {
if (!doc)
return BluebirdPromise.reject(new exceptions.AccessDeniedError("No U2F registration found"));
const u2f = ServerVariables.getU2F(req.app);
const u2f = ServerVariablesHandler.getU2F(req.app);
const appId: string = u2f_common.extract_app_id(req);
logger.info("U2F sign_request: Start authentication to app %s", appId);
logger.debug("U2F sign_request: appId=%s, keyHandle=%s", appId, JSON.stringify(doc.keyHandle));
logger.debug("U2F sign_request: appId=%s, keyHandle=%s", appId, JSON.stringify(doc.registration.keyHandle));
const request = u2f.request(appId, doc.keyHandle);
const request = u2f.request(appId, doc.registration.keyHandle);
const authenticationMessage: SignMessage = {
request: request,
keyHandle: doc.keyHandle
keyHandle: doc.registration.keyHandle
};
return BluebirdPromise.resolve(authenticationMessage);
})

View File

@ -8,12 +8,12 @@ import exceptions = require("../../Exceptions");
import winston = require("winston");
import AuthenticationValidator = require("../../AuthenticationValidator");
import ErrorReplies = require("../../ErrorReplies");
import ServerVariables = require("../../ServerVariables");
import { ServerVariablesHandler } from "../../ServerVariablesHandler";
import AuthenticationSession = require("../../AuthenticationSession");
function verify_filter(req: express.Request, res: express.Response): BluebirdPromise<void> {
const logger = ServerVariables.getLogger(req.app);
const accessController = ServerVariables.getAccessController(req.app);
const logger = ServerVariablesHandler.getLogger(req.app);
const accessController = ServerVariablesHandler.getAccessController(req.app);
const authSession = AuthenticationSession.get(req);
logger.debug("Verify: headers are %s", JSON.stringify(req.headers));
@ -40,7 +40,7 @@ function verify_filter(req: express.Request, res: express.Response): BluebirdPro
}
export default function (req: express.Request, res: express.Response) {
const logger = ServerVariables.getLogger(req.app);
const logger = ServerVariablesHandler.getLogger(req.app);
verify_filter(req, res)
.then(function () {
res.status(204);

View File

@ -0,0 +1,6 @@
export interface AuthenticationTraceDocument {
userId: string;
date: Date;
isAuthenticationSuccessful: boolean;
}

View File

@ -0,0 +1,15 @@
import { ICollectionFactory } from "./ICollectionFactory";
import { NedbCollectionFactory, NedbOptions } from "./nedb/NedbCollectionFactory";
import { MongoCollectionFactory } from "./mongo/MongoCollectionFactory";
import { IMongoClient } from "../connectors/mongo/IMongoClient";
export class CollectionFactoryFactory {
static createNedb(options: NedbOptions): ICollectionFactory {
return new NedbCollectionFactory(options);
}
static createMongo(client: IMongoClient): ICollectionFactory {
return new MongoCollectionFactory(client);
}
}

View File

@ -0,0 +1,11 @@
/* istanbul ignore next */
import BluebirdPromise = require("bluebird");
/* istanbul ignore next */
export interface ICollection {
find(query: any, sortKeys: any, count: number): BluebirdPromise<any>;
findOne(query: any): BluebirdPromise<any>;
update(query: any, updateQuery: any, options?: any): BluebirdPromise<any>;
remove(query: any): BluebirdPromise<any>;
insert(document: any): BluebirdPromise<any>;
}

View File

@ -0,0 +1,6 @@
import { ICollection } from "./ICollection";
export interface ICollectionFactory {
build(collectionName: string): ICollection;
}

View File

@ -0,0 +1,21 @@
import BluebirdPromise = require("bluebird");
import { TOTPSecretDocument } from "./TOTPSecretDocument";
import { U2FRegistrationDocument } from "./U2FRegistrationDocument";
import { U2FRegistration } from "../../../types/U2FRegistration";
import { TOTPSecret } from "../../../types/TOTPSecret";
import { AuthenticationTraceDocument } from "./AuthenticationTraceDocument";
import { IdentityValidationDocument } from "./IdentityValidationDocument";
export interface IUserDataStore {
saveU2FRegistration(userId: string, appId: string, registration: U2FRegistration): BluebirdPromise<void>;
retrieveU2FRegistration(userId: string, appId: string): BluebirdPromise<U2FRegistrationDocument>;
saveAuthenticationTrace(userId: string, isAuthenticationSuccessful: boolean): BluebirdPromise<void>;
retrieveLatestAuthenticationTraces(userId: string, isAuthenticationSuccessful: boolean, count: number): BluebirdPromise<AuthenticationTraceDocument[]>;
produceIdentityValidationToken(userId: string, token: string, challenge: string, maxAge: number): BluebirdPromise<any>;
consumeIdentityValidationToken(token: string, challenge: string): BluebirdPromise<IdentityValidationDocument>;
saveTOTPSecret(userId: string, secret: TOTPSecret): BluebirdPromise<void>;
retrieveTOTPSecret(userId: string): BluebirdPromise<TOTPSecretDocument>;
}

View File

@ -0,0 +1,7 @@
export interface IdentityValidationDocument {
userId: string;
token: string;
challenge: string;
maxDate: Date;
}

View File

@ -0,0 +1,6 @@
import { TOTPSecret } from "../../../types/TOTPSecret";
export interface TOTPSecretDocument {
userid: string;
secret: TOTPSecret;
}

View File

@ -0,0 +1,8 @@
import { U2FRegistration } from "../../../types/U2FRegistration";
export interface U2FRegistrationDocument {
userId: string;
appId: string;
registration: U2FRegistration;
}

View File

@ -0,0 +1,144 @@
import * as BluebirdPromise from "bluebird";
import * as path from "path";
import { IUserDataStore } from "./IUserDataStore";
import { ICollection } from "./ICollection";
import { ICollectionFactory } from "./ICollectionFactory";
import { TOTPSecretDocument } from "./TOTPSecretDocument";
import { U2FRegistrationDocument } from "./U2FRegistrationDocument";
import { U2FRegistration } from "../../../types/U2FRegistration";
import { TOTPSecret } from "../../../types/TOTPSecret";
import { AuthenticationTraceDocument } from "./AuthenticationTraceDocument";
import { IdentityValidationDocument } from "./IdentityValidationDocument";
// Constants
const IDENTITY_VALIDATION_TOKENS_COLLECTION_NAME = "identity_validation_tokens";
const AUTHENTICATION_TRACES_COLLECTION_NAME = "authentication_traces";
const U2F_REGISTRATIONS_COLLECTION_NAME = "u2f_registrations";
const TOTP_SECRETS_COLLECTION_NAME = "totp_secrets";
export interface U2FRegistrationKey {
userId: string;
appId: string;
}
// Source
export class UserDataStore implements IUserDataStore {
private u2fSecretCollection: ICollection;
private identityCheckTokensCollection: ICollection;
private authenticationTracesCollection: ICollection;
private totpSecretCollection: ICollection;
private collectionFactory: ICollectionFactory;
constructor(collectionFactory: ICollectionFactory) {
this.collectionFactory = collectionFactory;
this.u2fSecretCollection = this.collectionFactory.build(U2F_REGISTRATIONS_COLLECTION_NAME);
this.identityCheckTokensCollection = this.collectionFactory.build(IDENTITY_VALIDATION_TOKENS_COLLECTION_NAME);
this.authenticationTracesCollection = this.collectionFactory.build(AUTHENTICATION_TRACES_COLLECTION_NAME);
this.totpSecretCollection = this.collectionFactory.build(TOTP_SECRETS_COLLECTION_NAME);
}
saveU2FRegistration(userId: string, appId: string, registration: U2FRegistration): BluebirdPromise<void> {
const newDocument: U2FRegistrationDocument = {
userId: userId,
appId: appId,
registration: registration
};
const filter: U2FRegistrationKey = {
userId: userId,
appId: appId
};
return this.u2fSecretCollection.update(filter, newDocument, { upsert: true });
}
retrieveU2FRegistration(userId: string, appId: string): BluebirdPromise<U2FRegistrationDocument> {
const filter: U2FRegistrationKey = {
userId: userId,
appId: appId
};
return this.u2fSecretCollection.findOne(filter);
}
saveAuthenticationTrace(userId: string, isAuthenticationSuccessful: boolean): BluebirdPromise<void> {
const newDocument: AuthenticationTraceDocument = {
userId: userId,
date: new Date(),
isAuthenticationSuccessful: isAuthenticationSuccessful,
};
return this.authenticationTracesCollection.insert(newDocument);
}
retrieveLatestAuthenticationTraces(userId: string, isAuthenticationSuccessful: boolean, count: number): BluebirdPromise<AuthenticationTraceDocument[]> {
const q = {
userId: userId,
isAuthenticationSuccessful: isAuthenticationSuccessful
};
return this.authenticationTracesCollection.find(q, { date: -1 }, count);
}
produceIdentityValidationToken(userId: string, token: string, challenge: string, maxAge: number): BluebirdPromise<any> {
const newDocument: IdentityValidationDocument = {
userId: userId,
token: token,
challenge: challenge,
maxDate: new Date(new Date().getTime() + maxAge)
};
return this.identityCheckTokensCollection.insert(newDocument);
}
consumeIdentityValidationToken(token: string, challenge: string): BluebirdPromise<IdentityValidationDocument> {
const that = this;
const filter = {
token: token,
challenge: challenge
};
let identityValidationDocument: IdentityValidationDocument;
return this.identityCheckTokensCollection.findOne(filter)
.then(function (doc: IdentityValidationDocument) {
if (!doc) {
return BluebirdPromise.reject(new Error("Registration token does not exist"));
}
identityValidationDocument = doc;
const current_date = new Date();
if (current_date > doc.maxDate)
return BluebirdPromise.reject(new Error("Registration token is not valid anymore"));
return that.identityCheckTokensCollection.remove(filter);
})
.then(() => {
return BluebirdPromise.resolve(identityValidationDocument);
});
}
saveTOTPSecret(userId: string, secret: TOTPSecret): BluebirdPromise<void> {
const doc = {
userId: userId,
secret: secret
};
const filter = {
userId: userId
};
return this.totpSecretCollection.update(filter, doc, { upsert: true });
}
retrieveTOTPSecret(userId: string): BluebirdPromise<TOTPSecretDocument> {
const filter = {
userId: userId
};
return this.totpSecretCollection.findOne(filter);
}
}

View File

@ -0,0 +1,44 @@
import BluebirdPromise = require("bluebird");
import { ICollection } from "../ICollection";
import MongoDB = require("mongodb");
export class MongoCollection implements ICollection {
private collection: MongoDB.Collection;
constructor(collection: MongoDB.Collection) {
this.collection = collection;
}
find(query: any, sortKeys?: any, count?: number): BluebirdPromise<any> {
const q = this.collection.find(query).sort(sortKeys).limit(count);
const toArrayAsync = BluebirdPromise.promisify(q.toArray, { context: q });
return toArrayAsync();
}
findOne(query: any): BluebirdPromise<any> {
const findOneAsync = BluebirdPromise.promisify<any, any>(this.collection.findOne, { context: this.collection });
return findOneAsync(query);
}
update(query: any, updateQuery: any, options?: any): BluebirdPromise<any> {
const updateAsync = BluebirdPromise.promisify<any, any, any, any>(this.collection.update, { context: this.collection });
return updateAsync(query, updateQuery, options);
}
remove(query: any): BluebirdPromise<any> {
const removeAsync = BluebirdPromise.promisify<any, any>(this.collection.remove, { context: this.collection });
return removeAsync(query);
}
insert(document: any): BluebirdPromise<any> {
const insertAsync = BluebirdPromise.promisify<any, any>(this.collection.insert, { context: this.collection });
return insertAsync(document);
}
count(query: any): BluebirdPromise<number> {
const countAsync = BluebirdPromise.promisify<any, any>(this.collection.count, { context: this.collection });
return countAsync(query);
}
}

View File

@ -0,0 +1,19 @@
import BluebirdPromise = require("bluebird");
import { ICollection } from "../ICollection";
import { ICollectionFactory } from "../ICollectionFactory";
import { MongoCollection } from "./MongoCollection";
import path = require("path");
import MongoDB = require("mongodb");
import { IMongoClient } from "../../connectors/mongo/IMongoClient";
export class MongoCollectionFactory implements ICollectionFactory {
private mongoClient: IMongoClient;
constructor(mongoClient: IMongoClient) {
this.mongoClient = mongoClient;
}
build(collectionName: string): ICollection {
return new MongoCollection(this.mongoClient.collection(collectionName));
}
}

View File

@ -0,0 +1,38 @@
import BluebirdPromise = require("bluebird");
import { ICollection } from "../ICollection";
import Nedb = require("nedb");
import { NedbAsync } from "nedb";
export class NedbCollection implements ICollection {
private collection: NedbAsync;
constructor(options: Nedb.DataStoreOptions) {
this.collection = BluebirdPromise.promisifyAll(new Nedb(options)) as NedbAsync;
}
find(query: any, sortKeys?: any, count?: number): BluebirdPromise<any> {
const q = this.collection.find(query).sort(sortKeys).limit(count);
return BluebirdPromise.promisify(q.exec, { context: q })();
}
findOne(query: any): BluebirdPromise<any> {
return this.collection.findOneAsync(query);
}
update(query: any, updateQuery: any, options?: any): BluebirdPromise<any> {
return this.collection.updateAsync(query, updateQuery, options);
}
remove(query: any): BluebirdPromise<any> {
return this.collection.removeAsync(query);
}
insert(document: any): BluebirdPromise<any> {
return this.collection.insertAsync(document);
}
count(query: any): BluebirdPromise<number> {
return this.collection.countAsync(query);
}
}

View File

@ -0,0 +1,28 @@
import { ICollection } from "../ICollection";
import { ICollectionFactory } from "../ICollectionFactory";
import { NedbCollection } from "./NedbCollection";
import path = require("path");
import Nedb = require("nedb");
export interface NedbOptions {
inMemoryOnly?: boolean;
directory?: string;
}
export class NedbCollectionFactory implements ICollectionFactory {
private options: NedbOptions;
constructor(options: NedbOptions) {
this.options = options;
}
build(collectionName: string): ICollection {
const datastoreOptions = {
inMemoryOnly: this.options.inMemoryOnly || false,
autoload: true,
filename: (this.options.directory) ? path.resolve(this.options.directory, collectionName) : undefined
};
return new NedbCollection(datastoreOptions);
}
}

View File

@ -8,4 +8,4 @@ block form-header
<img class="header-img" src="/img/warning.png" alt="">
block content
<p>You are not authorized.</p>
<p>You are either not authorized or not <a href="/">logged in</a>.</p>

View File

@ -8,4 +8,4 @@ block form-header
<img class="header-img" src="/img/warning.png" alt="">
block content
<p>You are not authorized.</p>
<p>You are either not authorized or not <a href="/">logged in</a>.</p>

View File

@ -12,7 +12,7 @@ block content
</div>
<button class="btn btn-lg btn-primary btn-block" type="submit">Sign in</button>
<!-- <label class="checkbox pull-left"><input type="checkbox" value="remember-me">Remember me</label> -->
a(href=reset_password_request_endpoint, class="pull-right link") Forgot password?
a(href=reset_password_request_endpoint, class="pull-right link forgot-password") Forgot password?
<span class="clearfix"></span>
</form>

View File

@ -20,7 +20,7 @@ html
</div>
</div>
<div class="row poweredby-block">
<div class="poweredby col-xs-10 col-xs-offset-1 col-sm-2 col-sm-offset-8 col-md-6 col-md-offset-4">Powered by <a class="authelia-brand" href="https://github.com/clems4ever/authelia">Authelia</a></div>
<div class="poweredby col-xs-6 col-xs-offset-4 col-sm-6 col-sm-offset-4 col-md-6 col-md-offset-4">Powered by <a class="authelia-brand" href="https://github.com/clems4ever/authelia">Authelia</a></div>
</div>
</div>
</div>

View File

@ -9,14 +9,14 @@ block content
<div class="form-inputs">
<input type="text" class="form-control" id="token" placeholder="Token" required autofocus>
</div>
<button class="btn btn-lg btn-primary btn-block" type="submit">TOTP</button>
a(href=totp_identity_start_endpoint, class="pull-right link") Need to register?
<button class="btn btn-lg btn-primary btn-block totp-button" type="submit">TOTP</button>
a(href=totp_identity_start_endpoint, class="pull-right link register-totp") Need to register?
<span class="clearfix"></span>
</form>
<hr>
<form class="form-signin u2f">
<button class="btn btn-lg btn-primary btn-block" type="submit">U2F</button>
a(href=u2f_identity_start_endpoint, class="pull-right link") Need to register?
<button class="btn btn-lg btn-primary btn-block u2f-button" type="submit">U2F</button>
a(href=u2f_identity_start_endpoint, class="pull-right link register-u2f") Need to register?
<span class="clearfix"></span>
</form>

View File

@ -0,0 +1,5 @@
export interface U2FRegistration {
keyHandle: string;
publicKey: string;
}

View File

@ -5,8 +5,9 @@ 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>;
findOneAsync<T>(query: any): BluebirdPromise<T>;
insertAsync<T>(newDoc: T): BluebirdPromise<any>;
removeAsync(query: any): BluebirdPromise<any>;
countAsync(query: any): BluebirdPromise<number>
}
}

View File

@ -0,0 +1,38 @@
Feature: User has access restricted access to domains
Scenario: User john has admin access
When I register TOTP and login with user "john" and password "password"
Then I have access to:
| url |
| https://public.test.local:8080/secret.html |
| https://secret.test.local:8080/secret.html |
| https://secret1.test.local:8080/secret.html |
| https://secret2.test.local:8080/secret.html |
| https://mx1.mail.test.local:8080/secret.html |
| https://mx2.mail.test.local:8080/secret.html |
Scenario: User bob has restricted access
When I register TOTP and login with user "bob" and password "password"
Then I have access to:
| url |
| https://public.test.local:8080/secret.html |
| https://secret.test.local:8080/secret.html |
| https://secret2.test.local:8080/secret.html |
| https://mx1.mail.test.local:8080/secret.html |
| https://mx2.mail.test.local:8080/secret.html |
And I have no access to:
| url |
| https://secret1.test.local:8080/secret.html |
Scenario: User harry has restricted access
When I register TOTP and login with user "harry" and password "password"
Then I have access to:
| url |
| https://public.test.local:8080/secret.html |
| https://secret1.test.local:8080/secret.html |
And I have no access to:
| url |
| https://secret.test.local:8080/secret.html |
| https://secret2.test.local:8080/secret.html |
| https://mx1.mail.test.local:8080/secret.html |
| https://mx2.mail.test.local:8080/secret.html |

View File

@ -0,0 +1,53 @@
Feature: User validate first factor
Scenario: User succeeds first factor
Given I visit "https://auth.test.local:8080/"
When I set field "username" to "bob"
And I set field "password" to "password"
And I click on "Sign in"
Then I'm redirected to "https://auth.test.local:8080/secondfactor"
Scenario: User fails first factor
Given I visit "https://auth.test.local:8080/"
When I set field "username" to "john"
And I set field "password" to "bad-password"
And I click on "Sign in"
Then I get a notification with message "Error during authentication: Authetication failed. Please check your credentials."
Scenario: User succeeds TOTP second factor
Given I visit "https://auth.test.local:8080/"
And I login with user "john" and password "password"
And I register a TOTP secret called "Sec0"
When I visit "https://secret.test.local:8080/secret.html" and get redirected "https://auth.test.local:8080/"
And I login with user "john" and password "password"
And I use "Sec0" as TOTP token handle
And I click on "TOTP"
Then I'm redirected to "https://secret.test.local:8080/secret.html"
Scenario: User fails TOTP second factor
When I visit "https://secret.test.local:8080/secret.html" and get redirected "https://auth.test.local:8080/"
And I login with user "john" and password "password"
And I use "BADTOKEN" as TOTP token
And I click on "TOTP"
Then I get a notification with message "Error while validating TOTP token. Cause: error"
Scenario: User logs out
Given I visit "https://auth.test.local:8080/"
And I login with user "john" and password "password"
And I register a TOTP secret called "Sec0"
And I visit "https://auth.test.local:8080/"
And I login with user "john" and password "password"
And I use "Sec0" as TOTP token handle
When I visit "https://auth.test.local:8080/logout?redirect=https://www.google.fr"
And I visit "https://secret.test.local:8080/secret.html"
Then I'm redirected to "https://auth.test.local:8080/"
Scenario: Logout redirects user
Given I visit "https://auth.test.local:8080/"
And I login with user "john" and password "password"
And I register a TOTP secret called "Sec0"
And I visit "https://auth.test.local:8080/"
And I login with user "john" and password "password"
And I use "Sec0" as TOTP token handle
When I visit "https://auth.test.local:8080/logout?redirect=https://www.google.fr"
Then I'm redirected to "https://www.google.fr"

View File

@ -0,0 +1,7 @@
Feature: User is redirected to authelia when he is not authenticated
Scenario: User is redirected to authelia
Given I'm on https://home.test.local:8080
When I click on the link to secret.test.local
Then I'm redirected to "https://auth.test.local:8080/"

View File

@ -0,0 +1,33 @@
Feature: User is able to reset his password
Scenario: User is redirected to password reset page
Given I'm on https://auth.test.local:8080
When I click on the link "Forgot password?"
Then I'm redirected to "https://auth.test.local:8080/password-reset/request"
Scenario: User get an email with a link to reset password
Given I'm on https://auth.test.local:8080/password-reset/request
When I set field "username" to "james"
And I click on "Reset Password"
Then I get a notification with message "An email has been sent. Click on the link to change your password."
Scenario: User resets his password
Given I'm on https://auth.test.local:8080/password-reset/request
And I set field "username" to "james"
And I click on "Reset Password"
When I click on the link of the email
And I set field "password1" to "newpassword"
And I set field "password2" to "newpassword"
And I click on "Reset Password"
Then I'm redirected to "https://auth.test.local:8080/"
Scenario: User does not confirm new password
Given I'm on https://auth.test.local:8080/password-reset/request
And I set field "username" to "james"
And I click on "Reset Password"
When I click on the link of the email
And I set field "password1" to "newpassword"
And I set field "password2" to "newpassword2"
And I click on "Reset Password"
Then I get a notification with message "The passwords are different"

View File

@ -0,0 +1,13 @@
Feature: Authelia keeps user sessions despite the application restart
Scenario: Session is still valid after Authelia restarts
When I register TOTP and login with user "john" and password "password"
And the application restarts
Then I have access to:
| url |
| https://public.test.local:8080/secret.html |
| https://secret.test.local:8080/secret.html |
| https://secret1.test.local:8080/secret.html |
| https://secret2.test.local:8080/secret.html |
| https://mx1.mail.test.local:8080/secret.html |
| https://mx2.mail.test.local:8080/secret.html |

View File

@ -0,0 +1,17 @@
Feature: Non authenticated users have no access to certain pages
Scenario Outline: User has no access to protected pages
When I visit "<url>"
Then I get an error <error code>
Examples:
| url | error code |
| https://auth.test.local:8080/secondfactor | 401 |
| https://auth.test.local:8080/verify | 401 |
| https://auth.test.local:8080/secondfactor/u2f/identity/start | 401 |
| https://auth.test.local:8080/secondfactor/u2f/identity/finish | 403 |
| https://auth.test.local:8080/secondfactor/totp/identity/start | 401 |
| https://auth.test.local:8080/secondfactor/totp/identity/finish | 403 |
| https://auth.test.local:8080/password-reset/identity/start | 403 |
| https://auth.test.local:8080/password-reset/identity/finish | 403 |

View File

@ -0,0 +1,7 @@
import Cucumber = require("cucumber");
Cucumber.defineSupportCode(function({After}) {
After(function() {
return this.driver.quit();
});
});

View File

@ -0,0 +1,92 @@
import Cucumber = require("cucumber");
import seleniumWebdriver = require("selenium-webdriver");
import Assert = require("assert");
import Fs = require("fs");
import Speakeasy = require("speakeasy");
import CustomWorld = require("../support/world");
Cucumber.defineSupportCode(function ({ Given, When, Then }) {
When(/^I visit "(https:\/\/[a-z0-9:.\/=?-]+)"$/, function (link: string) {
return this.visit(link);
});
When("I set field {stringInDoubleQuotes} to {stringInDoubleQuotes}", function (fieldName: string, content: string) {
return this.setFieldTo(fieldName, content);
});
When("I click on {stringInDoubleQuotes}", function (text: string) {
return this.clickOnButton(text);
});
Given("I login with user {stringInDoubleQuotes} and password {stringInDoubleQuotes}", function (username: string, password: string) {
return this.loginWithUserPassword(username, password);
});
Given("I register a TOTP secret called {stringInDoubleQuotes}", function (handle: string) {
return this.registerTotpSecret(handle);
});
Given("I use {stringInDoubleQuotes} as TOTP token", function (token: string) {
return this.useTotpToken(token);
});
Given("I use {stringInDoubleQuotes} as TOTP token handle", function (handle) {
return this.useTotpTokenHandle(handle);
});
Then("I get a notification with message {stringInDoubleQuotes}", function (notificationMessage: string) {
const that = this;
that.driver.sleep(500);
return this.driver
.findElement(seleniumWebdriver.By.className("notifyjs-corner"))
.findElement(seleniumWebdriver.By.tagName("span"))
.findElement(seleniumWebdriver.By.xpath("//span[contains(.,'" + notificationMessage + "')]"));
});
When("I visit {stringInDoubleQuotes} and get redirected {stringInDoubleQuotes}", function (url: string, redirectUrl: string) {
const that = this;
return this.driver.get(url)
.then(function () {
return that.driver.wait(seleniumWebdriver.until.urlIs(redirectUrl), 2000);
});
});
Given("I register TOTP and login with user {stringInDoubleQuotes} and password {stringInDoubleQuotes}", function (username: string, password: string) {
return this.registerTotpAndSignin(username, password);
});
function hasAccessToSecret(link: string, driver: any) {
return driver.get(link)
.then(function () {
return driver.findElement(seleniumWebdriver.By.tagName("body")).getText()
.then(function (body: string) {
Assert(body.indexOf("This is a very important secret!") > -1);
});
});
}
function hasNoAccessToSecret(link: string, driver: any) {
return driver.get(link)
.then(function () {
return driver.wait(seleniumWebdriver.until.urlIs("https://auth.test.local:8080/"));
});
}
Then("I have access to:", function (dataTable: Cucumber.TableDefinition) {
const promises = [];
for (let i = 0; i < dataTable.rows().length; i++) {
const url = (dataTable.hashes() as any)[i].url;
promises.push(hasAccessToSecret(url, this.driver));
}
return Promise.all(promises);
});
Then("I have no access to:", function (dataTable: Cucumber.TableDefinition) {
const promises = [];
for (let i = 0; i < dataTable.rows().length; i++) {
const url = (dataTable.hashes() as any)[i].url;
promises.push(hasNoAccessToSecret(url, this.driver));
}
return Promise.all(promises);
});
});

View File

@ -0,0 +1,17 @@
import Cucumber = require("cucumber");
import seleniumWebdriver = require("selenium-webdriver");
import Assert = require("assert");
Cucumber.defineSupportCode(function ({ Given, When, Then }) {
Given("I'm on https://{string}", function (link: string) {
return this.driver.get("https://" + link);
});
When("I click on the link to {string}", function (link: string) {
return this.driver.findElement(seleniumWebdriver.By.linkText(link)).click();
});
Then("I'm redirected to {stringInDoubleQuotes}", function (link: string) {
return this.driver.wait(seleniumWebdriver.until.urlContains(link), 5000);
});
});

View File

@ -0,0 +1,20 @@
import Cucumber = require("cucumber");
import seleniumWebdriver = require("selenium-webdriver");
import Assert = require("assert");
import Fs = require("fs");
Cucumber.defineSupportCode(function ({ Given, When, Then }) {
When("I click on the link {stringInDoubleQuotes}", function (text: string) {
return this.driver.findElement(seleniumWebdriver.By.linkText(text)).click();
});
When("I click on the link of the email", function () {
const notif = Fs.readFileSync("./notifications/notification.txt").toString();
const regexp = new RegExp(/Link: (.+)/);
const match = regexp.exec(notif);
const link = match[1];
const that = this;
return this.driver.get(link);
});
});

View File

@ -0,0 +1,12 @@
import Cucumber = require("cucumber");
import seleniumWebdriver = require("selenium-webdriver");
import Assert = require("assert");
import ChildProcess = require("child_process");
import BluebirdPromise = require("bluebird");
Cucumber.defineSupportCode(function ({ Given, When, Then }) {
When(/^the application restarts$/, {timeout: 15 * 1000}, function () {
const exec = BluebirdPromise.promisify(ChildProcess.exec);
return exec("./scripts/example/dc-example.sh restart authelia && sleep 2");
});
});

View File

@ -0,0 +1,11 @@
import Cucumber = require("cucumber");
import seleniumWebdriver = require("selenium-webdriver");
import Assert = require("assert");
Cucumber.defineSupportCode(function ({ Given, When, Then }) {
Then("I get an error {number}", function (code: number) {
return this.driver
.findElement(seleniumWebdriver.By.tagName("h1"))
.findElement(seleniumWebdriver.By.xpath("//h1[contains(.,'Error " + code + "')]"));
});
});

View File

@ -0,0 +1,110 @@
require("chromedriver");
import seleniumWebdriver = require("selenium-webdriver");
import Cucumber = require("cucumber");
import Fs = require("fs");
import Speakeasy = require("speakeasy");
function CustomWorld() {
const that = this;
this.driver = new seleniumWebdriver.Builder()
.forBrowser("chrome")
.build();
this.totpSecrets = {};
this.visit = function (link: string) {
return this.driver.get(link);
};
this.setFieldTo = function (fieldName: string, content: string) {
return this.driver.findElement(seleniumWebdriver.By.id(fieldName))
.sendKeys(content);
};
this.clickOnButton = function (buttonText: string) {
return this.driver
.findElement(seleniumWebdriver.By.tagName("button"))
.findElement(seleniumWebdriver.By.xpath("//button[contains(.,'" + buttonText + "')]"))
.click();
};
this.loginWithUserPassword = function (username: string, password: string) {
return this.driver
.findElement(seleniumWebdriver.By.id("username"))
.sendKeys(username)
.then(function () {
return that.driver.findElement(seleniumWebdriver.By.id("password"))
.sendKeys(password);
})
.then(function () {
return that.driver.findElement(seleniumWebdriver.By.tagName("button"))
.click();
})
.then(function () {
return that.driver.wait(seleniumWebdriver.until.elementLocated(seleniumWebdriver.By.className("register-totp")), 4000);
});
};
this.registerTotpSecret = function (totpSecretHandle: string) {
return this.driver.findElement(seleniumWebdriver.By.className("register-totp")).click()
.then(function () {
const notif = Fs.readFileSync("./notifications/notification.txt").toString();
const regexp = new RegExp(/Link: (.+)/);
const match = regexp.exec(notif);
const link = match[1];
console.log("Link: " + link);
return that.driver.get(link);
})
.then(function () {
return that.driver.wait(seleniumWebdriver.until.elementLocated(seleniumWebdriver.By.id("secret")), 1000);
})
.then(function () {
return that.driver.findElement(seleniumWebdriver.By.id("secret")).getText();
})
.then(function (secret: string) {
that.totpSecrets[totpSecretHandle] = secret;
});
};
this.useTotpTokenHandle = function (totpSecretHandle: string) {
const token = Speakeasy.totp({
secret: this.totpSecrets[totpSecretHandle],
encoding: "base32"
});
return this.useTotpToken(token);
};
this.useTotpToken = function (totpSecret: string) {
return this.driver.findElement(seleniumWebdriver.By.id("token"))
.sendKeys(totpSecret);
};
this.registerTotpAndSignin = function (username: string, password: string) {
const totpHandle = "HANDLE";
const authUrl = "https://auth.test.local:8080/";
const that = this;
return this.visit(authUrl)
.then(function () {
return that.loginWithUserPassword(username, password);
})
.then(function () {
return that.registerTotpSecret(totpHandle);
})
.then(function () {
return that.visit(authUrl);
})
.then(function () {
return that.loginWithUserPassword(username, password);
})
.then(function () {
return that.useTotpTokenHandle(totpHandle);
})
.then(function () {
return that.clickOnButton("TOTP");
});
};
}
Cucumber.defineSupportCode(function ({ setWorldConstructor }) {
setWorldConstructor(CustomWorld);
});

View File

@ -1,4 +0,0 @@
FROM node:7-alpine
WORKDIR /usr/src

View File

@ -1,30 +0,0 @@
version: '2'
services:
integration-tests:
build: ./test/integration
command: ./node_modules/.bin/mocha --compilers ts:ts-node/register --recursive test/integration
volumes:
- ./:/usr/src
networks:
- example-network
nginx-tests:
image: nginx:alpine
volumes:
- ./example/nginx/html:/usr/share/nginx/html
- ./example/nginx/ssl:/etc/ssl
- ./test/integration/nginx.conf:/etc/nginx/nginx.conf
expose:
- "8080"
depends_on:
- authelia
networks:
example-network:
aliases:
- home.test.local
- secret.test.local
- secret1.test.local
- secret2.test.local
- mx1.mail.test.local
- mx2.mail.test.local
- auth.test.local

View File

@ -1,112 +0,0 @@
process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
import Request = require("request");
import Assert = require("assert");
import BluebirdPromise = require("bluebird");
import Util = require("util");
import Redis = require("redis");
import Endpoints = require("../../src/server/endpoints");
const RequestAsync = BluebirdPromise.promisifyAll(Request) as typeof Request;
const DOMAIN = "test.local";
const PORT = 8080;
const HOME_URL = Util.format("https://%s.%s:%d", "home", DOMAIN, PORT);
const SECRET_URL = Util.format("https://%s.%s:%d", "secret", DOMAIN, PORT);
const SECRET1_URL = Util.format("https://%s.%s:%d", "secret1", DOMAIN, PORT);
const SECRET2_URL = Util.format("https://%s.%s:%d", "secret2", DOMAIN, PORT);
const MX1_URL = Util.format("https://%s.%s:%d", "mx1.mail", DOMAIN, PORT);
const MX2_URL = Util.format("https://%s.%s:%d", "mx2.mail", DOMAIN, PORT);
const BASE_AUTH_URL = Util.format("https://%s.%s:%d", "auth", DOMAIN, PORT);
const FIRST_FACTOR_URL = Util.format("%s/api/firstfactor", BASE_AUTH_URL);
const LOGOUT_URL = Util.format("%s/logout", BASE_AUTH_URL);
const redisOptions = {
host: "redis",
port: 6379
};
describe("integration tests", function () {
let redisClient: Redis.RedisClient;
before(function () {
redisClient = Redis.createClient(redisOptions);
});
function str_contains(str: string, pattern: string) {
return str.indexOf(pattern) != -1;
}
function test_homepage_is_correct(body: string) {
Assert(str_contains(body, BASE_AUTH_URL + Endpoints.LOGOUT_GET + "?redirect=" + HOME_URL + "/"));
Assert(str_contains(body, HOME_URL + "/secret.html"));
Assert(str_contains(body, SECRET_URL + "/secret.html"));
Assert(str_contains(body, SECRET1_URL + "/secret.html"));
Assert(str_contains(body, SECRET2_URL + "/secret.html"));
Assert(str_contains(body, MX1_URL + "/secret.html"));
Assert(str_contains(body, MX2_URL + "/secret.html"));
Assert(str_contains(body, "Access the secret"));
}
it("should access the home page", function () {
return RequestAsync.getAsync(HOME_URL)
.then(function (response: Request.RequestResponse) {
Assert.equal(200, response.statusCode);
test_homepage_is_correct(response.body);
});
});
it("should access the authentication page", function () {
return RequestAsync.getAsync(BASE_AUTH_URL)
.then(function (response: Request.RequestResponse) {
Assert.equal(200, response.statusCode);
Assert(response.body.indexOf("Sign in") > -1);
});
});
it("should fail first factor when wrong credentials are provided", function () {
return RequestAsync.postAsync(FIRST_FACTOR_URL, {
json: true,
body: {
username: "john",
password: "wrong password"
}
})
.then(function (response: Request.RequestResponse) {
Assert.equal(401, response.statusCode);
});
});
it("should redirect when correct credentials are provided during first factor", function () {
return RequestAsync.postAsync(FIRST_FACTOR_URL, {
json: true,
body: {
username: "john",
password: "password"
}
})
.then(function (response: Request.RequestResponse) {
Assert.equal(302, response.statusCode);
});
});
it("should have registered four sessions in redis", function (done) {
redisClient.dbsize(function (err: Error, count: number) {
Assert.equal(3, count);
done();
});
});
it("should redirect to home page when logout is called", function () {
return RequestAsync.getAsync(Util.format("%s?redirect=%s", LOGOUT_URL, HOME_URL))
.then(function (response: Request.RequestResponse) {
Assert.equal(200, response.statusCode);
Assert(response.body.indexOf("Access the secret") > -1);
});
});
});

View File

@ -1,92 +0,0 @@
process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
import Request = require("request");
import Assert = require("assert");
import BluebirdPromise = require("bluebird");
import Util = require("util");
import Endpoints = require("../../src/server/endpoints");
const RequestAsync = BluebirdPromise.promisifyAll(Request) as typeof Request;
const DOMAIN = "test.local";
const PORT = 8080;
const HOME_URL = Util.format("https://%s.%s:%d", "home", DOMAIN, PORT);
const SECRET_URL = Util.format("https://%s.%s:%d", "secret", DOMAIN, PORT);
const SECRET1_URL = Util.format("https://%s.%s:%d", "secret1", DOMAIN, PORT);
const SECRET2_URL = Util.format("https://%s.%s:%d", "secret2", DOMAIN, PORT);
const MX1_URL = Util.format("https://%s.%s:%d", "mx1.mail", DOMAIN, PORT);
const MX2_URL = Util.format("https://%s.%s:%d", "mx2.mail", DOMAIN, PORT);
const BASE_AUTH_URL = Util.format("https://%s.%s:%d", "auth", DOMAIN, PORT);
const FIRST_FACTOR_URL = Util.format("%s/api/firstfactor", BASE_AUTH_URL);
const LOGOUT_URL = Util.format("%s/logout", BASE_AUTH_URL);
describe("test example environment", function () {
function str_contains(str: string, pattern: string) {
return str.indexOf(pattern) != -1;
}
function test_homepage_is_correct(body: string) {
Assert(str_contains(body, BASE_AUTH_URL + Endpoints.LOGOUT_GET + "?redirect=" + HOME_URL + "/"));
Assert(str_contains(body, HOME_URL + "/secret.html"));
Assert(str_contains(body, SECRET_URL + "/secret.html"));
Assert(str_contains(body, SECRET1_URL + "/secret.html"));
Assert(str_contains(body, SECRET2_URL + "/secret.html"));
Assert(str_contains(body, MX1_URL + "/secret.html"));
Assert(str_contains(body, MX2_URL + "/secret.html"));
Assert(str_contains(body, "Access the secret"));
}
it("should access the home page", function () {
return RequestAsync.getAsync(HOME_URL)
.then(function (response: Request.RequestResponse) {
Assert.equal(200, response.statusCode);
test_homepage_is_correct(response.body);
});
});
it("should access the authentication page", function () {
return RequestAsync.getAsync(BASE_AUTH_URL)
.then(function (response: Request.RequestResponse) {
Assert.equal(200, response.statusCode);
Assert(response.body.indexOf("Sign in") > -1);
});
});
it("should fail first factor when wrong credentials are provided", function () {
return RequestAsync.postAsync(FIRST_FACTOR_URL, {
json: true,
body: {
username: "john",
password: "wrong password"
}
})
.then(function (response: Request.RequestResponse) {
Assert.equal(401, response.statusCode);
});
});
it("should redirect when correct credentials are provided during first factor", function () {
return RequestAsync.postAsync(FIRST_FACTOR_URL, {
json: true,
body: {
username: "john",
password: "password"
}
})
.then(function (response: Request.RequestResponse) {
Assert.equal(302, response.statusCode);
});
});
it("should redirect to home page when logout is called", function () {
return RequestAsync.getAsync(Util.format("%s?redirect=%s", LOGOUT_URL, HOME_URL))
.then(function (response: Request.RequestResponse) {
Assert.equal(200, response.statusCode);
Assert(response.body.indexOf("Access the secret") > -1);
});
});
});

View File

@ -42,7 +42,7 @@ describe("test FirstFactorValidator", function () {
});
it("should fail with error 401", () => {
return should_fail_first_factor_validation(401, "Authetication failed. Please check your credentials");
return should_fail_first_factor_validation(401, "Authetication failed. Please check your credentials.");
});
});
});

View File

@ -1,72 +1,119 @@
import { AuthenticationRegulator } from "../../../src/server/lib/AuthenticationRegulator";
import UserDataStore from "../../../src/server/lib/UserDataStore";
import Sinon = require("sinon");
import BluebirdPromise = require("bluebird");
import { AuthenticationRegulator } from "../../../src/server/lib/AuthenticationRegulator";
import { UserDataStore } from "../../../src/server/lib/storage/UserDataStore";
import MockDate = require("mockdate");
import exceptions = require("../../../src/server/lib/Exceptions");
import nedb = require("nedb");
import { CollectionStub } from "./mocks/storage/CollectionStub";
import { CollectionFactoryStub } from "./mocks/storage/CollectionFactoryStub";
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";
describe("test authentication regulator", function () {
let collectionFactory: CollectionFactoryStub;
let collection: CollectionStub;
beforeEach(function () {
collectionFactory = new CollectionFactoryStub();
collection = new CollectionStub();
collectionFactory.buildStub.returns(collection);
});
it("should mark 2 authentication and regulate", function () {
const user = "USER";
collection.insertStub.returns(BluebirdPromise.resolve());
collection.findStub.returns(BluebirdPromise.resolve([{
userId: user,
date: new Date(),
isAuthenticationSuccessful: false
}, {
userId: user,
date: new Date(),
isAuthenticationSuccessful: true
}]));
const dataStore = new UserDataStore(collectionFactory);
const regulator = new AuthenticationRegulator(dataStore, 10);
return regulator.mark(user, false)
.then(function() {
.then(function () {
return regulator.mark(user, true);
})
.then(function() {
.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";
it("should mark 3 authentications and regulate (reject)", function (done) {
const user = "USER";
collection.insertStub.returns(BluebirdPromise.resolve());
collection.findStub.returns(BluebirdPromise.resolve([{
userId: user,
date: new Date(),
isAuthenticationSuccessful: false
}, {
userId: user,
date: new Date(),
isAuthenticationSuccessful: false
}, {
userId: user,
date: new Date(),
isAuthenticationSuccessful: false
}]));
const dataStore = new UserDataStore(collectionFactory);
const regulator = new AuthenticationRegulator(dataStore, 10);
regulator.mark(user, false)
.then(function() {
.then(function () {
return regulator.mark(user, false);
})
.then(function() {
.then(function () {
return regulator.mark(user, false);
})
.then(function() {
.then(function () {
return regulator.regulate(user);
})
.catch(exceptions.AuthenticationRegulationError, function() {
.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);
it("should mark 3 authentications separated by a lot of time and allow access to user", function (done) {
const user = "USER";
collection.insertStub.returns(BluebirdPromise.resolve());
collection.findStub.returns(BluebirdPromise.resolve([{
userId: user,
date: new Date("1/2/2000 06:00:15"),
isAuthenticationSuccessful: false
}, {
userId: user,
date: new Date("1/2/2000 00:00:15"),
isAuthenticationSuccessful: false
}, {
userId: user,
date: new Date("1/2/2000 00:00:00"),
isAuthenticationSuccessful: false
}]));
const data_store = new UserDataStore(collectionFactory);
const regulator = new AuthenticationRegulator(data_store, 10);
const user = "user";
MockDate.set("1/2/2000 00:00:00");
regulator.mark(user, false)
.then(function() {
.then(function () {
MockDate.set("1/2/2000 00:00:15");
return regulator.mark(user, false);
})
.then(function() {
.then(function () {
MockDate.set("1/2/2000 06:00:15");
return regulator.mark(user, false);
})
.then(function() {
.then(function () {
return regulator.regulate(user);
})
.then(function() {
.then(function () {
done();
});
});

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