From 73d5253297c83268f34adf1567bb0b92b4a3d999 Mon Sep 17 00:00:00 2001 From: Clement Michaud Date: Sun, 22 Oct 2017 17:42:05 +0200 Subject: [PATCH] Disable notifiers when server uses single factor method only Notifier is not mandatory when authentication method is single_factor for all sub-domains since there is no registration required. --- config.template.yml | 3 + .../src/lib/AuthenticationMethodCalculator.ts | 18 -- server/src/lib/AuthenticationSession.ts | 42 ---- .../src/lib/AuthenticationSessionHandler.ts | 44 +++++ server/src/lib/AuthenticationValidator.ts | 18 +- server/src/lib/FirstFactorValidator.ts | 18 +- server/src/lib/IdentityCheckMiddleware.ts | 10 +- server/src/lib/RestApi.ts | 84 -------- server/src/lib/Server.ts | 38 +--- server/src/lib/ServerVariablesInitializer.ts | 2 +- .../lib/authentication/MethodCalculator.ts | 40 ++++ server/src/lib/configuration/Validator.ts | 28 ++- server/src/lib/ldap/LdapClient.ts | 2 +- server/src/lib/routes/FirstFactorBlocker.ts | 25 --- server/src/lib/routes/firstfactor/get.ts | 28 +-- server/src/lib/routes/firstfactor/post.ts | 37 ++-- server/src/lib/routes/loggedin/get.ts | 22 ++- server/src/lib/routes/logout/get.ts | 4 +- .../lib/routes/password-reset/form/post.ts | 28 ++- server/src/lib/routes/secondfactor/get.ts | 43 +++-- .../src/lib/routes/secondfactor/redirect.ts | 27 +-- .../totp/identity/RegistrationHandler.ts | 71 ++++--- .../lib/routes/secondfactor/totp/sign/post.ts | 15 +- .../u2f/identity/RegistrationHandler.ts | 29 +-- .../routes/secondfactor/u2f/register/post.ts | 38 ++-- .../secondfactor/u2f/register_request/get.ts | 30 ++- .../lib/routes/secondfactor/u2f/sign/post.ts | 23 +-- .../secondfactor/u2f/sign_request/get.ts | 14 +- server/src/lib/routes/verify/get.ts | 69 +++---- server/src/lib/web_server/Configurator.ts | 45 +++++ server/src/lib/web_server/RestApi.ts | 139 +++++++++++++ .../middlewares/RequireTwoFactorEnabled.ts | 27 +++ .../RequireValidatedFirstFactor.ts | 26 +++ .../middlewares/WithHeadersLogged.ts | 12 ++ .../AuthenticationMethodCalculator.test.ts | 31 --- server/test/IdentityCheckMiddleware.test.ts | 10 +- .../authentication/MethodCalculator.test.ts | 74 +++++++ server/test/configuration/Validator.test.ts | 93 ++++++++- server/test/mocks/RequestLoggerStub.ts | 20 +- .../test/mocks/ServerVariablesMockBuilder.ts | 4 +- server/test/mocks/express.ts | 6 +- server/test/routes/firstfactor/post.test.ts | 18 +- .../test/routes/password-reset/post.test.ts | 56 +++--- server/test/routes/secondfactor/get.test.ts | 64 ++++++ .../totp/register/RegistrationHandler.test.ts | 1 - .../secondfactor/totp/sign/post.test.ts | 42 ++-- .../u2f/identity/RegistrationHandler.test.ts | 47 ++--- .../secondfactor/u2f/register/post.test.ts | 64 +++--- .../u2f/register_request/get.test.ts | 1 - .../routes/secondfactor/u2f/sign/post.test.ts | 1 - .../secondfactor/u2f/sign_request/get.test.ts | 2 - server/test/routes/verify/get.test.ts | 108 ++++------- server/test/server/PrivatePages.ts | 182 ------------------ server/test/server/PublicPages.ts | 173 ----------------- test/features/access-control.feature | 6 +- test/features/restrictions.feature | 46 +++-- ...r.feature => single-factor-domain.feature} | 0 test/features/single-factor-server.feature | 16 ++ .../step_definitions/authentication.ts | 34 +++- test/features/step_definitions/hooks.ts | 8 + .../features/step_definitions/restrictions.ts | 65 ++++++- test/features/support/world.ts | 11 +- 62 files changed, 1174 insertions(+), 1108 deletions(-) delete mode 100644 server/src/lib/AuthenticationMethodCalculator.ts delete mode 100644 server/src/lib/AuthenticationSession.ts create mode 100644 server/src/lib/AuthenticationSessionHandler.ts delete mode 100644 server/src/lib/RestApi.ts create mode 100644 server/src/lib/authentication/MethodCalculator.ts delete mode 100644 server/src/lib/routes/FirstFactorBlocker.ts create mode 100644 server/src/lib/web_server/Configurator.ts create mode 100644 server/src/lib/web_server/RestApi.ts create mode 100644 server/src/lib/web_server/middlewares/RequireTwoFactorEnabled.ts create mode 100644 server/src/lib/web_server/middlewares/RequireValidatedFirstFactor.ts create mode 100644 server/src/lib/web_server/middlewares/WithHeadersLogged.ts delete mode 100644 server/test/AuthenticationMethodCalculator.test.ts create mode 100644 server/test/authentication/MethodCalculator.test.ts create mode 100644 server/test/routes/secondfactor/get.test.ts delete mode 100644 server/test/server/PrivatePages.ts delete mode 100644 server/test/server/PublicPages.ts rename test/features/{single-factor.feature => single-factor-domain.feature} (100%) create mode 100644 test/features/single-factor-server.feature diff --git a/config.template.yml b/config.template.yml index f173907a9..1e39396e1 100644 --- a/config.template.yml +++ b/config.template.yml @@ -71,6 +71,9 @@ ldap: # values must be one of the two possible methods. # # Note: 'per_subdomain_methods' is optional. +# +# Note: authentication_methods is optional. If it is not set all sub-domains +# are protected by two factors. authentication_methods: default_method: two_factor per_subdomain_methods: diff --git a/server/src/lib/AuthenticationMethodCalculator.ts b/server/src/lib/AuthenticationMethodCalculator.ts deleted file mode 100644 index 8f0810dcf..000000000 --- a/server/src/lib/AuthenticationMethodCalculator.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { AuthenticationMethod, AuthenticationMethodsConfiguration } from "./configuration/Configuration"; - -export class AuthenticationMethodCalculator { - private configuration: AuthenticationMethodsConfiguration; - - constructor(config: AuthenticationMethodsConfiguration) { - this.configuration = config; - } - - compute(subDomain: string): AuthenticationMethod { - if (this.configuration - && this.configuration.per_subdomain_methods - && subDomain in this.configuration.per_subdomain_methods) { - return this.configuration.per_subdomain_methods[subDomain]; - } - return this.configuration.default_method; - } -} \ No newline at end of file diff --git a/server/src/lib/AuthenticationSession.ts b/server/src/lib/AuthenticationSession.ts deleted file mode 100644 index 1e91a8d02..000000000 --- a/server/src/lib/AuthenticationSession.ts +++ /dev/null @@ -1,42 +0,0 @@ - - -import express = require("express"); -import U2f = require("u2f"); -import BluebirdPromise = require("bluebird"); -import { AuthenticationSession } from "../../types/AuthenticationSession"; -import { IRequestLogger } from "./logging/IRequestLogger"; - -const INITIAL_AUTHENTICATION_SESSION: AuthenticationSession = { - first_factor: false, - second_factor: false, - last_activity_datetime: undefined, - userid: undefined, - email: undefined, - groups: [], - register_request: undefined, - sign_request: undefined, - identity_check: undefined, - redirect: undefined -}; - -export function reset(req: express.Request): void { - req.session.auth = Object.assign({}, INITIAL_AUTHENTICATION_SESSION, {}); - - // Initialize last activity with current time - req.session.auth.last_activity_datetime = new Date().getTime(); -} - -export function get(req: express.Request, logger: IRequestLogger): BluebirdPromise { - if (!req.session) { - const errorMsg = "Something is wrong with session cookies. Please check Redis is running and Authelia can contact it."; - logger.error(req, errorMsg); - return BluebirdPromise.reject(new Error(errorMsg)); - } - - if (!req.session.auth) { - logger.debug(req, "Authentication session %s was undefined. Resetting.", req.sessionID); - reset(req); - } - - return BluebirdPromise.resolve(req.session.auth); -} \ No newline at end of file diff --git a/server/src/lib/AuthenticationSessionHandler.ts b/server/src/lib/AuthenticationSessionHandler.ts new file mode 100644 index 000000000..2349490b8 --- /dev/null +++ b/server/src/lib/AuthenticationSessionHandler.ts @@ -0,0 +1,44 @@ + + +import express = require("express"); +import U2f = require("u2f"); +import BluebirdPromise = require("bluebird"); +import { AuthenticationSession } from "../../types/AuthenticationSession"; +import { IRequestLogger } from "./logging/IRequestLogger"; + +const INITIAL_AUTHENTICATION_SESSION: AuthenticationSession = { + first_factor: false, + second_factor: false, + last_activity_datetime: undefined, + userid: undefined, + email: undefined, + groups: [], + register_request: undefined, + sign_request: undefined, + identity_check: undefined, + redirect: undefined +}; + +export class AuthenticationSessionHandler { + static reset(req: express.Request): void { + req.session.auth = Object.assign({}, INITIAL_AUTHENTICATION_SESSION, {}); + + // Initialize last activity with current time + req.session.auth.last_activity_datetime = new Date().getTime(); + } + + static get(req: express.Request, logger: IRequestLogger): AuthenticationSession { + if (!req.session) { + const errorMsg = "Something is wrong with session cookies. Please check Redis is running and Authelia can contact it."; + logger.error(req, errorMsg); + throw new Error(errorMsg); + } + + if (!req.session.auth) { + logger.debug(req, "Authentication session %s was undefined. Resetting.", req.sessionID); + AuthenticationSessionHandler.reset(req); + } + + return req.session.auth; + } +} \ No newline at end of file diff --git a/server/src/lib/AuthenticationValidator.ts b/server/src/lib/AuthenticationValidator.ts index fabda338e..386368006 100644 --- a/server/src/lib/AuthenticationValidator.ts +++ b/server/src/lib/AuthenticationValidator.ts @@ -3,17 +3,15 @@ import BluebirdPromise = require("bluebird"); import express = require("express"); import objectPath = require("object-path"); import FirstFactorValidator = require("./FirstFactorValidator"); -import AuthenticationSessionHandler = require("./AuthenticationSession"); +import { AuthenticationSessionHandler } from "./AuthenticationSessionHandler"; import { IRequestLogger } from "./logging/IRequestLogger"; export function validate(req: express.Request, logger: IRequestLogger): BluebirdPromise { - return FirstFactorValidator.validate(req, logger) - .then(function () { - return AuthenticationSessionHandler.get(req, logger); - }) - .then(function (authSession) { - if (!authSession.second_factor) - return BluebirdPromise.reject("No second factor variable."); - return BluebirdPromise.resolve(); - }); + return FirstFactorValidator.validate(req, logger) + .then(function () { + const authSession = AuthenticationSessionHandler.get(req, logger); + if (!authSession.second_factor) + return BluebirdPromise.reject("No second factor variable."); + return BluebirdPromise.resolve(); + }); } \ No newline at end of file diff --git a/server/src/lib/FirstFactorValidator.ts b/server/src/lib/FirstFactorValidator.ts index 603dea204..36de5ae5d 100644 --- a/server/src/lib/FirstFactorValidator.ts +++ b/server/src/lib/FirstFactorValidator.ts @@ -3,17 +3,17 @@ import BluebirdPromise = require("bluebird"); import express = require("express"); import objectPath = require("object-path"); import Exceptions = require("./Exceptions"); -import AuthenticationSessionHandler = require("./AuthenticationSession"); +import { AuthenticationSessionHandler } from "./AuthenticationSessionHandler"; import { IRequestLogger } from "./logging/IRequestLogger"; export function validate(req: express.Request, logger: IRequestLogger): BluebirdPromise { - return AuthenticationSessionHandler.get(req, logger) - .then(function (authSession) { - if (!authSession.userid || !authSession.first_factor) - return BluebirdPromise.reject( - new Exceptions.FirstFactorValidationError( - "First factor has not been validated yet.")); + return new BluebirdPromise(function (resolve, reject) { + const authSession = AuthenticationSessionHandler.get(req, logger); - return BluebirdPromise.resolve(); - }); + if (!authSession.userid || !authSession.first_factor) + return reject(new Exceptions.FirstFactorValidationError( + "First factor has not been validated yet.")); + + resolve(); + }); } \ No newline at end of file diff --git a/server/src/lib/IdentityCheckMiddleware.ts b/server/src/lib/IdentityCheckMiddleware.ts index 8f017524a..662ddab6c 100644 --- a/server/src/lib/IdentityCheckMiddleware.ts +++ b/server/src/lib/IdentityCheckMiddleware.ts @@ -9,7 +9,7 @@ import ejs = require("ejs"); import { IUserDataStore } from "./storage/IUserDataStore"; import express = require("express"); import ErrorReplies = require("./ErrorReplies"); -import AuthenticationSessionHandler = require("./AuthenticationSession"); +import { AuthenticationSessionHandler } from "./AuthenticationSessionHandler"; import { AuthenticationSession } from "../../types/AuthenticationSession"; import { ServerVariables } from "./ServerVariables"; @@ -74,14 +74,9 @@ export function get_finish_validation(handler: IdentityValidable, return checkIdentityToken(req, identityToken) .then(function () { + authSession = AuthenticationSessionHandler.get(req, vars.logger); return handler.postValidationInit(req); }) - .then(function () { - return AuthenticationSessionHandler.get(req, vars.logger); - }) - .then(function (_authSession) { - authSession = _authSession; - }) .then(function () { return consumeToken(identityToken, handler.challenge(), vars.userDataStore); }) @@ -97,7 +92,6 @@ export function get_finish_validation(handler: IdentityValidable, }; } - export function get_start_validation(handler: IdentityValidable, postValidationEndpoint: string, vars: ServerVariables) diff --git a/server/src/lib/RestApi.ts b/server/src/lib/RestApi.ts deleted file mode 100644 index a8a82dee1..000000000 --- a/server/src/lib/RestApi.ts +++ /dev/null @@ -1,84 +0,0 @@ - -import Express = require("express"); -import { UserDataStore } from "./storage/UserDataStore"; -import { Winston } from "../../types/Dependencies"; - -import FirstFactorGet = require("./routes/firstfactor/get"); -import SecondFactorGet = require("./routes/secondfactor/get"); - -import FirstFactorPost = require("./routes/firstfactor/post"); -import LogoutGet = require("./routes/logout/get"); -import VerifyGet = require("./routes/verify/get"); -import TOTPSignGet = require("./routes/secondfactor/totp/sign/post"); - -import IdentityCheckMiddleware = require("./IdentityCheckMiddleware"); - -import TOTPRegistrationIdentityHandler from "./routes/secondfactor/totp/identity/RegistrationHandler"; -import U2FRegistrationIdentityHandler from "./routes/secondfactor/u2f/identity/RegistrationHandler"; -import ResetPasswordIdentityHandler from "./routes/password-reset/identity/PasswordResetHandler"; - -import U2FSignPost = require("./routes/secondfactor/u2f/sign/post"); -import U2FSignRequestGet = require("./routes/secondfactor/u2f/sign_request/get"); - -import U2FRegisterPost = require("./routes/secondfactor/u2f/register/post"); -import U2FRegisterRequestGet = require("./routes/secondfactor/u2f/register_request/get"); - -import ResetPasswordFormPost = require("./routes/password-reset/form/post"); -import ResetPasswordRequestPost = require("./routes/password-reset/request/get"); - -import Error401Get = require("./routes/error/401/get"); -import Error403Get = require("./routes/error/403/get"); -import Error404Get = require("./routes/error/404/get"); - -import LoggedIn = require("./routes/loggedin/get"); - -import { ServerVariables } from "./ServerVariables"; -import { IRequestLogger } from "./logging/IRequestLogger"; - -import Endpoints = require("../../../shared/api"); - -function withHeadersLogged(fn: (req: Express.Request, res: Express.Response) => void, - logger: IRequestLogger) { - return function (req: Express.Request, res: Express.Response) { - logger.debug(req, "Headers = %s", JSON.stringify(req.headers)); - fn(req, res); - }; -} - -export class RestApi { - static setup(app: Express.Application, vars: ServerVariables): void { - app.get(Endpoints.FIRST_FACTOR_GET, withHeadersLogged(FirstFactorGet.default(vars), vars.logger)); - app.get(Endpoints.SECOND_FACTOR_GET, withHeadersLogged(SecondFactorGet.default(vars), vars.logger)); - app.get(Endpoints.LOGOUT_GET, withHeadersLogged(LogoutGet.default, vars.logger)); - - IdentityCheckMiddleware.register(app, Endpoints.SECOND_FACTOR_TOTP_IDENTITY_START_GET, - Endpoints.SECOND_FACTOR_TOTP_IDENTITY_FINISH_GET, - new TOTPRegistrationIdentityHandler(vars.logger, vars.userDataStore, vars.totpHandler), vars); - - IdentityCheckMiddleware.register(app, Endpoints.SECOND_FACTOR_U2F_IDENTITY_START_GET, - Endpoints.SECOND_FACTOR_U2F_IDENTITY_FINISH_GET, - new U2FRegistrationIdentityHandler(vars.logger), vars); - - IdentityCheckMiddleware.register(app, Endpoints.RESET_PASSWORD_IDENTITY_START_GET, - Endpoints.RESET_PASSWORD_IDENTITY_FINISH_GET, - new ResetPasswordIdentityHandler(vars.logger, vars.ldapEmailsRetriever), vars); - - app.get(Endpoints.RESET_PASSWORD_REQUEST_GET, withHeadersLogged(ResetPasswordRequestPost.default, vars.logger)); - app.post(Endpoints.RESET_PASSWORD_FORM_POST, withHeadersLogged(ResetPasswordFormPost.default(vars), vars.logger)); - - app.get(Endpoints.VERIFY_GET, withHeadersLogged(VerifyGet.default(vars), vars.logger)); - app.post(Endpoints.FIRST_FACTOR_POST, withHeadersLogged(FirstFactorPost.default(vars), vars.logger)); - app.post(Endpoints.SECOND_FACTOR_TOTP_POST, withHeadersLogged(TOTPSignGet.default(vars), vars.logger)); - - app.get(Endpoints.SECOND_FACTOR_U2F_SIGN_REQUEST_GET, withHeadersLogged(U2FSignRequestGet.default(vars), vars.logger)); - app.post(Endpoints.SECOND_FACTOR_U2F_SIGN_POST, withHeadersLogged(U2FSignPost.default(vars), vars.logger)); - - app.get(Endpoints.SECOND_FACTOR_U2F_REGISTER_REQUEST_GET, withHeadersLogged(U2FRegisterRequestGet.default(vars), vars.logger)); - app.post(Endpoints.SECOND_FACTOR_U2F_REGISTER_POST, withHeadersLogged(U2FRegisterPost.default(vars), vars.logger)); - - app.get(Endpoints.ERROR_401_GET, withHeadersLogged(Error401Get.default, vars.logger)); - app.get(Endpoints.ERROR_403_GET, withHeadersLogged(Error403Get.default, vars.logger)); - app.get(Endpoints.ERROR_404_GET, withHeadersLogged(Error404Get.default, vars.logger)); - app.get(Endpoints.LOGGED_IN, withHeadersLogged(LoggedIn.default(vars), vars.logger)); - } -} diff --git a/server/src/lib/Server.ts b/server/src/lib/Server.ts index 94c2f60c1..ad8ebd2be 100644 --- a/server/src/lib/Server.ts +++ b/server/src/lib/Server.ts @@ -6,27 +6,17 @@ import { AppConfiguration, UserConfiguration } from "./configuration/Configurati import { GlobalDependencies } from "../../types/Dependencies"; import { UserDataStore } from "./storage/UserDataStore"; import { ConfigurationParser } from "./configuration/ConfigurationParser"; -import { RestApi } from "./RestApi"; import { SessionConfigurationBuilder } from "./configuration/SessionConfigurationBuilder"; import { GlobalLogger } from "./logging/GlobalLogger"; import { RequestLogger } from "./logging/RequestLogger"; import { ServerVariables } from "./ServerVariables"; import { ServerVariablesInitializer } from "./ServerVariablesInitializer"; +import { Configurator } from "./web_server/Configurator"; import * as Express from "express"; -import * as BodyParser from "body-parser"; import * as Path from "path"; import * as http from "http"; -const addRequestId = require("express-request-id")(); - -// Constants -const TRUST_PROXY = "trust proxy"; -const X_POWERED_BY = "x-powered-by"; -const VIEWS = "views"; -const VIEW_ENGINE = "view engine"; -const PUG = "pug"; - function clone(obj: any) { return JSON.parse(JSON.stringify(obj)); } @@ -35,35 +25,12 @@ export default class Server { private httpServer: http.Server; private globalLogger: GlobalLogger; private requestLogger: RequestLogger; - private serverVariables: ServerVariables; constructor(deps: GlobalDependencies) { this.globalLogger = new GlobalLogger(deps.winston); this.requestLogger = new RequestLogger(deps.winston); } - 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); - - app.use(Express.static(publicHtmlDirectory)); - app.use(BodyParser.urlencoded({ extended: false })); - app.use(BodyParser.json()); - app.use(deps.session(expressSessionOptions)); - app.use(addRequestId); - app.disable(X_POWERED_BY); - app.enable(TRUST_PROXY); - - app.set(VIEWS, viewsDirectory); - app.set(VIEW_ENGINE, PUG); - - RestApi.setup(app, this.serverVariables); - } - private displayConfigurations(userConfiguration: UserConfiguration, appConfiguration: AppConfiguration) { const displayableUserConfiguration = clone(userConfiguration); @@ -94,8 +61,7 @@ export default class Server { const that = this; return ServerVariablesInitializer.initialize(config, this.requestLogger, deps) .then(function (vars: ServerVariables) { - that.serverVariables = vars; - that.setupExpressApplication(config, app, deps); + Configurator.configure(config, app, vars, deps); return BluebirdPromise.resolve(); }); } diff --git a/server/src/lib/ServerVariablesInitializer.ts b/server/src/lib/ServerVariablesInitializer.ts index 7f60adcc8..46c91ff14 100644 --- a/server/src/lib/ServerVariablesInitializer.ts +++ b/server/src/lib/ServerVariablesInitializer.ts @@ -37,7 +37,7 @@ import { IMongoClient } from "./connectors/mongo/IMongoClient"; import { GlobalDependencies } from "../../types/Dependencies"; import { ServerVariables } from "./ServerVariables"; -import { AuthenticationMethodCalculator } from "./AuthenticationMethodCalculator"; +import { MethodCalculator } from "./authentication/MethodCalculator"; class UserDataStoreFactory { static create(config: Configuration.AppConfiguration): BluebirdPromise { diff --git a/server/src/lib/authentication/MethodCalculator.ts b/server/src/lib/authentication/MethodCalculator.ts new file mode 100644 index 000000000..d18a83f38 --- /dev/null +++ b/server/src/lib/authentication/MethodCalculator.ts @@ -0,0 +1,40 @@ +import { + AuthenticationMethod, + AuthenticationMethodsConfiguration +} from "../configuration/Configuration"; + +function computeIsSingleFactorOnlyMode( + configuration: AuthenticationMethodsConfiguration): boolean { + if (!configuration) + return false; + + const method: AuthenticationMethod = configuration.default_method; + if (configuration.default_method == "two_factor") + return false; + + if (configuration.per_subdomain_methods) { + for (const key in configuration.per_subdomain_methods) { + const method = configuration.per_subdomain_methods[key]; + if (method == "two_factor") + return false; + } + } + return true; +} + +export class MethodCalculator { + static compute(config: AuthenticationMethodsConfiguration, subDomain: string) + : AuthenticationMethod { + if (config + && config.per_subdomain_methods + && subDomain in config.per_subdomain_methods) { + return config.per_subdomain_methods[subDomain]; + } + return config.default_method; + } + + static isSingleFactorOnlyMode(config: AuthenticationMethodsConfiguration) + : boolean { + return computeIsSingleFactorOnlyMode(config); + } +} \ No newline at end of file diff --git a/server/src/lib/configuration/Validator.ts b/server/src/lib/configuration/Validator.ts index 26a983948..fecc02c77 100644 --- a/server/src/lib/configuration/Validator.ts +++ b/server/src/lib/configuration/Validator.ts @@ -3,8 +3,9 @@ import Path = require("path"); import Util = require("util"); import { UserConfiguration, StorageConfiguration, - NotifierConfiguration + NotifierConfiguration, AuthenticationMethodsConfiguration } from "./Configuration"; +import { MethodCalculator } from "../authentication/MethodCalculator"; function validateSchema(configuration: UserConfiguration): string[] { const schema = require(Path.resolve(__dirname, "./Configuration.schema.json")); @@ -34,7 +35,7 @@ function validateUnknownKeys(path: string, obj: any, knownKeys: string[]) { return []; } -function validateStorage(storage: any) { +function validateStorage(storage: any): string[] { const ERROR = "Storage must be either 'local' or 'mongo'"; if (!storage) @@ -53,22 +54,28 @@ function validateStorage(storage: any) { return []; } -function validateNotifier(notifier: NotifierConfiguration) { +function validateNotifier(notifier: NotifierConfiguration, + authenticationMethods: AuthenticationMethodsConfiguration): string[] { const ERROR = "Notifier must be either 'filesystem', 'email' or 'smtp'"; if (!notifier) return []; + if (!MethodCalculator.isSingleFactorOnlyMode(authenticationMethods)) { + if (Object.keys(notifier).length != 1) + return ["A notifier needs to be declared when server is used with two-factor"]; + + if (notifier && notifier.filesystem && notifier.email && notifier.smtp) + return [ERROR]; + + if (notifier && !notifier.filesystem && !notifier.email && !notifier.smtp) + return [ERROR]; + } + const errors = validateUnknownKeys("notifier", notifier, ["filesystem", "email", "smtp"]); if (errors.length > 0) return errors; - if (notifier && notifier.filesystem && notifier.email && notifier.smtp) - return [ERROR]; - - if (notifier && !notifier.filesystem && !notifier.email && !notifier.smtp) - return [ERROR]; - return []; } @@ -76,7 +83,8 @@ export class Validator { static isValid(configuration: any): string[] { const schemaErrors = validateSchema(configuration); const storageErrors = validateStorage(configuration.storage); - const notifierErrors = validateNotifier(configuration.notifier); + const notifierErrors = validateNotifier(configuration.notifier, + configuration.authentication_methods); return schemaErrors .concat(storageErrors) diff --git a/server/src/lib/ldap/LdapClient.ts b/server/src/lib/ldap/LdapClient.ts index 4b43cb3ce..a2709ceaf 100644 --- a/server/src/lib/ldap/LdapClient.ts +++ b/server/src/lib/ldap/LdapClient.ts @@ -32,7 +32,7 @@ export class LdapClient implements ILdapClient { clientLogger.level("trace"); }*/ - this.client = BluebirdPromise.promisifyAll(ldapClient) as LdapJs.ClientAsync; + this.client = BluebirdPromise.promisifyAll(ldapClient) as any; } bindAsync(username: string, password: string): BluebirdPromise { diff --git a/server/src/lib/routes/FirstFactorBlocker.ts b/server/src/lib/routes/FirstFactorBlocker.ts deleted file mode 100644 index 0fdbfdd32..000000000 --- a/server/src/lib/routes/FirstFactorBlocker.ts +++ /dev/null @@ -1,25 +0,0 @@ - -import express = require("express"); -import BluebirdPromise = require("bluebird"); -import FirstFactorValidator = require("../FirstFactorValidator"); -import Exceptions = require("../Exceptions"); -import ErrorReplies = require("../ErrorReplies"); -import objectPath = require("object-path"); -import AuthenticationSession = require("../AuthenticationSession"); -import UserMessages = require("../../../../shared/UserMessages"); -import { IRequestLogger } from "../logging/IRequestLogger"; - -type Handler = (req: express.Request, res: express.Response) => BluebirdPromise; - -export default function (callback: Handler, logger: IRequestLogger): Handler { - return function (req: express.Request, res: express.Response): BluebirdPromise { - return AuthenticationSession.get(req, logger) - .then(function (authSession) { - return FirstFactorValidator.validate(req, logger); - }) - .then(function () { - return callback(req, res); - }) - .catch(ErrorReplies.replyWithError401(req, res, logger)); - }; -} \ No newline at end of file diff --git a/server/src/lib/routes/firstfactor/get.ts b/server/src/lib/routes/firstfactor/get.ts index 700ea9323..ef6a4b5be 100644 --- a/server/src/lib/routes/firstfactor/get.ts +++ b/server/src/lib/routes/firstfactor/get.ts @@ -5,7 +5,7 @@ import winston = require("winston"); import Endpoints = require("../../../../../shared/api"); import AuthenticationValidator = require("../../AuthenticationValidator"); import BluebirdPromise = require("bluebird"); -import AuthenticationSession = require("../../AuthenticationSession"); +import { AuthenticationSessionHandler } from "../../AuthenticationSessionHandler"; import Constants = require("../../../../../shared/constants"); import Util = require("util"); import { ServerVariables } from "../../ServerVariables"; @@ -42,18 +42,18 @@ function renderFirstFactor(res: express.Response) { export default function (vars: ServerVariables) { return function (req: express.Request, res: express.Response): BluebirdPromise { - return AuthenticationSession.get(req, vars.logger) - .then(function (authSession) { - if (authSession.first_factor) { - if (authSession.second_factor) - redirectToService(req, res); - else - redirectToSecondFactorPage(req, res); - return BluebirdPromise.resolve(); - } - - renderFirstFactor(res); - return BluebirdPromise.resolve(); - }); + return new BluebirdPromise(function (resolve, reject) { + const authSession = AuthenticationSessionHandler.get(req, vars.logger); + if (authSession.first_factor) { + if (authSession.second_factor) + redirectToService(req, res); + else + redirectToSecondFactorPage(req, res); + resolve(); + return; + } + renderFirstFactor(res); + resolve(); + }); }; } \ No newline at end of file diff --git a/server/src/lib/routes/firstfactor/post.ts b/server/src/lib/routes/firstfactor/post.ts index 0d8c317d4..d72ed1da6 100644 --- a/server/src/lib/routes/firstfactor/post.ts +++ b/server/src/lib/routes/firstfactor/post.ts @@ -8,11 +8,11 @@ import { Regulator } from "../../regulation/Regulator"; import { GroupsAndEmails } from "../../ldap/IClient"; import Endpoint = require("../../../../../shared/api"); import ErrorReplies = require("../../ErrorReplies"); -import AuthenticationSessionHandler = require("../../AuthenticationSession"); +import { AuthenticationSessionHandler } from "../../AuthenticationSessionHandler"; import Constants = require("../../../../../shared/constants"); import { DomainExtractor } from "../../utils/DomainExtractor"; import UserMessages = require("../../../../../shared/UserMessages"); -import { AuthenticationMethodCalculator } from "../../AuthenticationMethodCalculator"; +import { MethodCalculator } from "../../authentication/MethodCalculator"; import { ServerVariables } from "../../ServerVariables"; import { AuthenticationSession } from "../../../../types/AuthenticationSession"; @@ -29,10 +29,7 @@ export default function (vars: ServerVariables) { return BluebirdPromise.reject(new Error("No username or password.")); } vars.logger.info(req, "Starting authentication of user \"%s\"", username); - return AuthenticationSessionHandler.get(req, vars.logger); - }) - .then(function (_authSession) { - authSession = _authSession; + authSession = AuthenticationSessionHandler.get(req, vars.logger); return vars.regulator.regulate(username); }) .then(function () { @@ -40,7 +37,8 @@ export default function (vars: ServerVariables) { return vars.ldapAuthenticator.authenticate(username, password); }) .then(function (groupsAndEmails: GroupsAndEmails) { - vars.logger.info(req, "LDAP binding successful. Retrieved information about user are %s", + vars.logger.info(req, + "LDAP binding successful. Retrieved information about user are %s", JSON.stringify(groupsAndEmails)); authSession.userid = username; authSession.first_factor = true; @@ -52,27 +50,24 @@ export default function (vars: ServerVariables) { const emails: string[] = groupsAndEmails.emails; const groups: string[] = groupsAndEmails.groups; const redirectHost: string = DomainExtractor.fromUrl(redirectUrl); - const authMethod = - new AuthenticationMethodCalculator(vars.config.authentication_methods) - .compute(redirectHost); - vars.logger.debug(req, "Authentication method for \"%s\" is \"%s\"", redirectHost, authMethod); + const authMethod = MethodCalculator.compute( + vars.config.authentication_methods, redirectHost); + vars.logger.debug(req, "Authentication method for \"%s\" is \"%s\"", + redirectHost, authMethod); - if (!emails || emails.length <= 0) { - const errMessage = - "No emails found. The user should have at least one email address to reset password."; - vars.logger.error(req, "%s", errMessage); - return BluebirdPromise.reject(new Error(errMessage)); - } - - authSession.email = emails[0]; + if (emails.length > 0) + authSession.email = emails[0]; authSession.groups = groups; vars.logger.debug(req, "Mark successful authentication to regulator."); vars.regulator.mark(username, true); if (authMethod == "single_factor") { + let newRedirectionUrl: string = redirectUrl; + if (!newRedirectionUrl) + newRedirectionUrl = Endpoint.LOGGED_IN; res.send({ - redirect: redirectUrl + redirect: newRedirectionUrl }); vars.logger.debug(req, "Redirect to '%s'", redirectUrl); } @@ -82,7 +77,7 @@ export default function (vars: ServerVariables) { newRedirectUrl += "?" + Constants.REDIRECT_QUERY_PARAM + "=" + encodeURIComponent(redirectUrl); } - vars.logger.debug(req, "Redirect to '%s'", newRedirectUrl, typeof redirectUrl); + vars.logger.debug(req, "Redirect to '%s'", newRedirectUrl); res.send({ redirect: newRedirectUrl }); diff --git a/server/src/lib/routes/loggedin/get.ts b/server/src/lib/routes/loggedin/get.ts index 432fa2e7c..283a041b1 100644 --- a/server/src/lib/routes/loggedin/get.ts +++ b/server/src/lib/routes/loggedin/get.ts @@ -1,21 +1,23 @@ import Express = require("express"); import Endpoints = require("../../../../../shared/api"); -import FirstFactorBlocker from "../FirstFactorBlocker"; import BluebirdPromise = require("bluebird"); -import AuthenticationSessionHandler = require("../../AuthenticationSession"); +import { AuthenticationSessionHandler } from "../../AuthenticationSessionHandler"; import { ServerVariables } from "../../ServerVariables"; +import ErrorReplies = require("../../ErrorReplies"); export default function (vars: ServerVariables) { function handler(req: Express.Request, res: Express.Response): BluebirdPromise { - return AuthenticationSessionHandler.get(req, vars.logger) - .then(function (authSession) { - res.render("already-logged-in", { - logout_endpoint: Endpoints.LOGOUT_GET, - username: authSession.userid, - redirection_url: vars.config.default_redirection_url - }); + return new BluebirdPromise(function (resolve, reject) { + const authSession = AuthenticationSessionHandler.get(req, vars.logger); + res.render("already-logged-in", { + logout_endpoint: Endpoints.LOGOUT_GET, + username: authSession.userid, + redirection_url: vars.config.default_redirection_url }); + resolve(); + }) + .catch(ErrorReplies.replyWithError401(req, res, vars.logger)); } - return FirstFactorBlocker(handler, vars.logger); + return handler; } diff --git a/server/src/lib/routes/logout/get.ts b/server/src/lib/routes/logout/get.ts index 3f2784658..eb8040959 100644 --- a/server/src/lib/routes/logout/get.ts +++ b/server/src/lib/routes/logout/get.ts @@ -1,10 +1,10 @@ import express = require("express"); -import AuthenticationSession = require("../../AuthenticationSession"); +import { AuthenticationSessionHandler } from "../../AuthenticationSessionHandler"; export default function(req: express.Request, res: express.Response) { const redirect_param = req.query.redirect; const redirect_url = redirect_param || "/"; - AuthenticationSession.reset(req); + AuthenticationSessionHandler.reset(req); res.redirect(redirect_url); } \ No newline at end of file diff --git a/server/src/lib/routes/password-reset/form/post.ts b/server/src/lib/routes/password-reset/form/post.ts index 7af57fce8..ed7bba687 100644 --- a/server/src/lib/routes/password-reset/form/post.ts +++ b/server/src/lib/routes/password-reset/form/post.ts @@ -3,7 +3,7 @@ import express = require("express"); import BluebirdPromise = require("bluebird"); import objectPath = require("object-path"); import exceptions = require("../../../Exceptions"); -import AuthenticationSessionHandler = require("../../../AuthenticationSession"); +import { AuthenticationSessionHandler } from "../../../AuthenticationSessionHandler"; import { AuthenticationSession } from "../../../../../types/AuthenticationSession"; import ErrorReplies = require("../../../ErrorReplies"); import UserMessages = require("../../../../../../shared/UserMessages"); @@ -16,16 +16,24 @@ export default function (vars: ServerVariables) { let authSession: AuthenticationSession; const newPassword = objectPath.get(req, "body.password"); - return AuthenticationSessionHandler.get(req, vars.logger) - .then(function (_authSession) { - authSession = _authSession; - vars.logger.info(req, "User %s wants to reset his/her password.", - authSession.identity_check.userid); - vars.logger.debug(req, "Challenge %s", authSession.identity_check.challenge); + return new BluebirdPromise(function (resolve, reject) { + authSession = AuthenticationSessionHandler.get(req, vars.logger); + if (!authSession.identity_check) { + reject(new Error("No identity check initiated")); + return; + } - if (authSession.identity_check.challenge != Constants.CHALLENGE) { - return BluebirdPromise.reject(new Error("Bad challenge.")); - } + vars.logger.info(req, "User %s wants to reset his/her password.", + authSession.identity_check.userid); + vars.logger.debug(req, "Challenge %s", authSession.identity_check.challenge); + + if (authSession.identity_check.challenge != Constants.CHALLENGE) { + reject(new Error("Bad challenge.")); + return; + } + resolve(); + }) + .then(function () { return vars.ldapPasswordUpdater.updatePassword(authSession.identity_check.userid, newPassword); }) .then(function () { diff --git a/server/src/lib/routes/secondfactor/get.ts b/server/src/lib/routes/secondfactor/get.ts index ac657266f..71e495f37 100644 --- a/server/src/lib/routes/secondfactor/get.ts +++ b/server/src/lib/routes/secondfactor/get.ts @@ -1,30 +1,37 @@ import Express = require("express"); import Endpoints = require("../../../../../shared/api"); -import FirstFactorBlocker = require("../FirstFactorBlocker"); import BluebirdPromise = require("bluebird"); -import AuthenticationSessionHandler = require("../../AuthenticationSession"); +import { AuthenticationSessionHandler } from "../../AuthenticationSessionHandler"; import { ServerVariables } from "../../ServerVariables"; +import { MethodCalculator } from "../../authentication/MethodCalculator"; const TEMPLATE_NAME = "secondfactor"; export default function (vars: ServerVariables) { - function handler(req: Express.Request, res: Express.Response): BluebirdPromise { - return AuthenticationSessionHandler.get(req, vars.logger) - .then(function (authSession) { - if (authSession.first_factor && authSession.second_factor) { - res.redirect(Endpoints.LOGGED_IN); - return BluebirdPromise.resolve(); - } + function handler(req: Express.Request, res: Express.Response) + : BluebirdPromise { - res.render(TEMPLATE_NAME, { - username: authSession.userid, - totp_identity_start_endpoint: Endpoints.SECOND_FACTOR_TOTP_IDENTITY_START_GET, - u2f_identity_start_endpoint: Endpoints.SECOND_FACTOR_U2F_IDENTITY_START_GET - }); - return BluebirdPromise.resolve(); + return new BluebirdPromise(function (resolve, reject) { + const isSingleFactorMode: boolean = MethodCalculator.isSingleFactorOnlyMode( + vars.config.authentication_methods); + const authSession = AuthenticationSessionHandler.get(req, vars.logger); + if (isSingleFactorMode + || (authSession.first_factor && authSession.second_factor)) { + res.redirect(Endpoints.LOGGED_IN); + resolve(); + return; + } + + res.render(TEMPLATE_NAME, { + username: authSession.userid, + totp_identity_start_endpoint: + Endpoints.SECOND_FACTOR_TOTP_IDENTITY_START_GET, + u2f_identity_start_endpoint: + Endpoints.SECOND_FACTOR_U2F_IDENTITY_START_GET }); + resolve(); + }); } - - return FirstFactorBlocker.default(handler, vars.logger); -} \ No newline at end of file + return handler; +} diff --git a/server/src/lib/routes/secondfactor/redirect.ts b/server/src/lib/routes/secondfactor/redirect.ts index adc1f5c42..595382188 100644 --- a/server/src/lib/routes/secondfactor/redirect.ts +++ b/server/src/lib/routes/secondfactor/redirect.ts @@ -4,7 +4,7 @@ import objectPath = require("object-path"); import winston = require("winston"); import Endpoints = require("../../../../../shared/api"); import { ServerVariables } from "../../ServerVariables"; -import AuthenticationSession = require("../../AuthenticationSession"); +import { AuthenticationSessionHandler } from "../../AuthenticationSessionHandler"; import BluebirdPromise = require("bluebird"); import ErrorReplies = require("../../ErrorReplies"); import UserMessages = require("../../../../../shared/UserMessages"); @@ -13,18 +13,19 @@ import Constants = require("../../../../../shared/constants"); export default function (vars: ServerVariables) { return function (req: express.Request, res: express.Response): BluebirdPromise { - return AuthenticationSession.get(req, vars.logger) - .then(function (authSession) { - let redirectUrl: string; - if (vars.config.default_redirection_url) { - redirectUrl = vars.config.default_redirection_url; - } - vars.logger.debug(req, "Request redirection to \"%s\".", redirectUrl); - res.json({ - redirect: redirectUrl - } as RedirectionMessage); - return BluebirdPromise.resolve(); - }) + + return new BluebirdPromise(function (resolve, reject) { + const authSession = AuthenticationSessionHandler.get(req, vars.logger); + let redirectUrl: string; + if (vars.config.default_redirection_url) { + redirectUrl = vars.config.default_redirection_url; + } + vars.logger.debug(req, "Request redirection to \"%s\".", redirectUrl); + res.json({ + redirect: redirectUrl + } as RedirectionMessage); + return resolve(); + }) .catch(ErrorReplies.replyWithError200(req, res, vars.logger, UserMessages.OPERATION_FAILED)); }; diff --git a/server/src/lib/routes/secondfactor/totp/identity/RegistrationHandler.ts b/server/src/lib/routes/secondfactor/totp/identity/RegistrationHandler.ts index c262efc01..db743ed46 100644 --- a/server/src/lib/routes/secondfactor/totp/identity/RegistrationHandler.ts +++ b/server/src/lib/routes/secondfactor/totp/identity/RegistrationHandler.ts @@ -9,12 +9,13 @@ import { PRE_VALIDATION_TEMPLATE } from "../../../../IdentityCheckPreValidationT import Constants = require("../constants"); import Endpoints = require("../../../../../../../shared/api"); import ErrorReplies = require("../../../../ErrorReplies"); -import AuthenticationSession = require("../../../../AuthenticationSession"); +import { AuthenticationSessionHandler } from "../../../../AuthenticationSessionHandler"; import UserMessages = require("../../../../../../../shared/UserMessages"); import FirstFactorValidator = require("../../../../FirstFactorValidator"); import { IRequestLogger } from "../../../../logging/IRequestLogger"; import { IUserDataStore } from "../../../../storage/IUserDataStore"; import { ITotpHandler } from "../../../../authentication/totp/ITotpHandler"; +import { TOTPSecret } from "../../../../../../types/TOTPSecret"; export default class RegistrationHandler implements IdentityValidable { @@ -35,21 +36,22 @@ export default class RegistrationHandler implements IdentityValidable { } private retrieveIdentity(req: express.Request): BluebirdPromise { - return AuthenticationSession.get(req, this.logger) - .then(function (authSession) { - const userid = authSession.userid; - const email = authSession.email; + const that = this; + return new BluebirdPromise(function (resolve, reject) { + const authSession = AuthenticationSessionHandler.get(req, that.logger); + const userid = authSession.userid; + const email = authSession.email; - if (!(userid && email)) { - return BluebirdPromise.reject(new Error("User ID or email is missing.")); - } + if (!(userid && email)) { + return reject(new Error("User ID or email is missing")); + } - const identity = { - email: email, - userid: userid - }; - return BluebirdPromise.resolve(identity); - }); + const identity = { + email: email, + userid: userid + }; + return resolve(identity); + }); } preValidationInit(req: express.Request): BluebirdPromise { @@ -70,26 +72,31 @@ export default class RegistrationHandler implements IdentityValidable { postValidationResponse(req: express.Request, res: express.Response): BluebirdPromise { const that = this; - return AuthenticationSession.get(req, this.logger) - .then(function (authSession) { - const userid = authSession.identity_check.userid; - const challenge = authSession.identity_check.challenge; + let secret: TOTPSecret; + let userId: string; + return new BluebirdPromise(function (resolve, reject) { + const authSession = AuthenticationSessionHandler.get(req, that.logger); + const challenge = authSession.identity_check.challenge; + userId = authSession.identity_check.userid; - if (challenge != Constants.CHALLENGE || !userid) { - return BluebirdPromise.reject(new Error("Bad challenge.")); - } - const secret = that.totp.generate(); - that.logger.debug(req, "Save the TOTP secret in DB."); - return that.userDataStore.saveTOTPSecret(userid, secret) - .then(function () { - AuthenticationSession.reset(req); + if (challenge != Constants.CHALLENGE || !userId) { + return reject(new Error("Bad challenge.")); + } + resolve(); + }) + .then(function () { + secret = that.totp.generate(); + that.logger.debug(req, "Save the TOTP secret in DB"); + return that.userDataStore.saveTOTPSecret(userId, secret); + }) + .then(function () { + AuthenticationSessionHandler.reset(req); - res.render(Constants.TEMPLATE_NAME, { - base32_secret: secret.base32, - otpauth_url: secret.otpauth_url, - login_endpoint: Endpoints.FIRST_FACTOR_GET - }); - }); + res.render(Constants.TEMPLATE_NAME, { + base32_secret: secret.base32, + otpauth_url: secret.otpauth_url, + login_endpoint: Endpoints.FIRST_FACTOR_GET + }); }) .catch(ErrorReplies.replyWithError200(req, res, that.logger, UserMessages.OPERATION_FAILED)); } diff --git a/server/src/lib/routes/secondfactor/totp/sign/post.ts b/server/src/lib/routes/secondfactor/totp/sign/post.ts index d3f9a28d2..45bbec94f 100644 --- a/server/src/lib/routes/secondfactor/totp/sign/post.ts +++ b/server/src/lib/routes/secondfactor/totp/sign/post.ts @@ -4,11 +4,10 @@ import objectPath = require("object-path"); import express = require("express"); import { TOTPSecretDocument } from "../../../../storage/TOTPSecretDocument"; import BluebirdPromise = require("bluebird"); -import FirstFactorBlocker from "../../../FirstFactorBlocker"; import Endpoints = require("../../../../../../../shared/api"); import redirect from "../../redirect"; import ErrorReplies = require("../../../../ErrorReplies"); -import AuthenticationSessionHandler = require("../../../../AuthenticationSession"); +import { AuthenticationSessionHandler } from "../../../../AuthenticationSessionHandler"; import { AuthenticationSession } from "../../../../../../types/AuthenticationSession"; import UserMessages = require("../../../../../../../shared/UserMessages"); import { ServerVariables } from "../../../../ServerVariables"; @@ -20,10 +19,12 @@ export default function (vars: ServerVariables) { let authSession: AuthenticationSession; const token = req.body.token; - return AuthenticationSessionHandler.get(req, vars.logger) - .then(function (_authSession) { - authSession = _authSession; - vars.logger.info(req, "Initiate TOTP validation for user \"%s\".", authSession.userid); + return new BluebirdPromise(function (resolve, reject) { + authSession = AuthenticationSessionHandler.get(req, vars.logger); + vars.logger.info(req, "Initiate TOTP validation for user \"%s\".", authSession.userid); + resolve(); + }) + .then(function () { return vars.userDataStore.retrieveTOTPSecret(authSession.userid); }) .then(function (doc: TOTPSecretDocument) { @@ -38,5 +39,5 @@ export default function (vars: ServerVariables) { .catch(ErrorReplies.replyWithError200(req, res, vars.logger, UserMessages.OPERATION_FAILED)); } - return FirstFactorBlocker(handler, vars.logger); + return handler; } diff --git a/server/src/lib/routes/secondfactor/u2f/identity/RegistrationHandler.ts b/server/src/lib/routes/secondfactor/u2f/identity/RegistrationHandler.ts index 7c99ec265..06fd44da8 100644 --- a/server/src/lib/routes/secondfactor/u2f/identity/RegistrationHandler.ts +++ b/server/src/lib/routes/secondfactor/u2f/identity/RegistrationHandler.ts @@ -7,7 +7,7 @@ import { IdentityValidable } from "../../../../IdentityCheckMiddleware"; import { Identity } from "../../../../../../types/Identity"; import { PRE_VALIDATION_TEMPLATE } from "../../../../IdentityCheckPreValidationTemplate"; import FirstFactorValidator = require("../../../../FirstFactorValidator"); -import AuthenticationSession = require("../../../../AuthenticationSession"); +import { AuthenticationSessionHandler } from "../../../../AuthenticationSessionHandler"; import { IRequestLogger } from "../../../../logging/IRequestLogger"; const CHALLENGE = "u2f-register"; @@ -28,21 +28,22 @@ export default class RegistrationHandler implements IdentityValidable { } private retrieveIdentity(req: express.Request): BluebirdPromise { - return AuthenticationSession.get(req, this.logger) - .then(function (authSession) { - const userid = authSession.userid; - const email = authSession.email; + const that = this; + return new BluebirdPromise(function(resolve, reject) { + const authSession = AuthenticationSessionHandler.get(req, that.logger); + const userid = authSession.userid; + const email = authSession.email; - if (!(userid && email)) { - return BluebirdPromise.reject(new Error("User ID or email is missing")); - } + if (!(userid && email)) { + return reject(new Error("User ID or email is missing")); + } - const identity = { - email: email, - userid: userid - }; - return BluebirdPromise.resolve(identity); - }); + const identity = { + email: email, + userid: userid + }; + return resolve(identity); + }); } preValidationInit(req: express.Request): BluebirdPromise { diff --git a/server/src/lib/routes/secondfactor/u2f/register/post.ts b/server/src/lib/routes/secondfactor/u2f/register/post.ts index 5649bb404..7296ccbe5 100644 --- a/server/src/lib/routes/secondfactor/u2f/register/post.ts +++ b/server/src/lib/routes/secondfactor/u2f/register/post.ts @@ -1,17 +1,15 @@ 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 } from "../../../../ServerVariables"; -import AuthenticationSessionHandler = require("../../../../AuthenticationSession"); +import { AuthenticationSessionHandler } from "../../../../AuthenticationSessionHandler"; import UserMessages = require("../../../../../../../shared/UserMessages"); import { AuthenticationSession } from "../../../../../../types/AuthenticationSession"; @@ -22,26 +20,25 @@ export default function (vars: ServerVariables) { const appid = u2f_common.extract_app_id(req); const registrationResponse: U2f.RegistrationData = req.body; - return AuthenticationSessionHandler.get(req, vars.logger) - .then(function (_authSession) { - authSession = _authSession; - const registrationRequest = authSession.register_request; + return new BluebirdPromise(function (resolve, reject) { + authSession = AuthenticationSessionHandler.get(req, vars.logger); + const registrationRequest = authSession.register_request; - if (!registrationRequest) { - return BluebirdPromise.reject(new Error("No registration request")); - } + if (!registrationRequest) { + return reject(new Error("No registration request")); + } - if (!authSession.identity_check - || authSession.identity_check.challenge != "u2f-register") { - return BluebirdPromise.reject(new Error("Bad challenge for registration request")); - } + if (!authSession.identity_check + || authSession.identity_check.challenge != "u2f-register") { + return reject(new Error("Bad challenge for registration request")); + } - vars.logger.info(req, "Finishing registration"); - vars.logger.debug(req, "RegistrationRequest = %s", JSON.stringify(registrationRequest)); - vars.logger.debug(req, "RegistrationResponse = %s", JSON.stringify(registrationResponse)); + vars.logger.info(req, "Finishing registration"); + vars.logger.debug(req, "RegistrationRequest = %s", JSON.stringify(registrationRequest)); + vars.logger.debug(req, "RegistrationResponse = %s", JSON.stringify(registrationResponse)); - return BluebirdPromise.resolve(vars.u2f.checkRegistration(registrationRequest, registrationResponse)); - }) + return resolve(vars.u2f.checkRegistration(registrationRequest, registrationResponse)); + }) .then(function (u2fResult: U2f.RegistrationResult | U2f.Error): BluebirdPromise { if (objectPath.has(u2fResult, "errorCode")) return BluebirdPromise.reject(new Error("Error while registering.")); @@ -63,6 +60,5 @@ export default function (vars: ServerVariables) { .catch(ErrorReplies.replyWithError200(req, res, vars.logger, UserMessages.OPERATION_FAILED)); } - - return FirstFactorBlocker(handler, vars.logger); + return handler; } diff --git a/server/src/lib/routes/secondfactor/u2f/register_request/get.ts b/server/src/lib/routes/secondfactor/u2f/register_request/get.ts index 87487ee1b..f611af933 100644 --- a/server/src/lib/routes/secondfactor/u2f/register_request/get.ts +++ b/server/src/lib/routes/secondfactor/u2f/register_request/get.ts @@ -6,9 +6,8 @@ import u2f_common = require("../U2FCommon"); import BluebirdPromise = require("bluebird"); import express = require("express"); import U2f = require("u2f"); -import FirstFactorBlocker from "../../../FirstFactorBlocker"; import ErrorReplies = require("../../../../ErrorReplies"); -import AuthenticationSessionHandler = require("../../../../AuthenticationSession"); +import { AuthenticationSessionHandler } from "../../../../AuthenticationSessionHandler"; import { AuthenticationSession } from "../../../../../../types/AuthenticationSession"; import UserMessages = require("../../../../../../../shared/UserMessages"); import { ServerVariables } from "../../../../ServerVariables"; @@ -18,21 +17,18 @@ export default function (vars: ServerVariables) { let authSession: AuthenticationSession; const appid: string = u2f_common.extract_app_id(req); - return AuthenticationSessionHandler.get(req, vars.logger) - .then(function (_authSession) { - authSession = _authSession; + return new BluebirdPromise(function (resolve, reject) { + authSession = AuthenticationSessionHandler.get(req, vars.logger); + if (!authSession.identity_check + || authSession.identity_check.challenge != "u2f-register") { + res.status(403); + res.send(); + return reject(new Error("Bad challenge.")); + } - if (!authSession.identity_check - || authSession.identity_check.challenge != "u2f-register") { - res.status(403); - res.send(); - return BluebirdPromise.reject(new Error("Bad challenge.")); - } - - vars.logger.info(req, "Starting registration for appId '%s'", appid); - - return BluebirdPromise.resolve(vars.u2f.request(appid)); - }) + vars.logger.info(req, "Starting registration for appId '%s'", appid); + return resolve(vars.u2f.request(appid)); + }) .then(function (registrationRequest: U2f.Request) { vars.logger.debug(req, "RegistrationRequest = %s", JSON.stringify(registrationRequest)); authSession.register_request = registrationRequest; @@ -43,5 +39,5 @@ export default function (vars: ServerVariables) { UserMessages.OPERATION_FAILED)); } - return FirstFactorBlocker(handler, vars.logger); + return handler; } \ No newline at end of file diff --git a/server/src/lib/routes/secondfactor/u2f/sign/post.ts b/server/src/lib/routes/secondfactor/u2f/sign/post.ts index fe3ade2dd..8e8436ef3 100644 --- a/server/src/lib/routes/secondfactor/u2f/sign/post.ts +++ b/server/src/lib/routes/secondfactor/u2f/sign/post.ts @@ -8,11 +8,10 @@ import { U2FRegistrationDocument } from "../../../../storage/U2FRegistrationDocu import { Winston } from "../../../../../../types/Dependencies"; import U2f = require("u2f"); import exceptions = require("../../../../Exceptions"); -import FirstFactorBlocker from "../../../FirstFactorBlocker"; import redirect from "../../redirect"; import ErrorReplies = require("../../../../ErrorReplies"); import { ServerVariables } from "../../../../ServerVariables"; -import AuthenticationSessionHandler = require("../../../../AuthenticationSession"); +import { AuthenticationSessionHandler } from "../../../../AuthenticationSessionHandler"; import UserMessages = require("../../../../../../../shared/UserMessages"); import { AuthenticationSession } from "../../../../../../types/AuthenticationSession"; @@ -21,14 +20,16 @@ export default function (vars: ServerVariables) { let authSession: AuthenticationSession; const appId = u2f_common.extract_app_id(req); - return AuthenticationSessionHandler.get(req, vars.logger) - .then(function (_authSession) { - authSession = _authSession; - if (!authSession.sign_request) { - const err = new Error("No sign request"); - ErrorReplies.replyWithError401(req, res, vars.logger)(err); - return BluebirdPromise.reject(err); - } + return new BluebirdPromise(function (resolve, reject) { + authSession = AuthenticationSessionHandler.get(req, vars.logger); + if (!authSession.sign_request) { + const err = new Error("No sign request"); + ErrorReplies.replyWithError401(req, res, vars.logger)(err); + return reject(err); + } + resolve(); + }) + .then(function () { const userid = authSession.userid; return vars.userDataStore.retrieveU2FRegistration(userid, appId); }) @@ -50,6 +51,6 @@ export default function (vars: ServerVariables) { UserMessages.OPERATION_FAILED)); } - return FirstFactorBlocker(handler, vars.logger); + return handler; } diff --git a/server/src/lib/routes/secondfactor/u2f/sign_request/get.ts b/server/src/lib/routes/secondfactor/u2f/sign_request/get.ts index 897af4ab4..640ca2550 100644 --- a/server/src/lib/routes/secondfactor/u2f/sign_request/get.ts +++ b/server/src/lib/routes/secondfactor/u2f/sign_request/get.ts @@ -9,9 +9,8 @@ import { U2FRegistrationDocument } from "../../../../storage/U2FRegistrationDocu import { Winston } from "../../../../../../types/Dependencies"; import exceptions = require("../../../../Exceptions"); import { SignMessage } from "../../../../../../../shared/SignMessage"; -import FirstFactorBlocker from "../../../FirstFactorBlocker"; import ErrorReplies = require("../../../../ErrorReplies"); -import AuthenticationSessionHandler = require("../../../../AuthenticationSession"); +import { AuthenticationSessionHandler } from "../../../../AuthenticationSessionHandler"; import UserMessages = require("../../../../../../../shared/UserMessages"); import { ServerVariables } from "../../../../ServerVariables"; import { AuthenticationSession } from "../../../../../../types/AuthenticationSession"; @@ -21,9 +20,11 @@ export default function (vars: ServerVariables) { let authSession: AuthenticationSession; const appId = u2f_common.extract_app_id(req); - return AuthenticationSessionHandler.get(req, vars.logger) - .then(function (_authSession) { - authSession = _authSession; + return new BluebirdPromise(function (resolve, reject) { + authSession = AuthenticationSessionHandler.get(req, vars.logger); + resolve(); + }) + .then(function () { return vars.userDataStore.retrieveU2FRegistration(authSession.userid, appId); }) .then(function (doc: U2FRegistrationDocument): BluebirdPromise { @@ -51,6 +52,5 @@ export default function (vars: ServerVariables) { .catch(ErrorReplies.replyWithError200(req, res, vars.logger, UserMessages.OPERATION_FAILED)); } - - return FirstFactorBlocker(handler, vars.logger); + return handler; } diff --git a/server/src/lib/routes/verify/get.ts b/server/src/lib/routes/verify/get.ts index 15c2cffc3..98993ec6a 100644 --- a/server/src/lib/routes/verify/get.ts +++ b/server/src/lib/routes/verify/get.ts @@ -7,13 +7,13 @@ import winston = require("winston"); import AuthenticationValidator = require("../../AuthenticationValidator"); import ErrorReplies = require("../../ErrorReplies"); import { AppConfiguration } from "../../configuration/Configuration"; -import AuthenticationSessionHandler = require("../../AuthenticationSession"); +import { AuthenticationSessionHandler } from "../../AuthenticationSessionHandler"; import { AuthenticationSession } from "../../../../types/AuthenticationSession"; import Constants = require("../../../../../shared/constants"); import Util = require("util"); import { DomainExtractor } from "../../utils/DomainExtractor"; import { ServerVariables } from "../../ServerVariables"; -import { AuthenticationMethodCalculator } from "../../AuthenticationMethodCalculator"; +import { MethodCalculator } from "../../authentication/MethodCalculator"; import { IRequestLogger } from "../../logging/IRequestLogger"; const FIRST_FACTOR_NOT_VALIDATED_MESSAGE = "First factor not yet validated"; @@ -50,49 +50,50 @@ function verify_inactivity(req: express.Request, function verify_filter(req: express.Request, res: express.Response, vars: ServerVariables): BluebirdPromise { - let _authSession: AuthenticationSession; + let authSession: AuthenticationSession; let username: string; let groups: string[]; - return AuthenticationSessionHandler.get(req, vars.logger) - .then(function (authSession) { - _authSession = authSession; - username = _authSession.userid; - groups = _authSession.groups; + return new BluebirdPromise(function (resolve, reject) { + authSession = AuthenticationSessionHandler.get(req, vars.logger); + username = authSession.userid; + groups = authSession.groups; - res.set("Redirect", encodeURIComponent("https://" + req.headers["host"] + - req.headers["x-original-uri"])); + res.set("Redirect", encodeURIComponent("https://" + req.headers["host"] + + req.headers["x-original-uri"])); - if (!_authSession.userid) - return BluebirdPromise.reject( - new exceptions.AccessDeniedError(FIRST_FACTOR_NOT_VALIDATED_MESSAGE)); + if (!authSession.userid) { + reject(new exceptions.AccessDeniedError( + Util.format("%s: %s.", FIRST_FACTOR_NOT_VALIDATED_MESSAGE, "userid is missing"))); + return; + } - const host = objectPath.get(req, "headers.host"); - const path = objectPath.get(req, "headers.x-original-uri"); + const host = objectPath.get(req, "headers.host"); + const path = objectPath.get(req, "headers.x-original-uri"); - const domain = DomainExtractor.fromHostHeader(host); - const authenticationMethod = - new AuthenticationMethodCalculator(vars.config.authentication_methods) - .compute(domain); - vars.logger.debug(req, "domain=%s, path=%s, user=%s, groups=%s", domain, path, - username, groups.join(",")); + const domain = DomainExtractor.fromHostHeader(host); + const authenticationMethod = + MethodCalculator.compute(vars.config.authentication_methods, domain); + vars.logger.debug(req, "domain=%s, path=%s, user=%s, groups=%s", domain, path, + username, groups.join(",")); - if (!_authSession.first_factor) - return BluebirdPromise.reject( - new exceptions.AccessDeniedError(FIRST_FACTOR_NOT_VALIDATED_MESSAGE)); + if (!authSession.first_factor) + return reject(new exceptions.AccessDeniedError( + Util.format("%s: %s.", FIRST_FACTOR_NOT_VALIDATED_MESSAGE, "first factor is false"))); - if (authenticationMethod == "two_factor" && !_authSession.second_factor) - return BluebirdPromise.reject( - new exceptions.AccessDeniedError(SECOND_FACTOR_NOT_VALIDATED_MESSAGE)); + if (authenticationMethod == "two_factor" && !authSession.second_factor) + return reject(new exceptions.AccessDeniedError( + Util.format("%s: %s.", SECOND_FACTOR_NOT_VALIDATED_MESSAGE, "second factor is false"))); - const isAllowed = vars.accessController.isAccessAllowed(domain, path, username, groups); - if (!isAllowed) return BluebirdPromise.reject( - new exceptions.DomainAccessDenied(Util.format("User '%s' does not have access to '%s'", - username, domain))); - return BluebirdPromise.resolve(); - }) + const isAllowed = vars.accessController.isAccessAllowed(domain, path, username, groups); + if (!isAllowed) return reject( + new exceptions.DomainAccessDenied(Util.format("User '%s' does not have access to '%s'", + username, domain))); + + resolve(); + }) .then(function () { - return verify_inactivity(req, _authSession, + return verify_inactivity(req, authSession, vars.config, vars.logger); }) .then(function () { diff --git a/server/src/lib/web_server/Configurator.ts b/server/src/lib/web_server/Configurator.ts new file mode 100644 index 000000000..90c7b7f5a --- /dev/null +++ b/server/src/lib/web_server/Configurator.ts @@ -0,0 +1,45 @@ +import { AppConfiguration } from "../configuration/Configuration"; +import { GlobalDependencies } from "../../../types/Dependencies"; +import { SessionConfigurationBuilder } from + "../configuration/SessionConfigurationBuilder"; +import Path = require("path"); +import Express = require("express"); +import * as BodyParser from "body-parser"; +import { RestApi } from "./RestApi"; +import { WithHeadersLogged } from "./middlewares/WithHeadersLogged"; +import { ServerVariables } from "../ServerVariables"; + +const addRequestId = require("express-request-id")(); + +// Constants +const TRUST_PROXY = "trust proxy"; +const X_POWERED_BY = "x-powered-by"; +const VIEWS = "views"; +const VIEW_ENGINE = "view engine"; +const PUG = "pug"; + +export class Configurator { + static configure(config: AppConfiguration, + app: Express.Application, + vars: ServerVariables, + deps: GlobalDependencies): void { + const viewsDirectory = Path.resolve(__dirname, "../../views"); + const publicHtmlDirectory = Path.resolve(__dirname, "../../public_html"); + + const expressSessionOptions = SessionConfigurationBuilder.build(config, deps); + + app.use(Express.static(publicHtmlDirectory)); + app.use(BodyParser.urlencoded({ extended: false })); + app.use(BodyParser.json()); + app.use(deps.session(expressSessionOptions)); + app.use(addRequestId); + app.use(WithHeadersLogged.middleware(vars.logger)); + app.disable(X_POWERED_BY); + app.enable(TRUST_PROXY); + + app.set(VIEWS, viewsDirectory); + app.set(VIEW_ENGINE, PUG); + + RestApi.setup(app, vars); + } +} \ No newline at end of file diff --git a/server/src/lib/web_server/RestApi.ts b/server/src/lib/web_server/RestApi.ts new file mode 100644 index 000000000..def5ea40d --- /dev/null +++ b/server/src/lib/web_server/RestApi.ts @@ -0,0 +1,139 @@ +import Express = require("express"); + +import FirstFactorGet = require("../routes/firstfactor/get"); +import SecondFactorGet = require("../routes/secondfactor/get"); + +import FirstFactorPost = require("../routes/firstfactor/post"); +import LogoutGet = require("../routes/logout/get"); +import VerifyGet = require("../routes/verify/get"); +import TOTPSignGet = require("../routes/secondfactor/totp/sign/post"); + +import IdentityCheckMiddleware = require("../IdentityCheckMiddleware"); + +import TOTPRegistrationIdentityHandler from "../routes/secondfactor/totp/identity/RegistrationHandler"; +import U2FRegistrationIdentityHandler from "../routes/secondfactor/u2f/identity/RegistrationHandler"; +import ResetPasswordIdentityHandler from "../routes/password-reset/identity/PasswordResetHandler"; + +import U2FSignPost = require("../routes/secondfactor/u2f/sign/post"); +import U2FSignRequestGet = require("../routes/secondfactor/u2f/sign_request/get"); + +import U2FRegisterPost = require("../routes/secondfactor/u2f/register/post"); +import U2FRegisterRequestGet = require("../routes/secondfactor/u2f/register_request/get"); + +import ResetPasswordFormPost = require("../routes/password-reset/form/post"); +import ResetPasswordRequestPost = require("../routes/password-reset/request/get"); + +import Error401Get = require("../routes/error/401/get"); +import Error403Get = require("../routes/error/403/get"); +import Error404Get = require("../routes/error/404/get"); + +import LoggedIn = require("../routes/loggedin/get"); + +import { ServerVariables } from "../ServerVariables"; +import Endpoints = require("../../../../shared/api"); +import { RequireValidatedFirstFactor } from "./middlewares/RequireValidatedFirstFactor"; +import { RequireTwoFactorEnabled } from "./middlewares/RequireTwoFactorEnabled"; + +function setupTotp(app: Express.Application, vars: ServerVariables) { + app.post(Endpoints.SECOND_FACTOR_TOTP_POST, + RequireTwoFactorEnabled.middleware(vars.logger, + vars.config.authentication_methods), + RequireValidatedFirstFactor.middleware(vars.logger), + TOTPSignGet.default(vars)); + + app.get(Endpoints.SECOND_FACTOR_TOTP_IDENTITY_START_GET, + RequireTwoFactorEnabled.middleware(vars.logger, + vars.config.authentication_methods), + RequireValidatedFirstFactor.middleware(vars.logger)); + + app.get(Endpoints.SECOND_FACTOR_TOTP_IDENTITY_FINISH_GET, + RequireTwoFactorEnabled.middleware(vars.logger, + vars.config.authentication_methods), + RequireValidatedFirstFactor.middleware(vars.logger)); + + IdentityCheckMiddleware.register(app, + Endpoints.SECOND_FACTOR_TOTP_IDENTITY_START_GET, + Endpoints.SECOND_FACTOR_TOTP_IDENTITY_FINISH_GET, + new TOTPRegistrationIdentityHandler(vars.logger, + vars.userDataStore, vars.totpHandler), + vars); +} + +function setupU2f(app: Express.Application, vars: ServerVariables) { + app.get(Endpoints.SECOND_FACTOR_U2F_SIGN_REQUEST_GET, + RequireTwoFactorEnabled.middleware(vars.logger, + vars.config.authentication_methods), + RequireValidatedFirstFactor.middleware(vars.logger), + U2FSignRequestGet.default(vars)); + + app.post(Endpoints.SECOND_FACTOR_U2F_SIGN_POST, + RequireTwoFactorEnabled.middleware(vars.logger, + vars.config.authentication_methods), + RequireValidatedFirstFactor.middleware(vars.logger), + U2FSignPost.default(vars)); + + app.get(Endpoints.SECOND_FACTOR_U2F_REGISTER_REQUEST_GET, + RequireTwoFactorEnabled.middleware(vars.logger, + vars.config.authentication_methods), + RequireValidatedFirstFactor.middleware(vars.logger), + U2FRegisterRequestGet.default(vars)); + + app.post(Endpoints.SECOND_FACTOR_U2F_REGISTER_POST, + RequireTwoFactorEnabled.middleware(vars.logger, + vars.config.authentication_methods), + RequireValidatedFirstFactor.middleware(vars.logger), + U2FRegisterPost.default(vars)); + + app.get(Endpoints.SECOND_FACTOR_U2F_IDENTITY_START_GET, + RequireTwoFactorEnabled.middleware(vars.logger, + vars.config.authentication_methods), + RequireValidatedFirstFactor.middleware(vars.logger)); + + app.get(Endpoints.SECOND_FACTOR_U2F_IDENTITY_FINISH_GET, + RequireTwoFactorEnabled.middleware(vars.logger, + vars.config.authentication_methods), + RequireValidatedFirstFactor.middleware(vars.logger)); + + IdentityCheckMiddleware.register(app, + Endpoints.SECOND_FACTOR_U2F_IDENTITY_START_GET, + Endpoints.SECOND_FACTOR_U2F_IDENTITY_FINISH_GET, + new U2FRegistrationIdentityHandler(vars.logger), vars); +} + +function setupResetPassword(app: Express.Application, vars: ServerVariables) { + IdentityCheckMiddleware.register(app, Endpoints.RESET_PASSWORD_IDENTITY_START_GET, + Endpoints.RESET_PASSWORD_IDENTITY_FINISH_GET, + new ResetPasswordIdentityHandler(vars.logger, vars.ldapEmailsRetriever), vars); + + app.get(Endpoints.RESET_PASSWORD_REQUEST_GET, ResetPasswordRequestPost.default); + app.post(Endpoints.RESET_PASSWORD_FORM_POST, ResetPasswordFormPost.default(vars)); +} + +function setupErrors(app: Express.Application) { + app.get(Endpoints.ERROR_401_GET, Error401Get.default); + app.get(Endpoints.ERROR_403_GET, Error403Get.default); + app.get(Endpoints.ERROR_404_GET, Error404Get.default); +} + +export class RestApi { + static setup(app: Express.Application, vars: ServerVariables): void { + app.get(Endpoints.FIRST_FACTOR_GET, FirstFactorGet.default(vars)); + + app.get(Endpoints.SECOND_FACTOR_GET, + RequireTwoFactorEnabled.middleware(vars.logger, + vars.config.authentication_methods), + RequireValidatedFirstFactor.middleware(vars.logger), + SecondFactorGet.default(vars)); + app.get(Endpoints.LOGOUT_GET, LogoutGet.default); + + app.get(Endpoints.VERIFY_GET, VerifyGet.default(vars)); + app.post(Endpoints.FIRST_FACTOR_POST, FirstFactorPost.default(vars)); + + setupTotp(app, vars); + setupU2f(app, vars); + setupResetPassword(app, vars); + setupErrors(app); + + app.get(Endpoints.LOGGED_IN, LoggedIn.default(vars)); + } +} diff --git a/server/src/lib/web_server/middlewares/RequireTwoFactorEnabled.ts b/server/src/lib/web_server/middlewares/RequireTwoFactorEnabled.ts new file mode 100644 index 000000000..c97b57d99 --- /dev/null +++ b/server/src/lib/web_server/middlewares/RequireTwoFactorEnabled.ts @@ -0,0 +1,27 @@ +import Express = require("express"); +import BluebirdPromise = require("bluebird"); +import ErrorReplies = require("../../ErrorReplies"); +import { IRequestLogger } from "../../logging/IRequestLogger"; +import { MethodCalculator } from "../../authentication/MethodCalculator"; +import { AuthenticationMethodsConfiguration } from + "../../configuration/Configuration"; + +export class RequireTwoFactorEnabled { + static middleware(logger: IRequestLogger, + configuration: AuthenticationMethodsConfiguration) { + + return function (req: Express.Request, res: Express.Response, + next: Express.NextFunction): void { + + const isSingleFactorMode = MethodCalculator.isSingleFactorOnlyMode( + configuration); + + if (isSingleFactorMode) { + ErrorReplies.replyWithError401(req, res, logger)(new Error( + "Restricted access because server is in single factor mode.")); + return; + } + next(); + }; + } +} \ No newline at end of file diff --git a/server/src/lib/web_server/middlewares/RequireValidatedFirstFactor.ts b/server/src/lib/web_server/middlewares/RequireValidatedFirstFactor.ts new file mode 100644 index 000000000..3a7af1548 --- /dev/null +++ b/server/src/lib/web_server/middlewares/RequireValidatedFirstFactor.ts @@ -0,0 +1,26 @@ +import Express = require("express"); +import BluebirdPromise = require("bluebird"); +import ErrorReplies = require("../../ErrorReplies"); +import { IRequestLogger } from "../../logging/IRequestLogger"; +import { AuthenticationSessionHandler } from "../../AuthenticationSessionHandler"; +import Exceptions = require("../../Exceptions"); + +export class RequireValidatedFirstFactor { + static middleware(logger: IRequestLogger) { + return function (req: Express.Request, res: Express.Response, + next: Express.NextFunction): BluebirdPromise { + + return new BluebirdPromise(function (resolve, reject) { + const authSession = AuthenticationSessionHandler.get(req, logger); + if (!authSession.userid || !authSession.first_factor) + return reject( + new Exceptions.FirstFactorValidationError( + "First factor has not been validated yet.")); + + next(); + resolve(); + }) + .catch(ErrorReplies.replyWithError401(req, res, logger)); + }; + } +} \ No newline at end of file diff --git a/server/src/lib/web_server/middlewares/WithHeadersLogged.ts b/server/src/lib/web_server/middlewares/WithHeadersLogged.ts new file mode 100644 index 000000000..139db1141 --- /dev/null +++ b/server/src/lib/web_server/middlewares/WithHeadersLogged.ts @@ -0,0 +1,12 @@ +import Express = require("express"); +import { IRequestLogger } from "../../logging/IRequestLogger"; + +export class WithHeadersLogged { + static middleware(logger: IRequestLogger) { + return function (req: Express.Request, res: Express.Response, + next: Express.NextFunction): void { + logger.debug(req, "Headers = %s", JSON.stringify(req.headers)); + next(); + }; + } +} \ No newline at end of file diff --git a/server/test/AuthenticationMethodCalculator.test.ts b/server/test/AuthenticationMethodCalculator.test.ts deleted file mode 100644 index 5c51d3359..000000000 --- a/server/test/AuthenticationMethodCalculator.test.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { AuthenticationMethodCalculator } from "../src/lib/AuthenticationMethodCalculator"; -import { AuthenticationMethodsConfiguration } from "../src/lib/configuration/Configuration"; -import Assert = require("assert"); - -describe("test authentication method calculator", function() { - it("should return default method when sub domain not overriden", function() { - const options1: AuthenticationMethodsConfiguration = { - default_method: "two_factor", - per_subdomain_methods: {} - }; - const options2: AuthenticationMethodsConfiguration = { - default_method: "single_factor", - per_subdomain_methods: {} - }; - const calculator1 = new AuthenticationMethodCalculator(options1); - const calculator2 = new AuthenticationMethodCalculator(options2); - Assert.equal(calculator1.compute("www.example.com"), "two_factor"); - Assert.equal(calculator2.compute("www.example.com"), "single_factor"); - }); - - it("should return overridden method when sub domain method is defined", function() { - const options1: AuthenticationMethodsConfiguration = { - default_method: "two_factor", - per_subdomain_methods: { - "www.example.com": "single_factor" - } - }; - const calculator1 = new AuthenticationMethodCalculator(options1); - Assert.equal(calculator1.compute("www.example.com"), "single_factor"); - }); -}); diff --git a/server/test/IdentityCheckMiddleware.test.ts b/server/test/IdentityCheckMiddleware.test.ts index eac778243..1f43a6542 100644 --- a/server/test/IdentityCheckMiddleware.test.ts +++ b/server/test/IdentityCheckMiddleware.test.ts @@ -1,7 +1,7 @@ import sinon = require("sinon"); import IdentityValidator = require("../src/lib/IdentityCheckMiddleware"); -import AuthenticationSessionHandler = require("../src/lib/AuthenticationSession"); +import { AuthenticationSessionHandler } from "../src/lib/AuthenticationSessionHandler"; import { AuthenticationSession } from "../types/AuthenticationSession"; import { UserDataStore } from "../src/lib/storage/UserDataStore"; import exceptions = require("../src/lib/Exceptions"); @@ -155,14 +155,10 @@ describe("test identity check process", function () { req.query.identity_token = "token"; req.session = {}; - let authSession: AuthenticationSession; + const authSession: AuthenticationSession = AuthenticationSessionHandler.get(req as any, vars.logger); const callback = IdentityValidator.get_finish_validation(identityValidable, vars); - return AuthenticationSessionHandler.get(req as any, vars.logger) - .then(function (_authSession) { - authSession = _authSession; - return callback(req as any, res as any, undefined); - }) + return callback(req as any, res as any, undefined) .then(function () { return BluebirdPromise.reject("Should fail"); }) .catch(function () { Assert.equal(authSession.identity_check.userid, "user"); diff --git a/server/test/authentication/MethodCalculator.test.ts b/server/test/authentication/MethodCalculator.test.ts new file mode 100644 index 000000000..45b3ee2f8 --- /dev/null +++ b/server/test/authentication/MethodCalculator.test.ts @@ -0,0 +1,74 @@ +import { MethodCalculator } + from "../../src/lib/authentication/MethodCalculator"; +import { AuthenticationMethodsConfiguration } + from "../../src/lib/configuration/Configuration"; +import Assert = require("assert"); + +describe("test MethodCalculator", function () { + describe("test compute method", function () { + it("should return default method when sub domain not overriden", + function () { + const options1: AuthenticationMethodsConfiguration = { + default_method: "two_factor", + per_subdomain_methods: {} + }; + const options2: AuthenticationMethodsConfiguration = { + default_method: "single_factor", + per_subdomain_methods: {} + }; + Assert.equal(MethodCalculator.compute(options1, "www.example.com"), + "two_factor"); + Assert.equal(MethodCalculator.compute(options2, "www.example.com"), + "single_factor"); + }); + + it("should return overridden method when sub domain method is defined", + function () { + const options1: AuthenticationMethodsConfiguration = { + default_method: "two_factor", + per_subdomain_methods: { + "www.example.com": "single_factor" + } + }; + Assert.equal(MethodCalculator.compute(options1, "www.example.com"), + "single_factor"); + Assert.equal(MethodCalculator.compute(options1, "home.example.com"), + "two_factor"); + }); + }); + + describe("test isSingleFactorOnlyMode method", function () { + it("should return true when default domains and all domains are single_factor", + function () { + const options: AuthenticationMethodsConfiguration = { + default_method: "single_factor", + per_subdomain_methods: { + "www.example.com": "single_factor" + } + }; + Assert(MethodCalculator.isSingleFactorOnlyMode(options)); + }); + + it("should return false when default domains is single_factor and at least one sub-domain is two_factor", function () { + const options: AuthenticationMethodsConfiguration = { + default_method: "single_factor", + per_subdomain_methods: { + "www.example.com": "two_factor", + "home.example.com": "single_factor" + } + }; + Assert(!MethodCalculator.isSingleFactorOnlyMode(options)); + }); + + it("should return false when default domains is two_factor", function () { + const options: AuthenticationMethodsConfiguration = { + default_method: "two_factor", + per_subdomain_methods: { + "www.example.com": "single_factor", + "home.example.com": "single_factor" + } + }; + Assert(!MethodCalculator.isSingleFactorOnlyMode(options)); + }); + }); +}); \ No newline at end of file diff --git a/server/test/configuration/Validator.test.ts b/server/test/configuration/Validator.test.ts index a797a12f5..f08a0d2e4 100644 --- a/server/test/configuration/Validator.test.ts +++ b/server/test/configuration/Validator.test.ts @@ -28,7 +28,7 @@ describe("test validator", function () { "data.regulation should have required property 'max_retries'", "data.session should have required property 'secret'", "Storage must be either 'local' or 'mongo'", - "Notifier must be either 'filesystem', 'email' or 'smtp'" + "A notifier needs to be declared when server is used with two-factor" ]); Assert.deepStrictEqual(Validator.isValid({ @@ -54,7 +54,7 @@ describe("test validator", function () { } }), [ "data.storage has unknown key 'abc'", - "data.notifier has unknown key 'abcd'" + "Notifier must be either 'filesystem', 'email' or 'smtp'" ]); }); @@ -89,4 +89,93 @@ describe("test validator", function () { } }), []); }); + + it("should return false when notifier is not defined while there is at least \ +one second factor enabled sub-domain", function () { + const options1 = { + ldap: { + base_dn: "dc=example,dc=com", + password: "password", + url: "ldap://ldap", + user: "user" + }, + authentication_methods: { + default_method: "two_factor" + }, + notifier: {}, + regulation: { + ban_time: 120, + find_time: 30, + max_retries: 3 + }, + session: { + secret: "unsecure_secret" + }, + storage: { + local: { + path: "/var/lib/authelia" + } + } + }; + const options2 = { + ldap: { + base_dn: "dc=example,dc=com", + password: "password", + url: "ldap://ldap", + user: "user" + }, + authentication_methods: { + default_method: "two_factor" + }, + notifier: { + email: { + username: "user@gmail.com", + password: "pass", + sender: "admin@example.com", + service: "gmail" + } + }, + regulation: { + ban_time: 120, + find_time: 30, + max_retries: 3 + }, + session: { + secret: "unsecure_secret" + }, + storage: { + local: { + path: "/var/lib/authelia" + } + } + }; + const options3 = { + ldap: { + base_dn: "dc=example,dc=com", + password: "password", + url: "ldap://ldap", + user: "user" + }, + authentication_methods: { + default_method: "single_factor" + }, + notifier: {}, + regulation: { + ban_time: 120, + find_time: 30, + max_retries: 3 + }, + session: { + secret: "unsecure_secret" + }, + storage: { + local: { + path: "/var/lib/authelia" + } + } + }; + Assert.deepStrictEqual(Validator.isValid(options1), ["A notifier needs to be declared when server is used with two-factor"]); + Assert.deepStrictEqual(Validator.isValid(options2), []); + Assert.deepStrictEqual(Validator.isValid(options3), []); + }); }); \ No newline at end of file diff --git a/server/test/mocks/RequestLoggerStub.ts b/server/test/mocks/RequestLoggerStub.ts index 3f4a6c168..9326ebfcc 100644 --- a/server/test/mocks/RequestLoggerStub.ts +++ b/server/test/mocks/RequestLoggerStub.ts @@ -1,26 +1,38 @@ import { IRequestLogger } from "../../src/lib/logging/IRequestLogger"; import Sinon = require("sinon"); +import { RequestLogger } from "../../src/lib/logging/RequestLogger"; +import Winston = require("winston"); +import Express = require("express"); export class RequestLoggerStub implements IRequestLogger { infoStub: Sinon.SinonStub; debugStub: Sinon.SinonStub; errorStub: Sinon.SinonStub; + private requestLogger: RequestLogger; - constructor() { + constructor(enableLogging?: boolean) { this.infoStub = Sinon.stub(); this.debugStub = Sinon.stub(); this.errorStub = Sinon.stub(); + if (enableLogging) + this.requestLogger = new RequestLogger(Winston); } info(req: Express.Request, message: string, ...args: any[]): void { - return this.infoStub(req, message, ...args); + if (this.requestLogger) + this.requestLogger.info(req, message, ...args); + this.infoStub(req, message, ...args); } debug(req: Express.Request, message: string, ...args: any[]): void { - return this.debugStub(req, message, ...args); + if (this.requestLogger) + this.requestLogger.info(req, message, ...args); + this.debugStub(req, message, ...args); } error(req: Express.Request, message: string, ...args: any[]): void { - return this.errorStub(req, message, ...args); + if (this.requestLogger) + this.requestLogger.info(req, message, ...args); + this.errorStub(req, message, ...args); } } \ No newline at end of file diff --git a/server/test/mocks/ServerVariablesMockBuilder.ts b/server/test/mocks/ServerVariablesMockBuilder.ts index accfb0652..b88727216 100644 --- a/server/test/mocks/ServerVariablesMockBuilder.ts +++ b/server/test/mocks/ServerVariablesMockBuilder.ts @@ -27,7 +27,7 @@ export interface ServerVariablesMock { } export class ServerVariablesMockBuilder { - static build(): { variables: ServerVariables, mocks: ServerVariablesMock} { + static build(enableLogging?: boolean): { variables: ServerVariables, mocks: ServerVariablesMock} { const mocks: ServerVariablesMock = { accessController: new AccessControllerStub(), config: { @@ -62,7 +62,7 @@ export class ServerVariablesMockBuilder { ldapAuthenticator: new AuthenticatorStub(), ldapEmailsRetriever: new EmailsRetrieverStub(), ldapPasswordUpdater: new PasswordUpdaterStub(), - logger: new RequestLoggerStub(), + logger: new RequestLoggerStub(enableLogging), notifier: new NotifierStub(), regulator: new RegulatorStub(), totpHandler: new TotpHandlerStub(), diff --git a/server/test/mocks/express.ts b/server/test/mocks/express.ts index b2adda983..c60240fa5 100644 --- a/server/test/mocks/express.ts +++ b/server/test/mocks/express.ts @@ -53,7 +53,11 @@ export function RequestMock(): RequestMock { return { app: { get: sinon.stub() - } + }, + headers: { + "x-forwarded-for": "127.0.0.1" + }, + session: {} }; } export function ResponseMock(): ResponseMock { diff --git a/server/test/routes/firstfactor/post.test.ts b/server/test/routes/firstfactor/post.test.ts index 9648ad1fd..55f57ef49 100644 --- a/server/test/routes/firstfactor/post.test.ts +++ b/server/test/routes/firstfactor/post.test.ts @@ -4,7 +4,7 @@ import BluebirdPromise = require("bluebird"); import Assert = require("assert"); import FirstFactorPost = require("../../../src/lib/routes/firstfactor/post"); import exceptions = require("../../../src/lib/Exceptions"); -import AuthenticationSessionHandler = require("../../../src/lib/AuthenticationSession"); +import { AuthenticationSessionHandler } from "../../../src/lib/AuthenticationSessionHandler"; import { AuthenticationSession } from "../../../types/AuthenticationSession"; import Endpoints = require("../../../../shared/api"); import AuthenticationRegulatorMock = require("../../mocks/AuthenticationRegulator"); @@ -20,6 +20,7 @@ describe("test the first factor validation route", function () { let groups: string[]; let vars: ServerVariables; let mocks: ServerVariablesMock; + let authSession: AuthenticationSession; beforeEach(function () { emails = ["test_ok@example.com"]; @@ -48,6 +49,7 @@ describe("test the first factor validation route", function () { }; res = ExpressMock.ResponseMock(); + authSession = AuthenticationSessionHandler.get(req as any, vars.logger); }); it("should reply with 204 if success", function () { @@ -56,12 +58,7 @@ describe("test the first factor validation route", function () { emails: emails, groups: groups })); - let authSession: AuthenticationSession; - return AuthenticationSessionHandler.get(req as any, vars.logger) - .then(function (_authSession) { - authSession = _authSession; - return FirstFactorPost.default(vars)(req as any, res as any); - }) + return FirstFactorPost.default(vars)(req as any, res as any) .then(function () { Assert.equal("username", authSession.userid); Assert(res.send.calledOnce); @@ -76,18 +73,13 @@ describe("test the first factor validation route", function () { it("should set first email address as user session variable", function () { const emails = ["test_ok@example.com"]; - let authSession: AuthenticationSession; mocks.ldapAuthenticator.authenticateStub.withArgs("username", "password") .returns(BluebirdPromise.resolve({ emails: emails, groups: groups })); - return AuthenticationSessionHandler.get(req as any, vars.logger) - .then(function (_authSession) { - authSession = _authSession; - return FirstFactorPost.default(vars)(req as any, res as any); - }) + return FirstFactorPost.default(vars)(req as any, res as any) .then(function () { Assert.equal("test_ok@example.com", authSession.email); }); diff --git a/server/test/routes/password-reset/post.test.ts b/server/test/routes/password-reset/post.test.ts index ed1d45b27..31055b44c 100644 --- a/server/test/routes/password-reset/post.test.ts +++ b/server/test/routes/password-reset/post.test.ts @@ -1,7 +1,8 @@ import PasswordResetFormPost = require("../../../src/lib/routes/password-reset/form/post"); import { PasswordUpdater } from "../../../src/lib/ldap/PasswordUpdater"; -import AuthenticationSessionHandler = require("../../../src/lib/AuthenticationSession"); +import { AuthenticationSessionHandler } from "../../../src/lib/AuthenticationSessionHandler"; +import { AuthenticationSession } from "../../../types/AuthenticationSession"; import { UserDataStore } from "../../../src/lib/storage/UserDataStore"; import Sinon = require("sinon"); import Assert = require("assert"); @@ -15,6 +16,7 @@ describe("test reset password route", function () { let res: ExpressMock.ResponseMock; let vars: ServerVariables; let mocks: ServerVariablesMock; + let authSession: AuthenticationSession; beforeEach(function () { req = { @@ -53,13 +55,11 @@ describe("test reset password route", function () { }; res = ExpressMock.ResponseMock(); - AuthenticationSessionHandler.get(req as any, vars.logger) - .then(function (authSession) { - authSession.userid = "user"; - authSession.email = "user@example.com"; - authSession.first_factor = true; - authSession.second_factor = false; - }); + authSession = AuthenticationSessionHandler.get(req as any, vars.logger); + authSession.userid = "user"; + authSession.email = "user@example.com"; + authSession.first_factor = true; + authSession.second_factor = false; }); describe("test reset password post", () => { @@ -69,14 +69,11 @@ describe("test reset password route", function () { mocks.ldapPasswordUpdater.updatePasswordStub.returns(BluebirdPromise.resolve()); - return AuthenticationSessionHandler.get(req as any, vars.logger) - .then(function (authSession) { - authSession.identity_check = { - userid: "user", - challenge: "reset-password" - }; - return PasswordResetFormPost.default(vars)(req as any, res as any); - }) + authSession.identity_check = { + userid: "user", + challenge: "reset-password" + }; + return PasswordResetFormPost.default(vars)(req as any, res as any) .then(function () { return AuthenticationSessionHandler.get(req as any, vars.logger); }).then(function (_authSession) { @@ -88,14 +85,11 @@ describe("test reset password route", function () { }); it("should fail if identity_challenge does not exist", function () { - return AuthenticationSessionHandler.get(req as any, vars.logger) - .then(function (authSession) { - authSession.identity_check = { - userid: "user", - challenge: undefined - }; - return PasswordResetFormPost.default(vars)(req as any, res as any); - }) + authSession.identity_check = { + userid: "user", + challenge: undefined + }; + return PasswordResetFormPost.default(vars)(req as any, res as any) .then(function () { Assert.equal(res.status.getCall(0).args[0], 200); Assert.deepEqual(res.send.getCall(0).args[0], { @@ -111,14 +105,12 @@ describe("test reset password route", function () { mocks.ldapPasswordUpdater.updatePasswordStub .returns(BluebirdPromise.reject("Internal error with LDAP")); - return AuthenticationSessionHandler.get(req as any, vars.logger) - .then(function (authSession) { - authSession.identity_check = { - challenge: "reset-password", - userid: "user" - }; - return PasswordResetFormPost.default(vars)(req as any, res as any); - }).then(function () { + authSession.identity_check = { + challenge: "reset-password", + userid: "user" + }; + return PasswordResetFormPost.default(vars)(req as any, res as any) + .then(function () { Assert.equal(res.status.getCall(0).args[0], 200); Assert.deepEqual(res.send.getCall(0).args[0], { error: "An error occurred during password reset. Your password has not been changed." diff --git a/server/test/routes/secondfactor/get.test.ts b/server/test/routes/secondfactor/get.test.ts new file mode 100644 index 000000000..7f6370964 --- /dev/null +++ b/server/test/routes/secondfactor/get.test.ts @@ -0,0 +1,64 @@ +import SecondFactorGet from "../../../src/lib/routes/secondfactor/get"; +import { ServerVariablesMockBuilder, ServerVariablesMock } + from "../../mocks/ServerVariablesMockBuilder"; +import { ServerVariables } from "../../../src/lib/ServerVariables"; +import Sinon = require("sinon"); +import ExpressMock = require("../../mocks/express"); +import Assert = require("assert"); +import Endpoints = require("../../../../shared/api"); +import BluebirdPromise = require("bluebird"); + +describe("test second factor GET endpoint handler", function () { + let mocks: ServerVariablesMock; + let vars: ServerVariables; + let req: ExpressMock.RequestMock; + let res: ExpressMock.ResponseMock; + + beforeEach(function () { + const s = ServerVariablesMockBuilder.build(true); + mocks = s.mocks; + vars = s.variables; + + req = ExpressMock.RequestMock(); + res = ExpressMock.ResponseMock(); + + req.session = { + auth: { + userid: "user", + first_factor: true, + second_factor: false + } + }; + }); + + describe("test redirection", function () { + it("should redirect to already logged in page if server is in single_factor mode", function () { + vars.config.authentication_methods.default_method = "single_factor"; + return SecondFactorGet(vars)(req as any, res as any) + .then(function () { + Assert(res.redirect.calledWith(Endpoints.LOGGED_IN)); + return BluebirdPromise.resolve(); + }); + }); + + it("should redirect to already logged in page if user already authenticated", function () { + vars.config.authentication_methods.default_method = "two_factor"; + req.session.auth.second_factor = true; + return SecondFactorGet(vars)(req as any, res as any) + .then(function () { + Assert(res.redirect.calledWith(Endpoints.LOGGED_IN)); + return BluebirdPromise.resolve(); + }); + }); + + it("should render second factor page", function () { + vars.config.authentication_methods.default_method = "two_factor"; + req.session.auth.second_factor = false; + return SecondFactorGet(vars)(req as any, res as any) + .then(function () { + Assert(res.render.calledWith("secondfactor")); + return BluebirdPromise.resolve(); + }); + }); + }); +}); \ No newline at end of file diff --git a/server/test/routes/secondfactor/totp/register/RegistrationHandler.test.ts b/server/test/routes/secondfactor/totp/register/RegistrationHandler.test.ts index 436c9fe38..37192702f 100644 --- a/server/test/routes/secondfactor/totp/register/RegistrationHandler.test.ts +++ b/server/test/routes/secondfactor/totp/register/RegistrationHandler.test.ts @@ -2,7 +2,6 @@ import Sinon = require("sinon"); import winston = require("winston"); import RegistrationHandler from "../../../../../src/lib/routes/secondfactor/totp/identity/RegistrationHandler"; import { Identity } from "../../../../../types/Identity"; -import AuthenticationSession = require("../../../../../src/lib/AuthenticationSession"); import { UserDataStore } from "../../../../../src/lib/storage/UserDataStore"; import assert = require("assert"); import BluebirdPromise = require("bluebird"); diff --git a/server/test/routes/secondfactor/totp/sign/post.test.ts b/server/test/routes/secondfactor/totp/sign/post.test.ts index 1cfc23e40..d6b3d9ce5 100644 --- a/server/test/routes/secondfactor/totp/sign/post.test.ts +++ b/server/test/routes/secondfactor/totp/sign/post.test.ts @@ -1,11 +1,9 @@ import BluebirdPromise = require("bluebird"); import Sinon = require("sinon"); -import assert = require("assert"); -import winston = require("winston"); - -import exceptions = require("../../../../../src/lib/Exceptions"); -import AuthenticationSessionHandler = require("../../../../../src/lib/AuthenticationSession"); +import Assert = require("assert"); +import Exceptions = require("../../../../../src/lib/Exceptions"); +import { AuthenticationSessionHandler } from "../../../../../src/lib/AuthenticationSessionHandler"; import { AuthenticationSession } from "../../../../../types/AuthenticationSession"; import SignPost = require("../../../../../src/lib/routes/secondfactor/totp/sign/post"); import { ServerVariables } from "../../../../../src/lib/ServerVariables"; @@ -27,9 +25,7 @@ describe("test totp route", function () { mocks = s.mocks; const app_get = Sinon.stub(); req = { - app: { - get: Sinon.stub().returns({ logger: winston }) - }, + app: {}, body: { token: "abc" }, @@ -47,13 +43,10 @@ describe("test totp route", function () { } }; mocks.userDataStore.retrieveTOTPSecretStub.returns(BluebirdPromise.resolve(doc)); - return AuthenticationSessionHandler.get(req as any, vars.logger) - .then(function (_authSession) { - authSession = _authSession; - authSession.userid = "user"; - authSession.first_factor = true; - authSession.second_factor = false; - }); + authSession = AuthenticationSessionHandler.get(req as any, vars.logger); + authSession.userid = "user"; + authSession.first_factor = true; + authSession.second_factor = false; }); @@ -61,7 +54,7 @@ describe("test totp route", function () { mocks.totpHandler.validateStub.returns(true); return SignPost.default(vars)(req as any, res as any) .then(function () { - assert.equal(true, authSession.second_factor); + Assert.equal(true, authSession.second_factor); return BluebirdPromise.resolve(); }); }); @@ -70,24 +63,13 @@ describe("test totp route", function () { mocks.totpHandler.validateStub.returns(false); return SignPost.default(vars)(req as any, res as any) .then(function () { - assert.equal(false, authSession.second_factor); - assert.equal(res.status.getCall(0).args[0], 200); - assert.deepEqual(res.send.getCall(0).args[0], { + Assert.equal(false, authSession.second_factor); + Assert.equal(res.status.getCall(0).args[0], 200); + Assert.deepEqual(res.send.getCall(0).args[0], { error: "Operation failed." }); return BluebirdPromise.resolve(); }); }); - - it("should send status code 401 when session has not been initiated", function () { - mocks.totpHandler.validateStub.returns(true); - req.session = {}; - return SignPost.default(vars)(req as any, res as any) - .then(function () { return BluebirdPromise.reject(new Error("It should fail")); }) - .catch(function () { - assert.equal(401, res.status.getCall(0).args[0]); - return BluebirdPromise.resolve(); - }); - }); }); diff --git a/server/test/routes/secondfactor/u2f/identity/RegistrationHandler.test.ts b/server/test/routes/secondfactor/u2f/identity/RegistrationHandler.test.ts index ce0de7d63..d10268f34 100644 --- a/server/test/routes/secondfactor/u2f/identity/RegistrationHandler.test.ts +++ b/server/test/routes/secondfactor/u2f/identity/RegistrationHandler.test.ts @@ -1,18 +1,15 @@ -import sinon = require("sinon"); -import winston = require("winston"); -import assert = require("assert"); +import Sinon = require("sinon"); +import Assert = require("assert"); import BluebirdPromise = require("bluebird"); import { Identity } from "../../../../../types/Identity"; import RegistrationHandler from "../../../../../src/lib/routes/secondfactor/u2f/identity/RegistrationHandler"; -import AuthenticationSession = require("../../../../../src/lib/AuthenticationSession"); - import ExpressMock = require("../../../../mocks/express"); import { UserDataStoreStub } from "../../../../mocks/storage/UserDataStoreStub"; import { ServerVariablesMock, ServerVariablesMockBuilder } from "../../../../mocks/ServerVariablesMockBuilder"; import { ServerVariables } from "../../../../../src/lib/ServerVariables"; -describe("test register handler", function () { +describe("test U2F register handler", function () { let req: ExpressMock.RequestMock; let res: ExpressMock.ResponseMock; let mocks: ServerVariablesMock; @@ -46,9 +43,9 @@ describe("test register handler", function () { mocks.userDataStore.consumeIdentityValidationTokenStub.returns(BluebirdPromise.resolve({})); res = ExpressMock.ResponseMock(); - res.send = sinon.spy(); - res.json = sinon.spy(); - res.status = sinon.spy(); + res.send = Sinon.spy(); + res.json = Sinon.spy(); + res.status = Sinon.spy(); }); describe("test u2f registration check", test_registration_check); @@ -63,31 +60,37 @@ describe("test register handler", function () { }); }); - it("should fail if userid is missing", function (done) { + it("should fail if userid is missing", function () { req.session.auth.first_factor = false; req.session.auth.userid = undefined; - new RegistrationHandler(vars.logger).preValidationInit(req as any) - .catch(function (err: Error) { - done(); + return new RegistrationHandler(vars.logger).preValidationInit(req as any) + .then(function () { + return BluebirdPromise.reject(new Error("should not be here")); + }, + function (err: Error) { + return BluebirdPromise.resolve(); }); }); - it("should fail if email is missing", function (done) { + it("should fail if email is missing", function () { req.session.auth.first_factor = false; req.session.auth.email = undefined; - new RegistrationHandler(vars.logger).preValidationInit(req as any) - .catch(function (err: Error) { - done(); + return new RegistrationHandler(vars.logger).preValidationInit(req as any) + .then(function () { + return BluebirdPromise.reject(new Error("should not be here")); + }, + function (err: Error) { + return BluebirdPromise.resolve(); }); }); - it("should succeed if first factor passed, userid and email are provided", function (done) { - new RegistrationHandler(vars.logger).preValidationInit(req as any) - .then(function (identity: Identity) { - done(); - }); + it("should succeed if first factor passed, userid and email are provided", function () { + req.session.auth.first_factor = true; + req.session.auth.email = "admin@example.com"; + req.session.auth.userid = "user"; + return new RegistrationHandler(vars.logger).preValidationInit(req as any); }); } }); diff --git a/server/test/routes/secondfactor/u2f/register/post.test.ts b/server/test/routes/secondfactor/u2f/register/post.test.ts index 8425cc7ec..88fc5ad4d 100644 --- a/server/test/routes/secondfactor/u2f/register/post.test.ts +++ b/server/test/routes/secondfactor/u2f/register/post.test.ts @@ -3,7 +3,8 @@ import sinon = require("sinon"); import BluebirdPromise = require("bluebird"); import assert = require("assert"); import U2FRegisterPost = require("../../../../../src/lib/routes/secondfactor/u2f/register/post"); -import AuthenticationSession = require("../../../../../src/lib/AuthenticationSession"); +import { AuthenticationSessionHandler } from "../../../../../src/lib/AuthenticationSessionHandler"; +import { AuthenticationSession } from "../../../../../types/AuthenticationSession"; import ExpressMock = require("../../../../mocks/express"); import { UserDataStoreStub } from "../../../../mocks/storage/UserDataStoreStub"; import { ServerVariablesMockBuilder, ServerVariablesMock } from "../../../../mocks/ServerVariablesMockBuilder"; @@ -15,6 +16,7 @@ describe("test u2f routes: register", function () { let res: ExpressMock.ResponseMock; let mocks: ServerVariablesMock; let vars: ServerVariables; + let authSession: AuthenticationSession; beforeEach(function () { req = ExpressMock.RequestMock(); @@ -48,6 +50,8 @@ describe("test u2f routes: register", function () { res.send = sinon.spy(); res.json = sinon.spy(); res.status = sinon.spy(); + + authSession = AuthenticationSessionHandler.get(req as any, vars.logger); }); describe("test registration", test_registration); @@ -62,20 +66,14 @@ describe("test u2f routes: register", function () { }; mocks.u2f.checkRegistrationStub.returns(BluebirdPromise.resolve(expectedStatus)); - return AuthenticationSession.get(req as any, vars.logger) - .then(function (authSession) { - authSession.register_request = { - appId: "app", - challenge: "challenge", - keyHandle: "key", - version: "U2F_V2" - }; - return U2FRegisterPost.default(vars)(req as any, res as any); - }) + authSession.register_request = { + appId: "app", + challenge: "challenge", + keyHandle: "key", + version: "U2F_V2" + }; + return U2FRegisterPost.default(vars)(req as any, res as any) .then(function () { - return AuthenticationSession.get(req as any, vars.logger); - }) - .then(function (authSession) { assert.equal("user", mocks.userDataStore.saveU2FRegistrationStub.getCall(0).args[0]); assert.equal(authSession.identity_check, undefined); }); @@ -84,17 +82,14 @@ describe("test u2f routes: register", function () { it("should return error message on finishRegistration error", function () { mocks.u2f.checkRegistrationStub.returns({ errorCode: 500 }); - return AuthenticationSession.get(req as any, vars.logger) - .then(function (authSession) { - authSession.register_request = { - appId: "app", - challenge: "challenge", - keyHandle: "key", - version: "U2F_V2" - }; + authSession.register_request = { + appId: "app", + challenge: "challenge", + keyHandle: "key", + version: "U2F_V2" + }; - return U2FRegisterPost.default(vars)(req as any, res as any); - }) + return U2FRegisterPost.default(vars)(req as any, res as any) .then(function () { return BluebirdPromise.reject(new Error("It should fail")); }) .catch(function () { assert.equal(200, res.status.getCall(0).args[0]); @@ -107,11 +102,8 @@ describe("test u2f routes: register", function () { it("should return error message when register_request is not provided", function () { mocks.u2f.checkRegistrationStub.returns(BluebirdPromise.resolve()); - return AuthenticationSession.get(req as any, vars.logger) - .then(function (authSession) { - authSession.register_request = undefined; - return U2FRegisterPost.default(vars)(req as any, res as any); - }) + authSession.register_request = undefined; + return U2FRegisterPost.default(vars)(req as any, res as any) .then(function () { return BluebirdPromise.reject(new Error("It should fail")); }) .catch(function () { assert.equal(200, res.status.getCall(0).args[0]); @@ -124,11 +116,8 @@ describe("test u2f routes: register", function () { it("should return error message when no auth request has been initiated", function () { mocks.u2f.checkRegistrationStub.returns(BluebirdPromise.resolve()); - return AuthenticationSession.get(req as any, vars.logger) - .then(function (authSession) { - authSession.register_request = undefined; - return U2FRegisterPost.default(vars)(req as any, res as any); - }) + authSession.register_request = undefined; + return U2FRegisterPost.default(vars)(req as any, res as any) .then(function () { return BluebirdPromise.reject(new Error("It should fail")); }) .catch(function () { assert.equal(200, res.status.getCall(0).args[0]); @@ -140,11 +129,8 @@ describe("test u2f routes: register", function () { }); it("should return error message when identity has not been verified", function () { - return AuthenticationSession.get(req as any, vars.logger) - .then(function (authSession) { - authSession.identity_check = undefined; - return U2FRegisterPost.default(vars)(req as any, res as any); - }) + authSession.identity_check = undefined; + return U2FRegisterPost.default(vars)(req as any, res as any) .then(function () { return BluebirdPromise.reject(new Error("It should fail")); }) .catch(function () { assert.equal(200, res.status.getCall(0).args[0]); diff --git a/server/test/routes/secondfactor/u2f/register_request/get.test.ts b/server/test/routes/secondfactor/u2f/register_request/get.test.ts index 90db3af69..c2f5dcb49 100644 --- a/server/test/routes/secondfactor/u2f/register_request/get.test.ts +++ b/server/test/routes/secondfactor/u2f/register_request/get.test.ts @@ -3,7 +3,6 @@ import sinon = require("sinon"); import BluebirdPromise = require("bluebird"); import Assert = require("assert"); import U2FRegisterRequestGet = require("../../../../../src/lib/routes/secondfactor/u2f/register_request/get"); -import AuthenticationSession = require("../../../../../src/lib/AuthenticationSession"); import ExpressMock = require("../../../../mocks/express"); import { UserDataStoreStub } from "../../../../mocks/storage/UserDataStoreStub"; import { ServerVariablesMockBuilder, ServerVariablesMock } from "../../../../mocks/ServerVariablesMockBuilder"; diff --git a/server/test/routes/secondfactor/u2f/sign/post.test.ts b/server/test/routes/secondfactor/u2f/sign/post.test.ts index 855112c74..e84e30a79 100644 --- a/server/test/routes/secondfactor/u2f/sign/post.test.ts +++ b/server/test/routes/secondfactor/u2f/sign/post.test.ts @@ -3,7 +3,6 @@ import sinon = require("sinon"); import BluebirdPromise = require("bluebird"); import Assert = require("assert"); import U2FSignPost = require("../../../../../src/lib/routes/secondfactor/u2f/sign/post"); -import AuthenticationSession = require("../../../../../src/lib/AuthenticationSession"); import { ServerVariables } from "../../../../../src/lib/ServerVariables"; import winston = require("winston"); diff --git a/server/test/routes/secondfactor/u2f/sign_request/get.test.ts b/server/test/routes/secondfactor/u2f/sign_request/get.test.ts index 2493d8c8a..0bf2ab190 100644 --- a/server/test/routes/secondfactor/u2f/sign_request/get.test.ts +++ b/server/test/routes/secondfactor/u2f/sign_request/get.test.ts @@ -3,8 +3,6 @@ import sinon = require("sinon"); import BluebirdPromise = require("bluebird"); import assert = require("assert"); import U2FSignRequestGet = require("../../../../../src/lib/routes/secondfactor/u2f/sign_request/get"); -import AuthenticationSessionHandler = require("../../../../../src/lib/AuthenticationSession"); -import { AuthenticationSession } from "../../../../../types/AuthenticationSession"; import ExpressMock = require("../../../../mocks/express"); import { UserDataStoreStub } from "../../../../mocks/storage/UserDataStoreStub"; import U2FMock = require("../../../../mocks/u2f"); diff --git a/server/test/routes/verify/get.test.ts b/server/test/routes/verify/get.test.ts index ea190dc18..f248ab3b7 100644 --- a/server/test/routes/verify/get.test.ts +++ b/server/test/routes/verify/get.test.ts @@ -1,9 +1,8 @@ import Assert = require("assert"); import VerifyGet = require("../../../src/lib/routes/verify/get"); -import AuthenticationSessionHandler = require("../../../src/lib/AuthenticationSession"); +import { AuthenticationSessionHandler } from "../../../src/lib/AuthenticationSessionHandler"; import { AuthenticationSession } from "../../../types/AuthenticationSession"; -import { AuthenticationMethodCalculator } from "../../../src/lib/AuthenticationMethodCalculator"; import { AuthenticationMethodsConfiguration } from "../../../src/lib/configuration/Configuration"; import Sinon = require("sinon"); import winston = require("winston"); @@ -18,37 +17,31 @@ describe("test /verify endpoint", function () { let res: ExpressMock.ResponseMock; let mocks: ServerVariablesMock; let vars: ServerVariables; + let authSession: AuthenticationSession; beforeEach(function () { req = ExpressMock.RequestMock(); res = ExpressMock.ResponseMock(); - req.session = {}; req.query = { redirect: "http://redirect.url" }; - req.app = { - get: Sinon.stub().returns({ logger: winston }) - }; AuthenticationSessionHandler.reset(req as any); - req.headers = {}; req.headers.host = "secret.example.com"; const s = ServerVariablesMockBuilder.build(); mocks = s.mocks; vars = s.variables; + vars.config.authentication_methods.default_method = "two_factor"; + + authSession = AuthenticationSessionHandler.get(req as any, vars.logger); }); it("should be already authenticated", function () { - req.session = {}; mocks.accessController.isAccessAllowedMock.returns(true); - AuthenticationSessionHandler.reset(req as any); - return AuthenticationSessionHandler.get(req as any, vars.logger) - .then(function (authSession) { - authSession.first_factor = true; - authSession.second_factor = true; - authSession.userid = "myuser"; - authSession.groups = ["mygroup", "othergroup"]; - return VerifyGet.default(vars)(req as express.Request, res as any); - }) + authSession.first_factor = true; + authSession.second_factor = true; + authSession.userid = "myuser"; + authSession.groups = ["mygroup", "othergroup"]; + return VerifyGet.default(vars)(req as express.Request, res as any) .then(function () { Sinon.assert.calledWithExactly(res.setHeader, "Remote-User", "myuser"); Sinon.assert.calledWithExactly(res.setHeader, "Remote-Groups", "mygroup,othergroup"); @@ -57,11 +50,7 @@ describe("test /verify endpoint", function () { }); function test_session(_authSession: AuthenticationSession, status_code: number) { - return AuthenticationSessionHandler.get(req as any, vars.logger) - .then(function (authSession) { - authSession = _authSession; - return VerifyGet.default(vars)(req as express.Request, res as any); - }) + return VerifyGet.default(vars)(req as express.Request, res as any) .then(function () { Assert.equal(status_code, res.status.getCall(0).args[0]); }); @@ -134,23 +123,20 @@ describe("test /verify endpoint", function () { }); it("should not be authenticated when domain is not allowed for user", function () { - return AuthenticationSessionHandler.get(req as any, vars.logger) - .then(function (authSession) { - authSession.first_factor = true; - authSession.second_factor = true; - authSession.userid = "myuser"; - req.headers.host = "test.example.com"; - mocks.accessController.isAccessAllowedMock.returns(false); + authSession.first_factor = true; + authSession.second_factor = true; + authSession.userid = "myuser"; + req.headers.host = "test.example.com"; + mocks.accessController.isAccessAllowedMock.returns(false); - return test_unauthorized_403({ - first_factor: true, - second_factor: true, - userid: "user", - groups: ["group1", "group2"], - email: undefined, - last_activity_datetime: new Date().getTime() - }); - }); + return test_unauthorized_403({ + first_factor: true, + second_factor: true, + userid: "user", + groups: ["group1", "group2"], + email: undefined, + last_activity_datetime: new Date().getTime() + }); }); }); }); @@ -168,12 +154,9 @@ describe("test /verify endpoint", function () { it("should be authenticated when first factor is validated and second factor is not", function () { mocks.accessController.isAccessAllowedMock.returns(true); - return AuthenticationSessionHandler.get(req as any, vars.logger) - .then(function (authSession) { - authSession.first_factor = true; - authSession.userid = "user1"; - return VerifyGet.default(vars)(req as express.Request, res as any); - }) + authSession.first_factor = true; + authSession.userid = "user1"; + return VerifyGet.default(vars)(req as express.Request, res as any) .then(function () { Assert(res.status.calledWith(204)); Assert(res.send.calledOnce); @@ -182,11 +165,8 @@ describe("test /verify endpoint", function () { it("should be rejected with 401 when first factor is not validated", function () { mocks.accessController.isAccessAllowedMock.returns(true); - return AuthenticationSessionHandler.get(req as any, vars.logger) - .then(function (authSession) { - authSession.first_factor = false; - return VerifyGet.default(vars)(req as express.Request, res as any); - }) + authSession.first_factor = false; + return VerifyGet.default(vars)(req as express.Request, res as any) .then(function () { Assert(res.status.calledWith(401)); }); @@ -199,15 +179,12 @@ describe("test /verify endpoint", function () { mocks.accessController.isAccessAllowedMock.returns(true); const currentTime = new Date().getTime() - 1000; AuthenticationSessionHandler.reset(req as any); - return AuthenticationSessionHandler.get(req as any, vars.logger) - .then(function (authSession) { - authSession.first_factor = true; - authSession.second_factor = true; - authSession.userid = "myuser"; - authSession.groups = ["mygroup", "othergroup"]; - authSession.last_activity_datetime = currentTime; - return VerifyGet.default(vars)(req as express.Request, res as any); - }) + authSession.first_factor = true; + authSession.second_factor = true; + authSession.userid = "myuser"; + authSession.groups = ["mygroup", "othergroup"]; + authSession.last_activity_datetime = currentTime; + return VerifyGet.default(vars)(req as express.Request, res as any) .then(function () { return AuthenticationSessionHandler.get(req as any, vars.logger); }) @@ -221,15 +198,12 @@ describe("test /verify endpoint", function () { mocks.accessController.isAccessAllowedMock.returns(true); const currentTime = new Date().getTime() - 1000; AuthenticationSessionHandler.reset(req as any); - return AuthenticationSessionHandler.get(req as any, vars.logger) - .then(function (authSession) { - authSession.first_factor = true; - authSession.second_factor = true; - authSession.userid = "myuser"; - authSession.groups = ["mygroup", "othergroup"]; - authSession.last_activity_datetime = currentTime; - return VerifyGet.default(vars)(req as express.Request, res as any); - }) + authSession.first_factor = true; + authSession.second_factor = true; + authSession.userid = "myuser"; + authSession.groups = ["mygroup", "othergroup"]; + authSession.last_activity_datetime = currentTime; + return VerifyGet.default(vars)(req as express.Request, res as any) .then(function () { return AuthenticationSessionHandler.get(req as any, vars.logger); }) diff --git a/server/test/server/PrivatePages.ts b/server/test/server/PrivatePages.ts deleted file mode 100644 index 18af2147f..000000000 --- a/server/test/server/PrivatePages.ts +++ /dev/null @@ -1,182 +0,0 @@ - -import Server from "../../src/lib/Server"; -import BluebirdPromise = require("bluebird"); -import speakeasy = require("speakeasy"); -import request = require("request"); -import nedb = require("nedb"); -import { GlobalDependencies } from "../../types/Dependencies"; -import { UserConfiguration } from "../../src/lib/configuration/Configuration"; -import { TOTPSecret } from "../../types/TOTPSecret"; -import U2FMock = require("./../mocks/u2f"); -import Endpoints = require("../../../shared/api"); -import Requests = require("../requests"); -import Assert = require("assert"); -import Sinon = require("sinon"); -import Winston = require("winston"); -import MockDate = require("mockdate"); -import ExpressSession = require("express-session"); -import ldapjs = require("ldapjs"); - -const requestp = BluebirdPromise.promisifyAll(request) as typeof request; - -const PORT = 8090; -const BASE_URL = "http://localhost:" + PORT; -const requests = Requests(PORT); - -describe("Private pages of the server must not be accessible without session", function () { - let server: Server; - let transporter: any; - let u2f: U2FMock.U2FMock; - - beforeEach(function () { - const config: UserConfiguration = { - port: PORT, - ldap: { - url: "ldap://127.0.0.1:389", - base_dn: "ou=users,dc=example,dc=com", - user: "cn=admin,dc=example,dc=com", - password: "password", - }, - session: { - secret: "session_secret", - expiration: 50000, - }, - regulation: { - max_retries: 3, - ban_time: 5 * 60, - find_time: 5 * 60 - }, - storage: { - local: { - in_memory: true - } - }, - notifier: { - email: { - username: "user@example.com", - password: "password", - sender: "admin@example.com", - service: "gmail" - } - } - }; - - const ldap_client = { - bind: Sinon.stub(), - search: Sinon.stub(), - modify: Sinon.stub(), - on: Sinon.spy() - }; - const ldap = { - Change: Sinon.spy(), - createClient: Sinon.spy(function () { - return ldap_client; - }) - }; - - u2f = U2FMock.U2FMock(); - - transporter = { - sendMail: Sinon.stub().yields() - }; - - const nodemailer = { - createTransport: Sinon.spy(function () { - return transporter; - }) - }; - - const ldap_document = { - object: { - mail: "test_ok@example.com", - } - }; - - const search_res = { - on: Sinon.spy(function (event: string, fn: (s: any) => void) { - if (event != "error") fn(ldap_document); - }) - }; - - ldap_client.bind.withArgs("cn=test_ok,ou=users,dc=example,dc=com", - "password").yields(undefined); - ldap_client.bind.withArgs("cn=admin,dc=example,dc=com", - "password").yields(undefined); - - ldap_client.bind.withArgs("cn=test_nok,ou=users,dc=example,dc=com", - "password").yields("error"); - - ldap_client.modify.yields(undefined); - ldap_client.search.yields(undefined, search_res); - - const deps: GlobalDependencies = { - u2f: u2f as any, - nedb: nedb, - ldapjs: ldap, - session: ExpressSession, - winston: Winston, - speakeasy: speakeasy, - ConnectRedis: Sinon.spy() - }; - - server = new Server(deps); - return server.start(config, deps); - }); - - afterEach(function () { - server.stop(); - }); - - describe("Second factor endpoints must be protected if first factor is not validated", function () { - function should_post_and_reply_with_401(url: string): BluebirdPromise { - return requestp.postAsync(url).then(function (response: request.RequestResponse) { - Assert.equal(response.statusCode, 401); - return BluebirdPromise.resolve(); - }); - } - - function should_get_and_reply_with_401(url: string): BluebirdPromise { - return requestp.getAsync(url).then(function (response: request.RequestResponse) { - Assert.equal(response.statusCode, 401); - return BluebirdPromise.resolve(); - }); - } - - it("should block " + Endpoints.SECOND_FACTOR_GET, function () { - return should_get_and_reply_with_401(BASE_URL + Endpoints.SECOND_FACTOR_GET); - }); - - it("should block " + Endpoints.SECOND_FACTOR_U2F_IDENTITY_START_GET, function () { - return should_get_and_reply_with_401(BASE_URL + Endpoints.SECOND_FACTOR_U2F_IDENTITY_START_GET); - }); - - it("should block " + Endpoints.SECOND_FACTOR_U2F_IDENTITY_FINISH_GET, function () { - return should_get_and_reply_with_401(BASE_URL + Endpoints.SECOND_FACTOR_U2F_IDENTITY_FINISH_GET + "?identity_token=dummy"); - }); - - it("should block " + Endpoints.SECOND_FACTOR_U2F_REGISTER_REQUEST_GET, function () { - return should_get_and_reply_with_401(BASE_URL + Endpoints.SECOND_FACTOR_U2F_REGISTER_REQUEST_GET); - }); - - it("should block " + Endpoints.SECOND_FACTOR_U2F_REGISTER_POST, function () { - return should_post_and_reply_with_401(BASE_URL + Endpoints.SECOND_FACTOR_U2F_REGISTER_POST); - }); - - it("should block " + Endpoints.SECOND_FACTOR_U2F_SIGN_REQUEST_GET, function () { - return should_get_and_reply_with_401(BASE_URL + Endpoints.SECOND_FACTOR_U2F_SIGN_REQUEST_GET); - }); - - it("should block " + Endpoints.SECOND_FACTOR_U2F_SIGN_POST, function () { - return should_post_and_reply_with_401(BASE_URL + Endpoints.SECOND_FACTOR_U2F_SIGN_POST); - }); - - it("should block " + Endpoints.SECOND_FACTOR_TOTP_POST, function () { - return should_post_and_reply_with_401(BASE_URL + Endpoints.SECOND_FACTOR_TOTP_POST); - }); - - it("should block " + Endpoints.LOGGED_IN, function () { - return should_get_and_reply_with_401(BASE_URL + Endpoints.LOGGED_IN); - }); - }); -}); - diff --git a/server/test/server/PublicPages.ts b/server/test/server/PublicPages.ts deleted file mode 100644 index cefd414a8..000000000 --- a/server/test/server/PublicPages.ts +++ /dev/null @@ -1,173 +0,0 @@ - -import Server from "../../src/lib/Server"; -import BluebirdPromise = require("bluebird"); -import speakeasy = require("speakeasy"); -import Request = require("request"); -import nedb = require("nedb"); -import { GlobalDependencies } from "../../types/Dependencies"; -import { UserConfiguration } from "../../src/lib/configuration/Configuration"; -import { TOTPSecret } from "../../types/TOTPSecret"; -import U2FMock = require("./../mocks/u2f"); -import Endpoints = require("../../../shared/api"); -import Requests = require("../requests"); -import Assert = require("assert"); -import Sinon = require("sinon"); -import Winston = require("winston"); -import MockDate = require("mockdate"); -import ExpressSession = require("express-session"); -import ldapjs = require("ldapjs"); - -const requestp = BluebirdPromise.promisifyAll(Request) as typeof Request; - -const PORT = 8090; -const BASE_URL = "http://localhost:" + PORT; -const requests = Requests(PORT); - -describe("Public pages of the server must be accessible without session", function () { - let server: Server; - let transporter: object; - let u2f: U2FMock.U2FMock; - - beforeEach(function () { - const config: UserConfiguration = { - port: PORT, - ldap: { - url: "ldap://127.0.0.1:389", - base_dn: "ou=users,dc=example,dc=com", - user: "cn=admin,dc=example,dc=com", - password: "password", - }, - session: { - secret: "session_secret", - expiration: 50000, - }, - storage: { - local: { - in_memory: true - } - }, - regulation: { - max_retries: 3, - ban_time: 5 * 60, - find_time: 5 * 60 - }, - notifier: { - email: { - username: "user@example.com", - password: "password", - sender: "admin@example.com", - service: "gmail" - } - } - }; - - const ldap_client = { - bind: Sinon.stub(), - search: Sinon.stub(), - modify: Sinon.stub(), - on: Sinon.spy() - }; - const ldap = { - Change: Sinon.spy(), - createClient: Sinon.spy(function () { - return ldap_client; - }) - }; - - u2f = U2FMock.U2FMock(); - - transporter = { - sendMail: Sinon.stub().yields() - }; - - const nodemailer = { - createTransport: Sinon.spy(function () { - return transporter; - }) - }; - - const ldap_document = { - object: { - mail: "test_ok@example.com", - } - }; - - const search_res = { - on: Sinon.spy(function (event: string, fn: (s: any) => void) { - if (event != "error") fn(ldap_document); - }) - }; - - ldap_client.bind.withArgs("cn=test_ok,ou=users,dc=example,dc=com", - "password").yields(undefined); - ldap_client.bind.withArgs("cn=admin,dc=example,dc=com", - "password").yields(undefined); - - ldap_client.bind.withArgs("cn=test_nok,ou=users,dc=example,dc=com", - "password").yields("error"); - - ldap_client.modify.yields(undefined); - ldap_client.search.yields(undefined, search_res); - - const deps: GlobalDependencies = { - u2f: u2f as any, - nedb: nedb, - ldapjs: ldap, - session: ExpressSession, - winston: Winston, - speakeasy: speakeasy, - ConnectRedis: Sinon.spy() - }; - - server = new Server(deps); - return server.start(config, deps); - }); - - afterEach(function () { - server.stop(); - }); - - describe("test GET " + Endpoints.FIRST_FACTOR_GET, function () { - test_login(); - }); - - describe("test GET " + Endpoints.LOGOUT_GET, function () { - test_logout(); - }); - - describe("test GET" + Endpoints.RESET_PASSWORD_REQUEST_GET, function () { - test_reset_password_form(); - }); - - - function test_reset_password_form() { - it("should serve the reset password form page", function (done) { - requestp.getAsync(BASE_URL + Endpoints.RESET_PASSWORD_REQUEST_GET) - .then(function (response: Request.RequestResponse) { - Assert.equal(response.statusCode, 200); - done(); - }); - }); - } - - function test_login() { - it("should serve the login page", function (done) { - requestp.getAsync(BASE_URL + Endpoints.FIRST_FACTOR_GET) - .then(function (response: Request.RequestResponse) { - Assert.equal(response.statusCode, 200); - done(); - }); - }); - } - - function test_logout() { - it("should logout and redirect to /", function (done) { - requestp.getAsync(BASE_URL + Endpoints.LOGOUT_GET) - .then(function (response: any) { - Assert.equal(response.req.path, "/"); - done(); - }); - }); - } -}); - diff --git a/test/features/access-control.feature b/test/features/access-control.feature index 0051c0ae6..1e01e5d46 100644 --- a/test/features/access-control.feature +++ b/test/features/access-control.feature @@ -17,7 +17,7 @@ Feature: User has access restricted access to domains | https://dev.test.local:8080/users/bob/secret.html | | https://admin.test.local:8080/secret.html | | https://mx1.mail.test.local:8080/secret.html | - | https://single_factor.test.local:8080/secret.html | + | https://single_factor.test.local:8080/secret.html | And I have no access to: | url | | https://mx2.mail.test.local:8080/secret.html | @@ -42,7 +42,7 @@ Feature: User has access restricted access to domains | https://admin.test.local:8080/secret.html | | https://dev.test.local:8080/users/john/secret.html | | https://dev.test.local:8080/users/harry/secret.html | - | https://single_factor.test.local:8080/secret.html | + | https://single_factor.test.local:8080/secret.html | @need-registered-user-harry Scenario: User harry has restricted access @@ -64,4 +64,4 @@ Feature: User has access restricted access to domains | https://dev.test.local:8080/users/john/secret.html | | https://mx1.mail.test.local:8080/secret.html | | https://mx2.mail.test.local:8080/secret.html | - | https://single_factor.test.local:8080/secret.html | + | https://single_factor.test.local:8080/secret.html | \ No newline at end of file diff --git a/test/features/restrictions.feature b/test/features/restrictions.feature index e331482b9..4ff7a1362 100644 --- a/test/features/restrictions.feature +++ b/test/features/restrictions.feature @@ -1,16 +1,36 @@ Feature: Non authenticated users have no access to certain pages - Scenario Outline: Anonymous user has no access to protected pages - When I visit "" - Then I get an error + Scenario: Anonymous user has no access to protected pages + Then I get the following status code when requesting: + | url | code | method | + | https://auth.test.local:8080/secondfactor | 401 | GET | + | https://auth.test.local:8080/secondfactor/u2f/identity/start | 401 | GET | + | https://auth.test.local:8080/secondfactor/u2f/identity/finish | 401 | GET | + | https://auth.test.local:8080/secondfactor/totp/identity/start | 401 | GET | + | https://auth.test.local:8080/secondfactor/totp/identity/finish | 401 | GET | + | https://auth.test.local:8080/api/totp | 401 | POST | + | https://auth.test.local:8080/api/u2f/sign_request | 401 | GET | + | https://auth.test.local:8080/api/u2f/sign | 401 | POST | + | https://auth.test.local:8080/api/u2f/register_request | 401 | GET | + | https://auth.test.local:8080/api/u2f/register | 401 | POST | - 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 | 401 | - | https://auth.test.local:8080/secondfactor/totp/identity/start | 401 | - | https://auth.test.local:8080/secondfactor/totp/identity/finish | 401 | - | https://auth.test.local:8080/password-reset/identity/start | 401 | - | https://auth.test.local:8080/password-reset/identity/finish | 401 | \ No newline at end of file + + @needs-single_factor-config + @need-registered-user-john + Scenario: User does not have acces to second factor related endpoints when in single factor mode + Given I post "https://auth.test.local:8080/api/firstfactor" with body: + | key | value | + | username | john | + | password | password | + Then I get the following status code when requesting: + | url | code | method | + | https://auth.test.local:8080/secondfactor | 401 | GET | + | https://auth.test.local:8080/secondfactor/u2f/identity/start | 401 | GET | + | https://auth.test.local:8080/secondfactor/u2f/identity/finish | 401 | GET | + | https://auth.test.local:8080/secondfactor/totp/identity/start | 401 | GET | + | https://auth.test.local:8080/secondfactor/totp/identity/finish | 401 | GET | + | https://auth.test.local:8080/api/totp | 401 | POST | + | https://auth.test.local:8080/api/u2f/sign_request | 401 | GET | + | https://auth.test.local:8080/api/u2f/sign | 401 | POST | + | https://auth.test.local:8080/api/u2f/register_request | 401 | GET | + | https://auth.test.local:8080/api/u2f/register | 401 | POST | \ No newline at end of file diff --git a/test/features/single-factor.feature b/test/features/single-factor-domain.feature similarity index 100% rename from test/features/single-factor.feature rename to test/features/single-factor-domain.feature diff --git a/test/features/single-factor-server.feature b/test/features/single-factor-server.feature new file mode 100644 index 000000000..b52c03aca --- /dev/null +++ b/test/features/single-factor-server.feature @@ -0,0 +1,16 @@ +@needs-single_factor-config +Feature: Server is configured as a single factor only server + + @need-registered-user-john + Scenario: User is redirected to service after first factor if allowed + When I visit "https://auth.test.local:8080/?redirect=https%3A%2F%2Fpublic.test.local%3A8080%2Fsecret.html" + And I login with user "john" and password "password" + Then I'm redirected to "https://public.test.local:8080/secret.html" + + @need-registered-user-john + Scenario: User is correctly redirected according to default redirection URL + When I visit "https://auth.test.local:8080" + And I login with user "john" and password "password" + Then I'm redirected to "https://auth.test.local:8080/loggedin" + And I sleep for 5 seconds + Then I'm redirected to "https://home.test.local:8080/" diff --git a/test/features/step_definitions/authentication.ts b/test/features/step_definitions/authentication.ts index 6899d5967..f7d923b8d 100644 --- a/test/features/step_definitions/authentication.ts +++ b/test/features/step_definitions/authentication.ts @@ -5,19 +5,20 @@ import Fs = require("fs"); import Speakeasy = require("speakeasy"); import CustomWorld = require("../support/world"); import BluebirdPromise = require("bluebird"); +import Request = require("request-promise"); Cucumber.defineSupportCode(function ({ Given, When, Then }) { When(/^I visit "(https:\/\/[a-zA-Z0-9:%&._\/=?-]+)"$/, function (link: string) { return this.visit(link); }); - When("I wait for notification to disappear", function() { + When("I wait for notification to disappear", function () { const that = this; const notificationEl = this.driver.findElement(seleniumWebdriver.By.className("notification")); return this.driver.wait(seleniumWebdriver.until.elementIsVisible(notificationEl), 15000) - .then(function() { - return that.driver.wait(seleniumWebdriver.until.elementIsNotVisible(notificationEl), 15000); - }) + .then(function () { + return that.driver.wait(seleniumWebdriver.until.elementIsNotVisible(notificationEl), 15000); + }) }) When("I set field {stringInDoubleQuotes} to {stringInDoubleQuotes}", function (fieldName: string, content: string) { @@ -104,4 +105,29 @@ and I use TOTP token handle {stringInDoubleQuotes}", } return BluebirdPromise.all(promises); }); + + function endpointReplyWith(context: any, link: string, method: string, + returnCode: number) { + return Request(link, { + method: method + }) + .then(function (response: string) { + Assert(response.indexOf("Error " + returnCode) >= 0); + return BluebirdPromise.resolve(); + }, function (response: any) { + Assert.equal(response.statusCode, returnCode); + return BluebirdPromise.resolve(); + }); + } + + Then("the following endpoints reply with:", function (dataTable: Cucumber.TableDefinition) { + const promises = []; + for (let i = 0; i < dataTable.rows().length; i++) { + const url: string = (dataTable.hashes() as any)[i].url; + const method: string = (dataTable.hashes() as any)[i].method; + const code: number = (dataTable.hashes() as any)[i].code; + promises.push(endpointReplyWith(this, url, method, code)); + } + return BluebirdPromise.all(promises); + }); }); \ No newline at end of file diff --git a/test/features/step_definitions/hooks.ts b/test/features/step_definitions/hooks.ts index b90192ce2..c7715910e 100644 --- a/test/features/step_definitions/hooks.ts +++ b/test/features/step_definitions/hooks.ts @@ -36,6 +36,13 @@ Cucumber.defineSupportCode(function ({ After, Before }) { "); } + function createSingleFactorConfiguration(): BluebirdPromise { + return exec("\ + cat config.template.yml | \ + sed 's/default_method: two_factor/default_method: single_factor/' > config.test.yml \ + "); + } + function declareNeedsConfiguration(tag: string, cb: () => BluebirdPromise) { Before({ tags: "@needs-" + tag + "-config", timeout: 20 * 1000 }, function () { return cb() @@ -54,6 +61,7 @@ Cucumber.defineSupportCode(function ({ After, Before }) { declareNeedsConfiguration("regulation", createRegulationConfiguration); declareNeedsConfiguration("inactivity", createInactivityConfiguration); + declareNeedsConfiguration("single_factor", createSingleFactorConfiguration); function registerUser(context: any, username: string) { let secret: Speakeasy.Key; diff --git a/test/features/step_definitions/restrictions.ts b/test/features/step_definitions/restrictions.ts index f319ffb8e..028113bb4 100644 --- a/test/features/step_definitions/restrictions.ts +++ b/test/features/step_definitions/restrictions.ts @@ -1,9 +1,72 @@ import Cucumber = require("cucumber"); import seleniumWebdriver = require("selenium-webdriver"); import Assert = require("assert"); +import Request = require("request-promise"); +import Bluebird = require("bluebird"); + +Cucumber.defineSupportCode(function ({ Given, When, Then, Before, After }) { + Before(function () { + this.jar = Request.jar(); + }) -Cucumber.defineSupportCode(function ({ Given, When, Then }) { Then("I get an error {number}", function (code: number) { return this.getErrorPage(code); }); + + When("I request {stringInDoubleQuotes} with method {stringInDoubleQuotes}", + function (url: string, method: string) { + const that = this; + + }) + + function requestAndExpectStatusCode(ctx: any, url: string, method: string, + expectedStatusCode: number) { + return Request(url, { + method: method, + jar: ctx.jar + }) + .then(function (body: string) { + return Bluebird.resolve(parseInt(body.match(/Error ([0-9]{3})/)[1])); + }, function (response: any) { + return Bluebird.resolve(response.statusCode) + }) + .then(function (statusCode: number) { + try { + Assert.equal(statusCode, expectedStatusCode); + } + catch (e) { + console.log(url); + console.log("%s (actual) != %s (expected)", statusCode, + expectedStatusCode); + throw e; + } + }) + } + + Then("I get the following status code when requesting:", + function (dataTable: Cucumber.TableDefinition) { + const promises: Bluebird[] = []; + for (let i = 0; i < dataTable.rows().length; i++) { + const url: string = (dataTable.hashes() as any)[i].url; + const method: string = (dataTable.hashes() as any)[i].method; + const code: number = (dataTable.hashes() as any)[i].code; + promises.push(requestAndExpectStatusCode(this, url, method, code)); + } + return Bluebird.all(promises); + }) + + When("I post {stringInDoubleQuotes} with body:", function (url: string, + dataTable: Cucumber.TableDefinition) { + const body = {}; + for (let i = 0; i < dataTable.rows().length; i++) { + const key = (dataTable.hashes() as any)[i].key; + const value = (dataTable.hashes() as any)[i].value; + body[key] = value; + } + return Request.post(url, { + body: body, + jar: this.jar, + json: true + }); + }) }); \ No newline at end of file diff --git a/test/features/support/world.ts b/test/features/support/world.ts index e75e71f9d..ce63e76e8 100644 --- a/test/features/support/world.ts +++ b/test/features/support/world.ts @@ -7,6 +7,8 @@ import Assert = require("assert"); import Request = require("request-promise"); import BluebirdPromise = require("bluebird"); +process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0" + function CustomWorld() { const that = this; this.driver = new seleniumWebdriver.Builder() @@ -38,8 +40,13 @@ function CustomWorld() { .findElement(seleniumWebdriver.By.tagName("h1")).getText(); }) .then(function (txt: string) { - Assert.equal(txt, "Error " + code); - }); + try { + Assert.equal(txt, "Error " + code); + } catch (e) { + console.log(txt); + throw e; + } + }) }; this.clickOnButton = function (buttonText: string) {