From e45ac39c8f14b291b98cc6bbfeb0fafbe3b0f69a Mon Sep 17 00:00:00 2001 From: Clement Michaud Date: Wed, 19 Jul 2017 21:06:12 +0200 Subject: [PATCH 1/2] Add Mongo as scalable and resilient storage backend --- config.template.yml | 10 +- example/mongo/docker-compose.yml | 6 + example/redis/docker-compose.yml | 2 +- package.json | 9 +- scripts/dc-dev.sh | 10 +- scripts/example/dc-example.sh | 9 +- scripts/example/deploy-example.sh | 2 +- scripts/integration-tests.sh | 9 +- src/server/lib/AuthenticationRegulator.ts | 33 +-- src/server/lib/FirstFactorValidator.ts | 8 +- src/server/lib/IdentityCheckMiddleware.ts | 38 +-- src/server/lib/RestApi.ts | 4 +- src/server/lib/Server.ts | 62 ++-- src/server/lib/ServerVariables.ts | 120 -------- src/server/lib/ServerVariablesHandler.ts | 151 ++++++++++ src/server/lib/SessionConfigurationBuilder.ts | 37 --- src/server/lib/UserDataStore.ts | 190 ------------- .../lib/access_control/AccessController.ts | 2 +- .../lib/access_control/PatternBuilder.ts | 2 +- .../lib/configuration/Configuration.d.ts} | 19 +- .../ConfigurationAdapter.ts | 13 +- .../SessionConfigurationBuilder.ts | 37 +++ .../lib/connectors/mongo/IMongoClient.d.ts | 5 + .../lib/connectors/mongo/IMongoConnector.d.ts | 6 + .../mongo/IMongoConnectorFactory.d.ts | 5 + .../lib/connectors/mongo/MongoClient.ts | 15 + .../lib/connectors/mongo/MongoConnector.ts | 22 ++ .../connectors/mongo/MongoConnectorFactory.ts | 12 + src/server/lib/ldap/Authenticator.ts | 2 +- src/server/lib/ldap/Client.ts | 2 +- src/server/lib/ldap/EmailsRetriever.ts | 2 +- src/server/lib/ldap/PasswordUpdater.ts | 2 +- src/server/lib/ldap/common.ts | 2 +- .../lib/notifiers/FileSystemNotifier.ts | 5 +- src/server/lib/notifiers/GMailNotifier.ts | 5 +- src/server/lib/notifiers/INotifier.d.ts | 7 + src/server/lib/notifiers/INotifier.ts | 7 - src/server/lib/notifiers/NotifierFactory.ts | 2 +- src/server/lib/routes/FirstFactorBlocker.ts | 4 +- src/server/lib/routes/error/401/get.ts | 5 +- src/server/lib/routes/error/403/get.ts | 5 +- src/server/lib/routes/error/404/get.ts | 5 +- src/server/lib/routes/firstfactor/get.ts | 4 +- src/server/lib/routes/firstfactor/post.ts | 12 +- .../lib/routes/password-reset/form/post.ts | 6 +- .../identity/PasswordResetHandler.ts | 6 +- .../lib/routes/secondfactor/redirect.ts | 2 +- .../totp/identity/RegistrationHandler.ts | 10 +- .../lib/routes/secondfactor/totp/sign/post.ts | 12 +- .../routes/secondfactor/u2f/register/post.ts | 17 +- .../secondfactor/u2f/register_request/get.ts | 8 +- .../lib/routes/secondfactor/u2f/sign/post.ts | 62 ++-- .../secondfactor/u2f/sign_request/get.ts | 24 +- src/server/lib/routes/verify/get.ts | 8 +- .../storage/AuthenticationTraceDocument.d.ts | 6 + .../lib/storage/CollectionFactoryFactory.ts | 15 + src/server/lib/storage/ICollection.d.ts | 11 + .../lib/storage/ICollectionFactory.d.ts | 6 + src/server/lib/storage/IUserDataStore.d.ts | 21 ++ .../storage/IdentityValidationDocument.d.ts | 7 + .../lib/storage/TOTPSecretDocument.d.ts | 6 + .../lib/storage/U2FRegistrationDocument.d.ts | 8 + src/server/lib/storage/UserDataStore.ts | 144 ++++++++++ .../lib/storage/mongo/MongoCollection.ts | 44 +++ .../storage/mongo/MongoCollectionFactory.ts | 19 ++ src/server/lib/storage/nedb/NedbCollection.ts | 38 +++ .../lib/storage/nedb/NedbCollectionFactory.ts | 28 ++ src/types/U2FRegistration.ts | 5 + src/types/nedb-async.d.ts | 3 +- .../server/AuthenticationRegulator.test.ts | 159 +++++++---- test/unit/server/ConfigurationAdapter.test.ts | 10 +- test/unit/server/DataPersistence.test.ts | 8 +- .../server/IdentityCheckMiddleware.test.ts | 29 +- test/unit/server/ServerConfiguration.test.ts | 14 +- .../SessionConfigurationBuilder.test.ts | 17 +- test/unit/server/UserDataStore.test.ts | 205 -------------- .../access_control/AccessController.test.ts | 2 +- .../access_control/PatternBuilder.test.ts | 2 +- .../connectors/mongo/MongoClient.test.ts | 38 +++ .../connectors/mongo/MongoConnector.test.ts | 45 +++ .../mongo/MongoConnectorFactory.test.ts | 13 + test/unit/server/ldap/Authenticator.test.ts | 2 +- test/unit/server/ldap/EmailsRetriever.test.ts | 2 +- test/unit/server/ldap/PasswordUpdater.test.ts | 2 +- test/unit/server/mocks/ServerVariablesMock.ts | 32 ++- test/unit/server/mocks/UserDataStore.ts | 22 -- .../mocks/connectors/mongo/MongoClientStub.ts | 15 + .../mocks/storage/CollectionFactoryStub.ts | 16 ++ .../server/mocks/storage/CollectionStub.ts | 39 +++ .../server/mocks/storage/UserDataStoreStub.ts | 65 +++++ .../unit/server/routes/errors/401/get.test.ts | 19 ++ .../unit/server/routes/errors/403/get.test.ts | 19 ++ .../unit/server/routes/errors/404/get.test.ts | 19 ++ .../server/routes/firstfactor/post.test.ts | 2 +- .../identity/PasswordResetHandler.test.ts | 26 +- .../server/routes/password-reset/post.test.ts | 30 +- .../totp/register/RegistrationHandler.test.ts | 17 +- .../secondfactor/totp/sign/post.test.ts | 12 +- .../u2f/identity/RegistrationHandler.test.ts | 13 +- .../secondfactor/u2f/register/post.test.ts | 15 +- .../u2f/register_request/get.test.ts | 14 +- .../routes/secondfactor/u2f/sign/post.test.ts | 151 +++++----- .../secondfactor/u2f/sign_request/get.test.ts | 53 ++-- test/unit/server/server/PrivatePages.ts | 6 +- test/unit/server/server/PublicPages.ts | 6 +- test/unit/server/server/Server.test.ts | 6 +- .../unit/server/storage/UserDataStore.test.ts | 264 ++++++++++++++++++ .../storage/mongo/MongoCollection.test.ts | 105 +++++++ .../mongo/MongoCollectionFactory.test.ts | 21 ++ .../storage/nedb/NedbCollection.test.ts | 136 +++++++++ .../nedb/NedbCollectionFactory.test.ts | 16 ++ .../authentication_audit.test.ts | 71 ----- .../user_data_store/totp_secret.test.ts | 73 ----- 113 files changed, 2056 insertions(+), 1200 deletions(-) create mode 100644 example/mongo/docker-compose.yml delete mode 100644 src/server/lib/ServerVariables.ts create mode 100644 src/server/lib/ServerVariablesHandler.ts delete mode 100644 src/server/lib/SessionConfigurationBuilder.ts delete mode 100644 src/server/lib/UserDataStore.ts rename src/{types/Configuration.ts => server/lib/configuration/Configuration.d.ts} (82%) rename src/server/lib/{ => configuration}/ConfigurationAdapter.ts (79%) create mode 100644 src/server/lib/configuration/SessionConfigurationBuilder.ts create mode 100644 src/server/lib/connectors/mongo/IMongoClient.d.ts create mode 100644 src/server/lib/connectors/mongo/IMongoConnector.d.ts create mode 100644 src/server/lib/connectors/mongo/IMongoConnectorFactory.d.ts create mode 100644 src/server/lib/connectors/mongo/MongoClient.ts create mode 100644 src/server/lib/connectors/mongo/MongoConnector.ts create mode 100644 src/server/lib/connectors/mongo/MongoConnectorFactory.ts create mode 100644 src/server/lib/notifiers/INotifier.d.ts delete mode 100644 src/server/lib/notifiers/INotifier.ts create mode 100644 src/server/lib/storage/AuthenticationTraceDocument.d.ts create mode 100644 src/server/lib/storage/CollectionFactoryFactory.ts create mode 100644 src/server/lib/storage/ICollection.d.ts create mode 100644 src/server/lib/storage/ICollectionFactory.d.ts create mode 100644 src/server/lib/storage/IUserDataStore.d.ts create mode 100644 src/server/lib/storage/IdentityValidationDocument.d.ts create mode 100644 src/server/lib/storage/TOTPSecretDocument.d.ts create mode 100644 src/server/lib/storage/U2FRegistrationDocument.d.ts create mode 100644 src/server/lib/storage/UserDataStore.ts create mode 100644 src/server/lib/storage/mongo/MongoCollection.ts create mode 100644 src/server/lib/storage/mongo/MongoCollectionFactory.ts create mode 100644 src/server/lib/storage/nedb/NedbCollection.ts create mode 100644 src/server/lib/storage/nedb/NedbCollectionFactory.ts create mode 100644 src/types/U2FRegistration.ts delete mode 100644 test/unit/server/UserDataStore.test.ts create mode 100644 test/unit/server/connectors/mongo/MongoClient.test.ts create mode 100644 test/unit/server/connectors/mongo/MongoConnector.test.ts create mode 100644 test/unit/server/connectors/mongo/MongoConnectorFactory.test.ts delete mode 100644 test/unit/server/mocks/UserDataStore.ts create mode 100644 test/unit/server/mocks/connectors/mongo/MongoClientStub.ts create mode 100644 test/unit/server/mocks/storage/CollectionFactoryStub.ts create mode 100644 test/unit/server/mocks/storage/CollectionStub.ts create mode 100644 test/unit/server/mocks/storage/UserDataStoreStub.ts create mode 100644 test/unit/server/routes/errors/401/get.test.ts create mode 100644 test/unit/server/routes/errors/403/get.test.ts create mode 100644 test/unit/server/routes/errors/404/get.test.ts create mode 100644 test/unit/server/storage/UserDataStore.test.ts create mode 100644 test/unit/server/storage/mongo/MongoCollection.test.ts create mode 100644 test/unit/server/storage/mongo/MongoCollectionFactory.test.ts create mode 100644 test/unit/server/storage/nedb/NedbCollection.test.ts create mode 100644 test/unit/server/storage/nedb/NedbCollectionFactory.test.ts delete mode 100644 test/unit/server/user_data_store/authentication_audit.test.ts delete mode 100644 test/unit/server/user_data_store/totp_secret.test.ts diff --git a/config.template.yml b/config.template.yml index 29625f84..e21ca996 100644 --- a/config.template.yml +++ b/config.template.yml @@ -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. diff --git a/example/mongo/docker-compose.yml b/example/mongo/docker-compose.yml new file mode 100644 index 00000000..3af27f21 --- /dev/null +++ b/example/mongo/docker-compose.yml @@ -0,0 +1,6 @@ +version: '2' +services: + mongo: + image: mongo:3.4 + networks: + - example-network diff --git a/example/redis/docker-compose.yml b/example/redis/docker-compose.yml index 5e415362..62cc6edd 100644 --- a/example/redis/docker-compose.yml +++ b/example/redis/docker-compose.yml @@ -1,6 +1,6 @@ version: '2' services: redis: - image: redis + image: redis:4.0-alpine networks: - example-network diff --git a/package.json b/package.json index 0ea74f55..f593dc54 100644 --- a/package.json +++ b/package.json @@ -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,7 @@ "@types/bluebird": "^3.5.4", "@types/body-parser": "^1.16.3", "@types/connect-redis": "0.0.6", + "@types/cors": "^2.8.1", "@types/ejs": "^2.3.33", "@types/express": "^4.0.35", "@types/express-session": "0.0.32", @@ -55,6 +56,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", @@ -89,7 +91,7 @@ "query-string": "^4.3.4", "request": "^2.81.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", @@ -107,7 +109,8 @@ "doc", "src/types", "dist", - "test" + "test", + "src/**/*.d.ts" ], "extension": [ ".ts" diff --git a/scripts/dc-dev.sh b/scripts/dc-dev.sh index 4d4bff93..91985ae8 100755 --- a/scripts/dc-dev.sh +++ b/scripts/dc-dev.sh @@ -2,4 +2,12 @@ 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 \ + -f test/integration/docker-compose.yml $* diff --git a/scripts/example/dc-example.sh b/scripts/example/dc-example.sh index 82c77f06..b5669a76 100755 --- a/scripts/example/dc-example.sh +++ b/scripts/example/dc-example.sh @@ -2,4 +2,11 @@ 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 \ + -f test/integration/docker-compose.yml $* diff --git a/scripts/example/deploy-example.sh b/scripts/example/deploy-example.sh index 4c50d935..bae41949 100755 --- a/scripts/example/deploy-example.sh +++ b/scripts/example/deploy-example.sh @@ -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 diff --git a/scripts/integration-tests.sh b/scripts/integration-tests.sh index 4b33a636..03b662d9 100755 --- a/scripts/integration-tests.sh +++ b/scripts/integration-tests.sh @@ -1,9 +1,10 @@ #!/bin/bash DC_SCRIPT=./scripts/example/dc-example.sh +EXPECTED_SERVICES_COUNT=6 start_services() { - $DC_SCRIPT up -d redis openldap authelia nginx nginx-tests + $DC_SCRIPT up -d mongo redis openldap authelia nginx nginx-tests sleep 3 } @@ -44,7 +45,7 @@ run_integration_tests() { $DC_SCRIPT logs authelia echo "Check number of services" - expect_services_count 5 + expect_services_count $EXPECTED_SERVICES_COUNT echo "Run integration tests..." $DC_SCRIPT run --rm integration-tests @@ -56,7 +57,7 @@ run_integration_tests() { run_system_tests() { echo "Start services..." start_services - expect_services_count 5 + expect_services_count $EXPECTED_SERVICES_COUNT ./node_modules/.bin/mocha --compilers ts:ts-node/register --recursive test/system shut_services @@ -67,7 +68,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 } diff --git a/src/server/lib/AuthenticationRegulator.ts b/src/server/lib/AuthenticationRegulator.ts index d15c6d16..6741586f 100644 --- a/src/server/lib/AuthenticationRegulator.ts +++ b/src/server/lib/AuthenticationRegulator.ts @@ -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 { - return this._user_data_store.save_authentication_trace(userid, REGULATION_TRACE_TYPE, is_success); + mark(userId: string, isAuthenticationSuccessful: boolean): BluebirdPromise { + return this.userDataStore.saveAuthenticationTrace(userId, isAuthenticationSuccessful); } - regulate(userid: string): BluebirdPromise { - return this._user_data_store.get_last_authentication_traces(userid, REGULATION_TRACE_TYPE, false, 3) - .then((docs: Array) => { + regulate(userId: string): BluebirdPromise { + 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."); } diff --git a/src/server/lib/FirstFactorValidator.ts b/src/server/lib/FirstFactorValidator.ts index 348b6840..d82a1ad2 100644 --- a/src/server/lib/FirstFactorValidator.ts +++ b/src/server/lib/FirstFactorValidator.ts @@ -6,9 +6,9 @@ import Exceptions = require("./Exceptions"); import AuthenticationSession = require("./AuthenticationSession"); export function validate(req: express.Request): BluebirdPromise { - const authSession = AuthenticationSession.get(req); - if (!authSession.userid || !authSession.first_factor) - return BluebirdPromise.reject(new Exceptions.FirstFactorValidationError("First factor has not been validated yet.")); + const authSession = AuthenticationSession.get(req); + if (!authSession.userid || !authSession.first_factor) + return BluebirdPromise.reject(new Exceptions.FirstFactorValidationError("First factor has not been validated yet.")); - return BluebirdPromise.resolve(); + return BluebirdPromise.resolve(); } \ No newline at end of file diff --git a/src/server/lib/IdentityCheckMiddleware.ts b/src/server/lib/IdentityCheckMiddleware.ts index 41cdd113..8523f9f0 100644 --- a/src/server/lib/IdentityCheckMiddleware.ts +++ b/src/server/lib/IdentityCheckMiddleware.ts @@ -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 { +function createAndSaveToken(userid: string, challenge: string, userDataStore: IUserDataStore, logger: Winston): BluebirdPromise { 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 { +function consumeToken(token: string, challenge: string, userDataStore: IUserDataStore, logger: Winston): BluebirdPromise { 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 { - 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(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 { - 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, "email"); - const userid = objectPath.get(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"); diff --git a/src/server/lib/RestApi.ts b/src/server/lib/RestApi.ts index ca51d9dd..b4ec2541 100644 --- a/src/server/lib/RestApi.ts +++ b/src/server/lib/RestApi.ts @@ -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); diff --git a/src/server/lib/Server.ts b/src/server/lib/Server.ts index 401844ba..bc42e883 100644 --- a/src/server/lib/Server.ts +++ b/src/server/lib/Server.ts @@ -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 { - 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 { + this.setupExpressApplication(config, app, deps); + return ServerVariablesHandler.initialize(app, config, deps); + } + private startServer(app: Express.Application, port: number) { return new BluebirdPromise((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 { + 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(); } diff --git a/src/server/lib/ServerVariables.ts b/src/server/lib/ServerVariables.ts deleted file mode 100644 index 5282052b..00000000 --- a/src/server/lib/ServerVariables.ts +++ /dev/null @@ -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; -} diff --git a/src/server/lib/ServerVariablesHandler.ts b/src/server/lib/ServerVariablesHandler.ts new file mode 100644 index 00000000..a7d6df7b --- /dev/null +++ b/src/server/lib/ServerVariablesHandler.ts @@ -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 { + 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 { + 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; + } +} diff --git a/src/server/lib/SessionConfigurationBuilder.ts b/src/server/lib/SessionConfigurationBuilder.ts deleted file mode 100644 index 3fa6c661..00000000 --- a/src/server/lib/SessionConfigurationBuilder.ts +++ /dev/null @@ -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; - } -} \ No newline at end of file diff --git a/src/server/lib/UserDataStore.ts b/src/server/lib/UserDataStore.ts deleted file mode 100644 index 0d74062a..00000000 --- a/src/server/lib/UserDataStore.ts +++ /dev/null @@ -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 { - 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 { - 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 { - 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 { - 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 { - 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 { - 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 { - 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; - } -} diff --git a/src/server/lib/access_control/AccessController.ts b/src/server/lib/access_control/AccessController.ts index e5b3605f..c23157ff 100644 --- a/src/server/lib/access_control/AccessController.ts +++ b/src/server/lib/access_control/AccessController.ts @@ -1,5 +1,5 @@ -import { ACLConfiguration } from "../../../types/Configuration"; +import { ACLConfiguration } from "../configuration/Configuration"; import PatternBuilder from "./PatternBuilder"; import { Winston } from "../../../types/Dependencies"; diff --git a/src/server/lib/access_control/PatternBuilder.ts b/src/server/lib/access_control/PatternBuilder.ts index 69d37278..17d4f7e6 100644 --- a/src/server/lib/access_control/PatternBuilder.ts +++ b/src/server/lib/access_control/PatternBuilder.ts @@ -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 { diff --git a/src/types/Configuration.ts b/src/server/lib/configuration/Configuration.d.ts similarity index 82% rename from src/types/Configuration.ts rename to src/server/lib/configuration/Configuration.d.ts index 8c2c9192..ec060873 100644 --- a/src/types/Configuration.ts +++ b/src/server/lib/configuration/Configuration.d.ts @@ -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; } diff --git a/src/server/lib/ConfigurationAdapter.ts b/src/server/lib/configuration/ConfigurationAdapter.ts similarity index 79% rename from src/server/lib/ConfigurationAdapter.ts rename to src/server/lib/configuration/ConfigurationAdapter.ts index 3cbca916..fe4c33eb 100644 --- a/src/server/lib/ConfigurationAdapter.ts +++ b/src/server/lib/configuration/ConfigurationAdapter.ts @@ -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(userConfiguration, "session.expiration", 3600000), // in ms redis: ObjectPath.get(userConfiguration, "session.redis") }, - store_directory: get_optional(userConfiguration, "store_directory", undefined), + storage: { + local: get_optional(userConfiguration, "storage.local", undefined), + mongo: get_optional(userConfiguration, "storage.mongo", undefined) + }, logs_level: get_optional(userConfiguration, "logs_level", "info"), notifier: ObjectPath.get(userConfiguration, "notifier"), access_control: ObjectPath.get(userConfiguration, "access_control") }; } -export default class ConfigurationAdapter { +export class ConfigurationAdapter { static adapt(userConfiguration: UserConfiguration): AppConfiguration { const appConfiguration = adaptFromUserConfiguration(userConfiguration); diff --git a/src/server/lib/configuration/SessionConfigurationBuilder.ts b/src/server/lib/configuration/SessionConfigurationBuilder.ts new file mode 100644 index 00000000..6e0c32af --- /dev/null +++ b/src/server/lib/configuration/SessionConfigurationBuilder.ts @@ -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; + } +} \ No newline at end of file diff --git a/src/server/lib/connectors/mongo/IMongoClient.d.ts b/src/server/lib/connectors/mongo/IMongoClient.d.ts new file mode 100644 index 00000000..b964ae75 --- /dev/null +++ b/src/server/lib/connectors/mongo/IMongoClient.d.ts @@ -0,0 +1,5 @@ +import MongoDB = require("mongodb"); + +export interface IMongoClient { + collection(name: string): MongoDB.Collection; +} \ No newline at end of file diff --git a/src/server/lib/connectors/mongo/IMongoConnector.d.ts b/src/server/lib/connectors/mongo/IMongoConnector.d.ts new file mode 100644 index 00000000..8ec4cda1 --- /dev/null +++ b/src/server/lib/connectors/mongo/IMongoConnector.d.ts @@ -0,0 +1,6 @@ +import BluebirdPromise = require("bluebird"); +import { IMongoClient } from "./IMongoClient"; + +export interface IMongoConnector { + connect(): BluebirdPromise; +} \ No newline at end of file diff --git a/src/server/lib/connectors/mongo/IMongoConnectorFactory.d.ts b/src/server/lib/connectors/mongo/IMongoConnectorFactory.d.ts new file mode 100644 index 00000000..b69b9271 --- /dev/null +++ b/src/server/lib/connectors/mongo/IMongoConnectorFactory.d.ts @@ -0,0 +1,5 @@ +import { IMongoConnector } from "./IMongoConnector"; + +export interface IMongoConnectorFactory { + create(url: string): IMongoConnector; +} \ No newline at end of file diff --git a/src/server/lib/connectors/mongo/MongoClient.ts b/src/server/lib/connectors/mongo/MongoClient.ts new file mode 100644 index 00000000..6d227868 --- /dev/null +++ b/src/server/lib/connectors/mongo/MongoClient.ts @@ -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); + } +} \ No newline at end of file diff --git a/src/server/lib/connectors/mongo/MongoConnector.ts b/src/server/lib/connectors/mongo/MongoConnector.ts new file mode 100644 index 00000000..6f3bcf48 --- /dev/null +++ b/src/server/lib/connectors/mongo/MongoConnector.ts @@ -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 { + const connectAsync = BluebirdPromise.promisify(MongoDB.MongoClient.connect); + return connectAsync(this.url) + .then(function (db: MongoDB.Db) { + return BluebirdPromise.resolve(new MongoClient(db)); + }); + } +} \ No newline at end of file diff --git a/src/server/lib/connectors/mongo/MongoConnectorFactory.ts b/src/server/lib/connectors/mongo/MongoConnectorFactory.ts new file mode 100644 index 00000000..9f57e108 --- /dev/null +++ b/src/server/lib/connectors/mongo/MongoConnectorFactory.ts @@ -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); + } +} \ No newline at end of file diff --git a/src/server/lib/ldap/Authenticator.ts b/src/server/lib/ldap/Authenticator.ts index bf936a46..26d9b5a0 100644 --- a/src/server/lib/ldap/Authenticator.ts +++ b/src/server/lib/ldap/Authenticator.ts @@ -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"; diff --git a/src/server/lib/ldap/Client.ts b/src/server/lib/ldap/Client.ts index c3c5632c..b59b33ee 100644 --- a/src/server/lib/ldap/Client.ts +++ b/src/server/lib/ldap/Client.ts @@ -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 { diff --git a/src/server/lib/ldap/EmailsRetriever.ts b/src/server/lib/ldap/EmailsRetriever.ts index edd2a6e2..fe4c3e78 100644 --- a/src/server/lib/ldap/EmailsRetriever.ts +++ b/src/server/lib/ldap/EmailsRetriever.ts @@ -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"; diff --git a/src/server/lib/ldap/PasswordUpdater.ts b/src/server/lib/ldap/PasswordUpdater.ts index 8134c3b7..c4e834e2 100644 --- a/src/server/lib/ldap/PasswordUpdater.ts +++ b/src/server/lib/ldap/PasswordUpdater.ts @@ -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"; diff --git a/src/server/lib/ldap/common.ts b/src/server/lib/ldap/common.ts index b9a4c755..6bc61833 100644 --- a/src/server/lib/ldap/common.ts +++ b/src/server/lib/ldap/common.ts @@ -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 { diff --git a/src/server/lib/notifiers/FileSystemNotifier.ts b/src/server/lib/notifiers/FileSystemNotifier.ts index 026545b2..a7b17145 100644 --- a/src/server/lib/notifiers/FileSystemNotifier.ts +++ b/src/server/lib/notifiers/FileSystemNotifier.ts @@ -5,13 +5,12 @@ 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; } diff --git a/src/server/lib/notifiers/GMailNotifier.ts b/src/server/lib/notifiers/GMailNotifier.ts index 384dc7d9..a601e258 100644 --- a/src/server/lib/notifiers/GMailNotifier.ts +++ b/src/server/lib/notifiers/GMailNotifier.ts @@ -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: { diff --git a/src/server/lib/notifiers/INotifier.d.ts b/src/server/lib/notifiers/INotifier.d.ts new file mode 100644 index 00000000..63f26097 --- /dev/null +++ b/src/server/lib/notifiers/INotifier.d.ts @@ -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; +} \ No newline at end of file diff --git a/src/server/lib/notifiers/INotifier.ts b/src/server/lib/notifiers/INotifier.ts deleted file mode 100644 index 2990517d..00000000 --- a/src/server/lib/notifiers/INotifier.ts +++ /dev/null @@ -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; -} \ No newline at end of file diff --git a/src/server/lib/notifiers/NotifierFactory.ts b/src/server/lib/notifiers/NotifierFactory.ts index 75f078ba..99d5852d 100644 --- a/src/server/lib/notifiers/NotifierFactory.ts +++ b/src/server/lib/notifiers/NotifierFactory.ts @@ -1,5 +1,5 @@ -import { NotifierConfiguration } from "../../../types/Configuration"; +import { NotifierConfiguration } from "../configuration/Configuration"; import { Nodemailer } from "../../../types/Dependencies"; import { INotifier } from "./INotifier"; diff --git a/src/server/lib/routes/FirstFactorBlocker.ts b/src/server/lib/routes/FirstFactorBlocker.ts index e2621c79..fb2ea86d 100644 --- a/src/server/lib/routes/FirstFactorBlocker.ts +++ b/src/server/lib/routes/FirstFactorBlocker.ts @@ -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; export default function (callback: Handler): Handler { return function (req: express.Request, res: express.Response): BluebirdPromise { - 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)); diff --git a/src/server/lib/routes/error/401/get.ts b/src/server/lib/routes/error/401/get.ts index 66658ffb..6dce84ee 100644 --- a/src/server/lib/routes/error/401/get.ts +++ b/src/server/lib/routes/error/401/get.ts @@ -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 { res.render("errors/401"); + return BluebirdPromise.resolve(); } diff --git a/src/server/lib/routes/error/403/get.ts b/src/server/lib/routes/error/403/get.ts index ca5e875c..5893ecd0 100644 --- a/src/server/lib/routes/error/403/get.ts +++ b/src/server/lib/routes/error/403/get.ts @@ -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 { res.render("errors/403"); + return BluebirdPromise.resolve(); } \ No newline at end of file diff --git a/src/server/lib/routes/error/404/get.ts b/src/server/lib/routes/error/404/get.ts index d5a7cab9..6693b6fc 100644 --- a/src/server/lib/routes/error/404/get.ts +++ b/src/server/lib/routes/error/404/get.ts @@ -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 { res.render("errors/404"); + return BluebirdPromise.resolve(); } \ No newline at end of file diff --git a/src/server/lib/routes/firstfactor/get.ts b/src/server/lib/routes/firstfactor/get.ts index 07e3fe71..6f6d065c 100644 --- a/src/server/lib/routes/firstfactor/get.ts +++ b/src/server/lib/routes/firstfactor/get.ts @@ -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)); diff --git a/src/server/lib/routes/firstfactor/post.ts b/src/server/lib/routes/firstfactor/post.ts index ded2b58c..770c1c95 100644 --- a/src/server/lib/routes/firstfactor/post.ts +++ b/src/server/lib/routes/firstfactor/post.ts @@ -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 { 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); diff --git a/src/server/lib/routes/password-reset/form/post.ts b/src/server/lib/routes/password-reset/form/post.ts index addfb161..dfd8cf16 100644 --- a/src/server/lib/routes/password-reset/form/post.ts +++ b/src/server/lib/routes/password-reset/form/post.ts @@ -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 { - 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(req, "body.password"); diff --git a/src/server/lib/routes/password-reset/identity/PasswordResetHandler.ts b/src/server/lib/routes/password-reset/identity/PasswordResetHandler.ts index f2c44183..edcba028 100644 --- a/src/server/lib/routes/password-reset/identity/PasswordResetHandler.ts +++ b/src/server/lib/routes/password-reset/identity/PasswordResetHandler.ts @@ -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 { - const logger = ServerVariables.getLogger(req.app); + const logger = ServerVariablesHandler.getLogger(req.app); const userid: string = objectPath.get(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"); diff --git a/src/server/lib/routes/secondfactor/redirect.ts b/src/server/lib/routes/secondfactor/redirect.ts index f0c8b43b..7ae69a3a 100644 --- a/src/server/lib/routes/secondfactor/redirect.ts +++ b/src/server/lib/routes/secondfactor/redirect.ts @@ -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) { diff --git a/src/server/lib/routes/secondfactor/totp/identity/RegistrationHandler.ts b/src/server/lib/routes/secondfactor/totp/identity/RegistrationHandler.ts index 1c499378..4ac56d99 100644 --- a/src/server/lib/routes/secondfactor/totp/identity/RegistrationHandler.ts +++ b/src/server/lib/routes/secondfactor/totp/identity/RegistrationHandler.ts @@ -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); diff --git a/src/server/lib/routes/secondfactor/totp/sign/post.ts b/src/server/lib/routes/secondfactor/totp/sign/post.ts index 1be2011d..91564a82 100644 --- a/src/server/lib/routes/secondfactor/totp/sign/post.ts +++ b/src/server/lib/routes/secondfactor/totp/sign/post.ts @@ -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 { - 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); diff --git a/src/server/lib/routes/secondfactor/u2f/register/post.ts b/src/server/lib/routes/secondfactor/u2f/register/post.ts index 92feb1c4..6e3f0abf 100644 --- a/src/server/lib/routes/secondfactor/u2f/register/post.ts +++ b/src/server/lib/routes/secondfactor/u2f/register/post.ts @@ -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 { - 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 { - const logger = ServerVariables.getLogger(req.app); - const userDataStore = ServerVariables.getUserDataStore(req.app); - const authSession = AuthenticationSession.get(req); + const logger = ServerVariablesHandler.getLogger(req.app); + const userDataStore = ServerVariablesHandler.getUserDataStore(req.app); + const authSession = AuthenticationSession.get(req); - if (!authSession.sign_request) { - const err = new Error("No sign request"); - ErrorReplies.replyWithError401(res, logger)(err); - return BluebirdPromise.reject(err); - } + if (!authSession.sign_request) { + const err = new Error("No sign request"); + ErrorReplies.replyWithError401(res, logger)(err); + return BluebirdPromise.reject(err); + } - const userid = authSession.userid; - const appid = u2f_common.extract_app_id(req); - return userDataStore.get_u2f_meta(userid, appid) - .then(function (doc: U2FRegistrationDocument): BluebirdPromise { - const appid = u2f_common.extract_app_id(req); - const u2f = ServerVariables.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)); - }) - .then(function (result: U2f.SignatureResult | U2f.Error): BluebirdPromise { - if (objectPath.has(result, "errorCode")) - return BluebirdPromise.reject(new Error("Error while signing")); - logger.info("U2F sign: Authentication successful"); - authSession.second_factor = true; - redirect(req, res); - return BluebirdPromise.resolve(); - }) - .catch(ErrorReplies.replyWithError500(res, logger)); + const userid = authSession.userid; + const appid = u2f_common.extract_app_id(req); + return userDataStore.retrieveU2FRegistration(userid, appid) + .then(function (doc: U2FRegistrationDocument): BluebirdPromise { + 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.registration.publicKey)); + }) + .then(function (result: U2f.SignatureResult | U2f.Error): BluebirdPromise { + if (objectPath.has(result, "errorCode")) + return BluebirdPromise.reject(new Error("Error while signing")); + logger.info("U2F sign: Authentication successful"); + authSession.second_factor = true; + redirect(req, res); + return BluebirdPromise.resolve(); + }) + .catch(ErrorReplies.replyWithError500(res, logger)); } diff --git a/src/server/lib/routes/secondfactor/u2f/sign_request/get.ts b/src/server/lib/routes/secondfactor/u2f/sign_request/get.ts index 5c5cf6c4..98ff0f57 100644 --- a/src/server/lib/routes/secondfactor/u2f/sign_request/get.ts +++ b/src/server/lib/routes/secondfactor/u2f/sign_request/get.ts @@ -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 { - 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 { 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); }) diff --git a/src/server/lib/routes/verify/get.ts b/src/server/lib/routes/verify/get.ts index e3a99f3d..1424bb30 100644 --- a/src/server/lib/routes/verify/get.ts +++ b/src/server/lib/routes/verify/get.ts @@ -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 { - 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); diff --git a/src/server/lib/storage/AuthenticationTraceDocument.d.ts b/src/server/lib/storage/AuthenticationTraceDocument.d.ts new file mode 100644 index 00000000..69818c05 --- /dev/null +++ b/src/server/lib/storage/AuthenticationTraceDocument.d.ts @@ -0,0 +1,6 @@ + +export interface AuthenticationTraceDocument { + userId: string; + date: Date; + isAuthenticationSuccessful: boolean; +} \ No newline at end of file diff --git a/src/server/lib/storage/CollectionFactoryFactory.ts b/src/server/lib/storage/CollectionFactoryFactory.ts new file mode 100644 index 00000000..e5781ed3 --- /dev/null +++ b/src/server/lib/storage/CollectionFactoryFactory.ts @@ -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); + } +} \ No newline at end of file diff --git a/src/server/lib/storage/ICollection.d.ts b/src/server/lib/storage/ICollection.d.ts new file mode 100644 index 00000000..caa6c2a8 --- /dev/null +++ b/src/server/lib/storage/ICollection.d.ts @@ -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; + findOne(query: any): BluebirdPromise; + update(query: any, updateQuery: any, options?: any): BluebirdPromise; + remove(query: any): BluebirdPromise; + insert(document: any): BluebirdPromise; +} \ No newline at end of file diff --git a/src/server/lib/storage/ICollectionFactory.d.ts b/src/server/lib/storage/ICollectionFactory.d.ts new file mode 100644 index 00000000..39eb42c7 --- /dev/null +++ b/src/server/lib/storage/ICollectionFactory.d.ts @@ -0,0 +1,6 @@ + +import { ICollection } from "./ICollection"; + +export interface ICollectionFactory { + build(collectionName: string): ICollection; +} \ No newline at end of file diff --git a/src/server/lib/storage/IUserDataStore.d.ts b/src/server/lib/storage/IUserDataStore.d.ts new file mode 100644 index 00000000..a03abb37 --- /dev/null +++ b/src/server/lib/storage/IUserDataStore.d.ts @@ -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; + retrieveU2FRegistration(userId: string, appId: string): BluebirdPromise; + + saveAuthenticationTrace(userId: string, isAuthenticationSuccessful: boolean): BluebirdPromise; + retrieveLatestAuthenticationTraces(userId: string, isAuthenticationSuccessful: boolean, count: number): BluebirdPromise; + + produceIdentityValidationToken(userId: string, token: string, challenge: string, maxAge: number): BluebirdPromise; + consumeIdentityValidationToken(token: string, challenge: string): BluebirdPromise; + + saveTOTPSecret(userId: string, secret: TOTPSecret): BluebirdPromise; + retrieveTOTPSecret(userId: string): BluebirdPromise; +} \ No newline at end of file diff --git a/src/server/lib/storage/IdentityValidationDocument.d.ts b/src/server/lib/storage/IdentityValidationDocument.d.ts new file mode 100644 index 00000000..e7fd7b3f --- /dev/null +++ b/src/server/lib/storage/IdentityValidationDocument.d.ts @@ -0,0 +1,7 @@ + +export interface IdentityValidationDocument { + userId: string; + token: string; + challenge: string; + maxDate: Date; +} \ No newline at end of file diff --git a/src/server/lib/storage/TOTPSecretDocument.d.ts b/src/server/lib/storage/TOTPSecretDocument.d.ts new file mode 100644 index 00000000..a6c0bf9e --- /dev/null +++ b/src/server/lib/storage/TOTPSecretDocument.d.ts @@ -0,0 +1,6 @@ +import { TOTPSecret } from "../../../types/TOTPSecret"; + +export interface TOTPSecretDocument { + userid: string; + secret: TOTPSecret; +} \ No newline at end of file diff --git a/src/server/lib/storage/U2FRegistrationDocument.d.ts b/src/server/lib/storage/U2FRegistrationDocument.d.ts new file mode 100644 index 00000000..efec6cb1 --- /dev/null +++ b/src/server/lib/storage/U2FRegistrationDocument.d.ts @@ -0,0 +1,8 @@ + +import { U2FRegistration } from "../../../types/U2FRegistration"; + +export interface U2FRegistrationDocument { + userId: string; + appId: string; + registration: U2FRegistration; +} \ No newline at end of file diff --git a/src/server/lib/storage/UserDataStore.ts b/src/server/lib/storage/UserDataStore.ts new file mode 100644 index 00000000..9297b559 --- /dev/null +++ b/src/server/lib/storage/UserDataStore.ts @@ -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 { + 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 { + const filter: U2FRegistrationKey = { + userId: userId, + appId: appId + }; + return this.u2fSecretCollection.findOne(filter); + } + + saveAuthenticationTrace(userId: string, isAuthenticationSuccessful: boolean): BluebirdPromise { + const newDocument: AuthenticationTraceDocument = { + userId: userId, + date: new Date(), + isAuthenticationSuccessful: isAuthenticationSuccessful, + }; + + return this.authenticationTracesCollection.insert(newDocument); + } + + retrieveLatestAuthenticationTraces(userId: string, isAuthenticationSuccessful: boolean, count: number): BluebirdPromise { + const q = { + userId: userId, + isAuthenticationSuccessful: isAuthenticationSuccessful + }; + + return this.authenticationTracesCollection.find(q, { date: -1 }, count); + } + + produceIdentityValidationToken(userId: string, token: string, challenge: string, maxAge: number): BluebirdPromise { + 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 { + 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 { + const doc = { + userId: userId, + secret: secret + }; + + const filter = { + userId: userId + }; + return this.totpSecretCollection.update(filter, doc, { upsert: true }); + } + + retrieveTOTPSecret(userId: string): BluebirdPromise { + const filter = { + userId: userId + }; + return this.totpSecretCollection.findOne(filter); + } +} diff --git a/src/server/lib/storage/mongo/MongoCollection.ts b/src/server/lib/storage/mongo/MongoCollection.ts new file mode 100644 index 00000000..8b96751b --- /dev/null +++ b/src/server/lib/storage/mongo/MongoCollection.ts @@ -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 { + const q = this.collection.find(query).sort(sortKeys).limit(count); + const toArrayAsync = BluebirdPromise.promisify(q.toArray, { context: q }); + return toArrayAsync(); + } + + findOne(query: any): BluebirdPromise { + const findOneAsync = BluebirdPromise.promisify(this.collection.findOne, { context: this.collection }); + return findOneAsync(query); + } + + update(query: any, updateQuery: any, options?: any): BluebirdPromise { + const updateAsync = BluebirdPromise.promisify(this.collection.update, { context: this.collection }); + return updateAsync(query, updateQuery, options); + } + + remove(query: any): BluebirdPromise { + const removeAsync = BluebirdPromise.promisify(this.collection.remove, { context: this.collection }); + return removeAsync(query); + } + + insert(document: any): BluebirdPromise { + const insertAsync = BluebirdPromise.promisify(this.collection.insert, { context: this.collection }); + return insertAsync(document); + } + + count(query: any): BluebirdPromise { + const countAsync = BluebirdPromise.promisify(this.collection.count, { context: this.collection }); + return countAsync(query); + } +} \ No newline at end of file diff --git a/src/server/lib/storage/mongo/MongoCollectionFactory.ts b/src/server/lib/storage/mongo/MongoCollectionFactory.ts new file mode 100644 index 00000000..6738ef1a --- /dev/null +++ b/src/server/lib/storage/mongo/MongoCollectionFactory.ts @@ -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)); + } +} \ No newline at end of file diff --git a/src/server/lib/storage/nedb/NedbCollection.ts b/src/server/lib/storage/nedb/NedbCollection.ts new file mode 100644 index 00000000..c42ea07a --- /dev/null +++ b/src/server/lib/storage/nedb/NedbCollection.ts @@ -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 { + const q = this.collection.find(query).sort(sortKeys).limit(count); + return BluebirdPromise.promisify(q.exec, { context: q })(); + } + + findOne(query: any): BluebirdPromise { + return this.collection.findOneAsync(query); + } + + update(query: any, updateQuery: any, options?: any): BluebirdPromise { + return this.collection.updateAsync(query, updateQuery, options); + } + + remove(query: any): BluebirdPromise { + return this.collection.removeAsync(query); + } + + insert(document: any): BluebirdPromise { + return this.collection.insertAsync(document); + } + + count(query: any): BluebirdPromise { + return this.collection.countAsync(query); + } +} \ No newline at end of file diff --git a/src/server/lib/storage/nedb/NedbCollectionFactory.ts b/src/server/lib/storage/nedb/NedbCollectionFactory.ts new file mode 100644 index 00000000..ca660c9f --- /dev/null +++ b/src/server/lib/storage/nedb/NedbCollectionFactory.ts @@ -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); + } +} \ No newline at end of file diff --git a/src/types/U2FRegistration.ts b/src/types/U2FRegistration.ts new file mode 100644 index 00000000..b6080af0 --- /dev/null +++ b/src/types/U2FRegistration.ts @@ -0,0 +1,5 @@ + +export interface U2FRegistration { + keyHandle: string; + publicKey: string; +} \ No newline at end of file diff --git a/src/types/nedb-async.d.ts b/src/types/nedb-async.d.ts index 1f4fe042..3f5c96b8 100644 --- a/src/types/nedb-async.d.ts +++ b/src/types/nedb-async.d.ts @@ -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; - findOneAsync(query: any): BluebirdPromise; + findOneAsync(query: any): BluebirdPromise; insertAsync(newDoc: T): BluebirdPromise; removeAsync(query: any): BluebirdPromise; + countAsync(query: any): BluebirdPromise } } \ No newline at end of file diff --git a/test/unit/server/AuthenticationRegulator.test.ts b/test/unit/server/AuthenticationRegulator.test.ts index 9eee3439..9b18623c 100644 --- a/test/unit/server/AuthenticationRegulator.test.ts +++ b/test/unit/server/AuthenticationRegulator.test.ts @@ -1,73 +1,120 @@ -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() { - return regulator.mark(user, true); - }) - .then(function() { - return regulator.regulate(user); - }); + .then(function () { + return regulator.mark(user, true); + }) + .then(function () { + return regulator.regulate(user); + }); }); - it("should mark 3 authentications and regulate (reject)", function(done) { - const options = { - inMemoryOnly: true - }; - const data_store = new UserDataStore(options, nedb); - const regulator = new AuthenticationRegulator(data_store, 10); - const user = "user"; + 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() { - return regulator.mark(user, false); - }) - .then(function() { - return regulator.mark(user, false); - }) - .then(function() { - return regulator.regulate(user); - }) - .catch(exceptions.AuthenticationRegulationError, function() { - done(); - }); + .then(function () { + return regulator.mark(user, false); + }) + .then(function () { + return regulator.mark(user, false); + }) + .then(function () { + return regulator.regulate(user); + }) + .catch(exceptions.AuthenticationRegulationError, function () { + done(); + }); }); - it("should mark 3 authentications and regulate (resolve)", function(done) { - const options = { - inMemoryOnly: true - }; - const data_store = new UserDataStore(options, nedb); + 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() { - MockDate.set("1/2/2000 00:00:15"); - return regulator.mark(user, false); - }) - .then(function() { - return regulator.mark(user, false); - }) - .then(function() { - return regulator.regulate(user); - }) - .then(function() { - done(); - }); + .then(function () { + MockDate.set("1/2/2000 00:00:15"); + return regulator.mark(user, false); + }) + .then(function () { + MockDate.set("1/2/2000 06:00:15"); + return regulator.mark(user, false); + }) + .then(function () { + return regulator.regulate(user); + }) + .then(function () { + done(); + }); }); -}); +}); \ No newline at end of file diff --git a/test/unit/server/ConfigurationAdapter.test.ts b/test/unit/server/ConfigurationAdapter.test.ts index c7f2b90a..7e4b6b6c 100644 --- a/test/unit/server/ConfigurationAdapter.test.ts +++ b/test/unit/server/ConfigurationAdapter.test.ts @@ -1,6 +1,6 @@ import * as Assert from "assert"; -import { UserConfiguration } from "../../../src/types/Configuration"; -import ConfigurationAdapter from "../../../src/server/lib/ConfigurationAdapter"; +import { UserConfiguration } from "../../../src/server/lib/configuration/Configuration"; +import { ConfigurationAdapter } from "../../../src/server/lib/configuration/ConfigurationAdapter"; describe("test config adapter", function() { function build_yaml_config(): UserConfiguration { @@ -17,7 +17,11 @@ describe("test config adapter", function() { secret: "secret", max_age: 40000 }, - store_directory: "/mydirectory", + storage: { + local: { + path: "/mydirectory" + } + }, logs_level: "debug", notifier: { gmail: { diff --git a/test/unit/server/DataPersistence.test.ts b/test/unit/server/DataPersistence.test.ts index 26d86c9b..0737fd40 100644 --- a/test/unit/server/DataPersistence.test.ts +++ b/test/unit/server/DataPersistence.test.ts @@ -3,7 +3,7 @@ import * as BluebirdPromise from "bluebird"; import * as request from "request"; import Server from "../../../src/server/lib/Server"; -import { UserConfiguration } from "../../../src/types/Configuration"; +import { UserConfiguration } from "../../../src/server/lib/configuration/Configuration"; import { GlobalDependencies } from "../../../src/types/Dependencies"; import * as tmp from "tmp"; import U2FMock = require("./mocks/u2f"); @@ -70,7 +70,11 @@ describe("test data persistence", function () { secret: "session_secret", expiration: 50000, }, - store_directory: tmpDir.name, + storage: { + local: { + path: tmpDir.name + } + }, notifier: { gmail: { username: "user@example.com", diff --git a/test/unit/server/IdentityCheckMiddleware.test.ts b/test/unit/server/IdentityCheckMiddleware.test.ts index a63c47ae..c2914359 100644 --- a/test/unit/server/IdentityCheckMiddleware.test.ts +++ b/test/unit/server/IdentityCheckMiddleware.test.ts @@ -2,24 +2,24 @@ import sinon = require("sinon"); import IdentityValidator = require("../../../src/server/lib/IdentityCheckMiddleware"); import AuthenticationSession = require("../../../src/server/lib/AuthenticationSession"); +import { UserDataStore } from "../../../src/server/lib/storage/UserDataStore"; + import exceptions = require("../../../src/server/lib/Exceptions"); import assert = require("assert"); -import winston = require("winston"); import Promise = require("bluebird"); import express = require("express"); import BluebirdPromise = require("bluebird"); import ExpressMock = require("./mocks/express"); -import UserDataStoreMock = require("./mocks/UserDataStore"); import NotifierMock = require("./mocks/Notifier"); import IdentityValidatorMock = require("./mocks/IdentityValidator"); import ServerVariablesMock = require("./mocks/ServerVariablesMock"); describe("test identity check process", function () { + let mocks: ServerVariablesMock.ServerVariablesMock; let req: ExpressMock.RequestMock; let res: ExpressMock.ResponseMock; - let userDataStore: UserDataStoreMock.UserDataStore; let notifier: NotifierMock.NotifierMock; let app: express.Application; let app_get: sinon.SinonStub; @@ -32,12 +32,6 @@ describe("test identity check process", function () { identityValidable = IdentityValidatorMock.IdentityValidableMock(); - userDataStore = UserDataStoreMock.UserDataStore(); - userDataStore.issue_identity_check_token = sinon.stub(); - userDataStore.issue_identity_check_token.returns(Promise.resolve()); - userDataStore.consume_identity_check_token = sinon.stub(); - userDataStore.consume_identity_check_token.returns(Promise.resolve({ userid: "user" })); - notifier = NotifierMock.NotifierMock(); notifier.notify = sinon.stub().returns(Promise.resolve()); @@ -47,11 +41,13 @@ describe("test identity check process", function () { req.query = {}; req.app = {}; - const mocks = ServerVariablesMock.mock(req.app); - mocks.logger = winston; - mocks.userDataStore = userDataStore as any; + + mocks = ServerVariablesMock.mock(req.app); mocks.notifier = notifier; + mocks.userDataStore.produceIdentityValidationTokenStub.returns(Promise.resolve()); + mocks.userDataStore.consumeIdentityValidationTokenStub.returns(Promise.resolve({ userId: "user" })); + app = express(); app_get = sinon.stub(app, "get"); app_post = sinon.stub(app, "post"); @@ -116,9 +112,9 @@ describe("test identity check process", function () { return callback(req as any, res as any, undefined) .then(function () { assert(notifier.notify.calledOnce); - assert(userDataStore.issue_identity_check_token.calledOnce); - assert.equal(userDataStore.issue_identity_check_token.getCall(0).args[0], "user"); - assert.equal(userDataStore.issue_identity_check_token.getCall(0).args[3], 240000); + assert(mocks.userDataStore.produceIdentityValidationTokenStub.calledOnce); + assert.equal(mocks.userDataStore.produceIdentityValidationTokenStub.getCall(0).args[0], "user"); + assert.equal(mocks.userDataStore.produceIdentityValidationTokenStub.getCall(0).args[3], 240000); }); }); } @@ -145,8 +141,7 @@ describe("test identity check process", function () { it("should return 500 if identity_token is provided but invalid", function () { req.query.identity_token = "token"; - userDataStore.consume_identity_check_token - .returns(BluebirdPromise.reject(new Error("Invalid token"))); + mocks.userDataStore.consumeIdentityValidationTokenStub.returns(BluebirdPromise.reject(new Error("Invalid token"))); const callback = IdentityValidator.get_finish_validation(identityValidable); return callback(req as any, res as any, undefined) diff --git a/test/unit/server/ServerConfiguration.test.ts b/test/unit/server/ServerConfiguration.test.ts index e5c17a03..a4b26fce 100644 --- a/test/unit/server/ServerConfiguration.test.ts +++ b/test/unit/server/ServerConfiguration.test.ts @@ -9,7 +9,7 @@ import u2f = require("u2f"); import nodemailer = require("nodemailer"); import session = require("express-session"); -import { AppConfiguration, UserConfiguration } from "../../../src/types/Configuration"; +import { AppConfiguration, UserConfiguration } from "../../../src/server/lib/configuration/Configuration"; import { GlobalDependencies, Nodemailer } from "../../../src/types/Dependencies"; import Server from "../../../src/server/lib/Server"; @@ -50,7 +50,7 @@ describe("test server configuration", function () { it("should set cookie scope to domain set in the config", function () { - const config = { + const config: UserConfiguration = { session: { domain: "example.com", secret: "secret" @@ -58,15 +58,21 @@ describe("test server configuration", function () { ldap: { url: "http://ldap", user: "user", - password: "password" + password: "password", + base_dn: "dc=example,dc=com" }, notifier: { gmail: { username: "user@example.com", password: "password" } + }, + storage: { + local: { + in_memory: true + } } - } as UserConfiguration; + }; const server = new Server(); server.start(config, deps); diff --git a/test/unit/server/SessionConfigurationBuilder.test.ts b/test/unit/server/SessionConfigurationBuilder.test.ts index b148d0d8..9cf071a4 100644 --- a/test/unit/server/SessionConfigurationBuilder.test.ts +++ b/test/unit/server/SessionConfigurationBuilder.test.ts @@ -1,6 +1,7 @@ -import SessionConfigurationBuilder from "../../../src/server/lib/SessionConfigurationBuilder"; -import { AppConfiguration } from "../../../src/types/Configuration"; +import { SessionConfigurationBuilder } from "../../../src/server/lib/configuration/SessionConfigurationBuilder"; +import { AppConfiguration } from "../../../src/server/lib/configuration/Configuration"; import { GlobalDependencies } from "../../../src/types/Dependencies"; + import ExpressSession = require("express-session"); import ConnectRedis = require("connect-redis"); import Sinon = require("sinon"); @@ -32,7 +33,11 @@ describe("test session configuration builder", function () { expiration: 3600, secret: "secret" }, - store_in_memory: true + storage: { + local: { + in_memory: true + } + } }; const deps: GlobalDependencies = { @@ -92,7 +97,11 @@ describe("test session configuration builder", function () { port: 6379 } }, - store_in_memory: true + storage: { + local: { + in_memory: true + } + } }; const RedisStoreMock = Sinon.spy(); diff --git a/test/unit/server/UserDataStore.test.ts b/test/unit/server/UserDataStore.test.ts deleted file mode 100644 index d482237a..00000000 --- a/test/unit/server/UserDataStore.test.ts +++ /dev/null @@ -1,205 +0,0 @@ - -import UserDataStore from "../../../src/server/lib/UserDataStore"; -import { U2FRegistrationDocument, Options } from "../../../src/server/lib/UserDataStore"; - -import nedb = require("nedb"); -import assert = require("assert"); -import Promise = require("bluebird"); -import sinon = require("sinon"); -import MockDate = require("mockdate"); - -describe("test user data store", () => { - let options: Options; - - beforeEach(function () { - options = { - inMemoryOnly: true - }; - }); - - - describe("test u2f meta", () => { - it("should save a u2f meta", function () { - const data_store = new UserDataStore(options, nedb); - - const userid = "user"; - const app_id = "https://localhost"; - const keyHandle = "keyhandle"; - const publicKey = "publicKey"; - - return data_store.set_u2f_meta(userid, app_id, keyHandle, publicKey) - .then(function (numUpdated) { - assert.equal(1, numUpdated); - return Promise.resolve(); - }); - }); - - it("should retrieve no u2f meta", function () { - const options = { - inMemoryOnly: true - }; - - const data_store = new UserDataStore(options, nedb); - - const userid = "user"; - const app_id = "https://localhost"; - const meta = { - publicKey: "pbk" - }; - - return data_store.get_u2f_meta(userid, app_id) - .then(function (doc) { - assert.equal(undefined, doc); - return Promise.resolve(); - }); - }); - - it("should insert and retrieve a u2f meta", function () { - const options = { - inMemoryOnly: true - }; - - const data_store = new UserDataStore(options, nedb); - - const userid = "user"; - const app_id = "https://localhost"; - const keyHandle = "keyHandle"; - const publicKey = "publicKey"; - - return data_store.set_u2f_meta(userid, app_id, keyHandle, publicKey) - .then(function (numUpdated: number) { - assert.equal(1, numUpdated); - return data_store.get_u2f_meta(userid, app_id); - }) - .then(function (doc: U2FRegistrationDocument) { - assert.deepEqual(keyHandle, doc.keyHandle); - assert.deepEqual(publicKey, doc.publicKey); - assert.deepEqual(userid, doc.userId); - assert.deepEqual(app_id, doc.appId); - assert("_id" in doc); - return Promise.resolve(); - }); - }); - }); - - - describe("test u2f registration token", () => { - it("should save u2f registration token", function () { - const data_store = new UserDataStore(options, nedb); - - const userid = "user"; - const token = "token"; - const max_age = 60; - const content = "abc"; - - return data_store.issue_identity_check_token(userid, token, content, max_age) - .then(function (document) { - assert.equal(document.userid, userid); - assert.equal(document.token, token); - assert.deepEqual(document.content, { userid: "user", data: content }); - assert("max_date" in document); - assert("_id" in document); - return Promise.resolve(); - }) - .catch(function (err) { - console.error(err); - return Promise.reject(err); - }); - }); - - it("should save u2f registration token and consume it", function (done) { - const data_store = new UserDataStore(options, nedb); - - const userid = "user"; - const token = "token"; - const max_age = 50; - - data_store.issue_identity_check_token(userid, token, {}, max_age) - .then(function (document) { - return data_store.consume_identity_check_token(token); - }) - .then(function () { - done(); - }) - .catch(function (err) { - console.error(err); - }); - }); - - it("should not be able to consume registration token twice", function (done) { - const data_store = new UserDataStore(options, nedb); - - const userid = "user"; - const token = "token"; - const max_age = 50; - - data_store.issue_identity_check_token(userid, token, {}, max_age) - .then(function (document) { - return data_store.consume_identity_check_token(token); - }) - .then(function (document) { - return data_store.consume_identity_check_token(token); - }) - .catch(function (err) { - console.error(err); - done(); - }); - }); - - it("should fail when token does not exist", function () { - const data_store = new UserDataStore(options, nedb); - - const token = "token"; - - return data_store.consume_identity_check_token(token) - .then(function (document) { - return Promise.reject("Error while checking token"); - }) - .catch(function (err) { - return Promise.resolve(err); - }); - }); - - it("should fail when token expired", function (done) { - const data_store = new UserDataStore(options, nedb); - - const userid = "user"; - const token = "token"; - const max_age = 60; - MockDate.set("1/1/2000"); - - data_store.issue_identity_check_token(userid, token, {}, max_age) - .then(function () { - MockDate.set("1/2/2000"); - return data_store.consume_identity_check_token(token); - }) - .catch(function (err) { - MockDate.reset(); - done(); - }); - }); - - it("should save the userid and some data with the token", function (done) { - const data_store = new UserDataStore(options, nedb); - - const userid = "user"; - const token = "token"; - const max_age = 60; - MockDate.set("1/1/2000"); - const data = "abc"; - - data_store.issue_identity_check_token(userid, token, data, max_age) - .then(function () { - return data_store.consume_identity_check_token(token); - }) - .then(function (content) { - const expected_content = { - userid: "user", - data: "abc" - }; - assert.deepEqual(content, expected_content); - done(); - }); - }); - }); -}); diff --git a/test/unit/server/access_control/AccessController.test.ts b/test/unit/server/access_control/AccessController.test.ts index 37ddb583..440cde3c 100644 --- a/test/unit/server/access_control/AccessController.test.ts +++ b/test/unit/server/access_control/AccessController.test.ts @@ -2,7 +2,7 @@ import assert = require("assert"); import winston = require("winston"); import { AccessController } from "../../../../src/server/lib/access_control/AccessController"; -import { ACLConfiguration } from "../../../../src/types/Configuration"; +import { ACLConfiguration } from "../../../../src/server/lib/configuration/Configuration"; describe("test access control manager", function () { let accessController: AccessController; diff --git a/test/unit/server/access_control/PatternBuilder.test.ts b/test/unit/server/access_control/PatternBuilder.test.ts index 82c3630b..9cfd08a5 100644 --- a/test/unit/server/access_control/PatternBuilder.test.ts +++ b/test/unit/server/access_control/PatternBuilder.test.ts @@ -3,7 +3,7 @@ import assert = require("assert"); import winston = require("winston"); import PatternBuilder from "../../../../src/server/lib/access_control/PatternBuilder"; -import { ACLConfiguration } from "../../../../src/types/Configuration"; +import { ACLConfiguration } from "../../../../src/server/lib/configuration/Configuration"; describe("test access control manager", function () { describe("test access control pattern builder when no configuration is provided", () => { diff --git a/test/unit/server/connectors/mongo/MongoClient.test.ts b/test/unit/server/connectors/mongo/MongoClient.test.ts new file mode 100644 index 00000000..c3f9a254 --- /dev/null +++ b/test/unit/server/connectors/mongo/MongoClient.test.ts @@ -0,0 +1,38 @@ +import Assert = require("assert"); +import Sinon = require("sinon"); +import MongoDB = require("mongodb"); +import { MongoClient } from "../../../../../src/server/lib/connectors/mongo/MongoClient"; + +describe("MongoClient", function () { + let mongoClientConnectStub: Sinon.SinonStub; + let mongoDatabase: any; + let mongoDatabaseCollectionStub: Sinon.SinonStub; + + describe("collection", function () { + before(function () { + mongoDatabaseCollectionStub = Sinon.stub(); + mongoDatabase = { + collection: mongoDatabaseCollectionStub + }; + + mongoClientConnectStub = Sinon.stub(MongoDB.MongoClient, "connect"); + mongoClientConnectStub.yields(undefined, mongoDatabase); + }); + + after(function () { + mongoClientConnectStub.restore(); + }); + + it("should create a collection", function () { + const COLLECTION_NAME = "mycollection"; + const client = new MongoClient(mongoDatabase); + + mongoDatabaseCollectionStub.returns({}); + + const collection = client.collection(COLLECTION_NAME); + + Assert(collection); + Assert(mongoDatabaseCollectionStub.calledWith(COLLECTION_NAME )); + }); + }); +}); diff --git a/test/unit/server/connectors/mongo/MongoConnector.test.ts b/test/unit/server/connectors/mongo/MongoConnector.test.ts new file mode 100644 index 00000000..1bc24d4b --- /dev/null +++ b/test/unit/server/connectors/mongo/MongoConnector.test.ts @@ -0,0 +1,45 @@ +import Assert = require("assert"); +import Sinon = require("sinon"); +import MongoDB = require("mongodb"); +import BluebirdPromise = require("bluebird"); +import { IMongoClient } from "../../../../../src/server/lib/connectors/mongo/IMongoClient"; +import { MongoConnector } from "../../../../../src/server/lib/connectors/mongo/MongoConnector"; + +describe("MongoConnector", function () { + let mongoClientConnectStub: Sinon.SinonStub; + describe("create", function () { + before(function () { + mongoClientConnectStub = Sinon.stub(MongoDB.MongoClient, "connect"); + }); + + after(function() { + mongoClientConnectStub.restore(); + }); + + it("should create a connector", function () { + mongoClientConnectStub.yields(undefined); + + const url = "mongodb://test.url"; + const connector = new MongoConnector(url); + return connector.connect() + .then(function (client: IMongoClient) { + Assert(client); + Assert(mongoClientConnectStub.calledWith(url)); + }); + }); + + it("should fail creating a connector", function () { + mongoClientConnectStub.yields(new Error("Error while creating mongo client")); + + const url = "mongodb://test.url"; + const connector = new MongoConnector(url); + return connector.connect() + .then(function () { return BluebirdPromise.reject(new Error("It should not be here")); }) + .error(function (client: IMongoClient) { + Assert(client); + Assert(mongoClientConnectStub.calledWith(url)); + return BluebirdPromise.resolve(); + }); + }); + }); +}); diff --git a/test/unit/server/connectors/mongo/MongoConnectorFactory.test.ts b/test/unit/server/connectors/mongo/MongoConnectorFactory.test.ts new file mode 100644 index 00000000..2428507c --- /dev/null +++ b/test/unit/server/connectors/mongo/MongoConnectorFactory.test.ts @@ -0,0 +1,13 @@ +import Assert = require("assert"); +import { MongoConnectorFactory } from "../../../../../src/server/lib/connectors/mongo/MongoConnectorFactory"; + +describe("MongoConnectorFactory", function () { + describe("create", function () { + it("should create a connector", function () { + const factory = new MongoConnectorFactory(); + const connector = factory.create("mongodb://test.url"); + + Assert(connector); + }); + }); +}); diff --git a/test/unit/server/ldap/Authenticator.test.ts b/test/unit/server/ldap/Authenticator.test.ts index cc652f6b..9753163c 100644 --- a/test/unit/server/ldap/Authenticator.test.ts +++ b/test/unit/server/ldap/Authenticator.test.ts @@ -1,6 +1,6 @@ import { Authenticator } from "../../../../src/server/lib/ldap/Authenticator"; -import { LdapConfiguration } from "../../../../src/types/Configuration"; +import { LdapConfiguration } from "../../../../src/server/lib/configuration/Configuration"; import sinon = require("sinon"); import BluebirdPromise = require("bluebird"); diff --git a/test/unit/server/ldap/EmailsRetriever.test.ts b/test/unit/server/ldap/EmailsRetriever.test.ts index a5abe5a5..1ad68768 100644 --- a/test/unit/server/ldap/EmailsRetriever.test.ts +++ b/test/unit/server/ldap/EmailsRetriever.test.ts @@ -1,6 +1,6 @@ import { EmailsRetriever } from "../../../../src/server/lib/ldap/EmailsRetriever"; -import { LdapConfiguration } from "../../../../src/types/Configuration"; +import { LdapConfiguration } from "../../../../src/server/lib/configuration/Configuration"; import sinon = require("sinon"); import BluebirdPromise = require("bluebird"); diff --git a/test/unit/server/ldap/PasswordUpdater.test.ts b/test/unit/server/ldap/PasswordUpdater.test.ts index 3d2e49a4..9cf318aa 100644 --- a/test/unit/server/ldap/PasswordUpdater.test.ts +++ b/test/unit/server/ldap/PasswordUpdater.test.ts @@ -1,6 +1,6 @@ import { PasswordUpdater } from "../../../../src/server/lib/ldap/PasswordUpdater"; -import { LdapConfiguration } from "../../../../src/types/Configuration"; +import { LdapConfiguration } from "../../../../src/server/lib/configuration/Configuration"; import sinon = require("sinon"); import BluebirdPromise = require("bluebird"); diff --git a/test/unit/server/mocks/ServerVariablesMock.ts b/test/unit/server/mocks/ServerVariablesMock.ts index 8f5b65a0..6281b333 100644 --- a/test/unit/server/mocks/ServerVariablesMock.ts +++ b/test/unit/server/mocks/ServerVariablesMock.ts @@ -1,14 +1,18 @@ import sinon = require("sinon"); import express = require("express"); -import {  ServerVariables, VARIABLES_KEY }  from "../../../../src/server/lib/ServerVariables"; +import winston = require("winston"); +import { UserDataStoreStub } from "./storage/UserDataStoreStub"; +import { ServerVariables, VARIABLES_KEY }  from "../../../../src/server/lib/ServerVariablesHandler"; export interface ServerVariablesMock { logger: any; - ldap: any; + ldapAuthenticator: any; + ldapEmailsRetriever: any; + ldapPasswordUpdater: any; totpValidator: any; totpGenerator: any; u2f: any; - userDataStore: any; + userDataStore: UserDataStoreStub; notifier: any; regulator: any; config: any; @@ -16,20 +20,20 @@ export interface ServerVariablesMock { } -export function mock(app: express.Application): ServerVariables { - const mocks: ServerVariables = { - accessController: sinon.stub() as any, - config: sinon.stub() as any, +export function mock(app: express.Application): ServerVariablesMock { + const mocks: ServerVariablesMock = { + accessController: sinon.stub(), + config: sinon.stub(), ldapAuthenticator: sinon.stub() as any, ldapEmailsRetriever: sinon.stub() as any, ldapPasswordUpdater: sinon.stub() as any, - logger: sinon.stub() as any, - notifier: sinon.stub() as any, - regulator: sinon.stub() as any, - totpGenerator: sinon.stub() as any, - totpValidator: sinon.stub() as any, - u2f: sinon.stub() as any, - userDataStore: sinon.stub() as any, + logger: winston, + notifier: sinon.stub(), + regulator: sinon.stub(), + totpGenerator: sinon.stub(), + totpValidator: sinon.stub(), + u2f: sinon.stub(), + userDataStore: new UserDataStoreStub() }; app.get = sinon.stub().withArgs(VARIABLES_KEY).returns(mocks); return mocks; diff --git a/test/unit/server/mocks/UserDataStore.ts b/test/unit/server/mocks/UserDataStore.ts deleted file mode 100644 index 4a4daa6a..00000000 --- a/test/unit/server/mocks/UserDataStore.ts +++ /dev/null @@ -1,22 +0,0 @@ - -import sinon = require("sinon"); - -export interface UserDataStore { - set_u2f_meta: sinon.SinonStub; - get_u2f_meta: sinon.SinonStub; - issue_identity_check_token: sinon.SinonStub; - consume_identity_check_token: sinon.SinonStub; - get_totp_secret: sinon.SinonStub; - set_totp_secret: sinon.SinonStub; -} - -export function UserDataStore(): UserDataStore { - return { - set_u2f_meta: sinon.stub(), - get_u2f_meta: sinon.stub(), - issue_identity_check_token: sinon.stub(), - consume_identity_check_token: sinon.stub(), - get_totp_secret: sinon.stub(), - set_totp_secret: sinon.stub() - }; -} diff --git a/test/unit/server/mocks/connectors/mongo/MongoClientStub.ts b/test/unit/server/mocks/connectors/mongo/MongoClientStub.ts new file mode 100644 index 00000000..2ee4d063 --- /dev/null +++ b/test/unit/server/mocks/connectors/mongo/MongoClientStub.ts @@ -0,0 +1,15 @@ +import Sinon = require("sinon"); +import MongoDB = require("mongodb"); +import { IMongoClient } from "../../../../../../src/server/lib/connectors/mongo/IMongoClient"; + +export class MongoClientStub implements IMongoClient { + public collectionStub: Sinon.SinonStub; + + constructor() { + this.collectionStub = Sinon.stub(); + } + + collection(name: string): MongoDB.Collection { + return this.collectionStub(name); + } +} \ No newline at end of file diff --git a/test/unit/server/mocks/storage/CollectionFactoryStub.ts b/test/unit/server/mocks/storage/CollectionFactoryStub.ts new file mode 100644 index 00000000..74936b28 --- /dev/null +++ b/test/unit/server/mocks/storage/CollectionFactoryStub.ts @@ -0,0 +1,16 @@ +import BluebirdPromise = require("bluebird"); +import Sinon = require("sinon"); +import { ICollection } from "../../../../../src/server/lib/storage/ICollection"; +import { ICollectionFactory } from "../../../../../src/server/lib/storage/ICollectionFactory"; + +export class CollectionFactoryStub implements ICollectionFactory { + buildStub: Sinon.SinonStub; + + constructor() { + this.buildStub = Sinon.stub(); + } + + build(collectionName: string): ICollection { + return this.buildStub(collectionName); + } +} diff --git a/test/unit/server/mocks/storage/CollectionStub.ts b/test/unit/server/mocks/storage/CollectionStub.ts new file mode 100644 index 00000000..6b83267f --- /dev/null +++ b/test/unit/server/mocks/storage/CollectionStub.ts @@ -0,0 +1,39 @@ +import BluebirdPromise = require("bluebird"); +import Sinon = require("sinon"); +import { ICollection } from "../../../../../src/server/lib/storage/ICollection"; + +export class CollectionStub implements ICollection { + findStub: Sinon.SinonStub; + findOneStub: Sinon.SinonStub; + updateStub: Sinon.SinonStub; + removeStub: Sinon.SinonStub; + insertStub: Sinon.SinonStub; + + constructor() { + this.findStub = Sinon.stub(); + this.findOneStub = Sinon.stub(); + this.updateStub = Sinon.stub(); + this.removeStub = Sinon.stub(); + this.insertStub = Sinon.stub(); + } + + find(filter: any, sortKeys: any, count: number): BluebirdPromise { + return this.findStub(filter, sortKeys, count); + } + + findOne(filter: any): BluebirdPromise { + return this.findOneStub(filter); + } + + update(filter: any, document: any, options: any): BluebirdPromise { + return this.updateStub(filter, document, options); + } + + remove(filter: any): BluebirdPromise { + return this.removeStub(filter); + } + + insert(document: any): BluebirdPromise { + return this.insertStub(document); + } +} diff --git a/test/unit/server/mocks/storage/UserDataStoreStub.ts b/test/unit/server/mocks/storage/UserDataStoreStub.ts new file mode 100644 index 00000000..cb6aa1ff --- /dev/null +++ b/test/unit/server/mocks/storage/UserDataStoreStub.ts @@ -0,0 +1,65 @@ +import Sinon = require("sinon"); +import BluebirdPromise = require("bluebird"); + +import { TOTPSecretDocument } from "../../../../../src/server/lib/storage/TOTPSecretDocument"; +import { U2FRegistrationDocument } from "../../../../../src/server/lib/storage/U2FRegistrationDocument"; +import { U2FRegistration } from "../../../../../src/types/U2FRegistration"; +import { TOTPSecret } from "../../../../../src/types/TOTPSecret"; +import { AuthenticationTraceDocument } from "../../../../../src/server/lib/storage/AuthenticationTraceDocument"; +import { IdentityValidationDocument } from "../../../../../src/server/lib/storage/IdentityValidationDocument"; + +import { IUserDataStore } from "../../../../../src/server/lib/storage/IUserDataStore"; + +export class UserDataStoreStub implements IUserDataStore { + saveU2FRegistrationStub: Sinon.SinonStub; + retrieveU2FRegistrationStub: Sinon.SinonStub; + saveAuthenticationTraceStub: Sinon.SinonStub; + retrieveLatestAuthenticationTracesStub: Sinon.SinonStub; + produceIdentityValidationTokenStub: Sinon.SinonStub; + consumeIdentityValidationTokenStub: Sinon.SinonStub; + saveTOTPSecretStub: Sinon.SinonStub; + retrieveTOTPSecretStub: Sinon.SinonStub; + + constructor() { + this.saveU2FRegistrationStub = Sinon.stub(); + this.retrieveU2FRegistrationStub = Sinon.stub(); + this.saveAuthenticationTraceStub = Sinon.stub(); + this.retrieveLatestAuthenticationTracesStub = Sinon.stub(); + this.produceIdentityValidationTokenStub = Sinon.stub(); + this.consumeIdentityValidationTokenStub = Sinon.stub(); + this.saveTOTPSecretStub = Sinon.stub(); + this.retrieveTOTPSecretStub = Sinon.stub(); + } + + saveU2FRegistration(userId: string, appId: string, registration: U2FRegistration): BluebirdPromise { + return this.saveU2FRegistrationStub(userId, appId, registration); + } + + retrieveU2FRegistration(userId: string, appId: string): BluebirdPromise { + return this.retrieveU2FRegistrationStub(userId, appId); + } + + saveAuthenticationTrace(userId: string, isAuthenticationSuccessful: boolean): BluebirdPromise { + return this.saveAuthenticationTraceStub(userId, isAuthenticationSuccessful); + } + + retrieveLatestAuthenticationTraces(userId: string, isAuthenticationSuccessful: boolean, count: number): BluebirdPromise { + return this.retrieveLatestAuthenticationTracesStub(userId, isAuthenticationSuccessful, count); + } + + produceIdentityValidationToken(userId: string, token: string, challenge: string, maxAge: number): BluebirdPromise { + return this.produceIdentityValidationTokenStub(userId, token, challenge, maxAge); + } + + consumeIdentityValidationToken(token: string, challenge: string): BluebirdPromise { + return this.consumeIdentityValidationTokenStub(token, challenge); + } + + saveTOTPSecret(userId: string, secret: TOTPSecret): BluebirdPromise { + return this.saveTOTPSecretStub(userId, secret); + } + + retrieveTOTPSecret(userId: string): BluebirdPromise { + return this.retrieveTOTPSecretStub(userId); + } +} \ No newline at end of file diff --git a/test/unit/server/routes/errors/401/get.test.ts b/test/unit/server/routes/errors/401/get.test.ts new file mode 100644 index 00000000..2728cb1f --- /dev/null +++ b/test/unit/server/routes/errors/401/get.test.ts @@ -0,0 +1,19 @@ +import Sinon = require("sinon"); +import Express = require("express"); +import Assert = require("assert"); +import Get401 from "../../../../../../src/server/lib/routes/error/401/get"; + +describe("Server error 401", function () { + it("should render the page", function () { + const req = {} as Express.Request; + const res = { + render: Sinon.stub() + }; + + return Get401(req, res as any) + .then(function () { + Assert(res.render.calledOnce); + Assert(res.render.calledWith("errors/401")); + }); + }); +}); \ No newline at end of file diff --git a/test/unit/server/routes/errors/403/get.test.ts b/test/unit/server/routes/errors/403/get.test.ts new file mode 100644 index 00000000..b3b78721 --- /dev/null +++ b/test/unit/server/routes/errors/403/get.test.ts @@ -0,0 +1,19 @@ +import Sinon = require("sinon"); +import Express = require("express"); +import Assert = require("assert"); +import Get403 from "../../../../../../src/server/lib/routes/error/403/get"; + +describe("Server error 403", function () { + it("should render the page", function () { + const req = {} as Express.Request; + const res = { + render: Sinon.stub() + }; + + return Get403(req, res as any) + .then(function () { + Assert(res.render.calledOnce); + Assert(res.render.calledWith("errors/403")); + }); + }); +}); \ No newline at end of file diff --git a/test/unit/server/routes/errors/404/get.test.ts b/test/unit/server/routes/errors/404/get.test.ts new file mode 100644 index 00000000..fe20a4d1 --- /dev/null +++ b/test/unit/server/routes/errors/404/get.test.ts @@ -0,0 +1,19 @@ +import Sinon = require("sinon"); +import Express = require("express"); +import Assert = require("assert"); +import Get404 from "../../../../../../src/server/lib/routes/error/404/get"; + +describe("Server error 404", function () { + it("should render the page", function () { + const req = {} as Express.Request; + const res = { + render: Sinon.stub() + }; + + return Get404(req, res as any) + .then(function () { + Assert(res.render.calledOnce); + Assert(res.render.calledWith("errors/404")); + }); + }); +}); \ No newline at end of file diff --git a/test/unit/server/routes/firstfactor/post.test.ts b/test/unit/server/routes/firstfactor/post.test.ts index dea06aab..aa04fc2a 100644 --- a/test/unit/server/routes/firstfactor/post.test.ts +++ b/test/unit/server/routes/firstfactor/post.test.ts @@ -13,7 +13,7 @@ import AuthenticationRegulatorMock = require("../../mocks/AuthenticationRegulato import AccessControllerMock = require("../../mocks/AccessController"); import ExpressMock = require("../../mocks/express"); import ServerVariablesMock = require("../../mocks/ServerVariablesMock"); -import { ServerVariables } from "../../../../../src/server/lib/ServerVariables"; +import { ServerVariables } from "../../../../../src/server/lib/ServerVariablesHandler"; describe("test the first factor validation route", function () { let req: ExpressMock.RequestMock; diff --git a/test/unit/server/routes/password-reset/identity/PasswordResetHandler.test.ts b/test/unit/server/routes/password-reset/identity/PasswordResetHandler.test.ts index 177f5f4b..512a0eff 100644 --- a/test/unit/server/routes/password-reset/identity/PasswordResetHandler.test.ts +++ b/test/unit/server/routes/password-reset/identity/PasswordResetHandler.test.ts @@ -1,22 +1,21 @@ import PasswordResetHandler from "../../../../../../src/server/lib/routes/password-reset/identity/PasswordResetHandler"; import PasswordUpdater = require("../../../../../../src/server/lib/ldap/PasswordUpdater"); -import { ServerVariables } from "../../../../../../src/server/lib/ServerVariables"; -import sinon = require("sinon"); +import { ServerVariablesHandler } from "../../../../../../src/server/lib/ServerVariablesHandler"; +import { UserDataStore } from "../../../../../../src/server/lib/storage/UserDataStore"; +import Sinon = require("sinon"); import winston = require("winston"); import assert = require("assert"); import BluebirdPromise = require("bluebird"); import ExpressMock = require("../../../mocks/express"); -import { UserDataStore } from "../../../mocks/UserDataStore"; import ServerVariablesMock = require("../../../mocks/ServerVariablesMock"); describe("test reset password identity check", function () { let req: ExpressMock.RequestMock; let res: ExpressMock.ResponseMock; - let userDataStore: UserDataStore; let configuration: any; - let serverVariables: ServerVariables; + let serverVariables: ServerVariablesMock.ServerVariablesMock; beforeEach(function () { req = { @@ -24,7 +23,7 @@ describe("test reset password identity check", function () { userid: "user" }, app: { - get: sinon.stub() + get: Sinon.stub() }, session: { auth_session: { @@ -45,14 +44,10 @@ describe("test reset password identity check", function () { serverVariables = ServerVariablesMock.mock(req.app); - - userDataStore = UserDataStore(); - userDataStore.set_u2f_meta.returns(BluebirdPromise.resolve({})); - userDataStore.get_u2f_meta.returns(BluebirdPromise.resolve({})); - userDataStore.issue_identity_check_token.returns(BluebirdPromise.resolve({})); - userDataStore.consume_identity_check_token.returns(BluebirdPromise.resolve({})); - serverVariables.userDataStore = userDataStore as any; - + serverVariables.userDataStore.saveU2FRegistrationStub.returns(BluebirdPromise.resolve({})); + serverVariables.userDataStore.retrieveU2FRegistrationStub.returns(BluebirdPromise.resolve({})); + serverVariables.userDataStore.produceIdentityValidationTokenStub.returns(BluebirdPromise.resolve({})); + serverVariables.userDataStore.consumeIdentityValidationTokenStub.returns(BluebirdPromise.resolve({})); configuration = { ldap: { @@ -64,9 +59,8 @@ describe("test reset password identity check", function () { serverVariables.logger = winston; serverVariables.config = configuration; serverVariables.ldapEmailsRetriever = { - retrieve: sinon.stub() + retrieve: Sinon.stub() } as any; - res = ExpressMock.ResponseMock(); }); diff --git a/test/unit/server/routes/password-reset/post.test.ts b/test/unit/server/routes/password-reset/post.test.ts index 40ec930b..73e46e6d 100644 --- a/test/unit/server/routes/password-reset/post.test.ts +++ b/test/unit/server/routes/password-reset/post.test.ts @@ -2,23 +2,22 @@ import PasswordResetFormPost = require("../../../../../src/server/lib/routes/password-reset/form/post"); import { PasswordUpdater } from "../../../../../src/server/lib/ldap/PasswordUpdater"; import AuthenticationSession = require("../../../../../src/server/lib/AuthenticationSession"); -import { ServerVariables } from "../../../../../src/server/lib/ServerVariables"; -import sinon = require("sinon"); +import { ServerVariablesHandler } from "../../../../../src/server/lib/ServerVariablesHandler"; +import { UserDataStore } from "../../../../../src/server/lib/storage/UserDataStore"; +import Sinon = require("sinon"); import winston = require("winston"); import assert = require("assert"); import BluebirdPromise = require("bluebird"); import ExpressMock = require("../../mocks/express"); -import { UserDataStore } from "../../mocks/UserDataStore"; import ServerVariablesMock = require("../../mocks/ServerVariablesMock"); describe("test reset password route", function () { let req: ExpressMock.RequestMock; let res: ExpressMock.ResponseMock; - let userDataStore: UserDataStore; let configuration: any; let authSession: AuthenticationSession.AuthenticationSession; - let serverVariables: ServerVariables; + let serverVariables: ServerVariablesMock.ServerVariablesMock; beforeEach(function () { req = { @@ -26,7 +25,7 @@ describe("test reset password route", function () { userid: "user" }, app: { - get: sinon.stub() + get: Sinon.stub() }, session: {}, headers: { @@ -46,13 +45,10 @@ describe("test reset password route", function () { }; serverVariables = ServerVariablesMock.mock(req.app); - userDataStore = UserDataStore(); - userDataStore.set_u2f_meta.returns(BluebirdPromise.resolve({})); - userDataStore.get_u2f_meta.returns(BluebirdPromise.resolve({})); - userDataStore.issue_identity_check_token.returns(BluebirdPromise.resolve({})); - userDataStore.consume_identity_check_token.returns(BluebirdPromise.resolve({})); - serverVariables.userDataStore = userDataStore as any; - + serverVariables.userDataStore.saveU2FRegistrationStub.returns(BluebirdPromise.resolve({})); + serverVariables.userDataStore.retrieveU2FRegistrationStub.returns(BluebirdPromise.resolve({})); + serverVariables.userDataStore.produceIdentityValidationTokenStub.returns(BluebirdPromise.resolve({})); + serverVariables.userDataStore.consumeIdentityValidationTokenStub.returns(BluebirdPromise.resolve({})); configuration = { ldap: { @@ -65,7 +61,7 @@ describe("test reset password route", function () { serverVariables.config = configuration; serverVariables.ldapPasswordUpdater = { - updatePassword: sinon.stub() + updatePassword: Sinon.stub() } as any; res = ExpressMock.ResponseMock(); @@ -96,7 +92,7 @@ describe("test reset password route", function () { userid: "user", challenge: undefined }; - res.send = sinon.spy(function () { + res.send = Sinon.spy(function () { assert.equal(res.status.getCall(0).args[0], 403); done(); }); @@ -111,8 +107,8 @@ describe("test reset password route", function () { req.body = {}; req.body.password = "new-password"; - (serverVariables.ldapPasswordUpdater.updatePassword as sinon.SinonStub).returns(BluebirdPromise.reject("Internal error with LDAP")); - res.send = sinon.spy(function () { + (serverVariables.ldapPasswordUpdater.updatePassword as Sinon.SinonStub).returns(BluebirdPromise.reject("Internal error with LDAP")); + res.send = Sinon.spy(function () { assert.equal(res.status.getCall(0).args[0], 500); done(); }); diff --git a/test/unit/server/routes/secondfactor/totp/register/RegistrationHandler.test.ts b/test/unit/server/routes/secondfactor/totp/register/RegistrationHandler.test.ts index ec5b850d..2895c4e8 100644 --- a/test/unit/server/routes/secondfactor/totp/register/RegistrationHandler.test.ts +++ b/test/unit/server/routes/secondfactor/totp/register/RegistrationHandler.test.ts @@ -1,19 +1,18 @@ -import sinon = require("sinon"); +import Sinon = require("sinon"); import winston = require("winston"); import RegistrationHandler from "../../../../../../../src/server/lib/routes/secondfactor/totp/identity/RegistrationHandler"; import { Identity } from "../../../../../../../src/types/Identity"; import AuthenticationSession = require("../../../../../../../src/server/lib/AuthenticationSession"); +import { UserDataStore } from "../../../../../../../src/server/lib/storage/UserDataStore"; import assert = require("assert"); import BluebirdPromise = require("bluebird"); import ExpressMock = require("../../../../mocks/express"); -import UserDataStoreMock = require("../../../../mocks/UserDataStore"); import ServerVariablesMock = require("../../../../mocks/ServerVariablesMock"); describe("test totp register", function () { let req: ExpressMock.RequestMock; let res: ExpressMock.ResponseMock; - let userDataStore: UserDataStoreMock.UserDataStore; const registrationHandler: RegistrationHandler = new RegistrationHandler(); let authSession: AuthenticationSession.AuthenticationSession; @@ -37,13 +36,11 @@ describe("test totp register", function () { inMemoryOnly: true }; - userDataStore = UserDataStoreMock.UserDataStore(); - userDataStore.set_u2f_meta = sinon.stub().returns(BluebirdPromise.resolve({})); - userDataStore.get_u2f_meta = sinon.stub().returns(BluebirdPromise.resolve({})); - userDataStore.issue_identity_check_token = sinon.stub().returns(BluebirdPromise.resolve({})); - userDataStore.consume_identity_check_token = sinon.stub().returns(BluebirdPromise.resolve({})); - userDataStore.set_totp_secret = sinon.stub().returns(BluebirdPromise.resolve({})); - mocks.userDataStore = userDataStore as any; + mocks.userDataStore.saveU2FRegistrationStub.returns(BluebirdPromise.resolve({})); + mocks.userDataStore.retrieveU2FRegistrationStub.returns(BluebirdPromise.resolve({})); + mocks.userDataStore.produceIdentityValidationTokenStub.returns(BluebirdPromise.resolve({})); + mocks.userDataStore.consumeIdentityValidationTokenStub.returns(BluebirdPromise.resolve({})); + mocks.userDataStore.saveTOTPSecretStub.returns(BluebirdPromise.resolve({})); res = ExpressMock.ResponseMock(); }); diff --git a/test/unit/server/routes/secondfactor/totp/sign/post.test.ts b/test/unit/server/routes/secondfactor/totp/sign/post.test.ts index 21b2393e..0c6b9684 100644 --- a/test/unit/server/routes/secondfactor/totp/sign/post.test.ts +++ b/test/unit/server/routes/secondfactor/totp/sign/post.test.ts @@ -9,15 +9,14 @@ import AuthenticationSession = require("../../../../../../../src/server/lib/Auth import SignPost = require("../../../../../../../src/server/lib/routes/secondfactor/totp/sign/post"); import ExpressMock = require("../../../../mocks/express"); -import UserDataStoreMock = require("../../../../mocks/UserDataStore"); import TOTPValidatorMock = require("../../../../mocks/TOTPValidator"); import ServerVariablesMock = require("../../../../mocks/ServerVariablesMock"); +import { UserDataStoreStub } from "../../../../mocks/storage/UserDataStoreStub"; describe("test totp route", function () { let req: ExpressMock.RequestMock; let res: ExpressMock.ResponseMock; let totpValidator: TOTPValidatorMock.TOTPValidatorMock; - let userDataStore: UserDataStoreMock.UserDataStore; let authSession: AuthenticationSession.AuthenticationSession; beforeEach(function () { @@ -42,20 +41,17 @@ describe("test totp route", function () { const config = { totp_secret: "secret" }; totpValidator = TOTPValidatorMock.TOTPValidatorMock(); - userDataStore = UserDataStoreMock.UserDataStore(); - const doc = { userid: "user", secret: { base32: "ABCDEF" } }; - userDataStore.get_totp_secret.returns(BluebirdPromise.resolve(doc)); + mocks.userDataStore.retrieveTOTPSecretStub.returns(BluebirdPromise.resolve(doc)); mocks.logger = winston; - mocks.totpValidator = totpValidator as any; - mocks.config = config as any; - mocks.userDataStore = userDataStore as any; + mocks.totpValidator = totpValidator; + mocks.config = config; }); diff --git a/test/unit/server/routes/secondfactor/u2f/identity/RegistrationHandler.test.ts b/test/unit/server/routes/secondfactor/u2f/identity/RegistrationHandler.test.ts index 117bfbaf..4fb13a28 100644 --- a/test/unit/server/routes/secondfactor/u2f/identity/RegistrationHandler.test.ts +++ b/test/unit/server/routes/secondfactor/u2f/identity/RegistrationHandler.test.ts @@ -8,13 +8,12 @@ import RegistrationHandler from "../../../../../../../src/server/lib/routes/seco import AuthenticationSession = require("../../../../../../../src/server/lib/AuthenticationSession"); import ExpressMock = require("../../../../mocks/express"); -import UserDataStoreMock = require("../../../../mocks/UserDataStore"); +import { UserDataStoreStub } from "../../../../mocks/storage/UserDataStoreStub"; import ServerVariablesMock = require("../../../../mocks/ServerVariablesMock"); describe("test register handler", function () { let req: ExpressMock.RequestMock; let res: ExpressMock.ResponseMock; - let userDataStore: UserDataStoreMock.UserDataStore; let authSession: AuthenticationSession.AuthenticationSession; beforeEach(function () { @@ -36,12 +35,10 @@ describe("test register handler", function () { inMemoryOnly: true }; - userDataStore = UserDataStoreMock.UserDataStore(); - userDataStore.set_u2f_meta = sinon.stub().returns(BluebirdPromise.resolve({})); - userDataStore.get_u2f_meta = sinon.stub().returns(BluebirdPromise.resolve({})); - userDataStore.issue_identity_check_token = sinon.stub().returns(BluebirdPromise.resolve({})); - userDataStore.consume_identity_check_token = sinon.stub().returns(BluebirdPromise.resolve({})); - mocks.userDataStore = userDataStore as any; + mocks.userDataStore.saveU2FRegistrationStub.returns(BluebirdPromise.resolve({})); + mocks.userDataStore.retrieveU2FRegistrationStub.returns(BluebirdPromise.resolve({})); + mocks.userDataStore.produceIdentityValidationTokenStub.returns(BluebirdPromise.resolve({})); + mocks.userDataStore.consumeIdentityValidationTokenStub.returns(BluebirdPromise.resolve({})); res = ExpressMock.ResponseMock(); res.send = sinon.spy(); diff --git a/test/unit/server/routes/secondfactor/u2f/register/post.test.ts b/test/unit/server/routes/secondfactor/u2f/register/post.test.ts index 1b928fc7..5c5dfd80 100644 --- a/test/unit/server/routes/secondfactor/u2f/register/post.test.ts +++ b/test/unit/server/routes/secondfactor/u2f/register/post.test.ts @@ -4,11 +4,11 @@ import BluebirdPromise = require("bluebird"); import assert = require("assert"); import U2FRegisterPost = require("../../../../../../../src/server/lib/routes/secondfactor/u2f/register/post"); import AuthenticationSession = require("../../../../../../../src/server/lib/AuthenticationSession"); -import { ServerVariables } from "../../../../../../../src/server/lib/ServerVariables"; +import { ServerVariablesHandler } from "../../../../../../../src/server/lib/ServerVariablesHandler"; import winston = require("winston"); import ExpressMock = require("../../../../mocks/express"); -import UserDataStoreMock = require("../../../../mocks/UserDataStore"); +import { UserDataStoreStub } from "../../../../mocks/storage/UserDataStoreStub"; import U2FMock = require("../../../../mocks/u2f"); import ServerVariablesMock = require("../../../../mocks/ServerVariablesMock"); import U2f = require("u2f"); @@ -16,8 +16,7 @@ import U2f = require("u2f"); describe("test u2f routes: register", function () { let req: ExpressMock.RequestMock; let res: ExpressMock.ResponseMock; - let userDataStore: UserDataStoreMock.UserDataStore; - let mocks: ServerVariables; + let mocks: ServerVariablesMock.ServerVariablesMock; let authSession: AuthenticationSession.AuthenticationSession; beforeEach(function () { @@ -44,10 +43,8 @@ describe("test u2f routes: register", function () { inMemoryOnly: true }; - userDataStore = UserDataStoreMock.UserDataStore(); - userDataStore.set_u2f_meta = sinon.stub().returns(BluebirdPromise.resolve({})); - userDataStore.get_u2f_meta = sinon.stub().returns(BluebirdPromise.resolve({})); - mocks.userDataStore = userDataStore as any; + mocks.userDataStore.saveU2FRegistrationStub.returns(BluebirdPromise.resolve({})); + mocks.userDataStore.retrieveU2FRegistrationStub.returns(BluebirdPromise.resolve({})); res = ExpressMock.ResponseMock(); res.send = sinon.spy(); @@ -77,7 +74,7 @@ describe("test u2f routes: register", function () { mocks.u2f = u2f_mock; return U2FRegisterPost.default(req as any, res as any) .then(function () { - assert.equal("user", userDataStore.set_u2f_meta.getCall(0).args[0]); + assert.equal("user", mocks.userDataStore.saveU2FRegistrationStub.getCall(0).args[0]); assert.equal(authSession.identity_check, undefined); }); }); diff --git a/test/unit/server/routes/secondfactor/u2f/register_request/get.test.ts b/test/unit/server/routes/secondfactor/u2f/register_request/get.test.ts index 55473eb5..a3e77064 100644 --- a/test/unit/server/routes/secondfactor/u2f/register_request/get.test.ts +++ b/test/unit/server/routes/secondfactor/u2f/register_request/get.test.ts @@ -4,11 +4,11 @@ import BluebirdPromise = require("bluebird"); import assert = require("assert"); import U2FRegisterRequestGet = require("../../../../../../../src/server/lib/routes/secondfactor/u2f/register_request/get"); import AuthenticationSession = require("../../../../../../../src/server/lib/AuthenticationSession"); -import { ServerVariables } from "../../../../../../../src/server/lib/ServerVariables"; +import { ServerVariablesHandler } from "../../../../../../../src/server/lib/ServerVariablesHandler"; import winston = require("winston"); import ExpressMock = require("../../../../mocks/express"); -import UserDataStoreMock = require("../../../../mocks/UserDataStore"); +import { UserDataStoreStub } from "../../../../mocks/storage/UserDataStoreStub"; import U2FMock = require("../../../../mocks/u2f"); import ServerVariablesMock = require("../../../../mocks/ServerVariablesMock"); import U2f = require("u2f"); @@ -16,8 +16,7 @@ import U2f = require("u2f"); describe("test u2f routes: register_request", function () { let req: ExpressMock.RequestMock; let res: ExpressMock.ResponseMock; - let userDataStore: UserDataStoreMock.UserDataStore; - let mocks: ServerVariables; + let mocks: ServerVariablesMock.ServerVariablesMock; let authSession: AuthenticationSession.AuthenticationSession; beforeEach(function () { @@ -44,10 +43,9 @@ describe("test u2f routes: register_request", function () { inMemoryOnly: true }; - userDataStore = UserDataStoreMock.UserDataStore(); - userDataStore.set_u2f_meta = sinon.stub().returns(BluebirdPromise.resolve({})); - userDataStore.get_u2f_meta = sinon.stub().returns(BluebirdPromise.resolve({})); - mocks.userDataStore = userDataStore as any; + + mocks.userDataStore.saveU2FRegistrationStub.returns(BluebirdPromise.resolve({})); + mocks.userDataStore.retrieveU2FRegistrationStub.returns(BluebirdPromise.resolve({})); res = ExpressMock.ResponseMock(); res.send = sinon.spy(); diff --git a/test/unit/server/routes/secondfactor/u2f/sign/post.test.ts b/test/unit/server/routes/secondfactor/u2f/sign/post.test.ts index b56b9c8a..0d8a1ce8 100644 --- a/test/unit/server/routes/secondfactor/u2f/sign/post.test.ts +++ b/test/unit/server/routes/secondfactor/u2f/sign/post.test.ts @@ -1,101 +1,102 @@ import sinon = require("sinon"); import BluebirdPromise = require("bluebird"); -import assert = require("assert"); +import Assert = require("assert"); import U2FSignPost = require("../../../../../../../src/server/lib/routes/secondfactor/u2f/sign/post"); import AuthenticationSession = require("../../../../../../../src/server/lib/AuthenticationSession"); -import { ServerVariables } from "../../../../../../../src/server/lib/ServerVariables"; +import { ServerVariablesHandler } from "../../../../../../../src/server/lib/ServerVariablesHandler"; import winston = require("winston"); import ExpressMock = require("../../../../mocks/express"); -import UserDataStoreMock = require("../../../../mocks/UserDataStore"); import ServerVariablesMock = require("../../../../mocks/ServerVariablesMock"); import U2FMock = require("../../../../mocks/u2f"); import U2f = require("u2f"); describe("test u2f routes: sign", function () { - let req: ExpressMock.RequestMock; - let res: ExpressMock.ResponseMock; - let userDataStore: UserDataStoreMock.UserDataStore; - let mocks: ServerVariables; - let authSession: AuthenticationSession.AuthenticationSession; + let req: ExpressMock.RequestMock; + let res: ExpressMock.ResponseMock; + let authSession: AuthenticationSession.AuthenticationSession; + let mocks: ServerVariablesMock.ServerVariablesMock; - beforeEach(function () { - req = ExpressMock.RequestMock(); - req.app = {}; + beforeEach(function () { + req = ExpressMock.RequestMock(); + req.app = {}; - mocks = ServerVariablesMock.mock(req.app); - mocks.logger = winston; + mocks = ServerVariablesMock.mock(req.app); + mocks.logger = winston; - req.session = {}; - AuthenticationSession.reset(req as any); - authSession = AuthenticationSession.get(req as any); - authSession.userid = "user"; - authSession.first_factor = true; - authSession.second_factor = false; - authSession.identity_check = { - challenge: "u2f-register", - userid: "user" - }; - req.headers = {}; - req.headers.host = "localhost"; + req.session = {}; + AuthenticationSession.reset(req as any); + authSession = AuthenticationSession.get(req as any); + authSession.userid = "user"; + authSession.first_factor = true; + authSession.second_factor = false; + authSession.identity_check = { + challenge: "u2f-register", + userid: "user" + }; + req.headers = {}; + req.headers.host = "localhost"; - const options = { - inMemoryOnly: true - }; + const options = { + inMemoryOnly: true + }; - userDataStore = UserDataStoreMock.UserDataStore(); - userDataStore.set_u2f_meta = sinon.stub().returns(BluebirdPromise.resolve({})); - userDataStore.get_u2f_meta = sinon.stub().returns(BluebirdPromise.resolve({})); - mocks.userDataStore = userDataStore as any; + res = ExpressMock.ResponseMock(); + res.send = sinon.spy(); + res.json = sinon.spy(); + res.status = sinon.spy(); + }); - res = ExpressMock.ResponseMock(); - res.send = sinon.spy(); - res.json = sinon.spy(); - res.status = sinon.spy(); - }); + it("should return status code 204", function () { + const expectedStatus = { + keyHandle: "keyHandle", + publicKey: "pbk", + certificate: "cert" + }; + const u2f_mock = U2FMock.U2FMock(); + u2f_mock.checkSignature.returns(expectedStatus); - describe("test signing", () => { - it("should return status code 204", function () { - const expectedStatus = { - keyHandle: "keyHandle", - publicKey: "pbk", - certificate: "cert" - }; - const u2f_mock = U2FMock.U2FMock(); - u2f_mock.checkSignature.returns(expectedStatus); + mocks.userDataStore.retrieveU2FRegistrationStub.returns(BluebirdPromise.resolve({ + registration: { + publicKey: "PUBKEY" + } + })); - authSession.sign_request = { - appId: "app", - challenge: "challenge", - keyHandle: "key", - version: "U2F_V2" - }; - mocks.u2f = u2f_mock; - return U2FSignPost.default(req as any, res as any) - .then(function () { - assert(authSession.second_factor); - }); - }); + authSession.sign_request = { + appId: "app", + challenge: "challenge", + keyHandle: "key", + version: "U2F_V2" + }; + mocks.u2f = u2f_mock; + return U2FSignPost.default(req as any, res as any) + .then(function () { + Assert(authSession.second_factor); + }); + }); - it("should return unauthorized error on registration request internal error", function (done) { - res.send = sinon.spy(function (data: any) { - assert.equal(500, res.status.getCall(0).args[0]); - done(); - }); + it("should return unauthorized error on registration request internal error", function () { + mocks.userDataStore.retrieveU2FRegistrationStub.returns(BluebirdPromise.resolve({ + registration: { + publicKey: "PUBKEY" + } + })); - const u2f_mock = U2FMock.U2FMock(); - u2f_mock.checkSignature.returns({ errorCode: 500 }); + const u2f_mock = U2FMock.U2FMock(); + u2f_mock.checkSignature.returns({ errorCode: 500 }); - authSession.sign_request = { - appId: "app", - challenge: "challenge", - keyHandle: "key", - version: "U2F_V2" - }; - mocks.u2f = u2f_mock; - U2FSignPost.default(req as any, res as any); - }); - }); + authSession.sign_request = { + appId: "app", + challenge: "challenge", + keyHandle: "key", + version: "U2F_V2" + }; + mocks.u2f = u2f_mock; + return U2FSignPost.default(req as any, res as any) + .then(function () { + Assert.equal(500, res.status.getCall(0).args[0]); + }); + }); }); diff --git a/test/unit/server/routes/secondfactor/u2f/sign_request/get.test.ts b/test/unit/server/routes/secondfactor/u2f/sign_request/get.test.ts index bc5261a1..b3026b03 100644 --- a/test/unit/server/routes/secondfactor/u2f/sign_request/get.test.ts +++ b/test/unit/server/routes/secondfactor/u2f/sign_request/get.test.ts @@ -4,11 +4,11 @@ import BluebirdPromise = require("bluebird"); import assert = require("assert"); import U2FSignRequestGet = require("../../../../../../../src/server/lib/routes/secondfactor/u2f/sign_request/get"); import AuthenticationSession = require("../../../../../../../src/server/lib/AuthenticationSession"); -import { ServerVariables } from "../../../../../../../src/server/lib/ServerVariables"; +import { ServerVariablesHandler } from "../../../../../../../src/server/lib/ServerVariablesHandler"; import winston = require("winston"); import ExpressMock = require("../../../../mocks/express"); -import UserDataStoreMock = require("../../../../mocks/UserDataStore"); +import { UserDataStoreStub } from "../../../../mocks/storage/UserDataStoreStub"; import ServerVariablesMock = require("../../../../mocks/ServerVariablesMock"); import U2FMock = require("../../../../mocks/u2f"); import U2f = require("u2f"); @@ -18,8 +18,7 @@ import { SignMessage } from "../../../../../../../src/server/lib/routes/secondfa describe("test u2f routes: sign_request", function () { let req: ExpressMock.RequestMock; let res: ExpressMock.ResponseMock; - let userDataStore: UserDataStoreMock.UserDataStore; - let mocks: ServerVariables; + let mocks: ServerVariablesMock.ServerVariablesMock; let authSession: AuthenticationSession.AuthenticationSession; beforeEach(function () { @@ -48,37 +47,35 @@ describe("test u2f routes: sign_request", function () { inMemoryOnly: true }; - userDataStore = UserDataStoreMock.UserDataStore(); - userDataStore.set_u2f_meta = sinon.stub().returns(BluebirdPromise.resolve({})); - userDataStore.get_u2f_meta = sinon.stub().returns(BluebirdPromise.resolve({})); - mocks.userDataStore = userDataStore as any; - res = ExpressMock.ResponseMock(); res.send = sinon.spy(); res.json = sinon.spy(); res.status = sinon.spy(); }); - describe("test signing request", test_signing_request); + it("should send back the sign request and save it in the session", function () { + const expectedRequest: U2f.RegistrationResult = { + keyHandle: "keyHandle", + publicKey: "publicKey", + certificate: "Certificate", + successful: true + }; + const u2f_mock = U2FMock.U2FMock(); + u2f_mock.request.returns(expectedRequest); - function test_signing_request() { - it("should send back the sign request and save it in the session", function () { - const expectedRequest: U2f.RegistrationResult = { - keyHandle: "keyHandle", - publicKey: "publicKey", - certificate: "Certificate", - successful: true - }; - const u2f_mock = U2FMock.U2FMock(); - u2f_mock.request.returns(expectedRequest); + mocks.userDataStore.retrieveU2FRegistrationStub.returns(BluebirdPromise.resolve({ + registration: { + publicKey: "PUBKEY", + keyHandle: "KeyHandle" + } + })); - mocks.u2f = u2f_mock; - return U2FSignRequestGet.default(req as any, res as any) - .then(function () { - assert.deepEqual(expectedRequest, authSession.sign_request); - assert.deepEqual(expectedRequest, res.json.getCall(0).args[0].request); - }); - }); - } + mocks.u2f = u2f_mock; + return U2FSignRequestGet.default(req as any, res as any) + .then(function () { + assert.deepEqual(expectedRequest, authSession.sign_request); + assert.deepEqual(expectedRequest, res.json.getCall(0).args[0].request); + }); + }); }); diff --git a/test/unit/server/server/PrivatePages.ts b/test/unit/server/server/PrivatePages.ts index 32cc6316..2f8c4414 100644 --- a/test/unit/server/server/PrivatePages.ts +++ b/test/unit/server/server/PrivatePages.ts @@ -41,7 +41,11 @@ describe("Private pages of the server must not be accessible without session", f secret: "session_secret", expiration: 50000, }, - store_in_memory: true, + storage: { + local: { + in_memory: true + } + }, notifier: { gmail: { username: "user@example.com", diff --git a/test/unit/server/server/PublicPages.ts b/test/unit/server/server/PublicPages.ts index 92365270..f24c382e 100644 --- a/test/unit/server/server/PublicPages.ts +++ b/test/unit/server/server/PublicPages.ts @@ -41,7 +41,11 @@ describe("Public pages of the server must be accessible without session", functi secret: "session_secret", expiration: 50000, }, - store_in_memory: true, + storage: { + local: { + in_memory: true + } + }, notifier: { gmail: { username: "user@example.com", diff --git a/test/unit/server/server/Server.test.ts b/test/unit/server/server/Server.test.ts index 7e06b815..fcaf757a 100644 --- a/test/unit/server/server/Server.test.ts +++ b/test/unit/server/server/Server.test.ts @@ -44,7 +44,11 @@ describe("test the server", function () { secret: "session_secret", expiration: 50000, }, - store_in_memory: true, + storage: { + local: { + in_memory: true + } + }, notifier: { gmail: { username: "user@example.com", diff --git a/test/unit/server/storage/UserDataStore.test.ts b/test/unit/server/storage/UserDataStore.test.ts new file mode 100644 index 00000000..e941ce65 --- /dev/null +++ b/test/unit/server/storage/UserDataStore.test.ts @@ -0,0 +1,264 @@ + +import * as Assert from "assert"; +import * as Sinon from "sinon"; +import * as MockDate from "mockdate"; +import BluebirdPromise = require("bluebird"); + +import { UserDataStore } from "../../../../src/server/lib/storage/UserDataStore"; +import { TOTPSecret } from "../../../../src/types/TOTPSecret"; +import { U2FRegistration } from "../../../../src/types/U2FRegistration"; +import { AuthenticationTraceDocument } from "../../../../src/server/lib/storage/AuthenticationTraceDocument"; +import { CollectionStub } from "../mocks/storage/CollectionStub"; +import { CollectionFactoryStub } from "../mocks/storage/CollectionFactoryStub"; + +describe("test user data store", function () { + let factory: CollectionFactoryStub; + let collection: CollectionStub; + let userId: string; + let appId: string; + let totpSecret: TOTPSecret; + let u2fRegistration: U2FRegistration; + + beforeEach(function () { + factory = new CollectionFactoryStub(); + collection = new CollectionStub(); + + userId = "user"; + appId = "https://myappId"; + + totpSecret = { + ascii: "abc", + base32: "ABCDKZLEFZGREJK", + otpauth_url: "totp://test" + }; + + u2fRegistration = { + keyHandle: "KEY_HANDLE", + publicKey: "publickey" + }; + }); + + it("should correctly creates collections", function () { + new UserDataStore(factory); + + Assert.equal(4, factory.buildStub.callCount); + Assert(factory.buildStub.calledWith("authentication_traces")); + Assert(factory.buildStub.calledWith("identity_validation_tokens")); + Assert(factory.buildStub.calledWith("u2f_registrations")); + Assert(factory.buildStub.calledWith("totp_secrets")); + }); + + describe("TOTP secrets collection", function () { + it("should save a totp secret", function () { + factory.buildStub.returns(collection); + collection.updateStub.returns(BluebirdPromise.resolve()); + + const dataStore = new UserDataStore(factory); + + return dataStore.saveTOTPSecret(userId, totpSecret) + .then(function (doc) { + Assert(collection.updateStub.calledOnce); + Assert(collection.updateStub.calledWith({ userId: userId }, { + userId: userId, + secret: totpSecret + }, { upsert: true })); + return BluebirdPromise.resolve(); + }); + }); + + it("should retrieve a totp secret", function () { + factory.buildStub.returns(collection); + collection.findOneStub.withArgs().returns(BluebirdPromise.resolve()); + + const dataStore = new UserDataStore(factory); + + return dataStore.retrieveTOTPSecret(userId) + .then(function (doc) { + Assert(collection.findOneStub.calledOnce); + Assert(collection.findOneStub.calledWith({ userId: userId })); + return BluebirdPromise.resolve(); + }); + }); + }); + + describe("U2F secrets collection", function () { + it("should save a U2F secret", function () { + factory.buildStub.returns(collection); + collection.updateStub.returns(BluebirdPromise.resolve()); + + const dataStore = new UserDataStore(factory); + + return dataStore.saveU2FRegistration(userId, appId, u2fRegistration) + .then(function (doc) { + Assert(collection.updateStub.calledOnce); + Assert(collection.updateStub.calledWith({ + userId: userId, + appId: appId + }, { + userId: userId, + appId: appId, + registration: u2fRegistration + }, { upsert: true })); + return BluebirdPromise.resolve(); + }); + }); + + it("should retrieve a U2F secret", function () { + factory.buildStub.returns(collection); + collection.findOneStub.withArgs().returns(BluebirdPromise.resolve()); + + const dataStore = new UserDataStore(factory); + + return dataStore.retrieveU2FRegistration(userId, appId) + .then(function (doc) { + Assert(collection.findOneStub.calledOnce); + Assert(collection.findOneStub.calledWith({ + userId: userId, + appId: appId + })); + return BluebirdPromise.resolve(); + }); + }); + }); + + + describe("Regulator traces collection", function () { + it("should save a trace", function () { + factory.buildStub.returns(collection); + collection.insertStub.returns(BluebirdPromise.resolve()); + + const dataStore = new UserDataStore(factory); + + return dataStore.saveAuthenticationTrace(userId, true) + .then(function (doc) { + Assert(collection.insertStub.calledOnce); + Assert(collection.insertStub.calledWith({ + userId: userId, + date: Sinon.match.date, + isAuthenticationSuccessful: true + })); + return BluebirdPromise.resolve(); + }); + }); + + function should_retrieve_latest_authentication_traces(count: number, status: boolean) { + factory.buildStub.returns(collection); + collection.findStub.withArgs().returns(BluebirdPromise.resolve()); + + const dataStore = new UserDataStore(factory); + + return dataStore.retrieveLatestAuthenticationTraces(userId, status, count) + .then(function (doc: AuthenticationTraceDocument[]) { + Assert(collection.findStub.calledOnce); + Assert(collection.findStub.calledWith({ + userId: userId, + isAuthenticationSuccessful: status, + }, { date: -1 }, count)); + return BluebirdPromise.resolve(); + }); + } + + it("should retrieve 3 latest failed authentication traces", function () { + should_retrieve_latest_authentication_traces(3, false); + }); + + it("should retrieve 4 latest successful authentication traces", function () { + should_retrieve_latest_authentication_traces(4, true); + }); + }); + + + describe("Identity validation collection", function () { + it("should save a identity validation token", function () { + factory.buildStub.returns(collection); + collection.insertStub.returns(BluebirdPromise.resolve()); + + const dataStore = new UserDataStore(factory); + const maxAge = 400; + const token = "TOKEN"; + const challenge = "CHALLENGE"; + + return dataStore.produceIdentityValidationToken(userId, token, challenge, maxAge) + .then(function (doc) { + Assert(collection.insertStub.calledOnce); + Assert(collection.insertStub.calledWith({ + userId: userId, + token: token, + challenge: challenge, + maxDate: Sinon.match.date + })); + return BluebirdPromise.resolve(); + }); + }); + + it("should consume an identity token successfully", function () { + factory.buildStub.returns(collection); + + MockDate.set(100); + + const token = "TOKEN"; + const challenge = "CHALLENGE"; + + collection.findOneStub.withArgs().returns(BluebirdPromise.resolve({ + userId: "USER", + token: token, + challenge: challenge, + maxDate: new Date() + })); + collection.removeStub.returns(BluebirdPromise.resolve()); + + const dataStore = new UserDataStore(factory); + + MockDate.set(80); + + return dataStore.consumeIdentityValidationToken(token, challenge) + .then(function (doc) { + MockDate.reset(); + Assert(collection.findOneStub.calledOnce); + Assert(collection.findOneStub.calledWith({ + token: token, + challenge: challenge + })); + + Assert(collection.removeStub.calledOnce); + Assert(collection.removeStub.calledWith({ + token: token, + challenge: challenge + })); + return BluebirdPromise.resolve(); + }); + }); + + it("should consume an expired identity token", function () { + factory.buildStub.returns(collection); + + MockDate.set(0); + + const token = "TOKEN"; + const challenge = "CHALLENGE"; + + collection.findOneStub.withArgs().returns(BluebirdPromise.resolve({ + userId: "USER", + token: token, + challenge: challenge, + maxDate: new Date() + })); + + const dataStore = new UserDataStore(factory); + + MockDate.set(80000); + + return dataStore.consumeIdentityValidationToken(token, challenge) + .then(function () { return BluebirdPromise.reject(new Error("should not be here")); }) + .catch(function () { + MockDate.reset(); + Assert(collection.findOneStub.calledOnce); + Assert(collection.findOneStub.calledWith({ + token: token, + challenge: challenge + })); + return BluebirdPromise.resolve(); + }); + }); + }); +}); diff --git a/test/unit/server/storage/mongo/MongoCollection.test.ts b/test/unit/server/storage/mongo/MongoCollection.test.ts new file mode 100644 index 00000000..ab07a1df --- /dev/null +++ b/test/unit/server/storage/mongo/MongoCollection.test.ts @@ -0,0 +1,105 @@ +import Assert = require("assert"); +import Sinon = require("sinon"); +import MongoDB = require("mongodb"); +import BluebirdPromise = require("bluebird"); +import { MongoClientStub } from "../../mocks/connectors/mongo/MongoClientStub"; +import { MongoCollection } from "../../../../../src/server/lib/storage/mongo/MongoCollection"; + +describe("MongoCollection", function () { + let mongoCollectionStub: MongoDB.Collection; + let findStub: Sinon.SinonStub; + let findOneStub: Sinon.SinonStub; + let insertStub: Sinon.SinonStub; + let updateStub: Sinon.SinonStub; + let removeStub: Sinon.SinonStub; + let countStub: Sinon.SinonStub; + + before(function () { + const Collection = require("mongodb").Collection; + mongoCollectionStub = Sinon.createStubInstance(Collection); + findStub = mongoCollectionStub.find as Sinon.SinonStub; + findOneStub = mongoCollectionStub.findOne as Sinon.SinonStub; + insertStub = mongoCollectionStub.insert as Sinon.SinonStub; + updateStub = mongoCollectionStub.update as Sinon.SinonStub; + removeStub = mongoCollectionStub.remove as Sinon.SinonStub; + countStub = mongoCollectionStub.count as Sinon.SinonStub; + }); + + describe("find", function () { + it("should find a document in the collection", function () { + const collection = new MongoCollection(mongoCollectionStub); + findStub.returns({ + sort: Sinon.stub().returns({ + limit: Sinon.stub().returns({ + toArray: Sinon.stub().yields(undefined, []) + }) + }) + }); + + return collection.find({ key: "KEY" }) + .then(function () { + Assert(findStub.calledWith({ key: "KEY" })); + }); + }); + }); + + describe("findOne", function () { + it("should find one document in the collection", function () { + const collection = new MongoCollection(mongoCollectionStub); + findOneStub.yields(undefined, {}); + + return collection.findOne({ key: "KEY" }) + .then(function () { + Assert(findOneStub.calledWith({ key: "KEY" })); + }); + }); + }); + + describe("insert", function () { + it("should insert a document in the collection", function () { + const collection = new MongoCollection(mongoCollectionStub); + insertStub.yields(undefined, {}); + + return collection.insert({ key: "KEY" }) + .then(function () { + Assert(insertStub.calledWith({ key: "KEY" })); + }); + }); + }); + + describe("update", function () { + it("should update a document in the collection", function () { + const collection = new MongoCollection(mongoCollectionStub); + updateStub.yields(undefined, {}); + + return collection.update({ key: "KEY" }, { key: "KEY", value: 1 }) + .then(function () { + Assert(updateStub.calledWith({ key: "KEY" }, { key: "KEY", value: 1 })); + }); + }); + }); + + describe("remove", function () { + it("should remove a document in the collection", function () { + const collection = new MongoCollection(mongoCollectionStub); + removeStub.yields(undefined, {}); + + return collection.remove({ key: "KEY" }) + .then(function () { + Assert(removeStub.calledWith({ key: "KEY" })); + }); + }); + }); + + describe("count", function () { + it("should count documents in the collection", function () { + const collection = new MongoCollection(mongoCollectionStub); + countStub.yields(undefined, {}); + + return collection.count({ key: "KEY" }) + .then(function () { + Assert(countStub.calledWith({ key: "KEY" })); + }); + }); + }); +}); \ No newline at end of file diff --git a/test/unit/server/storage/mongo/MongoCollectionFactory.test.ts b/test/unit/server/storage/mongo/MongoCollectionFactory.test.ts new file mode 100644 index 00000000..c414c92e --- /dev/null +++ b/test/unit/server/storage/mongo/MongoCollectionFactory.test.ts @@ -0,0 +1,21 @@ +import Assert = require("assert"); +import Sinon = require("sinon"); +import { MongoClientStub } from "../../mocks/connectors/mongo/MongoClientStub"; +import { MongoCollectionFactory } from "../../../../../src/server/lib/storage/mongo/MongoCollectionFactory"; + +describe("MongoCollectionFactory", function () { + let mongoClient: MongoClientStub; + + before(function() { + mongoClient = new MongoClientStub(); + }); + + describe("create", function () { + it("should create a collection", function () { + const COLLECTION_NAME = "COLLECTION_NAME"; + + const factory = new MongoCollectionFactory(mongoClient); + Assert(factory.build(COLLECTION_NAME)); + }); + }); +}); diff --git a/test/unit/server/storage/nedb/NedbCollection.test.ts b/test/unit/server/storage/nedb/NedbCollection.test.ts new file mode 100644 index 00000000..9d6a9a6f --- /dev/null +++ b/test/unit/server/storage/nedb/NedbCollection.test.ts @@ -0,0 +1,136 @@ +import Sinon = require("sinon"); +import Assert = require("assert"); + +import { NedbCollection } from "../../../../../src/server/lib/storage/nedb/NedbCollection"; + +describe("NedbCollection", function () { + describe("insert", function () { + it("should insert one entry", function () { + const nedbOptions = { + inMemoryOnly: true + }; + const collection = new NedbCollection(nedbOptions); + + collection.insert({ key: "coucou" }); + + return collection.count({}).then(function (count: number) { + Assert.equal(1, count); + }); + }); + + it("should insert three entries", function () { + const nedbOptions = { + inMemoryOnly: true + }; + const collection = new NedbCollection(nedbOptions); + + collection.insert({ key: "coucou" }); + collection.insert({ key: "hello" }); + collection.insert({ key: "hey" }); + + return collection.count({}).then(function (count: number) { + Assert.equal(3, count); + }); + }); + }); + + describe("find", function () { + let collection: NedbCollection; + before(function () { + const nedbOptions = { + inMemoryOnly: true + }; + collection = new NedbCollection(nedbOptions); + + collection.insert({ key: "coucou", value: 1 }); + collection.insert({ key: "hello" }); + collection.insert({ key: "hey" }); + collection.insert({ key: "coucou", value: 2 }); + }); + + it("should find one hello", function () { + return collection.find({ key: "hello" }, { key: 1 }) + .then(function (docs: { key: string }[]) { + Assert.equal(1, docs.length); + Assert(docs[0].key == "hello"); + }); + }); + + it("should find two coucou", function () { + return collection.find({ key: "coucou" }, { value: 1 }) + .then(function (docs: { value: number }[]) { + Assert.equal(2, docs.length); + }); + }); + }); + + describe("findOne", function () { + let collection: NedbCollection; + before(function () { + const nedbOptions = { + inMemoryOnly: true + }; + collection = new NedbCollection(nedbOptions); + + collection.insert({ key: "coucou", value: 1 }); + collection.insert({ key: "coucou", value: 1 }); + collection.insert({ key: "coucou", value: 1 }); + collection.insert({ key: "coucou", value: 1 }); + }); + + it("should find two coucou", function () { + const doc = { key: "coucou", value: 1 }; + return collection.count(doc) + .then(function (count: number) { + Assert.equal(4, count); + return collection.findOne(doc); + }); + }); + }); + + describe("update", function () { + let collection: NedbCollection; + before(function () { + const nedbOptions = { + inMemoryOnly: true + }; + collection = new NedbCollection(nedbOptions); + + collection.insert({ key: "coucou", value: 1 }); + }); + + it("should update the value", function () { + return collection.update({ key: "coucou" }, { key: "coucou", value: 2 }, { multi: true }) + .then(function () { + return collection.find({ key: "coucou" }); + }) + .then(function (docs: { key: string, value: number }[]) { + Assert.equal(1, docs.length); + Assert.equal(2, docs[0].value); + }); + }); + }); + + describe("update", function () { + let collection: NedbCollection; + before(function () { + const nedbOptions = { + inMemoryOnly: true + }; + collection = new NedbCollection(nedbOptions); + + collection.insert({ key: "coucou" }); + collection.insert({ key: "hello" }); + }); + + it("should update the value", function () { + return collection.remove({ key: "coucou" }) + .then(function () { + return collection.count({}); + }) + .then(function (count: number) { + Assert.equal(1, count); + }); + }); + }); +}); \ No newline at end of file diff --git a/test/unit/server/storage/nedb/NedbCollectionFactory.test.ts b/test/unit/server/storage/nedb/NedbCollectionFactory.test.ts new file mode 100644 index 00000000..4994a8f8 --- /dev/null +++ b/test/unit/server/storage/nedb/NedbCollectionFactory.test.ts @@ -0,0 +1,16 @@ +import Sinon = require("sinon"); +import Assert = require("assert"); + +import { NedbCollectionFactory } from "../../../../../src/server/lib/storage/nedb/NedbCollectionFactory"; + +describe("NedbCollectionFactory", function() { + it("should create a nedb collection", function() { + const nedbOptions = { + inMemoryOnly: true + }; + const factory = new NedbCollectionFactory(nedbOptions); + + const collection = factory.build("mycollection"); + Assert(collection); + }); +}); \ No newline at end of file diff --git a/test/unit/server/user_data_store/authentication_audit.test.ts b/test/unit/server/user_data_store/authentication_audit.test.ts deleted file mode 100644 index 5d1b824f..00000000 --- a/test/unit/server/user_data_store/authentication_audit.test.ts +++ /dev/null @@ -1,71 +0,0 @@ - -import * as assert from "assert"; -import * as Promise from "bluebird"; -import * as sinon from "sinon"; -import * as MockDate from "mockdate"; -import UserDataStore from "../../../../src/server/lib/UserDataStore"; -import nedb = require("nedb"); - -describe("test user data store", function() { - describe("test authentication traces", test_authentication_traces); -}); - -function test_authentication_traces() { - it("should save an authentication trace in db", function() { - const options = { - inMemoryOnly: true - }; - - const data_store = new UserDataStore(options, nedb); - const userid = "user"; - const type = "1stfactor"; - const is_success = false; - return data_store.save_authentication_trace(userid, type, is_success) - .then(function(doc) { - assert("_id" in doc); - assert.equal(doc.userid, "user"); - assert.equal(doc.is_success, false); - assert.equal(doc.type, "1stfactor"); - return Promise.resolve(); - }); - }); - - it("should return 3 last authentication traces", function() { - const options = { - inMemoryOnly: true - }; - - const data_store = new UserDataStore(options, nedb); - const userid = "user"; - const type = "1stfactor"; - const is_success = false; - MockDate.set("2/1/2000"); - return data_store.save_authentication_trace(userid, type, false) - .then(function(doc) { - MockDate.set("1/2/2000"); - return data_store.save_authentication_trace(userid, type, true); - }) - .then(function(doc) { - MockDate.set("1/7/2000"); - return data_store.save_authentication_trace(userid, type, false); - }) - .then(function(doc) { - MockDate.set("1/2/2000"); - return data_store.save_authentication_trace(userid, type, false); - }) - .then(function(doc) { - MockDate.set("1/5/2000"); - return data_store.save_authentication_trace(userid, type, false); - }) - .then(function(doc) { - return data_store.get_last_authentication_traces(userid, type, false, 3); - }) - .then(function(docs) { - assert.equal(docs.length, 3); - assert.deepEqual(docs[0].date, new Date("2/1/2000")); - assert.deepEqual(docs[1].date, new Date("1/7/2000")); - assert.deepEqual(docs[2].date, new Date("1/5/2000")); - return Promise.resolve(); - }); - }); -} diff --git a/test/unit/server/user_data_store/totp_secret.test.ts b/test/unit/server/user_data_store/totp_secret.test.ts deleted file mode 100644 index 4a5b3922..00000000 --- a/test/unit/server/user_data_store/totp_secret.test.ts +++ /dev/null @@ -1,73 +0,0 @@ - -import * as assert from "assert"; -import * as Promise from "bluebird"; -import * as sinon from "sinon"; -import * as MockDate from "mockdate"; -import UserDataStore from "../../../../src/server/lib/UserDataStore"; -import nedb = require("nedb"); - -describe("test user data store", function() { - describe("test totp secrets store", test_totp_secrets); -}); - -function test_totp_secrets() { - it("should save and reload a totp secret", function() { - const options = { - inMemoryOnly: true - }; - - const data_store = new UserDataStore(options, nedb); - const userid = "user"; - const secret = { - ascii: "abc", - base32: "ABCDKZLEFZGREJK", - otpauth_url: "totp://test" - }; - - return data_store.set_totp_secret(userid, secret) - .then(function() { - return data_store.get_totp_secret(userid); - }) - .then(function(doc) { - assert("_id" in doc); - assert.equal(doc.userid, "user"); - assert.equal(doc.secret.ascii, "abc"); - assert.equal(doc.secret.base32, "ABCDKZLEFZGREJK"); - return Promise.resolve(); - }); - }); - - it("should only remember last secret", function() { - const options = { - inMemoryOnly: true - }; - - const data_store = new UserDataStore(options, nedb); - const userid = "user"; - const secret1 = { - ascii: "abc", - base32: "ABCDKZLEFZGREJK", - otpauth_url: "totp://test" - }; - const secret2 = { - ascii: "def", - base32: "XYZABC", - otpauth_url: "totp://test" - }; - - return data_store.set_totp_secret(userid, secret1) - .then(function() { - return data_store.set_totp_secret(userid, secret2); - }) - .then(function() { - return data_store.get_totp_secret(userid); - }) - .then(function(doc) { - assert("_id" in doc); - assert.equal(doc.userid, "user"); - assert.equal(doc.secret.ascii, "def"); - assert.equal(doc.secret.base32, "XYZABC"); - return Promise.resolve(); - }); - }); -} From c12a085f8e85fbc1dab3c0dd683d30db6a9e00f6 Mon Sep 17 00:00:00 2001 From: Clement Michaud Date: Wed, 26 Jul 2017 23:45:26 +0200 Subject: [PATCH 2/2] Replace mocha integration tests by cucumber tests --- .travis.yml | 24 +++- Gruntfile.js | 17 ++- README.md | 1 + config.template.yml | 2 +- example/ldap/base.ldif | 7 ++ example/nginx/html/index.html | 8 +- example/nginx/nginx.conf | 3 +- package.json | 11 +- scripts/dc-dev.sh | 5 +- scripts/example/dc-example.sh | 3 +- scripts/integration-tests.sh | 40 +------ .../firstfactor/FirstFactorValidator.ts | 2 +- .../reset-password/reset-password-request.ts | 2 +- src/server/lib/AuthenticationRegulator.ts | 28 ++--- src/server/lib/ldap/Client.ts | 4 +- .../lib/notifiers/FileSystemNotifier.ts | 4 +- src/server/views/errors/401.pug | 2 +- src/server/views/errors/403.pug | 2 +- src/server/views/firstfactor.pug | 2 +- src/server/views/layout/layout.pug | 2 +- src/server/views/secondfactor.pug | 8 +- test/features/access-control.feature | 38 ++++++ test/features/authentication.feature | 53 +++++++++ test/features/redirection.feature | 7 ++ test/features/reset-password.feature | 33 ++++++ test/features/resilience.feature | 13 ++ test/features/restrictions.feature | 17 +++ test/features/step_definitions/after.ts | 7 ++ .../step_definitions/authentication.ts | 92 ++++++++++++++ test/features/step_definitions/redirection.ts | 17 +++ .../step_definitions/reset-password.ts | 20 ++++ test/features/step_definitions/resilience.ts | 12 ++ .../features/step_definitions/restrictions.ts | 11 ++ test/features/support/world.ts | 110 +++++++++++++++++ test/integration/Dockerfile | 4 - test/integration/docker-compose.yml | 30 ----- test/integration/main.ts | 112 ------------------ test/system/main.ts | 92 -------------- .../firstfactor/FirstFactorValidator.test.ts | 2 +- 39 files changed, 517 insertions(+), 330 deletions(-) create mode 100644 test/features/access-control.feature create mode 100644 test/features/authentication.feature create mode 100644 test/features/redirection.feature create mode 100644 test/features/reset-password.feature create mode 100644 test/features/resilience.feature create mode 100644 test/features/restrictions.feature create mode 100644 test/features/step_definitions/after.ts create mode 100644 test/features/step_definitions/authentication.ts create mode 100644 test/features/step_definitions/redirection.ts create mode 100644 test/features/step_definitions/reset-password.ts create mode 100644 test/features/step_definitions/resilience.ts create mode 100644 test/features/step_definitions/restrictions.ts create mode 100644 test/features/support/world.ts delete mode 100644 test/integration/Dockerfile delete mode 100644 test/integration/docker-compose.yml delete mode 100644 test/integration/main.ts delete mode 100644 test/system/main.ts diff --git a/.travis.yml b/.travis.yml index b5052c4f..2ec3f9f5 100644 --- a/.travis.yml +++ b/.travis.yml @@ -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 + - 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 diff --git a/Gruntfile.js b/Gruntfile.js index ba2c93e8..c5b590bb 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -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']); }; diff --git a/README.md b/README.md index 66996086..aa8d2d07 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/config.template.yml b/config.template.yml index e21ca996..638b5920 100644 --- a/config.template.yml +++ b/config.template.yml @@ -48,7 +48,7 @@ ldap: # beginning of the pattern. access_control: default: - - home.test.local + - public.test.local groups: admin: - '*.test.local' diff --git a/example/ldap/base.ldif b/example/ldap/base.ldif index f1fbdb88..06e962c0 100644 --- a/example/ldap/base.ldif +++ b/example/ldap/base.ldif @@ -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= diff --git a/example/nginx/html/index.html b/example/nginx/html/index.html index 8f76ab5b..f009d515 100644 --- a/example/nginx/html/index.html +++ b/example/nginx/html/index.html @@ -9,6 +9,9 @@ You need to log in to access the secret!

Try to access it via one of the following links.
    +
  • + public.test.local +
  • secret.test.local
  • @@ -18,9 +21,6 @@
  • secret2.test.local
  • -
  • - home.test.local -
  • mx1.mail.test.local
  • @@ -45,7 +45,7 @@
    • Default policy
        -
      • home.test.local
      • +
      • public.test.local
    • Groups policy diff --git a/example/nginx/nginx.conf b/example/nginx/nginx.conf index 53e4e3b8..c9688f8f 100644 --- a/example/nginx/nginx.conf +++ b/example/nginx/nginx.conf @@ -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; diff --git a/package.json b/package.json index f593dc54..33c88032 100644 --- a/package.json +++ b/package.json @@ -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" }, @@ -48,6 +48,7 @@ "@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", @@ -64,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", @@ -71,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", @@ -82,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", @@ -90,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": "^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", diff --git a/scripts/dc-dev.sh b/scripts/dc-dev.sh index 91985ae8..b9cd0630 100755 --- a/scripts/dc-dev.sh +++ b/scripts/dc-dev.sh @@ -3,11 +3,10 @@ set -e docker-compose \ - -f docker-compose.base.yml \ + -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 \ - -f test/integration/docker-compose.yml $* + -f example/ldap/docker-compose.yml $* diff --git a/scripts/example/dc-example.sh b/scripts/example/dc-example.sh index b5669a76..640976d6 100755 --- a/scripts/example/dc-example.sh +++ b/scripts/example/dc-example.sh @@ -8,5 +8,4 @@ docker-compose \ -f example/mongo/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 $* + -f example/ldap/docker-compose.yml $* diff --git a/scripts/integration-tests.sh b/scripts/integration-tests.sh index 03b662d9..10d4973d 100755 --- a/scripts/integration-tests.sh +++ b/scripts/integration-tests.sh @@ -1,10 +1,10 @@ #!/bin/bash DC_SCRIPT=./scripts/example/dc-example.sh -EXPECTED_SERVICES_COUNT=6 +EXPECTED_SERVICES_COUNT=5 start_services() { - $DC_SCRIPT up -d mongo redis openldap authelia nginx nginx-tests + $DC_SCRIPT up -d mongo redis openldap authelia nginx sleep 3 } @@ -27,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 - - 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 $EXPECTED_SERVICES_COUNT - - 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 $EXPECTED_SERVICES_COUNT - ./node_modules/.bin/mocha --compilers ts:ts-node/register --recursive test/system + sleep 5 + ./node_modules/.bin/grunt run:integration-tests shut_services } @@ -80,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 diff --git a/src/client/firstfactor/FirstFactorValidator.ts b/src/client/firstfactor/FirstFactorValidator.ts index 369cd535..07a27f7d 100644 --- a/src/client/firstfactor/FirstFactorValidator.ts +++ b/src/client/firstfactor/FirstFactorValidator.ts @@ -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)); }); }); diff --git a/src/client/reset-password/reset-password-request.ts b/src/client/reset-password/reset-password-request.ts index e390fbc5..60630977 100644 --- a/src/client/reset-password/reset-password-request.ts +++ b/src/client/reset-password/reset-password-request.ts @@ -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); diff --git a/src/server/lib/AuthenticationRegulator.ts b/src/server/lib/AuthenticationRegulator.ts index 6741586f..b3319b4e 100644 --- a/src/server/lib/AuthenticationRegulator.ts +++ b/src/server/lib/AuthenticationRegulator.ts @@ -2,7 +2,7 @@ import * as BluebirdPromise from "bluebird"; import exceptions = require("./Exceptions"); import { UserDataStore } from "./storage/UserDataStore"; -import {AuthenticationTraceDocument} from "./storage/AuthenticationTraceDocument"; +import { AuthenticationTraceDocument } from "./storage/AuthenticationTraceDocument"; const MAX_AUTHENTICATION_COUNT_IN_TIME_RANGE = 3; @@ -22,19 +22,19 @@ export class AuthenticationRegulator { regulate(userId: string): BluebirdPromise { 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 + .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 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."); + } + return BluebirdPromise.resolve(); - } - - 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."); - } - - return BluebirdPromise.resolve(); - }); + }); } } diff --git a/src/server/lib/ldap/Client.ts b/src/server/lib/ldap/Client.ts index b59b33ee..d2a6fc6b 100644 --- a/src/server/lib/ldap/Client.ts +++ b/src/server/lib/ldap/Client.ts @@ -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; } diff --git a/src/server/lib/notifiers/FileSystemNotifier.ts b/src/server/lib/notifiers/FileSystemNotifier.ts index a7b17145..042877ac 100644 --- a/src/server/lib/notifiers/FileSystemNotifier.ts +++ b/src/server/lib/notifiers/FileSystemNotifier.ts @@ -1,7 +1,7 @@ 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"; @@ -17,7 +17,7 @@ export class FileSystemNotifier implements INotifier { notify(identity: Identity, subject: string, link: string): BluebirdPromise { const content = util.format("Date: %s\nUser: %s\nSubject: %s\nLink: %s", new Date().toString(), identity.userid, subject, link); - const writeFilePromised = BluebirdPromise.promisify(fs.writeFile); + const writeFilePromised: any = BluebirdPromise.promisify(Fs.writeFile); return writeFilePromised(this.filename, content); } } diff --git a/src/server/views/errors/401.pug b/src/server/views/errors/401.pug index dad56a9b..3cb97413 100644 --- a/src/server/views/errors/401.pug +++ b/src/server/views/errors/401.pug @@ -8,4 +8,4 @@ block form-header block content -

      You are not authorized.

      +

      You are either not authorized or not logged in.

      diff --git a/src/server/views/errors/403.pug b/src/server/views/errors/403.pug index 934e7508..b2dc8be6 100644 --- a/src/server/views/errors/403.pug +++ b/src/server/views/errors/403.pug @@ -8,4 +8,4 @@ block form-header block content -

      You are not authorized.

      +

      You are either not authorized or not logged in.

      diff --git a/src/server/views/firstfactor.pug b/src/server/views/firstfactor.pug index fbed5fdb..ee8ed48a 100644 --- a/src/server/views/firstfactor.pug +++ b/src/server/views/firstfactor.pug @@ -12,7 +12,7 @@ block content - 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? diff --git a/src/server/views/layout/layout.pug b/src/server/views/layout/layout.pug index c267782a..2c0246a0 100644 --- a/src/server/views/layout/layout.pug +++ b/src/server/views/layout/layout.pug @@ -20,7 +20,7 @@ html
      -
      Powered by Authelia
      +
      Powered by Authelia
      diff --git a/src/server/views/secondfactor.pug b/src/server/views/secondfactor.pug index 1f824c2c..9ed8a7b5 100644 --- a/src/server/views/secondfactor.pug +++ b/src/server/views/secondfactor.pug @@ -9,14 +9,14 @@ block content
      - - a(href=totp_identity_start_endpoint, class="pull-right link") Need to register? + + a(href=totp_identity_start_endpoint, class="pull-right link register-totp") Need to register?
      diff --git a/test/features/access-control.feature b/test/features/access-control.feature new file mode 100644 index 00000000..d853a62b --- /dev/null +++ b/test/features/access-control.feature @@ -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 | \ No newline at end of file diff --git a/test/features/authentication.feature b/test/features/authentication.feature new file mode 100644 index 00000000..b17c11c7 --- /dev/null +++ b/test/features/authentication.feature @@ -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" \ No newline at end of file diff --git a/test/features/redirection.feature b/test/features/redirection.feature new file mode 100644 index 00000000..7b8f24ac --- /dev/null +++ b/test/features/redirection.feature @@ -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/" + diff --git a/test/features/reset-password.feature b/test/features/reset-password.feature new file mode 100644 index 00000000..b383840c --- /dev/null +++ b/test/features/reset-password.feature @@ -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" \ No newline at end of file diff --git a/test/features/resilience.feature b/test/features/resilience.feature new file mode 100644 index 00000000..a580db4e --- /dev/null +++ b/test/features/resilience.feature @@ -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 | \ No newline at end of file diff --git a/test/features/restrictions.feature b/test/features/restrictions.feature new file mode 100644 index 00000000..f87cd3ae --- /dev/null +++ b/test/features/restrictions.feature @@ -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 "" + Then I get an error + + 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 | + \ No newline at end of file diff --git a/test/features/step_definitions/after.ts b/test/features/step_definitions/after.ts new file mode 100644 index 00000000..c5132ddb --- /dev/null +++ b/test/features/step_definitions/after.ts @@ -0,0 +1,7 @@ +import Cucumber = require("cucumber"); + +Cucumber.defineSupportCode(function({After}) { + After(function() { + return this.driver.quit(); + }); +}); \ No newline at end of file diff --git a/test/features/step_definitions/authentication.ts b/test/features/step_definitions/authentication.ts new file mode 100644 index 00000000..ae86f3f4 --- /dev/null +++ b/test/features/step_definitions/authentication.ts @@ -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); + }); +}); \ No newline at end of file diff --git a/test/features/step_definitions/redirection.ts b/test/features/step_definitions/redirection.ts new file mode 100644 index 00000000..240fa318 --- /dev/null +++ b/test/features/step_definitions/redirection.ts @@ -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); + }); +}); \ No newline at end of file diff --git a/test/features/step_definitions/reset-password.ts b/test/features/step_definitions/reset-password.ts new file mode 100644 index 00000000..e1b1c5e0 --- /dev/null +++ b/test/features/step_definitions/reset-password.ts @@ -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); + }); +}); \ No newline at end of file diff --git a/test/features/step_definitions/resilience.ts b/test/features/step_definitions/resilience.ts new file mode 100644 index 00000000..1eb5f15a --- /dev/null +++ b/test/features/step_definitions/resilience.ts @@ -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"); + }); +}); \ No newline at end of file diff --git a/test/features/step_definitions/restrictions.ts b/test/features/step_definitions/restrictions.ts new file mode 100644 index 00000000..cf3ab289 --- /dev/null +++ b/test/features/step_definitions/restrictions.ts @@ -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 + "')]")); + }); +}); \ No newline at end of file diff --git a/test/features/support/world.ts b/test/features/support/world.ts new file mode 100644 index 00000000..367b4df0 --- /dev/null +++ b/test/features/support/world.ts @@ -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); +}); \ No newline at end of file diff --git a/test/integration/Dockerfile b/test/integration/Dockerfile deleted file mode 100644 index 16e8a209..00000000 --- a/test/integration/Dockerfile +++ /dev/null @@ -1,4 +0,0 @@ -FROM node:7-alpine - -WORKDIR /usr/src - diff --git a/test/integration/docker-compose.yml b/test/integration/docker-compose.yml deleted file mode 100644 index 090a132b..00000000 --- a/test/integration/docker-compose.yml +++ /dev/null @@ -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 diff --git a/test/integration/main.ts b/test/integration/main.ts deleted file mode 100644 index 0f6db7d5..00000000 --- a/test/integration/main.ts +++ /dev/null @@ -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); - }); - }); -}); \ No newline at end of file diff --git a/test/system/main.ts b/test/system/main.ts deleted file mode 100644 index 7e7591b9..00000000 --- a/test/system/main.ts +++ /dev/null @@ -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); - }); - }); -}); \ No newline at end of file diff --git a/test/unit/client/firstfactor/FirstFactorValidator.test.ts b/test/unit/client/firstfactor/FirstFactorValidator.test.ts index 7ac115d0..73a686dc 100644 --- a/test/unit/client/firstfactor/FirstFactorValidator.test.ts +++ b/test/unit/client/firstfactor/FirstFactorValidator.test.ts @@ -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."); }); }); }); \ No newline at end of file