From b9fa786df6894e1e32d8c61b491baeb7178ebdb5 Mon Sep 17 00:00:00 2001 From: Clement Michaud Date: Tue, 17 Oct 2017 00:35:34 +0200 Subject: [PATCH] Refactor endpoints to get server variables as input parameters This refactoring aims to ease testability and clean up a lot of soft touchy typings in test code. This is the first step of this refactoring introducing the concept and implementing missing interfaces and stubs. At the end of the day, ServerVariablesHandler should completely disappear and every variable should be injected in the endpoint handler builder itself. --- Gruntfile.js | 2 + config.template.yml | 4 +- config.test.yml | 2 +- scripts/travis.sh | 3 +- .../src/lib/AuthenticationMethodCalculator.ts | 5 +- server/src/lib/AuthenticationSession.ts | 5 + server/src/lib/RestApi.ts | 9 +- server/src/lib/Server.ts | 23 ++- server/src/lib/ServerVariables.ts | 28 ++- server/src/lib/ServerVariablesHandler.ts | 52 +++--- server/src/lib/TOTPGenerator.ts | 24 --- server/src/lib/TOTPValidator.ts | 27 --- .../lib/authentication/totp/ITotpHandler.ts | 14 ++ .../lib/authentication/totp/TotpHandler.ts | 27 +++ .../src/lib/authentication/u2f/IU2fHandler.ts | 9 + .../src/lib/authentication/u2f/U2fHandler.ts | 24 +++ server/src/lib/regulation/IRegulator.ts | 6 + .../Regulator.ts} | 9 +- server/src/lib/routes/firstfactor/post.ts | 168 +++++++++--------- .../src/lib/routes/secondfactor/redirect.ts | 3 +- .../totp/identity/RegistrationHandler.ts | 4 +- .../lib/routes/secondfactor/totp/sign/post.ts | 50 +++--- server/src/lib/routes/verify/get.ts | 53 +++--- server/test/ServerConfiguration.test.ts | 11 +- server/test/TOTPValidator.test.ts | 41 ----- .../authentication/totp/Validator.test.ts | 39 ++++ server/test/mocks/NotifierStub.ts | 16 ++ server/test/mocks/RegulatorStub.ts | 21 +++ .../test/mocks/ServerVariablesMockBuilder.ts | 91 ++++++++++ server/test/mocks/TotpHandlerStub.ts | 22 +++ server/test/mocks/U2fHandlerStub.ts | 31 ++++ server/test/mocks/ldap/AuthenticatorStub.ts | 16 ++ server/test/mocks/ldap/EmailsRetrieverStub.ts | 16 ++ server/test/mocks/ldap/PasswordUpdaterStub.ts | 16 ++ .../Regulator.test.ts} | 26 +-- server/test/routes/firstfactor/post.test.ts | 64 +++---- .../secondfactor/totp/sign/post.test.ts | 42 ++--- server/test/routes/verify/get.test.ts | 79 ++++---- 38 files changed, 667 insertions(+), 415 deletions(-) delete mode 100644 server/src/lib/TOTPGenerator.ts delete mode 100644 server/src/lib/TOTPValidator.ts create mode 100644 server/src/lib/authentication/totp/ITotpHandler.ts create mode 100644 server/src/lib/authentication/totp/TotpHandler.ts create mode 100644 server/src/lib/authentication/u2f/IU2fHandler.ts create mode 100644 server/src/lib/authentication/u2f/U2fHandler.ts create mode 100644 server/src/lib/regulation/IRegulator.ts rename server/src/lib/{AuthenticationRegulator.ts => regulation/Regulator.ts} (87%) delete mode 100644 server/test/TOTPValidator.test.ts create mode 100644 server/test/authentication/totp/Validator.test.ts create mode 100644 server/test/mocks/NotifierStub.ts create mode 100644 server/test/mocks/RegulatorStub.ts create mode 100644 server/test/mocks/ServerVariablesMockBuilder.ts create mode 100644 server/test/mocks/TotpHandlerStub.ts create mode 100644 server/test/mocks/U2fHandlerStub.ts create mode 100644 server/test/mocks/ldap/AuthenticatorStub.ts create mode 100644 server/test/mocks/ldap/EmailsRetrieverStub.ts create mode 100644 server/test/mocks/ldap/PasswordUpdaterStub.ts rename server/test/{AuthenticationRegulator.test.ts => regulation/Regulator.test.ts} (84%) diff --git a/Gruntfile.js b/Gruntfile.js index 5564951cf..e0400f536 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -194,6 +194,8 @@ module.exports = function (grunt) { grunt.registerTask('build', ['build-client', 'build-server']); grunt.registerTask('build-dist', ['build', 'run:minify', 'cssmin', 'run:include-minified-script']); + grunt.registerTask('schema', ['run:generate-config-schema']) + grunt.registerTask('docker-build', ['run:docker-build']); grunt.registerTask('default', ['build-dist']); diff --git a/config.template.yml b/config.template.yml index 80f277abe..5505e2f20 100644 --- a/config.template.yml +++ b/config.template.yml @@ -153,8 +153,8 @@ session: # The secret to encrypt the session cookie. secret: unsecure_session_secret - # The time before the cookie expires. - expiration: 3600000 + # The time in ms before the cookie expires and session is reset. + expiration: 3600000 # 1 hour # The domain to protect. # Note: the authenticator must also be in that domain. If empty, the cookie diff --git a/config.test.yml b/config.test.yml index 842e77656..8d1597dbb 100644 --- a/config.test.yml +++ b/config.test.yml @@ -133,7 +133,7 @@ session: secret: unsecure_secret # The time before the cookie expires. - expiration: 3600000 + expiration: 10000 # The domain to protect. # Note: the authenticator must also be in that domain. If empty, the cookie diff --git a/scripts/travis.sh b/scripts/travis.sh index 332b24501..bd6ac908c 100755 --- a/scripts/travis.sh +++ b/scripts/travis.sh @@ -5,7 +5,8 @@ set -e docker --version docker-compose --version -grunt run:generate-config-schema +# Generate configuration schema +grunt schema # Run unit tests grunt test-unit diff --git a/server/src/lib/AuthenticationMethodCalculator.ts b/server/src/lib/AuthenticationMethodCalculator.ts index 3791a9dbe..8f0810dcf 100644 --- a/server/src/lib/AuthenticationMethodCalculator.ts +++ b/server/src/lib/AuthenticationMethodCalculator.ts @@ -8,8 +8,11 @@ export class AuthenticationMethodCalculator { } compute(subDomain: string): AuthenticationMethod { - if (subDomain in this.configuration.per_subdomain_methods) + 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 index 9414d55d7..ad692f96f 100644 --- a/server/src/lib/AuthenticationSession.ts +++ b/server/src/lib/AuthenticationSession.ts @@ -9,6 +9,7 @@ export interface AuthenticationSession { userid: string; first_factor: boolean; second_factor: boolean; + last_activity_datetime: Date; identity_check?: { challenge: string; userid: string; @@ -23,6 +24,7 @@ export interface AuthenticationSession { const INITIAL_AUTHENTICATION_SESSION: AuthenticationSession = { first_factor: false, second_factor: false, + last_activity_datetime: undefined, userid: undefined, email: undefined, groups: [], @@ -36,6 +38,9 @@ export function reset(req: express.Request): void { const logger = ServerVariablesHandler.getLogger(req.app); logger.debug(req, "Authentication session %s is being reset.", req.sessionID); req.session.auth = Object.assign({}, INITIAL_AUTHENTICATION_SESSION, {}); + + // Initialize last activity with current time + req.session.auth.last_activity_datetime = new Date(); } export function get(req: express.Request): BluebirdPromise { diff --git a/server/src/lib/RestApi.ts b/server/src/lib/RestApi.ts index 34686c719..74a240d09 100644 --- a/server/src/lib/RestApi.ts +++ b/server/src/lib/RestApi.ts @@ -33,6 +33,7 @@ import Error404Get = require("./routes/error/404/get"); import LoggedIn = require("./routes/loggedin/get"); import { ServerVariablesHandler } from "./ServerVariablesHandler"; +import { ServerVariables } from "./ServerVariables"; import Endpoints = require("../../../shared/api"); @@ -45,7 +46,7 @@ function withLog(fn: (req: Express.Request, res: Express.Response) => void) { } export class RestApi { - static setup(app: Express.Application): void { + static setup(app: Express.Application, vars: ServerVariables): void { app.get(Endpoints.FIRST_FACTOR_GET, withLog(FirstFactorGet.default)); app.get(Endpoints.SECOND_FACTOR_GET, withLog(SecondFactorGet.default)); app.get(Endpoints.LOGOUT_GET, withLog(LogoutGet.default)); @@ -62,9 +63,9 @@ export class RestApi { app.get(Endpoints.RESET_PASSWORD_REQUEST_GET, withLog(ResetPasswordRequestPost.default)); app.post(Endpoints.RESET_PASSWORD_FORM_POST, withLog(ResetPasswordFormPost.default)); - app.get(Endpoints.VERIFY_GET, withLog(VerifyGet.default)); - app.post(Endpoints.FIRST_FACTOR_POST, withLog(FirstFactorPost.default)); - app.post(Endpoints.SECOND_FACTOR_TOTP_POST, withLog(TOTPSignGet.default)); + app.get(Endpoints.VERIFY_GET, withLog(VerifyGet.default(vars))); + app.post(Endpoints.FIRST_FACTOR_POST, withLog(FirstFactorPost.default(vars))); + app.post(Endpoints.SECOND_FACTOR_TOTP_POST, withLog(TOTPSignGet.default(vars))); app.get(Endpoints.SECOND_FACTOR_U2F_SIGN_REQUEST_GET, withLog(U2FSignRequestGet.default)); app.post(Endpoints.SECOND_FACTOR_U2F_SIGN_POST, withLog(U2FSignPost.default)); diff --git a/server/src/lib/Server.ts b/server/src/lib/Server.ts index a5e6162bd..00d7e3d93 100644 --- a/server/src/lib/Server.ts +++ b/server/src/lib/Server.ts @@ -4,16 +4,14 @@ import ObjectPath = require("object-path"); import { AccessController } from "./access_control/AccessController"; import { AppConfiguration, UserConfiguration } from "./configuration/Configuration"; import { GlobalDependencies } from "../../types/Dependencies"; -import { AuthenticationRegulator } from "./AuthenticationRegulator"; import { UserDataStore } from "./storage/UserDataStore"; import { ConfigurationParser } from "./configuration/ConfigurationParser"; -import { TOTPValidator } from "./TOTPValidator"; -import { TOTPGenerator } from "./TOTPGenerator"; import { RestApi } from "./RestApi"; -import { ServerVariablesHandler } from "./ServerVariablesHandler"; +import { ServerVariablesHandler, ServerVariablesInitializer } from "./ServerVariablesHandler"; import { SessionConfigurationBuilder } from "./configuration/SessionConfigurationBuilder"; import { GlobalLogger } from "./logging/GlobalLogger"; import { RequestLogger } from "./logging/RequestLogger"; +import { ServerVariables } from "./ServerVariables"; import * as Express from "express"; import * as BodyParser from "body-parser"; @@ -37,13 +35,16 @@ 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 { + private setupExpressApplication(config: AppConfiguration, + app: Express.Application, + deps: GlobalDependencies): void { const viewsDirectory = Path.resolve(__dirname, "../views"); const publicHtmlDirectory = Path.resolve(__dirname, "../public_html"); @@ -60,7 +61,7 @@ export default class Server { app.set(VIEWS, viewsDirectory); app.set(VIEW_ENGINE, PUG); - RestApi.setup(app); + RestApi.setup(app, this.serverVariables); } private displayConfigurations(userConfiguration: UserConfiguration, @@ -90,8 +91,14 @@ export default class Server { } private setup(config: AppConfiguration, app: Express.Application, deps: GlobalDependencies): BluebirdPromise { - this.setupExpressApplication(config, app, deps); - return ServerVariablesHandler.initialize(app, config, this.requestLogger, deps); + const that = this; + return ServerVariablesInitializer.initialize(config, this.requestLogger, deps) + .then(function (vars: ServerVariables) { + that.serverVariables = vars; + that.setupExpressApplication(config, app, deps); + ServerVariablesHandler.setup(app, vars); + return BluebirdPromise.resolve(); + }); } private startServer(app: Express.Application, port: number) { diff --git a/server/src/lib/ServerVariables.ts b/server/src/lib/ServerVariables.ts index 065a1dafa..a7b1e2e5e 100644 --- a/server/src/lib/ServerVariables.ts +++ b/server/src/lib/ServerVariables.ts @@ -1,33 +1,25 @@ -import U2F = require("u2f"); - import { IRequestLogger } from "./logging/IRequestLogger"; import { IAuthenticator } from "./ldap/IAuthenticator"; import { IPasswordUpdater } from "./ldap/IPasswordUpdater"; import { IEmailsRetriever } from "./ldap/IEmailsRetriever"; - -import { TOTPValidator } from "./TOTPValidator"; -import { TOTPGenerator } from "./TOTPGenerator"; +import { ITotpHandler } from "./authentication/totp/ITotpHandler"; +import { IU2fHandler } from "./authentication/u2f/IU2fHandler"; import { IUserDataStore } from "./storage/IUserDataStore"; import { INotifier } from "./notifiers/INotifier"; -import { AuthenticationRegulator } from "./AuthenticationRegulator"; -import Configuration = require("./configuration/Configuration"); -import { AccessController } from "./access_control/AccessController"; -import { AuthenticationMethodCalculator } from "./AuthenticationMethodCalculator"; - - +import { IRegulator } from "./regulation/IRegulator"; +import { AppConfiguration } from "./configuration/Configuration"; +import { IAccessController } from "./access_control/IAccessController"; export interface ServerVariables { logger: IRequestLogger; ldapAuthenticator: IAuthenticator; ldapPasswordUpdater: IPasswordUpdater; ldapEmailsRetriever: IEmailsRetriever; - totpValidator: TOTPValidator; - totpGenerator: TOTPGenerator; - u2f: typeof U2F; + totpHandler: ITotpHandler; + u2f: IU2fHandler; userDataStore: IUserDataStore; notifier: INotifier; - regulator: AuthenticationRegulator; - config: Configuration.AppConfiguration; - accessController: AccessController; - authenticationMethodsCalculator: AuthenticationMethodCalculator; + regulator: IRegulator; + config: AppConfiguration; + accessController: IAccessController; } \ No newline at end of file diff --git a/server/src/lib/ServerVariablesHandler.ts b/server/src/lib/ServerVariablesHandler.ts index caf7a6332..c0a84765f 100644 --- a/server/src/lib/ServerVariablesHandler.ts +++ b/server/src/lib/ServerVariablesHandler.ts @@ -16,18 +16,19 @@ import { EmailsRetriever } from "./ldap/EmailsRetriever"; import { ClientFactory } from "./ldap/ClientFactory"; import { LdapClientFactory } from "./ldap/LdapClientFactory"; -import { TOTPValidator } from "./TOTPValidator"; -import { TOTPGenerator } from "./TOTPGenerator"; - +import { TotpHandler } from "./authentication/totp/TotpHandler"; +import { ITotpHandler } from "./authentication/totp/ITotpHandler"; import { NotifierFactory } from "./notifiers/NotifierFactory"; import { MailSenderBuilder } from "./notifiers/MailSenderBuilder"; import { IUserDataStore } from "./storage/IUserDataStore"; import { UserDataStore } from "./storage/UserDataStore"; import { INotifier } from "./notifiers/INotifier"; -import { AuthenticationRegulator } from "./AuthenticationRegulator"; +import { Regulator } from "./regulation/Regulator"; +import { IRegulator } from "./regulation/IRegulator"; import Configuration = require("./configuration/Configuration"); import { AccessController } from "./access_control/AccessController"; +import { IAccessController } from "./access_control/IAccessController"; import { CollectionFactoryFactory } from "./storage/CollectionFactoryFactory"; import { ICollectionFactory } from "./storage/ICollectionFactory"; import { MongoCollectionFactory } from "./storage/mongo/MongoCollectionFactory"; @@ -67,9 +68,9 @@ class UserDataStoreFactory { } } -export class ServerVariablesHandler { - static initialize(app: express.Application, config: Configuration.AppConfiguration, requestLogger: IRequestLogger, - deps: GlobalDependencies): BluebirdPromise { +export class ServerVariablesInitializer { + static initialize(config: Configuration.AppConfiguration, requestLogger: IRequestLogger, + deps: GlobalDependencies): BluebirdPromise { const mailSenderBuilder = new MailSenderBuilder(Nodemailer); const notifier = NotifierFactory.build(config.notifier, mailSenderBuilder); const ldapClientFactory = new LdapClientFactory(config.ldap, deps.ldapjs); @@ -79,13 +80,11 @@ export class ServerVariablesHandler { const ldapPasswordUpdater = new PasswordUpdater(config.ldap, clientFactory); const ldapEmailsRetriever = new EmailsRetriever(config.ldap, clientFactory); const accessController = new AccessController(config.access_control, deps.winston); - const totpValidator = new TOTPValidator(deps.speakeasy); - const totpGenerator = new TOTPGenerator(deps.speakeasy); - const authenticationMethodCalculator = new AuthenticationMethodCalculator(config.authentication_methods); + const totpHandler = new TotpHandler(deps.speakeasy); return UserDataStoreFactory.create(config) .then(function (userDataStore: UserDataStore) { - const regulator = new AuthenticationRegulator(userDataStore, config.regulation.max_retries, + const regulator = new Regulator(userDataStore, config.regulation.max_retries, config.regulation.find_time, config.regulation.ban_time); const variables: ServerVariables = { @@ -97,15 +96,18 @@ export class ServerVariablesHandler { logger: requestLogger, notifier: notifier, regulator: regulator, - totpGenerator: totpGenerator, - totpValidator: totpValidator, + totpHandler: totpHandler, u2f: deps.u2f, - userDataStore: userDataStore, - authenticationMethodsCalculator: authenticationMethodCalculator + userDataStore: userDataStore }; - - app.set(VARIABLES_KEY, variables); + return BluebirdPromise.resolve(variables); }); + } +} + +export class ServerVariablesHandler { + static setup(app: express.Application, variables: ServerVariables): void { + app.set(VARIABLES_KEY, variables); } static getLogger(app: express.Application): IRequestLogger { @@ -136,27 +138,19 @@ export class ServerVariablesHandler { return (app.get(VARIABLES_KEY) as ServerVariables).config; } - static getAuthenticationRegulator(app: express.Application): AuthenticationRegulator { + static getAuthenticationRegulator(app: express.Application): IRegulator { return (app.get(VARIABLES_KEY) as ServerVariables).regulator; } - static getAccessController(app: express.Application): AccessController { + static getAccessController(app: express.Application): IAccessController { return (app.get(VARIABLES_KEY) as ServerVariables).accessController; } - static getTOTPGenerator(app: express.Application): TOTPGenerator { - return (app.get(VARIABLES_KEY) as ServerVariables).totpGenerator; - } - - static getTOTPValidator(app: express.Application): TOTPValidator { - return (app.get(VARIABLES_KEY) as ServerVariables).totpValidator; + static getTotpHandler(app: express.Application): ITotpHandler { + return (app.get(VARIABLES_KEY) as ServerVariables).totpHandler; } static getU2F(app: express.Application): typeof U2F { return (app.get(VARIABLES_KEY) as ServerVariables).u2f; } - - static getAuthenticationMethodCalculator(app: express.Application): AuthenticationMethodCalculator { - return (app.get(VARIABLES_KEY) as ServerVariables).authenticationMethodsCalculator; - } } diff --git a/server/src/lib/TOTPGenerator.ts b/server/src/lib/TOTPGenerator.ts deleted file mode 100644 index c0c7133bf..000000000 --- a/server/src/lib/TOTPGenerator.ts +++ /dev/null @@ -1,24 +0,0 @@ - -import Speakeasy = require("speakeasy"); -import BluebirdPromise = require("bluebird"); -import { TOTPSecret } from "../../types/TOTPSecret"; - -interface GenerateSecretOptions { - length?: number; - symbols?: boolean; - otpauth_url?: boolean; - name?: string; - issuer?: string; -} - -export class TOTPGenerator { - private speakeasy: typeof Speakeasy; - - constructor(speakeasy: typeof Speakeasy) { - this.speakeasy = speakeasy; - } - - generate(options?: GenerateSecretOptions): TOTPSecret { - return this.speakeasy.generateSecret(options); - } -} \ No newline at end of file diff --git a/server/src/lib/TOTPValidator.ts b/server/src/lib/TOTPValidator.ts deleted file mode 100644 index d99be78b8..000000000 --- a/server/src/lib/TOTPValidator.ts +++ /dev/null @@ -1,27 +0,0 @@ -import Speakeasy = require("speakeasy"); -import BluebirdPromise = require("bluebird"); - -const TOTP_ENCODING = "base32"; -const WINDOW: number = 1; - -export class TOTPValidator { - private speakeasy: typeof Speakeasy; - - constructor(speakeasy: typeof Speakeasy) { - this.speakeasy = speakeasy; - } - - validate(token: string, secret: string): BluebirdPromise { - const isValid = this.speakeasy.totp.verify({ - secret: secret, - encoding: TOTP_ENCODING, - token: token, - window: WINDOW - } as any); - - if (isValid) - return BluebirdPromise.resolve(); - else - return BluebirdPromise.reject(new Error("Wrong TOTP token.")); - } -} \ No newline at end of file diff --git a/server/src/lib/authentication/totp/ITotpHandler.ts b/server/src/lib/authentication/totp/ITotpHandler.ts new file mode 100644 index 000000000..2830790f5 --- /dev/null +++ b/server/src/lib/authentication/totp/ITotpHandler.ts @@ -0,0 +1,14 @@ +import { TOTPSecret } from "../../../../types/TOTPSecret"; + +export interface GenerateSecretOptions { + length?: number; + symbols?: boolean; + otpauth_url?: boolean; + name?: string; + issuer?: string; +} + +export interface ITotpHandler { + generate(options?: GenerateSecretOptions): TOTPSecret; + validate(token: string, secret: string): boolean; +} \ No newline at end of file diff --git a/server/src/lib/authentication/totp/TotpHandler.ts b/server/src/lib/authentication/totp/TotpHandler.ts new file mode 100644 index 000000000..9e1419e92 --- /dev/null +++ b/server/src/lib/authentication/totp/TotpHandler.ts @@ -0,0 +1,27 @@ +import { ITotpHandler, GenerateSecretOptions } from "./ITotpHandler"; +import { TOTPSecret } from "../../../../types/TOTPSecret"; +import Speakeasy = require("speakeasy"); + +const TOTP_ENCODING = "base32"; +const WINDOW: number = 1; + +export class TotpHandler implements ITotpHandler { + private speakeasy: typeof Speakeasy; + + constructor(speakeasy: typeof Speakeasy) { + this.speakeasy = speakeasy; + } + + generate(options?: GenerateSecretOptions): TOTPSecret { + return this.speakeasy.generateSecret(options); + } + + validate(token: string, secret: string): boolean { + return this.speakeasy.totp.verify({ + secret: secret, + encoding: TOTP_ENCODING, + token: token, + window: WINDOW + } as any); + } +} \ No newline at end of file diff --git a/server/src/lib/authentication/u2f/IU2fHandler.ts b/server/src/lib/authentication/u2f/IU2fHandler.ts new file mode 100644 index 000000000..b9b7d6f26 --- /dev/null +++ b/server/src/lib/authentication/u2f/IU2fHandler.ts @@ -0,0 +1,9 @@ +import U2f = require("u2f"); + +export interface IU2fHandler { + request(appId: string, keyHandle?: string): U2f.Request; + checkRegistration(registrationRequest: U2f.Request, registrationResponse: U2f.RegistrationData) + : U2f.RegistrationResult | U2f.Error; + checkSignature(signatureRequest: U2f.Request, signatureResponse: U2f.SignatureData, publicKey: string) + : U2f.SignatureResult | U2f.Error; +} \ No newline at end of file diff --git a/server/src/lib/authentication/u2f/U2fHandler.ts b/server/src/lib/authentication/u2f/U2fHandler.ts new file mode 100644 index 000000000..bf3891e5b --- /dev/null +++ b/server/src/lib/authentication/u2f/U2fHandler.ts @@ -0,0 +1,24 @@ +import { IU2fHandler } from "./IU2fHandler"; +import U2f = require("u2f"); + +export class U2fHandler implements IU2fHandler { + private u2f: typeof U2f; + + constructor(u2f: typeof U2f) { + this.u2f = u2f; + } + + request(appId: string, keyHandle?: string): U2f.Request { + return this.u2f.request(appId, keyHandle); + } + + checkRegistration(registrationRequest: U2f.Request, registrationResponse: U2f.RegistrationData) + : U2f.RegistrationResult | U2f.Error { + return this.u2f.checkRegistration(registrationRequest, registrationResponse); + } + + checkSignature(signatureRequest: U2f.Request, signatureResponse: U2f.SignatureData, publicKey: string) + : U2f.SignatureResult | U2f.Error { + return this.u2f.checkSignature(signatureRequest, signatureResponse, publicKey); + } +} diff --git a/server/src/lib/regulation/IRegulator.ts b/server/src/lib/regulation/IRegulator.ts new file mode 100644 index 000000000..c49425b24 --- /dev/null +++ b/server/src/lib/regulation/IRegulator.ts @@ -0,0 +1,6 @@ +import BluebirdPromise = require("bluebird"); + +export interface IRegulator { + mark(userId: string, isAuthenticationSuccessful: boolean): BluebirdPromise; + regulate(userId: string): BluebirdPromise; +} \ No newline at end of file diff --git a/server/src/lib/AuthenticationRegulator.ts b/server/src/lib/regulation/Regulator.ts similarity index 87% rename from server/src/lib/AuthenticationRegulator.ts rename to server/src/lib/regulation/Regulator.ts index a04547335..1037a6a17 100644 --- a/server/src/lib/AuthenticationRegulator.ts +++ b/server/src/lib/regulation/Regulator.ts @@ -1,10 +1,11 @@ import * as BluebirdPromise from "bluebird"; -import exceptions = require("./Exceptions"); -import { IUserDataStore } from "./storage/IUserDataStore"; -import { AuthenticationTraceDocument } from "./storage/AuthenticationTraceDocument"; +import exceptions = require("../Exceptions"); +import { IUserDataStore } from "../storage/IUserDataStore"; +import { AuthenticationTraceDocument } from "../storage/AuthenticationTraceDocument"; +import { IRegulator } from "./IRegulator"; -export class AuthenticationRegulator { +export class Regulator implements IRegulator { private userDataStore: IUserDataStore; private banTime: number; private findTime: number; diff --git a/server/src/lib/routes/firstfactor/post.ts b/server/src/lib/routes/firstfactor/post.ts index e32ffc58e..1aacb50ce 100644 --- a/server/src/lib/routes/firstfactor/post.ts +++ b/server/src/lib/routes/firstfactor/post.ts @@ -4,7 +4,7 @@ import objectPath = require("object-path"); import BluebirdPromise = require("bluebird"); import express = require("express"); import { AccessController } from "../../access_control/AccessController"; -import { AuthenticationRegulator } from "../../AuthenticationRegulator"; +import { Regulator } from "../../regulation/Regulator"; import { GroupsAndEmails } from "../../ldap/IClient"; import Endpoint = require("../../../../../shared/api"); import ErrorReplies = require("../../ErrorReplies"); @@ -13,89 +13,89 @@ import AuthenticationSession = require("../../AuthenticationSession"); import Constants = require("../../../../../shared/constants"); import { DomainExtractor } from "../../utils/DomainExtractor"; import UserMessages = require("../../../../../shared/UserMessages"); +import { AuthenticationMethodCalculator } from "../../AuthenticationMethodCalculator"; +import { ServerVariables } from "../../ServerVariables"; -export default function (req: express.Request, res: express.Response): BluebirdPromise { - const username: string = req.body.username; - const password: string = req.body.password; +export default function (vars: ServerVariables) { + return function (req: express.Request, res: express.Response) + : BluebirdPromise { + const username: string = req.body.username; + const password: string = req.body.password; + let authSession: AuthenticationSession.AuthenticationSession; - const logger = ServerVariablesHandler.getLogger(req.app); - const ldap = ServerVariablesHandler.getLdapAuthenticator(req.app); - const config = ServerVariablesHandler.getConfiguration(req.app); - const regulator = ServerVariablesHandler.getAuthenticationRegulator(req.app); - const accessController = ServerVariablesHandler.getAccessController(req.app); - const authenticationMethodsCalculator = - ServerVariablesHandler.getAuthenticationMethodCalculator(req.app); - let authSession: AuthenticationSession.AuthenticationSession; - - return BluebirdPromise.resolve() - .then(function () { - if (!username || !password) { - return BluebirdPromise.reject(new Error("No username or password.")); - } - logger.info(req, "Starting authentication of user \"%s\"", username); - return AuthenticationSession.get(req); - }) - .then(function (_authSession: AuthenticationSession.AuthenticationSession) { - authSession = _authSession; - return regulator.regulate(username); - }) - .then(function () { - logger.info(req, "No regulation applied."); - return ldap.authenticate(username, password); - }) - .then(function (groupsAndEmails: GroupsAndEmails) { - logger.info(req, "LDAP binding successful. Retrieved information about user are %s", - JSON.stringify(groupsAndEmails)); - authSession.userid = username; - authSession.first_factor = true; - const redirectUrl = req.query[Constants.REDIRECT_QUERY_PARAM] !== "undefined" - // Fuck, don't know why it is a string! - ? req.query[Constants.REDIRECT_QUERY_PARAM] - : undefined; - - const emails: string[] = groupsAndEmails.emails; - const groups: string[] = groupsAndEmails.groups; - const redirectHost: string = DomainExtractor.fromUrl(redirectUrl); - const authMethod = authenticationMethodsCalculator.compute(redirectHost); - 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."; - logger.error(req, "%s", errMessage); - return BluebirdPromise.reject(new Error(errMessage)); - } - - authSession.email = emails[0]; - authSession.groups = groups; - - logger.debug(req, "Mark successful authentication to regulator."); - regulator.mark(username, true); - - if (authMethod == "basic_auth") { - res.send({ - redirect: redirectUrl - }); - logger.debug(req, "Redirect to '%s'", redirectUrl); - } - else if (authMethod == "two_factor") { - let newRedirectUrl = Endpoint.SECOND_FACTOR_GET; - if (redirectUrl) { - newRedirectUrl += "?" + Constants.REDIRECT_QUERY_PARAM + "=" - + encodeURIComponent(redirectUrl); + return BluebirdPromise.resolve() + .then(function () { + if (!username || !password) { + return BluebirdPromise.reject(new Error("No username or password.")); } - logger.debug(req, "Redirect to '%s'", newRedirectUrl, typeof redirectUrl); - res.send({ - redirect: newRedirectUrl - }); - } - else { - return BluebirdPromise.reject(new Error("Unknown authentication method for this domain.")); - } - return BluebirdPromise.resolve(); - }) - .catch(Exceptions.LdapBindError, function (err: Error) { - regulator.mark(username, false); - return ErrorReplies.replyWithError200(req, res, logger, UserMessages.OPERATION_FAILED)(err); - }) - .catch(ErrorReplies.replyWithError200(req, res, logger, UserMessages.OPERATION_FAILED)); -} + vars.logger.info(req, "Starting authentication of user \"%s\"", username); + return AuthenticationSession.get(req); + }) + .then(function (_authSession: AuthenticationSession.AuthenticationSession) { + authSession = _authSession; + return vars.regulator.regulate(username); + }) + .then(function () { + vars.logger.info(req, "No regulation applied."); + return vars.ldapAuthenticator.authenticate(username, password); + }) + .then(function (groupsAndEmails: GroupsAndEmails) { + vars.logger.info(req, "LDAP binding successful. Retrieved information about user are %s", + JSON.stringify(groupsAndEmails)); + authSession.userid = username; + authSession.first_factor = true; + const redirectUrl = req.query[Constants.REDIRECT_QUERY_PARAM] !== "undefined" + // Fuck, don't know why it is a string! + ? req.query[Constants.REDIRECT_QUERY_PARAM] + : undefined; + + 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); + + 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]; + authSession.groups = groups; + + vars.logger.debug(req, "Mark successful authentication to regulator."); + vars.regulator.mark(username, true); + + if (authMethod == "basic_auth") { + res.send({ + redirect: redirectUrl + }); + vars.logger.debug(req, "Redirect to '%s'", redirectUrl); + } + else if (authMethod == "two_factor") { + let newRedirectUrl = Endpoint.SECOND_FACTOR_GET; + if (redirectUrl) { + newRedirectUrl += "?" + Constants.REDIRECT_QUERY_PARAM + "=" + + encodeURIComponent(redirectUrl); + } + vars.logger.debug(req, "Redirect to '%s'", newRedirectUrl, typeof redirectUrl); + res.send({ + redirect: newRedirectUrl + }); + } + else { + return BluebirdPromise.reject(new Error("Unknown authentication method for this domain.")); + } + return BluebirdPromise.resolve(); + }) + .catch(Exceptions.LdapBindError, function (err: Error) { + vars.regulator.mark(username, false); + return ErrorReplies.replyWithError200(req, res, vars.logger, UserMessages.OPERATION_FAILED)(err); + }) + .catch(ErrorReplies.replyWithError200(req, res, vars.logger, UserMessages.OPERATION_FAILED)); + }; +} \ No newline at end of file diff --git a/server/src/lib/routes/secondfactor/redirect.ts b/server/src/lib/routes/secondfactor/redirect.ts index 4f258e8ab..cbfba27b7 100644 --- a/server/src/lib/routes/secondfactor/redirect.ts +++ b/server/src/lib/routes/secondfactor/redirect.ts @@ -7,6 +7,7 @@ import { ServerVariablesHandler } from "../../ServerVariablesHandler"; import AuthenticationSession = require("../../AuthenticationSession"); import BluebirdPromise = require("bluebird"); import ErrorReplies = require("../../ErrorReplies"); +import UserMessages = require("../../../../../shared/UserMessages"); export default function (req: express.Request, res: express.Response): BluebirdPromise { const logger = ServerVariablesHandler.getLogger(req.app); @@ -19,5 +20,5 @@ export default function (req: express.Request, res: express.Response): BluebirdP return BluebirdPromise.resolve(); }) .catch(ErrorReplies.replyWithError200(req, res, logger, - "Unexpected error.")); + UserMessages.OPERATION_FAILED)); } \ No newline at end of file diff --git a/server/src/lib/routes/secondfactor/totp/identity/RegistrationHandler.ts b/server/src/lib/routes/secondfactor/totp/identity/RegistrationHandler.ts index d289773fe..c63cbae7b 100644 --- a/server/src/lib/routes/secondfactor/totp/identity/RegistrationHandler.ts +++ b/server/src/lib/routes/secondfactor/totp/identity/RegistrationHandler.ts @@ -67,8 +67,8 @@ export default class RegistrationHandler implements IdentityValidable { } const userDataStore = ServerVariablesHandler.getUserDataStore(req.app); - const totpGenerator = ServerVariablesHandler.getTOTPGenerator(req.app); - const secret = totpGenerator.generate(); + const totpHandler = ServerVariablesHandler.getTotpHandler(req.app); + const secret = totpHandler.generate(); logger.debug(req, "Save the TOTP secret in DB"); return userDataStore.saveTOTPSecret(userid, secret) diff --git a/server/src/lib/routes/secondfactor/totp/sign/post.ts b/server/src/lib/routes/secondfactor/totp/sign/post.ts index d9b5afd9f..194242bbf 100644 --- a/server/src/lib/routes/secondfactor/totp/sign/post.ts +++ b/server/src/lib/routes/secondfactor/totp/sign/post.ts @@ -11,34 +11,34 @@ import ErrorReplies = require("../../../../ErrorReplies"); import { ServerVariablesHandler } from "./../../../../ServerVariablesHandler"; import AuthenticationSession = require("../../../../AuthenticationSession"); import UserMessages = require("../../../../../../../shared/UserMessages"); +import { ServerVariables } from "../../../../ServerVariables"; const UNAUTHORIZED_MESSAGE = "Unauthorized access"; -export default FirstFactorBlocker(handler); +export default function (vars: ServerVariables) { + function handler(req: express.Request, res: express.Response): BluebirdPromise { + let authSession: AuthenticationSession.AuthenticationSession; + const token = req.body.token; -export function handler(req: express.Request, res: express.Response): BluebirdPromise { - let authSession: AuthenticationSession.AuthenticationSession; - const logger = ServerVariablesHandler.getLogger(req.app); - const token = req.body.token; - const totpValidator = ServerVariablesHandler.getTOTPValidator(req.app); - const userDataStore = ServerVariablesHandler.getUserDataStore(req.app); + return AuthenticationSession.get(req) + .then(function (_authSession: AuthenticationSession.AuthenticationSession) { + authSession = _authSession; + vars.logger.info(req, "Initiate TOTP validation for user '%s'.", authSession.userid); + return vars.userDataStore.retrieveTOTPSecret(authSession.userid); + }) + .then(function (doc: TOTPSecretDocument) { + vars.logger.debug(req, "TOTP secret is %s", JSON.stringify(doc)); - return AuthenticationSession.get(req) - .then(function (_authSession: AuthenticationSession.AuthenticationSession) { - authSession = _authSession; - logger.info(req, "Initiate TOTP validation for user '%s'.", authSession.userid); - return userDataStore.retrieveTOTPSecret(authSession.userid); - }) - .then(function (doc: TOTPSecretDocument) { - logger.debug(req, "TOTP secret is %s", JSON.stringify(doc)); - return totpValidator.validate(token, doc.secret.base32); - }) - .then(function () { - logger.debug(req, "TOTP validation succeeded."); - authSession.second_factor = true; - redirect(req, res); - return BluebirdPromise.resolve(); - }) - .catch(ErrorReplies.replyWithError200(req, res, logger, - UserMessages.OPERATION_FAILED)); + if (!vars.totpHandler.validate(token, doc.secret.base32)) + return BluebirdPromise.reject(new Error("Invalid TOTP token.")); + + vars.logger.debug(req, "TOTP validation succeeded."); + authSession.second_factor = true; + redirect(req, res); + return BluebirdPromise.resolve(); + }) + .catch(ErrorReplies.replyWithError200(req, res, vars.logger, + UserMessages.OPERATION_FAILED)); + } + return FirstFactorBlocker(handler); } diff --git a/server/src/lib/routes/verify/get.ts b/server/src/lib/routes/verify/get.ts index 87c40f891..1afb53852 100644 --- a/server/src/lib/routes/verify/get.ts +++ b/server/src/lib/routes/verify/get.ts @@ -6,19 +6,22 @@ import exceptions = require("../../Exceptions"); import winston = require("winston"); import AuthenticationValidator = require("../../AuthenticationValidator"); import ErrorReplies = require("../../ErrorReplies"); -import { ServerVariablesHandler } from "../../ServerVariablesHandler"; +import { AppConfiguration } from "../../configuration/Configuration"; import AuthenticationSession = require("../../AuthenticationSession"); import Constants = require("../../../../../shared/constants"); import Util = require("util"); import { DomainExtractor } from "../../utils/DomainExtractor"; +import { ServerVariables } from "../../ServerVariables"; +import { AuthenticationMethodCalculator } from "../../AuthenticationMethodCalculator"; const FIRST_FACTOR_NOT_VALIDATED_MESSAGE = "First factor not yet validated"; const SECOND_FACTOR_NOT_VALIDATED_MESSAGE = "Second factor not yet validated"; -function verify_filter(req: express.Request, res: express.Response): BluebirdPromise { - const logger = ServerVariablesHandler.getLogger(req.app); - const accessController = ServerVariablesHandler.getAccessController(req.app); - const authenticationMethodsCalculator = ServerVariablesHandler.getAuthenticationMethodCalculator(req.app); +const REMOTE_USER = "Remote-User"; +const REMOTE_GROUPS = "Remote-Groups"; + +function verify_filter(req: express.Request, res: express.Response, + vars: ServerVariables): BluebirdPromise { return AuthenticationSession.get(req) .then(function (authSession) { @@ -35,15 +38,17 @@ function verify_filter(req: express.Request, res: express.Response): BluebirdPro const path = objectPath.get(req, "headers.x-original-uri"); const domain = DomainExtractor.fromHostHeader(host); - const authenticationMethod = authenticationMethodsCalculator.compute(domain); - logger.debug(req, "domain=%s, path=%s, user=%s, groups=%s", domain, path, + 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(",")); if (!authSession.first_factor) return BluebirdPromise.reject( new exceptions.AccessDeniedError(FIRST_FACTOR_NOT_VALIDATED_MESSAGE)); - const isAllowed = accessController.isAccessAllowed(domain, path, username, groups); + 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))); @@ -52,25 +57,27 @@ function verify_filter(req: express.Request, res: express.Response): BluebirdPro return BluebirdPromise.reject( new exceptions.AccessDeniedError(SECOND_FACTOR_NOT_VALIDATED_MESSAGE)); - res.setHeader("Remote-User", username); - res.setHeader("Remote-Groups", groups.join(",")); + res.setHeader(REMOTE_USER, username); + res.setHeader(REMOTE_GROUPS, groups.join(",")); return BluebirdPromise.resolve(); }); } -export default function (req: express.Request, res: express.Response): BluebirdPromise { - const logger = ServerVariablesHandler.getLogger(req.app); - return verify_filter(req, res) - .then(function () { - res.status(204); - res.send(); - return BluebirdPromise.resolve(); - }) - // The user is authenticated but has restricted access -> 403 - .catch(exceptions.DomainAccessDenied, ErrorReplies - .replyWithError403(req, res, logger)) - // The user is not yet authenticated -> 401 - .catch(ErrorReplies.replyWithError401(req, res, logger)); +export default function (vars: ServerVariables) { + return function (req: express.Request, res: express.Response) + : BluebirdPromise { + return verify_filter(req, res, vars) + .then(function () { + res.status(204); + res.send(); + return BluebirdPromise.resolve(); + }) + // The user is authenticated but has restricted access -> 403 + .catch(exceptions.DomainAccessDenied, ErrorReplies + .replyWithError403(req, res, vars.logger)) + // The user is not yet authenticated -> 401 + .catch(ErrorReplies.replyWithError401(req, res, vars.logger)); + }; } diff --git a/server/test/ServerConfiguration.test.ts b/server/test/ServerConfiguration.test.ts index 715363520..bc7f705b6 100644 --- a/server/test/ServerConfiguration.test.ts +++ b/server/test/ServerConfiguration.test.ts @@ -1,5 +1,5 @@ -import assert = require("assert"); +import Assert = require("assert"); import Sinon = require("sinon"); import nedb = require("nedb"); import express = require("express"); @@ -73,9 +73,10 @@ describe("test server configuration", function () { }; const server = new Server(deps); - server.start(config, deps); - - assert(sessionMock.calledOnce); - assert.equal(sessionMock.getCall(0).args[0].cookie.domain, "example.com"); + server.start(config, deps) + .then(function () { + Assert(sessionMock.calledOnce); + Assert.equal(sessionMock.getCall(0).args[0].cookie.domain, "example.com"); + }); }); }); diff --git a/server/test/TOTPValidator.test.ts b/server/test/TOTPValidator.test.ts deleted file mode 100644 index 2856583f1..000000000 --- a/server/test/TOTPValidator.test.ts +++ /dev/null @@ -1,41 +0,0 @@ - -import { TOTPValidator } from "../src/lib/TOTPValidator"; -import Sinon = require("sinon"); -import Speakeasy = require("speakeasy"); - -describe("test TOTP validation", function() { - let totpValidator: TOTPValidator; - let totpValidateStub: Sinon.SinonStub; - - beforeEach(() => { - totpValidateStub = Sinon.stub(Speakeasy.totp, "verify"); - totpValidator = new TOTPValidator(Speakeasy); - }); - - afterEach(function() { - totpValidateStub.restore(); - }); - - it("should validate the TOTP token", function() { - const totp_secret = "NBD2ZV64R9UV1O7K"; - const token = "token"; - totpValidateStub.withArgs({ - secret: totp_secret, - token: token, - encoding: "base32", - window: 1 - }).returns(true); - return totpValidator.validate(token, totp_secret); - }); - - it("should not validate a wrong TOTP token", function(done) { - const totp_secret = "NBD2ZV64R9UV1O7K"; - const token = "wrong token"; - totpValidateStub.returns(false); - totpValidator.validate(token, totp_secret) - .catch(function() { - done(); - }); - }); -}); - diff --git a/server/test/authentication/totp/Validator.test.ts b/server/test/authentication/totp/Validator.test.ts new file mode 100644 index 000000000..3d0518440 --- /dev/null +++ b/server/test/authentication/totp/Validator.test.ts @@ -0,0 +1,39 @@ + +import { TotpHandler } from "../../../src/lib/authentication/totp/TotpHandler"; +import Sinon = require("sinon"); +import Speakeasy = require("speakeasy"); +import Assert = require("assert"); + +describe("test TOTP validation", function() { + let totpValidator: TotpHandler; + let validateStub: Sinon.SinonStub; + + beforeEach(() => { + validateStub = Sinon.stub(Speakeasy.totp, "verify"); + totpValidator = new TotpHandler(Speakeasy); + }); + + afterEach(function() { + validateStub.restore(); + }); + + it("should validate the TOTP token", function() { + const totp_secret = "NBD2ZV64R9UV1O7K"; + const token = "token"; + validateStub.withArgs({ + secret: totp_secret, + token: token, + encoding: "base32", + window: 1 + }).returns(true); + Assert(totpValidator.validate(token, totp_secret)); + }); + + it("should not validate a wrong TOTP token", function() { + const totp_secret = "NBD2ZV64R9UV1O7K"; + const token = "wrong token"; + validateStub.returns(false); + Assert(!totpValidator.validate(token, totp_secret)); + }); +}); + diff --git a/server/test/mocks/NotifierStub.ts b/server/test/mocks/NotifierStub.ts new file mode 100644 index 000000000..94a178e82 --- /dev/null +++ b/server/test/mocks/NotifierStub.ts @@ -0,0 +1,16 @@ +import Sinon = require("sinon"); +import BluebirdPromise = require("bluebird"); + +import { INotifier } from "../../src/lib/notifiers/INotifier"; + +export class NotifierStub implements INotifier { + notifyStub: Sinon.SinonStub; + + constructor() { + this.notifyStub = Sinon.stub(); + } + + notify(to: string, subject: string, link: string): BluebirdPromise { + return this.notifyStub(to, subject, link); + } +} \ No newline at end of file diff --git a/server/test/mocks/RegulatorStub.ts b/server/test/mocks/RegulatorStub.ts new file mode 100644 index 000000000..203c0e23a --- /dev/null +++ b/server/test/mocks/RegulatorStub.ts @@ -0,0 +1,21 @@ +import Sinon = require("sinon"); +import BluebirdPromise = require("bluebird"); +import { IRegulator } from "../../src/lib/regulation/IRegulator"; + +export class RegulatorStub implements IRegulator { + markStub: Sinon.SinonStub; + regulateStub: Sinon.SinonStub; + + constructor() { + this.markStub = Sinon.stub(); + this.regulateStub = Sinon.stub(); + } + + mark(userId: string, isAuthenticationSuccessful: boolean): BluebirdPromise { + return this.markStub(userId, isAuthenticationSuccessful); + } + + regulate(userId: string): BluebirdPromise { + return this.regulateStub(userId); + } +} \ No newline at end of file diff --git a/server/test/mocks/ServerVariablesMockBuilder.ts b/server/test/mocks/ServerVariablesMockBuilder.ts new file mode 100644 index 000000000..accfb0652 --- /dev/null +++ b/server/test/mocks/ServerVariablesMockBuilder.ts @@ -0,0 +1,91 @@ +import { ServerVariables } from "../../src/lib/ServerVariables"; + +import { AppConfiguration } from "../../src/lib/configuration/Configuration"; +import { AuthenticatorStub } from "./ldap/AuthenticatorStub"; +import { EmailsRetrieverStub } from "./ldap/EmailsRetrieverStub"; +import { PasswordUpdaterStub } from "./ldap/PasswordUpdaterStub"; +import { AccessControllerStub } from "./AccessControllerStub"; +import { RequestLoggerStub } from "./RequestLoggerStub"; +import { NotifierStub } from "./NotifierStub"; +import { RegulatorStub } from "./RegulatorStub"; +import { TotpHandlerStub } from "./TotpHandlerStub"; +import { UserDataStoreStub } from "./storage/UserDataStoreStub"; +import { U2fHandlerStub } from "./U2fHandlerStub"; + +export interface ServerVariablesMock { + accessController: AccessControllerStub; + config: AppConfiguration; + ldapAuthenticator: AuthenticatorStub; + ldapEmailsRetriever: EmailsRetrieverStub; + ldapPasswordUpdater: PasswordUpdaterStub; + logger: RequestLoggerStub; + notifier: NotifierStub; + regulator: RegulatorStub; + totpHandler: TotpHandlerStub; + userDataStore: UserDataStoreStub; + u2f: U2fHandlerStub; +} + +export class ServerVariablesMockBuilder { + static build(): { variables: ServerVariables, mocks: ServerVariablesMock} { + const mocks: ServerVariablesMock = { + accessController: new AccessControllerStub(), + config: { + access_control: {}, + authentication_methods: { + default_method: "two_factor" + }, + ldap: { + url: "ldap://ldap", + user: "user", + password: "password", + mail_attribute: "mail", + users_dn: "ou=users,dc=example,dc=com", + groups_dn: "ou=groups,dc=example,dc=com", + users_filter: "cn={0}", + groups_filter: "member={dn}", + group_name_attribute: "cn" + }, + logs_level: "debug", + notifier: {}, + port: 8080, + regulation: { + ban_time: 50, + find_time: 50, + max_retries: 3 + }, + session: { + secret: "my_secret" + }, + storage: {} + }, + ldapAuthenticator: new AuthenticatorStub(), + ldapEmailsRetriever: new EmailsRetrieverStub(), + ldapPasswordUpdater: new PasswordUpdaterStub(), + logger: new RequestLoggerStub(), + notifier: new NotifierStub(), + regulator: new RegulatorStub(), + totpHandler: new TotpHandlerStub(), + userDataStore: new UserDataStoreStub(), + u2f: new U2fHandlerStub() + }; + const vars: ServerVariables = { + accessController: mocks.accessController, + config: mocks.config, + ldapAuthenticator: mocks.ldapAuthenticator, + ldapEmailsRetriever: mocks.ldapEmailsRetriever, + ldapPasswordUpdater: mocks.ldapPasswordUpdater, + logger: mocks.logger, + notifier: mocks.notifier, + regulator: mocks.regulator, + totpHandler: mocks.totpHandler, + userDataStore: mocks.userDataStore, + u2f: mocks.u2f + }; + + return { + variables: vars, + mocks: mocks + }; + } +} \ No newline at end of file diff --git a/server/test/mocks/TotpHandlerStub.ts b/server/test/mocks/TotpHandlerStub.ts new file mode 100644 index 000000000..276dbb15a --- /dev/null +++ b/server/test/mocks/TotpHandlerStub.ts @@ -0,0 +1,22 @@ +import Sinon = require("sinon"); +import BluebirdPromise = require("bluebird"); +import { ITotpHandler, GenerateSecretOptions } from "../../src/lib/authentication/totp/ITotpHandler"; +import { TOTPSecret } from "../../types/TOTPSecret"; + +export class TotpHandlerStub implements ITotpHandler { + generateStub: Sinon.SinonStub; + validateStub: Sinon.SinonStub; + + constructor() { + this.generateStub = Sinon.stub(); + this.validateStub = Sinon.stub(); + } + + generate(options?: GenerateSecretOptions): TOTPSecret { + return this.generateStub(options); + } + + validate(token: string, secret: string): boolean { + return this.validateStub(token, secret); + } +} \ No newline at end of file diff --git a/server/test/mocks/U2fHandlerStub.ts b/server/test/mocks/U2fHandlerStub.ts new file mode 100644 index 000000000..95256641f --- /dev/null +++ b/server/test/mocks/U2fHandlerStub.ts @@ -0,0 +1,31 @@ +import Sinon = require("sinon"); +import BluebirdPromise = require("bluebird"); +import U2f = require("u2f"); +import { IU2fHandler } from "../../src/lib/authentication/u2f/IU2fHandler"; + + +export class U2fHandlerStub implements IU2fHandler { + requestStub: Sinon.SinonStub; + checkRegistrationStub: Sinon.SinonStub; + checkSignatureStub: Sinon.SinonStub; + + constructor() { + this.requestStub = Sinon.stub(); + this.checkRegistrationStub = Sinon.stub(); + this.checkSignatureStub = Sinon.stub(); + } + + request(appId: string, keyHandle?: string): U2f.Request { + return this.requestStub(appId, keyHandle); + } + + checkRegistration(registrationRequest: U2f.Request, registrationResponse: U2f.RegistrationData) + : U2f.RegistrationResult | U2f.Error { + return this.checkRegistrationStub(registrationRequest, registrationResponse); + } + + checkSignature(signatureRequest: U2f.Request, signatureResponse: U2f.SignatureData, publicKey: string) + : U2f.SignatureResult | U2f.Error { + return this.checkSignatureStub(signatureRequest, signatureResponse, publicKey); + } +} \ No newline at end of file diff --git a/server/test/mocks/ldap/AuthenticatorStub.ts b/server/test/mocks/ldap/AuthenticatorStub.ts new file mode 100644 index 000000000..4102b0fe8 --- /dev/null +++ b/server/test/mocks/ldap/AuthenticatorStub.ts @@ -0,0 +1,16 @@ +import BluebirdPromise = require("bluebird"); +import { IAuthenticator } from "../../../src/lib/ldap/IAuthenticator"; +import { GroupsAndEmails } from "../../../src/lib/ldap/IClient"; +import Sinon = require("sinon"); + +export class AuthenticatorStub implements IAuthenticator { + authenticateStub: Sinon.SinonStub; + + constructor() { + this.authenticateStub = Sinon.stub(); + } + + authenticate(username: string, password: string): BluebirdPromise { + return this.authenticateStub(username, password); + } +} \ No newline at end of file diff --git a/server/test/mocks/ldap/EmailsRetrieverStub.ts b/server/test/mocks/ldap/EmailsRetrieverStub.ts new file mode 100644 index 000000000..0e2b87546 --- /dev/null +++ b/server/test/mocks/ldap/EmailsRetrieverStub.ts @@ -0,0 +1,16 @@ +import BluebirdPromise = require("bluebird"); +import { IClient } from "../../../src/lib/ldap/IClient"; +import { IEmailsRetriever } from "../../../src/lib/ldap/IEmailsRetriever"; +import Sinon = require("sinon"); + +export class EmailsRetrieverStub implements IEmailsRetriever { + retrieveStub: Sinon.SinonStub; + + constructor() { + this.retrieveStub = Sinon.stub(); + } + + retrieve(username: string, client?: IClient): BluebirdPromise { + return this.retrieveStub(username, client); + } +} \ No newline at end of file diff --git a/server/test/mocks/ldap/PasswordUpdaterStub.ts b/server/test/mocks/ldap/PasswordUpdaterStub.ts new file mode 100644 index 000000000..9443dddbb --- /dev/null +++ b/server/test/mocks/ldap/PasswordUpdaterStub.ts @@ -0,0 +1,16 @@ +import BluebirdPromise = require("bluebird"); +import { IClient } from "../../../src/lib/ldap/IClient"; +import { IPasswordUpdater } from "../../../src/lib/ldap/IPasswordUpdater"; +import Sinon = require("sinon"); + +export class PasswordUpdaterStub implements IPasswordUpdater { + updatePasswordStub: Sinon.SinonStub; + + constructor() { + this.updatePasswordStub = Sinon.stub(); + } + + updatePassword(username: string, newPassword: string): BluebirdPromise { + return this.updatePasswordStub(username, newPassword); + } +} \ No newline at end of file diff --git a/server/test/AuthenticationRegulator.test.ts b/server/test/regulation/Regulator.test.ts similarity index 84% rename from server/test/AuthenticationRegulator.test.ts rename to server/test/regulation/Regulator.test.ts index f3e40aa2d..5f10fd056 100644 --- a/server/test/AuthenticationRegulator.test.ts +++ b/server/test/regulation/Regulator.test.ts @@ -3,10 +3,10 @@ import Sinon = require("sinon"); import BluebirdPromise = require("bluebird"); import Assert = require("assert"); -import { AuthenticationRegulator } from "../src/lib/AuthenticationRegulator"; +import { Regulator } from "../../src/lib/regulation/Regulator"; import MockDate = require("mockdate"); -import exceptions = require("../src/lib/Exceptions"); -import { UserDataStoreStub } from "./mocks/storage/UserDataStoreStub"; +import exceptions = require("../../src/lib/Exceptions"); +import { UserDataStoreStub } from "../mocks/storage/UserDataStoreStub"; describe("test authentication regulator", function () { const USER1 = "USER1"; @@ -39,13 +39,13 @@ describe("test authentication regulator", function () { MockDate.reset(); }); - function markAuthenticationAt(regulator: AuthenticationRegulator, user: string, time: string, success: boolean) { + function markAuthenticationAt(regulator: Regulator, user: string, time: string, success: boolean) { MockDate.set(time); return regulator.mark(user, success); } it("should mark 2 authentication and regulate (accept)", function () { - const regulator = new AuthenticationRegulator(userDataStoreStub, 3, 10, 10); + const regulator = new Regulator(userDataStoreStub, 3, 10, 10); return regulator.mark(USER1, false) .then(function () { @@ -57,7 +57,7 @@ describe("test authentication regulator", function () { }); it("should mark 3 authentications and regulate (reject)", function () { - const regulator = new AuthenticationRegulator(userDataStoreStub, 3, 10, 10); + const regulator = new Regulator(userDataStoreStub, 3, 10, 10); return regulator.mark(USER1, false) .then(function () { @@ -76,7 +76,7 @@ describe("test authentication regulator", function () { }); it("should mark 1 failed, 1 successful and 1 failed authentications within minimum time and regulate (accept)", function () { - const regulator = new AuthenticationRegulator(userDataStoreStub, 3, 60, 30); + const regulator = new Regulator(userDataStoreStub, 3, 60, 30); return markAuthenticationAt(regulator, USER1, "1/2/2000 00:00:00", false) .then(function () { @@ -109,7 +109,7 @@ describe("test authentication regulator", function () { }); it("should regulate user if number of failures is greater than 3 in allowed time lapse", function () { - function markAuthentications(regulator: AuthenticationRegulator, user: string) { + function markAuthentications(regulator: Regulator, user: string) { return markAuthenticationAt(regulator, user, "1/2/2000 00:00:00", false) .then(function () { return markAuthenticationAt(regulator, user, "1/2/2000 00:00:45", false); @@ -122,8 +122,8 @@ describe("test authentication regulator", function () { }); } - const regulator1 = new AuthenticationRegulator(userDataStoreStub, 3, 60, 60); - const regulator2 = new AuthenticationRegulator(userDataStoreStub, 3, 2 * 60, 60); + const regulator1 = new Regulator(userDataStoreStub, 3, 60, 60); + const regulator2 = new Regulator(userDataStoreStub, 3, 2 * 60, 60); const p1 = markAuthentications(regulator1, USER1); const p2 = markAuthentications(regulator2, USER2); @@ -138,7 +138,7 @@ describe("test authentication regulator", function () { }); it("should user wait after regulation to authenticate again", function () { - function markAuthentications(regulator: AuthenticationRegulator, user: string) { + function markAuthentications(regulator: Regulator, user: string) { return markAuthenticationAt(regulator, user, "1/2/2000 00:00:00", false) .then(function () { return markAuthenticationAt(regulator, user, "1/2/2000 00:00:10", false); @@ -161,13 +161,13 @@ describe("test authentication regulator", function () { }); } - const regulator = new AuthenticationRegulator(userDataStoreStub, 4, 30, 30); + const regulator = new Regulator(userDataStoreStub, 4, 30, 30); return markAuthentications(regulator, USER1); }); it("should disable regulation when max_retries is set to 0", function () { const maxRetries = 0; - const regulator = new AuthenticationRegulator(userDataStoreStub, maxRetries, 60, 30); + const regulator = new Regulator(userDataStoreStub, maxRetries, 60, 30); return markAuthenticationAt(regulator, USER1, "1/2/2000 00:00:00", false) .then(function () { return markAuthenticationAt(regulator, USER1, "1/2/2000 00:00:10", false); diff --git a/server/test/routes/firstfactor/post.test.ts b/server/test/routes/firstfactor/post.test.ts index 98de498bb..eb5fafec3 100644 --- a/server/test/routes/firstfactor/post.test.ts +++ b/server/test/routes/firstfactor/post.test.ts @@ -1,8 +1,8 @@ -import sinon = require("sinon"); +import Sinon = require("sinon"); import BluebirdPromise = require("bluebird"); import Assert = require("assert"); -import winston = require("winston"); +import Winston = require("winston"); import FirstFactorPost = require("../../../src/lib/routes/firstfactor/post"); import exceptions = require("../../../src/lib/Exceptions"); @@ -12,7 +12,7 @@ import Endpoints = require("../../../../shared/api"); import AuthenticationRegulatorMock = require("../../mocks/AuthenticationRegulator"); import { AccessControllerStub } from "../../mocks/AccessControllerStub"; import ExpressMock = require("../../mocks/express"); -import ServerVariablesMock = require("../../mocks/ServerVariablesMock"); +import { ServerVariablesMock, ServerVariablesMockBuilder } from "../../mocks/ServerVariablesMockBuilder"; import { ServerVariables } from "../../../src/lib/ServerVariables"; describe("test the first factor validation route", function () { @@ -20,32 +20,23 @@ describe("test the first factor validation route", function () { let res: ExpressMock.ResponseMock; let emails: string[]; let groups: string[]; - let configuration; - let regulator: AuthenticationRegulatorMock.AuthenticationRegulatorMock; - let accessController: AccessControllerStub; - let serverVariables: ServerVariables; + let vars: ServerVariables; + let mocks: ServerVariablesMock; beforeEach(function () { - configuration = { - ldap: { - base_dn: "ou=users,dc=example,dc=com", - user_name_attribute: "uid" - } - }; - emails = ["test_ok@example.com"]; groups = ["group1", "group2" ]; + const s = ServerVariablesMockBuilder.build(); + mocks = s.mocks; + vars = s.variables; - accessController = new AccessControllerStub(); - accessController.isAccessAllowedMock.returns(true); - - regulator = AuthenticationRegulatorMock.AuthenticationRegulatorMock(); - regulator.regulate.returns(BluebirdPromise.resolve()); - regulator.mark.returns(BluebirdPromise.resolve()); + mocks.accessController.isAccessAllowedMock.returns(true); + mocks.regulator.regulateStub.returns(BluebirdPromise.resolve()); + mocks.regulator.markStub.returns(BluebirdPromise.resolve()); req = { app: { - get: sinon.stub().returns({ logger: winston }) + get: Sinon.stub().returns({ logger: Winston }) }, body: { username: "username", @@ -62,20 +53,11 @@ describe("test the first factor validation route", function () { }; AuthenticationSession.reset(req as any); - - serverVariables = ServerVariablesMock.mock(req.app); - serverVariables.ldapAuthenticator = { - authenticate: sinon.stub() - } as any; - serverVariables.config = configuration as any; - serverVariables.regulator = regulator as any; - serverVariables.accessController = accessController as any; - res = ExpressMock.ResponseMock(); }); it("should reply with 204 if success", function () { - (serverVariables.ldapAuthenticator as any).authenticate.withArgs("username", "password") + mocks.ldapAuthenticator.authenticateStub.withArgs("username", "password") .returns(BluebirdPromise.resolve({ emails: emails, groups: groups @@ -84,7 +66,7 @@ describe("test the first factor validation route", function () { return AuthenticationSession.get(req as any) .then(function (_authSession: AuthenticationSession.AuthenticationSession) { authSession = _authSession; - return FirstFactorPost.default(req as any, res as any); + return FirstFactorPost.default(vars)(req as any, res as any); }) .then(function () { Assert.equal("username", authSession.userid); @@ -93,15 +75,15 @@ describe("test the first factor validation route", function () { }); it("should retrieve email from LDAP", function () { - (serverVariables.ldapAuthenticator as any).authenticate.withArgs("username", "password") + mocks.ldapAuthenticator.authenticateStub.withArgs("username", "password") .returns(BluebirdPromise.resolve([{ mail: ["test@example.com"] }])); - return FirstFactorPost.default(req as any, res as any); + return FirstFactorPost.default(vars)(req as any, res as any); }); it("should set first email address as user session variable", function () { const emails = ["test_ok@example.com"]; let authSession: AuthenticationSession.AuthenticationSession; - (serverVariables.ldapAuthenticator as any).authenticate.withArgs("username", "password") + mocks.ldapAuthenticator.authenticateStub.withArgs("username", "password") .returns(BluebirdPromise.resolve({ emails: emails, groups: groups @@ -110,7 +92,7 @@ describe("test the first factor validation route", function () { return AuthenticationSession.get(req as any) .then(function (_authSession: AuthenticationSession.AuthenticationSession) { authSession = _authSession; - return FirstFactorPost.default(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); @@ -118,13 +100,13 @@ describe("test the first factor validation route", function () { }); it("should return error message when LDAP authenticator throws", function () { - (serverVariables.ldapAuthenticator as any).authenticate.withArgs("username", "password") + mocks.ldapAuthenticator.authenticateStub.withArgs("username", "password") .returns(BluebirdPromise.reject(new exceptions.LdapBindError("Bad credentials"))); - return FirstFactorPost.default(req as any, res as any) + return FirstFactorPost.default(vars)(req as any, res as any) .then(function () { Assert.equal(res.status.getCall(0).args[0], 200); - Assert.equal(regulator.mark.getCall(0).args[0], "username"); + Assert.equal(mocks.regulator.markStub.getCall(0).args[0], "username"); Assert.deepEqual(res.send.getCall(0).args[0], { error: "Operation failed." }); @@ -133,8 +115,8 @@ describe("test the first factor validation route", function () { it("should return error message when regulator rejects authentication", function () { const err = new exceptions.AuthenticationRegulationError("Authentication regulation..."); - regulator.regulate.returns(BluebirdPromise.reject(err)); - return FirstFactorPost.default(req as any, res as any) + mocks.regulator.regulateStub.returns(BluebirdPromise.reject(err)); + return FirstFactorPost.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], { diff --git a/server/test/routes/secondfactor/totp/sign/post.test.ts b/server/test/routes/secondfactor/totp/sign/post.test.ts index 22bfa5134..373308c3f 100644 --- a/server/test/routes/secondfactor/totp/sign/post.test.ts +++ b/server/test/routes/secondfactor/totp/sign/post.test.ts @@ -1,41 +1,44 @@ import BluebirdPromise = require("bluebird"); -import sinon = require("sinon"); +import Sinon = require("sinon"); import assert = require("assert"); import winston = require("winston"); import exceptions = require("../../../../../src/lib/Exceptions"); import AuthenticationSession = require("../../../../../src/lib/AuthenticationSession"); import SignPost = require("../../../../../src/lib/routes/secondfactor/totp/sign/post"); +import { ServerVariables } from "../../../../../src/lib/ServerVariables"; import ExpressMock = require("../../../../mocks/express"); -import TOTPValidatorMock = require("../../../../mocks/TOTPValidator"); -import ServerVariablesMock = require("../../../../mocks/ServerVariablesMock"); import { UserDataStoreStub } from "../../../../mocks/storage/UserDataStoreStub"; +import { ServerVariablesMock, ServerVariablesMockBuilder } from "../../../../mocks/ServerVariablesMockBuilder"; describe("test totp route", function () { let req: ExpressMock.RequestMock; let res: ExpressMock.ResponseMock; - let totpValidator: TOTPValidatorMock.TOTPValidatorMock; let authSession: AuthenticationSession.AuthenticationSession; + let vars: ServerVariables; + let mocks: ServerVariablesMock; beforeEach(function () { - const app_get = sinon.stub(); + const s = ServerVariablesMockBuilder.build(); + vars = s.variables; + mocks = s.mocks; + const app_get = Sinon.stub(); req = { app: { - get: sinon.stub().returns({ logger: winston }) + get: Sinon.stub().returns({ logger: winston }) }, body: { token: "abc" }, - session: {} + session: {}, + query: { + redirect: "http://redirect" + } }; - AuthenticationSession.reset(req as any); - const mocks = ServerVariablesMock.mock(req.app); res = ExpressMock.ResponseMock(); - - const config = { totp_secret: "secret" }; - totpValidator = TOTPValidatorMock.TOTPValidatorMock(); + AuthenticationSession.reset(req as any); const doc = { userid: "user", @@ -44,9 +47,6 @@ describe("test totp route", function () { } }; mocks.userDataStore.retrieveTOTPSecretStub.returns(BluebirdPromise.resolve(doc)); - mocks.totpValidator = totpValidator; - mocks.config = config; - return AuthenticationSession.get(req as any) .then(function (_authSession: AuthenticationSession.AuthenticationSession) { authSession = _authSession; @@ -58,8 +58,8 @@ describe("test totp route", function () { it("should send status code 200 when totp is valid", function () { - totpValidator.validate.returns(BluebirdPromise.resolve("ok")); - return SignPost.default(req as any, res as any) + mocks.totpHandler.validateStub.returns(true); + return SignPost.default(vars)(req as any, res as any) .then(function () { assert.equal(true, authSession.second_factor); return BluebirdPromise.resolve(); @@ -67,8 +67,8 @@ describe("test totp route", function () { }); it("should send error message when totp is not valid", function () { - totpValidator.validate.returns(BluebirdPromise.reject(new exceptions.InvalidTOTPError("Bad TOTP token"))); - return SignPost.default(req as any, res as any) + 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); @@ -80,9 +80,9 @@ describe("test totp route", function () { }); it("should send status code 401 when session has not been initiated", function () { - totpValidator.validate.returns(BluebirdPromise.resolve("abc")); + mocks.totpHandler.validateStub.returns(true); req.session = {}; - return SignPost.default(req as any, res as any) + 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]); diff --git a/server/test/routes/verify/get.test.ts b/server/test/routes/verify/get.test.ts index 7167199b1..cc89a2fee 100644 --- a/server/test/routes/verify/get.test.ts +++ b/server/test/routes/verify/get.test.ts @@ -4,27 +4,21 @@ import VerifyGet = require("../../../src/lib/routes/verify/get"); import AuthenticationSession = require("../../../src/lib/AuthenticationSession"); import { AuthenticationMethodCalculator } from "../../../src/lib/AuthenticationMethodCalculator"; import { AuthenticationMethodsConfiguration } from "../../../src/lib/configuration/Configuration"; - import Sinon = require("sinon"); import winston = require("winston"); import BluebirdPromise = require("bluebird"); - import express = require("express"); - import ExpressMock = require("../../mocks/express"); -import { AccessControllerStub } from "../../mocks/AccessControllerStub"; -import ServerVariablesMock = require("../../mocks/ServerVariablesMock"); +import { ServerVariables } from "../../../src/lib/ServerVariables"; +import { ServerVariablesMockBuilder, ServerVariablesMock } from "../../mocks/ServerVariablesMockBuilder"; -describe("test authentication token verification", function () { +describe("test /verify endpoint", function () { let req: ExpressMock.RequestMock; let res: ExpressMock.ResponseMock; - let accessController: AccessControllerStub; - let mocks: any; + let mocks: ServerVariablesMock; + let vars: ServerVariables; beforeEach(function () { - accessController = new AccessControllerStub(); - accessController.isAccessAllowedMock.returns(true); - req = ExpressMock.RequestMock(); res = ExpressMock.ResponseMock(); req.session = {}; @@ -37,20 +31,14 @@ describe("test authentication token verification", function () { AuthenticationSession.reset(req as any); req.headers = {}; req.headers.host = "secret.example.com"; - mocks = ServerVariablesMock.mock(req.app); - mocks.config = {} as any; - mocks.accessController = accessController as any; - const options: AuthenticationMethodsConfiguration = { - default_method: "two_factor", - per_subdomain_methods: { - "redirect.url": "basic_auth" - } - }; - mocks.authenticationMethodsCalculator = new AuthenticationMethodCalculator(options); + const s = ServerVariablesMockBuilder.build(); + mocks = s.mocks; + vars = s.variables; }); it("should be already authenticated", function () { req.session = {}; + mocks.accessController.isAccessAllowedMock.returns(true); AuthenticationSession.reset(req as any); return AuthenticationSession.get(req as any) .then(function (authSession: AuthenticationSession.AuthenticationSession) { @@ -58,7 +46,7 @@ describe("test authentication token verification", function () { authSession.second_factor = true; authSession.userid = "myuser"; authSession.groups = ["mygroup", "othergroup"]; - return VerifyGet.default(req as express.Request, res as any); + return VerifyGet.default(vars)(req as express.Request, res as any); }) .then(function () { Sinon.assert.calledWithExactly(res.setHeader, "Remote-User", "myuser"); @@ -71,26 +59,30 @@ describe("test authentication token verification", function () { return AuthenticationSession.get(req as any) .then(function (authSession) { authSession = _authSession; - return VerifyGet.default(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]); }); } - function test_non_authenticated_401(auth_session: AuthenticationSession.AuthenticationSession) { - return test_session(auth_session, 401); + function test_non_authenticated_401(authSession: AuthenticationSession.AuthenticationSession) { + return test_session(authSession, 401); } - function test_unauthorized_403(auth_session: AuthenticationSession.AuthenticationSession) { - return test_session(auth_session, 403); + function test_unauthorized_403(authSession: AuthenticationSession.AuthenticationSession) { + return test_session(authSession, 403); } - function test_authorized(auth_session: AuthenticationSession.AuthenticationSession) { - return test_session(auth_session, 204); + function test_authorized(authSession: AuthenticationSession.AuthenticationSession) { + return test_session(authSession, 204); } describe("given user tries to access a 2-factor endpoint", function () { + before(function() { + mocks.accessController.isAccessAllowedMock.returns(true); + }); + describe("given different cases of session", function () { it("should not be authenticated when second factor is missing", function () { return test_non_authenticated_401({ @@ -99,6 +91,7 @@ describe("test authentication token verification", function () { second_factor: false, email: undefined, groups: [], + last_activity_datetime: new Date() }); }); @@ -109,6 +102,7 @@ describe("test authentication token verification", function () { second_factor: true, email: undefined, groups: [], + last_activity_datetime: new Date() }); }); @@ -119,6 +113,7 @@ describe("test authentication token verification", function () { second_factor: false, email: undefined, groups: [], + last_activity_datetime: new Date() }); }); @@ -129,6 +124,7 @@ describe("test authentication token verification", function () { second_factor: false, email: undefined, groups: [], + last_activity_datetime: new Date() }); }); @@ -138,22 +134,20 @@ describe("test authentication token verification", function () { it("should not be authenticated when domain is not allowed for user", function () { return AuthenticationSession.get(req as any) - .then(function (authSession: AuthenticationSession.AuthenticationSession) { + .then(function (authSession) { authSession.first_factor = true; authSession.second_factor = true; authSession.userid = "myuser"; - req.headers.host = "test.example.com"; - - accessController.isAccessAllowedMock.returns(false); - accessController.isAccessAllowedMock.withArgs("test.example.com", "user", ["group1", "group2"]).returns(true); + mocks.accessController.isAccessAllowedMock.returns(false); return test_unauthorized_403({ first_factor: true, second_factor: true, userid: "user", groups: ["group1", "group2"], - email: undefined + email: undefined, + last_activity_datetime: new Date() }); }); }); @@ -166,14 +160,18 @@ describe("test authentication token verification", function () { redirect: "http://redirect.url" }; req.headers["host"] = "redirect.url"; + mocks.config.authentication_methods.per_subdomain_methods = { + "redirect.url": "basic_auth" + }; }); - it("should be authenticated when first factor is validated and not second factor", function () { + it("should be authenticated when first factor is validated and second factor is not", function () { + mocks.accessController.isAccessAllowedMock.returns(true); return AuthenticationSession.get(req as any) - .then(function (authSession: AuthenticationSession.AuthenticationSession) { + .then(function (authSession) { authSession.first_factor = true; authSession.userid = "user1"; - return VerifyGet.default(req as express.Request, res as any); + return VerifyGet.default(vars)(req as express.Request, res as any); }) .then(function () { Assert(res.status.calledWith(204)); @@ -182,10 +180,11 @@ describe("test authentication token verification", function () { }); it("should be rejected with 401 when first factor is not validated", function () { + mocks.accessController.isAccessAllowedMock.returns(true); return AuthenticationSession.get(req as any) - .then(function (authSession: AuthenticationSession.AuthenticationSession) { + .then(function (authSession) { authSession.first_factor = false; - return VerifyGet.default(req as express.Request, res as any); + return VerifyGet.default(vars)(req as express.Request, res as any); }) .then(function () { Assert(res.status.calledWith(401));