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.
pull/175/head
Clement Michaud 2017-10-17 00:35:34 +02:00
parent 34a595863a
commit b9fa786df6
38 changed files with 667 additions and 415 deletions

View File

@ -194,6 +194,8 @@ module.exports = function (grunt) {
grunt.registerTask('build', ['build-client', 'build-server']); grunt.registerTask('build', ['build-client', 'build-server']);
grunt.registerTask('build-dist', ['build', 'run:minify', 'cssmin', 'run:include-minified-script']); 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('docker-build', ['run:docker-build']);
grunt.registerTask('default', ['build-dist']); grunt.registerTask('default', ['build-dist']);

View File

@ -153,8 +153,8 @@ session:
# The secret to encrypt the session cookie. # The secret to encrypt the session cookie.
secret: unsecure_session_secret secret: unsecure_session_secret
# The time before the cookie expires. # The time in ms before the cookie expires and session is reset.
expiration: 3600000 expiration: 3600000 # 1 hour
# The domain to protect. # The domain to protect.
# Note: the authenticator must also be in that domain. If empty, the cookie # Note: the authenticator must also be in that domain. If empty, the cookie

View File

@ -133,7 +133,7 @@ session:
secret: unsecure_secret secret: unsecure_secret
# The time before the cookie expires. # The time before the cookie expires.
expiration: 3600000 expiration: 10000
# The domain to protect. # The domain to protect.
# Note: the authenticator must also be in that domain. If empty, the cookie # Note: the authenticator must also be in that domain. If empty, the cookie

View File

@ -5,7 +5,8 @@ set -e
docker --version docker --version
docker-compose --version docker-compose --version
grunt run:generate-config-schema # Generate configuration schema
grunt schema
# Run unit tests # Run unit tests
grunt test-unit grunt test-unit

View File

@ -8,8 +8,11 @@ export class AuthenticationMethodCalculator {
} }
compute(subDomain: string): AuthenticationMethod { 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.per_subdomain_methods[subDomain];
}
return this.configuration.default_method; return this.configuration.default_method;
} }
} }

View File

@ -9,6 +9,7 @@ export interface AuthenticationSession {
userid: string; userid: string;
first_factor: boolean; first_factor: boolean;
second_factor: boolean; second_factor: boolean;
last_activity_datetime: Date;
identity_check?: { identity_check?: {
challenge: string; challenge: string;
userid: string; userid: string;
@ -23,6 +24,7 @@ export interface AuthenticationSession {
const INITIAL_AUTHENTICATION_SESSION: AuthenticationSession = { const INITIAL_AUTHENTICATION_SESSION: AuthenticationSession = {
first_factor: false, first_factor: false,
second_factor: false, second_factor: false,
last_activity_datetime: undefined,
userid: undefined, userid: undefined,
email: undefined, email: undefined,
groups: [], groups: [],
@ -36,6 +38,9 @@ export function reset(req: express.Request): void {
const logger = ServerVariablesHandler.getLogger(req.app); const logger = ServerVariablesHandler.getLogger(req.app);
logger.debug(req, "Authentication session %s is being reset.", req.sessionID); logger.debug(req, "Authentication session %s is being reset.", req.sessionID);
req.session.auth = Object.assign({}, INITIAL_AUTHENTICATION_SESSION, {}); 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<AuthenticationSession> { export function get(req: express.Request): BluebirdPromise<AuthenticationSession> {

View File

@ -33,6 +33,7 @@ import Error404Get = require("./routes/error/404/get");
import LoggedIn = require("./routes/loggedin/get"); import LoggedIn = require("./routes/loggedin/get");
import { ServerVariablesHandler } from "./ServerVariablesHandler"; import { ServerVariablesHandler } from "./ServerVariablesHandler";
import { ServerVariables } from "./ServerVariables";
import Endpoints = require("../../../shared/api"); import Endpoints = require("../../../shared/api");
@ -45,7 +46,7 @@ function withLog(fn: (req: Express.Request, res: Express.Response) => void) {
} }
export class RestApi { 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.FIRST_FACTOR_GET, withLog(FirstFactorGet.default));
app.get(Endpoints.SECOND_FACTOR_GET, withLog(SecondFactorGet.default)); app.get(Endpoints.SECOND_FACTOR_GET, withLog(SecondFactorGet.default));
app.get(Endpoints.LOGOUT_GET, withLog(LogoutGet.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.get(Endpoints.RESET_PASSWORD_REQUEST_GET, withLog(ResetPasswordRequestPost.default));
app.post(Endpoints.RESET_PASSWORD_FORM_POST, withLog(ResetPasswordFormPost.default)); app.post(Endpoints.RESET_PASSWORD_FORM_POST, withLog(ResetPasswordFormPost.default));
app.get(Endpoints.VERIFY_GET, withLog(VerifyGet.default)); app.get(Endpoints.VERIFY_GET, withLog(VerifyGet.default(vars)));
app.post(Endpoints.FIRST_FACTOR_POST, withLog(FirstFactorPost.default)); app.post(Endpoints.FIRST_FACTOR_POST, withLog(FirstFactorPost.default(vars)));
app.post(Endpoints.SECOND_FACTOR_TOTP_POST, withLog(TOTPSignGet.default)); app.post(Endpoints.SECOND_FACTOR_TOTP_POST, withLog(TOTPSignGet.default(vars)));
app.get(Endpoints.SECOND_FACTOR_U2F_SIGN_REQUEST_GET, withLog(U2FSignRequestGet.default)); app.get(Endpoints.SECOND_FACTOR_U2F_SIGN_REQUEST_GET, withLog(U2FSignRequestGet.default));
app.post(Endpoints.SECOND_FACTOR_U2F_SIGN_POST, withLog(U2FSignPost.default)); app.post(Endpoints.SECOND_FACTOR_U2F_SIGN_POST, withLog(U2FSignPost.default));

View File

@ -4,16 +4,14 @@ import ObjectPath = require("object-path");
import { AccessController } from "./access_control/AccessController"; import { AccessController } from "./access_control/AccessController";
import { AppConfiguration, UserConfiguration } from "./configuration/Configuration"; import { AppConfiguration, UserConfiguration } from "./configuration/Configuration";
import { GlobalDependencies } from "../../types/Dependencies"; import { GlobalDependencies } from "../../types/Dependencies";
import { AuthenticationRegulator } from "./AuthenticationRegulator";
import { UserDataStore } from "./storage/UserDataStore"; import { UserDataStore } from "./storage/UserDataStore";
import { ConfigurationParser } from "./configuration/ConfigurationParser"; import { ConfigurationParser } from "./configuration/ConfigurationParser";
import { TOTPValidator } from "./TOTPValidator";
import { TOTPGenerator } from "./TOTPGenerator";
import { RestApi } from "./RestApi"; import { RestApi } from "./RestApi";
import { ServerVariablesHandler } from "./ServerVariablesHandler"; import { ServerVariablesHandler, ServerVariablesInitializer } from "./ServerVariablesHandler";
import { SessionConfigurationBuilder } from "./configuration/SessionConfigurationBuilder"; import { SessionConfigurationBuilder } from "./configuration/SessionConfigurationBuilder";
import { GlobalLogger } from "./logging/GlobalLogger"; import { GlobalLogger } from "./logging/GlobalLogger";
import { RequestLogger } from "./logging/RequestLogger"; import { RequestLogger } from "./logging/RequestLogger";
import { ServerVariables } from "./ServerVariables";
import * as Express from "express"; import * as Express from "express";
import * as BodyParser from "body-parser"; import * as BodyParser from "body-parser";
@ -37,13 +35,16 @@ export default class Server {
private httpServer: http.Server; private httpServer: http.Server;
private globalLogger: GlobalLogger; private globalLogger: GlobalLogger;
private requestLogger: RequestLogger; private requestLogger: RequestLogger;
private serverVariables: ServerVariables;
constructor(deps: GlobalDependencies) { constructor(deps: GlobalDependencies) {
this.globalLogger = new GlobalLogger(deps.winston); this.globalLogger = new GlobalLogger(deps.winston);
this.requestLogger = new RequestLogger(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 viewsDirectory = Path.resolve(__dirname, "../views");
const publicHtmlDirectory = Path.resolve(__dirname, "../public_html"); const publicHtmlDirectory = Path.resolve(__dirname, "../public_html");
@ -60,7 +61,7 @@ export default class Server {
app.set(VIEWS, viewsDirectory); app.set(VIEWS, viewsDirectory);
app.set(VIEW_ENGINE, PUG); app.set(VIEW_ENGINE, PUG);
RestApi.setup(app); RestApi.setup(app, this.serverVariables);
} }
private displayConfigurations(userConfiguration: UserConfiguration, private displayConfigurations(userConfiguration: UserConfiguration,
@ -90,8 +91,14 @@ export default class Server {
} }
private setup(config: AppConfiguration, app: Express.Application, deps: GlobalDependencies): BluebirdPromise<void> { private setup(config: AppConfiguration, app: Express.Application, deps: GlobalDependencies): BluebirdPromise<void> {
this.setupExpressApplication(config, app, deps); const that = this;
return ServerVariablesHandler.initialize(app, config, this.requestLogger, deps); 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) { private startServer(app: Express.Application, port: number) {

View File

@ -1,33 +1,25 @@
import U2F = require("u2f");
import { IRequestLogger } from "./logging/IRequestLogger"; import { IRequestLogger } from "./logging/IRequestLogger";
import { IAuthenticator } from "./ldap/IAuthenticator"; import { IAuthenticator } from "./ldap/IAuthenticator";
import { IPasswordUpdater } from "./ldap/IPasswordUpdater"; import { IPasswordUpdater } from "./ldap/IPasswordUpdater";
import { IEmailsRetriever } from "./ldap/IEmailsRetriever"; import { IEmailsRetriever } from "./ldap/IEmailsRetriever";
import { ITotpHandler } from "./authentication/totp/ITotpHandler";
import { TOTPValidator } from "./TOTPValidator"; import { IU2fHandler } from "./authentication/u2f/IU2fHandler";
import { TOTPGenerator } from "./TOTPGenerator";
import { IUserDataStore } from "./storage/IUserDataStore"; import { IUserDataStore } from "./storage/IUserDataStore";
import { INotifier } from "./notifiers/INotifier"; import { INotifier } from "./notifiers/INotifier";
import { AuthenticationRegulator } from "./AuthenticationRegulator"; import { IRegulator } from "./regulation/IRegulator";
import Configuration = require("./configuration/Configuration"); import { AppConfiguration } from "./configuration/Configuration";
import { AccessController } from "./access_control/AccessController"; import { IAccessController } from "./access_control/IAccessController";
import { AuthenticationMethodCalculator } from "./AuthenticationMethodCalculator";
export interface ServerVariables { export interface ServerVariables {
logger: IRequestLogger; logger: IRequestLogger;
ldapAuthenticator: IAuthenticator; ldapAuthenticator: IAuthenticator;
ldapPasswordUpdater: IPasswordUpdater; ldapPasswordUpdater: IPasswordUpdater;
ldapEmailsRetriever: IEmailsRetriever; ldapEmailsRetriever: IEmailsRetriever;
totpValidator: TOTPValidator; totpHandler: ITotpHandler;
totpGenerator: TOTPGenerator; u2f: IU2fHandler;
u2f: typeof U2F;
userDataStore: IUserDataStore; userDataStore: IUserDataStore;
notifier: INotifier; notifier: INotifier;
regulator: AuthenticationRegulator; regulator: IRegulator;
config: Configuration.AppConfiguration; config: AppConfiguration;
accessController: AccessController; accessController: IAccessController;
authenticationMethodsCalculator: AuthenticationMethodCalculator;
} }

View File

@ -16,18 +16,19 @@ import { EmailsRetriever } from "./ldap/EmailsRetriever";
import { ClientFactory } from "./ldap/ClientFactory"; import { ClientFactory } from "./ldap/ClientFactory";
import { LdapClientFactory } from "./ldap/LdapClientFactory"; import { LdapClientFactory } from "./ldap/LdapClientFactory";
import { TOTPValidator } from "./TOTPValidator"; import { TotpHandler } from "./authentication/totp/TotpHandler";
import { TOTPGenerator } from "./TOTPGenerator"; import { ITotpHandler } from "./authentication/totp/ITotpHandler";
import { NotifierFactory } from "./notifiers/NotifierFactory"; import { NotifierFactory } from "./notifiers/NotifierFactory";
import { MailSenderBuilder } from "./notifiers/MailSenderBuilder"; import { MailSenderBuilder } from "./notifiers/MailSenderBuilder";
import { IUserDataStore } from "./storage/IUserDataStore"; import { IUserDataStore } from "./storage/IUserDataStore";
import { UserDataStore } from "./storage/UserDataStore"; import { UserDataStore } from "./storage/UserDataStore";
import { INotifier } from "./notifiers/INotifier"; 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 Configuration = require("./configuration/Configuration");
import { AccessController } from "./access_control/AccessController"; import { AccessController } from "./access_control/AccessController";
import { IAccessController } from "./access_control/IAccessController";
import { CollectionFactoryFactory } from "./storage/CollectionFactoryFactory"; import { CollectionFactoryFactory } from "./storage/CollectionFactoryFactory";
import { ICollectionFactory } from "./storage/ICollectionFactory"; import { ICollectionFactory } from "./storage/ICollectionFactory";
import { MongoCollectionFactory } from "./storage/mongo/MongoCollectionFactory"; import { MongoCollectionFactory } from "./storage/mongo/MongoCollectionFactory";
@ -67,9 +68,9 @@ class UserDataStoreFactory {
} }
} }
export class ServerVariablesHandler { export class ServerVariablesInitializer {
static initialize(app: express.Application, config: Configuration.AppConfiguration, requestLogger: IRequestLogger, static initialize(config: Configuration.AppConfiguration, requestLogger: IRequestLogger,
deps: GlobalDependencies): BluebirdPromise<void> { deps: GlobalDependencies): BluebirdPromise<ServerVariables> {
const mailSenderBuilder = new MailSenderBuilder(Nodemailer); const mailSenderBuilder = new MailSenderBuilder(Nodemailer);
const notifier = NotifierFactory.build(config.notifier, mailSenderBuilder); const notifier = NotifierFactory.build(config.notifier, mailSenderBuilder);
const ldapClientFactory = new LdapClientFactory(config.ldap, deps.ldapjs); const ldapClientFactory = new LdapClientFactory(config.ldap, deps.ldapjs);
@ -79,13 +80,11 @@ export class ServerVariablesHandler {
const ldapPasswordUpdater = new PasswordUpdater(config.ldap, clientFactory); const ldapPasswordUpdater = new PasswordUpdater(config.ldap, clientFactory);
const ldapEmailsRetriever = new EmailsRetriever(config.ldap, clientFactory); const ldapEmailsRetriever = new EmailsRetriever(config.ldap, clientFactory);
const accessController = new AccessController(config.access_control, deps.winston); const accessController = new AccessController(config.access_control, deps.winston);
const totpValidator = new TOTPValidator(deps.speakeasy); const totpHandler = new TotpHandler(deps.speakeasy);
const totpGenerator = new TOTPGenerator(deps.speakeasy);
const authenticationMethodCalculator = new AuthenticationMethodCalculator(config.authentication_methods);
return UserDataStoreFactory.create(config) return UserDataStoreFactory.create(config)
.then(function (userDataStore: UserDataStore) { .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); config.regulation.find_time, config.regulation.ban_time);
const variables: ServerVariables = { const variables: ServerVariables = {
@ -97,15 +96,18 @@ export class ServerVariablesHandler {
logger: requestLogger, logger: requestLogger,
notifier: notifier, notifier: notifier,
regulator: regulator, regulator: regulator,
totpGenerator: totpGenerator, totpHandler: totpHandler,
totpValidator: totpValidator,
u2f: deps.u2f, u2f: deps.u2f,
userDataStore: userDataStore, userDataStore: userDataStore
authenticationMethodsCalculator: authenticationMethodCalculator
}; };
return BluebirdPromise.resolve(variables);
app.set(VARIABLES_KEY, variables);
}); });
}
}
export class ServerVariablesHandler {
static setup(app: express.Application, variables: ServerVariables): void {
app.set(VARIABLES_KEY, variables);
} }
static getLogger(app: express.Application): IRequestLogger { static getLogger(app: express.Application): IRequestLogger {
@ -136,27 +138,19 @@ export class ServerVariablesHandler {
return (app.get(VARIABLES_KEY) as ServerVariables).config; 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; 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; return (app.get(VARIABLES_KEY) as ServerVariables).accessController;
} }
static getTOTPGenerator(app: express.Application): TOTPGenerator { static getTotpHandler(app: express.Application): ITotpHandler {
return (app.get(VARIABLES_KEY) as ServerVariables).totpGenerator; return (app.get(VARIABLES_KEY) as ServerVariables).totpHandler;
}
static getTOTPValidator(app: express.Application): TOTPValidator {
return (app.get(VARIABLES_KEY) as ServerVariables).totpValidator;
} }
static getU2F(app: express.Application): typeof U2F { static getU2F(app: express.Application): typeof U2F {
return (app.get(VARIABLES_KEY) as ServerVariables).u2f; return (app.get(VARIABLES_KEY) as ServerVariables).u2f;
} }
static getAuthenticationMethodCalculator(app: express.Application): AuthenticationMethodCalculator {
return (app.get(VARIABLES_KEY) as ServerVariables).authenticationMethodsCalculator;
}
} }

View File

@ -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);
}
}

View File

@ -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<void> {
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."));
}
}

View File

@ -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;
}

View File

@ -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);
}
}

View File

@ -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;
}

View File

@ -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);
}
}

View File

@ -0,0 +1,6 @@
import BluebirdPromise = require("bluebird");
export interface IRegulator {
mark(userId: string, isAuthenticationSuccessful: boolean): BluebirdPromise<void>;
regulate(userId: string): BluebirdPromise<void>;
}

View File

@ -1,10 +1,11 @@
import * as BluebirdPromise from "bluebird"; import * as BluebirdPromise from "bluebird";
import exceptions = require("./Exceptions"); import exceptions = require("../Exceptions");
import { IUserDataStore } from "./storage/IUserDataStore"; import { IUserDataStore } from "../storage/IUserDataStore";
import { AuthenticationTraceDocument } from "./storage/AuthenticationTraceDocument"; import { AuthenticationTraceDocument } from "../storage/AuthenticationTraceDocument";
import { IRegulator } from "./IRegulator";
export class AuthenticationRegulator { export class Regulator implements IRegulator {
private userDataStore: IUserDataStore; private userDataStore: IUserDataStore;
private banTime: number; private banTime: number;
private findTime: number; private findTime: number;

View File

@ -4,7 +4,7 @@ import objectPath = require("object-path");
import BluebirdPromise = require("bluebird"); import BluebirdPromise = require("bluebird");
import express = require("express"); import express = require("express");
import { AccessController } from "../../access_control/AccessController"; import { AccessController } from "../../access_control/AccessController";
import { AuthenticationRegulator } from "../../AuthenticationRegulator"; import { Regulator } from "../../regulation/Regulator";
import { GroupsAndEmails } from "../../ldap/IClient"; import { GroupsAndEmails } from "../../ldap/IClient";
import Endpoint = require("../../../../../shared/api"); import Endpoint = require("../../../../../shared/api");
import ErrorReplies = require("../../ErrorReplies"); import ErrorReplies = require("../../ErrorReplies");
@ -13,89 +13,89 @@ import AuthenticationSession = require("../../AuthenticationSession");
import Constants = require("../../../../../shared/constants"); import Constants = require("../../../../../shared/constants");
import { DomainExtractor } from "../../utils/DomainExtractor"; import { DomainExtractor } from "../../utils/DomainExtractor";
import UserMessages = require("../../../../../shared/UserMessages"); import UserMessages = require("../../../../../shared/UserMessages");
import { AuthenticationMethodCalculator } from "../../AuthenticationMethodCalculator";
import { ServerVariables } from "../../ServerVariables";
export default function (req: express.Request, res: express.Response): BluebirdPromise<void> { export default function (vars: ServerVariables) {
const username: string = req.body.username; return function (req: express.Request, res: express.Response)
const password: string = req.body.password; : BluebirdPromise<void> {
const username: string = req.body.username;
const password: string = req.body.password;
let authSession: AuthenticationSession.AuthenticationSession;
const logger = ServerVariablesHandler.getLogger(req.app); return BluebirdPromise.resolve()
const ldap = ServerVariablesHandler.getLdapAuthenticator(req.app); .then(function () {
const config = ServerVariablesHandler.getConfiguration(req.app); if (!username || !password) {
const regulator = ServerVariablesHandler.getAuthenticationRegulator(req.app); return BluebirdPromise.reject(new Error("No username or password."));
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);
} }
logger.debug(req, "Redirect to '%s'", newRedirectUrl, typeof redirectUrl); vars.logger.info(req, "Starting authentication of user \"%s\"", username);
res.send({ return AuthenticationSession.get(req);
redirect: newRedirectUrl })
}); .then(function (_authSession: AuthenticationSession.AuthenticationSession) {
} authSession = _authSession;
else { return vars.regulator.regulate(username);
return BluebirdPromise.reject(new Error("Unknown authentication method for this domain.")); })
} .then(function () {
return BluebirdPromise.resolve(); vars.logger.info(req, "No regulation applied.");
}) return vars.ldapAuthenticator.authenticate(username, password);
.catch(Exceptions.LdapBindError, function (err: Error) { })
regulator.mark(username, false); .then(function (groupsAndEmails: GroupsAndEmails) {
return ErrorReplies.replyWithError200(req, res, logger, UserMessages.OPERATION_FAILED)(err); vars.logger.info(req, "LDAP binding successful. Retrieved information about user are %s",
}) JSON.stringify(groupsAndEmails));
.catch(ErrorReplies.replyWithError200(req, res, logger, UserMessages.OPERATION_FAILED)); 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));
};
} }

View File

@ -7,6 +7,7 @@ import { ServerVariablesHandler } from "../../ServerVariablesHandler";
import AuthenticationSession = require("../../AuthenticationSession"); import AuthenticationSession = require("../../AuthenticationSession");
import BluebirdPromise = require("bluebird"); import BluebirdPromise = require("bluebird");
import ErrorReplies = require("../../ErrorReplies"); import ErrorReplies = require("../../ErrorReplies");
import UserMessages = require("../../../../../shared/UserMessages");
export default function (req: express.Request, res: express.Response): BluebirdPromise<void> { export default function (req: express.Request, res: express.Response): BluebirdPromise<void> {
const logger = ServerVariablesHandler.getLogger(req.app); const logger = ServerVariablesHandler.getLogger(req.app);
@ -19,5 +20,5 @@ export default function (req: express.Request, res: express.Response): BluebirdP
return BluebirdPromise.resolve(); return BluebirdPromise.resolve();
}) })
.catch(ErrorReplies.replyWithError200(req, res, logger, .catch(ErrorReplies.replyWithError200(req, res, logger,
"Unexpected error.")); UserMessages.OPERATION_FAILED));
} }

View File

@ -67,8 +67,8 @@ export default class RegistrationHandler implements IdentityValidable {
} }
const userDataStore = ServerVariablesHandler.getUserDataStore(req.app); const userDataStore = ServerVariablesHandler.getUserDataStore(req.app);
const totpGenerator = ServerVariablesHandler.getTOTPGenerator(req.app); const totpHandler = ServerVariablesHandler.getTotpHandler(req.app);
const secret = totpGenerator.generate(); const secret = totpHandler.generate();
logger.debug(req, "Save the TOTP secret in DB"); logger.debug(req, "Save the TOTP secret in DB");
return userDataStore.saveTOTPSecret(userid, secret) return userDataStore.saveTOTPSecret(userid, secret)

View File

@ -11,34 +11,34 @@ import ErrorReplies = require("../../../../ErrorReplies");
import { ServerVariablesHandler } from "./../../../../ServerVariablesHandler"; import { ServerVariablesHandler } from "./../../../../ServerVariablesHandler";
import AuthenticationSession = require("../../../../AuthenticationSession"); import AuthenticationSession = require("../../../../AuthenticationSession");
import UserMessages = require("../../../../../../../shared/UserMessages"); import UserMessages = require("../../../../../../../shared/UserMessages");
import { ServerVariables } from "../../../../ServerVariables";
const UNAUTHORIZED_MESSAGE = "Unauthorized access"; const UNAUTHORIZED_MESSAGE = "Unauthorized access";
export default FirstFactorBlocker(handler); export default function (vars: ServerVariables) {
function handler(req: express.Request, res: express.Response): BluebirdPromise<void> {
let authSession: AuthenticationSession.AuthenticationSession;
const token = req.body.token;
export function handler(req: express.Request, res: express.Response): BluebirdPromise<void> { return AuthenticationSession.get(req)
let authSession: AuthenticationSession.AuthenticationSession; .then(function (_authSession: AuthenticationSession.AuthenticationSession) {
const logger = ServerVariablesHandler.getLogger(req.app); authSession = _authSession;
const token = req.body.token; vars.logger.info(req, "Initiate TOTP validation for user '%s'.", authSession.userid);
const totpValidator = ServerVariablesHandler.getTOTPValidator(req.app); return vars.userDataStore.retrieveTOTPSecret(authSession.userid);
const userDataStore = ServerVariablesHandler.getUserDataStore(req.app); })
.then(function (doc: TOTPSecretDocument) {
vars.logger.debug(req, "TOTP secret is %s", JSON.stringify(doc));
return AuthenticationSession.get(req) if (!vars.totpHandler.validate(token, doc.secret.base32))
.then(function (_authSession: AuthenticationSession.AuthenticationSession) { return BluebirdPromise.reject(new Error("Invalid TOTP token."));
authSession = _authSession;
logger.info(req, "Initiate TOTP validation for user '%s'.", authSession.userid); vars.logger.debug(req, "TOTP validation succeeded.");
return userDataStore.retrieveTOTPSecret(authSession.userid); authSession.second_factor = true;
}) redirect(req, res);
.then(function (doc: TOTPSecretDocument) { return BluebirdPromise.resolve();
logger.debug(req, "TOTP secret is %s", JSON.stringify(doc)); })
return totpValidator.validate(token, doc.secret.base32); .catch(ErrorReplies.replyWithError200(req, res, vars.logger,
}) UserMessages.OPERATION_FAILED));
.then(function () { }
logger.debug(req, "TOTP validation succeeded."); return FirstFactorBlocker(handler);
authSession.second_factor = true;
redirect(req, res);
return BluebirdPromise.resolve();
})
.catch(ErrorReplies.replyWithError200(req, res, logger,
UserMessages.OPERATION_FAILED));
} }

View File

@ -6,19 +6,22 @@ import exceptions = require("../../Exceptions");
import winston = require("winston"); import winston = require("winston");
import AuthenticationValidator = require("../../AuthenticationValidator"); import AuthenticationValidator = require("../../AuthenticationValidator");
import ErrorReplies = require("../../ErrorReplies"); import ErrorReplies = require("../../ErrorReplies");
import { ServerVariablesHandler } from "../../ServerVariablesHandler"; import { AppConfiguration } from "../../configuration/Configuration";
import AuthenticationSession = require("../../AuthenticationSession"); import AuthenticationSession = require("../../AuthenticationSession");
import Constants = require("../../../../../shared/constants"); import Constants = require("../../../../../shared/constants");
import Util = require("util"); import Util = require("util");
import { DomainExtractor } from "../../utils/DomainExtractor"; 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 FIRST_FACTOR_NOT_VALIDATED_MESSAGE = "First factor not yet validated";
const SECOND_FACTOR_NOT_VALIDATED_MESSAGE = "Second 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<void> { const REMOTE_USER = "Remote-User";
const logger = ServerVariablesHandler.getLogger(req.app); const REMOTE_GROUPS = "Remote-Groups";
const accessController = ServerVariablesHandler.getAccessController(req.app);
const authenticationMethodsCalculator = ServerVariablesHandler.getAuthenticationMethodCalculator(req.app); function verify_filter(req: express.Request, res: express.Response,
vars: ServerVariables): BluebirdPromise<void> {
return AuthenticationSession.get(req) return AuthenticationSession.get(req)
.then(function (authSession) { .then(function (authSession) {
@ -35,15 +38,17 @@ function verify_filter(req: express.Request, res: express.Response): BluebirdPro
const path = objectPath.get<express.Request, string>(req, "headers.x-original-uri"); const path = objectPath.get<express.Request, string>(req, "headers.x-original-uri");
const domain = DomainExtractor.fromHostHeader(host); const domain = DomainExtractor.fromHostHeader(host);
const authenticationMethod = authenticationMethodsCalculator.compute(domain); const authenticationMethod =
logger.debug(req, "domain=%s, path=%s, user=%s, groups=%s", domain, path, 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(",")); username, groups.join(","));
if (!authSession.first_factor) if (!authSession.first_factor)
return BluebirdPromise.reject( return BluebirdPromise.reject(
new exceptions.AccessDeniedError(FIRST_FACTOR_NOT_VALIDATED_MESSAGE)); 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( if (!isAllowed) return BluebirdPromise.reject(
new exceptions.DomainAccessDenied(Util.format("User '%s' does not have access to '%s'", new exceptions.DomainAccessDenied(Util.format("User '%s' does not have access to '%s'",
username, domain))); username, domain)));
@ -52,25 +57,27 @@ function verify_filter(req: express.Request, res: express.Response): BluebirdPro
return BluebirdPromise.reject( return BluebirdPromise.reject(
new exceptions.AccessDeniedError(SECOND_FACTOR_NOT_VALIDATED_MESSAGE)); new exceptions.AccessDeniedError(SECOND_FACTOR_NOT_VALIDATED_MESSAGE));
res.setHeader("Remote-User", username); res.setHeader(REMOTE_USER, username);
res.setHeader("Remote-Groups", groups.join(",")); res.setHeader(REMOTE_GROUPS, groups.join(","));
return BluebirdPromise.resolve(); return BluebirdPromise.resolve();
}); });
} }
export default function (req: express.Request, res: express.Response): BluebirdPromise<void> { export default function (vars: ServerVariables) {
const logger = ServerVariablesHandler.getLogger(req.app); return function (req: express.Request, res: express.Response)
return verify_filter(req, res) : BluebirdPromise<void> {
.then(function () { return verify_filter(req, res, vars)
res.status(204); .then(function () {
res.send(); res.status(204);
return BluebirdPromise.resolve(); res.send();
}) return BluebirdPromise.resolve();
// The user is authenticated but has restricted access -> 403 })
.catch(exceptions.DomainAccessDenied, ErrorReplies // The user is authenticated but has restricted access -> 403
.replyWithError403(req, res, logger)) .catch(exceptions.DomainAccessDenied, ErrorReplies
// The user is not yet authenticated -> 401 .replyWithError403(req, res, vars.logger))
.catch(ErrorReplies.replyWithError401(req, res, logger)); // The user is not yet authenticated -> 401
.catch(ErrorReplies.replyWithError401(req, res, vars.logger));
};
} }

View File

@ -1,5 +1,5 @@
import assert = require("assert"); import Assert = require("assert");
import Sinon = require("sinon"); import Sinon = require("sinon");
import nedb = require("nedb"); import nedb = require("nedb");
import express = require("express"); import express = require("express");
@ -73,9 +73,10 @@ describe("test server configuration", function () {
}; };
const server = new Server(deps); const server = new Server(deps);
server.start(config, deps); server.start(config, deps)
.then(function () {
assert(sessionMock.calledOnce); Assert(sessionMock.calledOnce);
assert.equal(sessionMock.getCall(0).args[0].cookie.domain, "example.com"); Assert.equal(sessionMock.getCall(0).args[0].cookie.domain, "example.com");
});
}); });
}); });

View File

@ -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();
});
});
});

View File

@ -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));
});
});

View File

@ -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<void> {
return this.notifyStub(to, subject, link);
}
}

View File

@ -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<void> {
return this.markStub(userId, isAuthenticationSuccessful);
}
regulate(userId: string): BluebirdPromise<void> {
return this.regulateStub(userId);
}
}

View File

@ -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
};
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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<GroupsAndEmails> {
return this.authenticateStub(username, password);
}
}

View File

@ -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<string[]> {
return this.retrieveStub(username, client);
}
}

View File

@ -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<void> {
return this.updatePasswordStub(username, newPassword);
}
}

View File

@ -3,10 +3,10 @@ import Sinon = require("sinon");
import BluebirdPromise = require("bluebird"); import BluebirdPromise = require("bluebird");
import Assert = require("assert"); import Assert = require("assert");
import { AuthenticationRegulator } from "../src/lib/AuthenticationRegulator"; import { Regulator } from "../../src/lib/regulation/Regulator";
import MockDate = require("mockdate"); import MockDate = require("mockdate");
import exceptions = require("../src/lib/Exceptions"); import exceptions = require("../../src/lib/Exceptions");
import { UserDataStoreStub } from "./mocks/storage/UserDataStoreStub"; import { UserDataStoreStub } from "../mocks/storage/UserDataStoreStub";
describe("test authentication regulator", function () { describe("test authentication regulator", function () {
const USER1 = "USER1"; const USER1 = "USER1";
@ -39,13 +39,13 @@ describe("test authentication regulator", function () {
MockDate.reset(); 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); MockDate.set(time);
return regulator.mark(user, success); return regulator.mark(user, success);
} }
it("should mark 2 authentication and regulate (accept)", function () { 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) return regulator.mark(USER1, false)
.then(function () { .then(function () {
@ -57,7 +57,7 @@ describe("test authentication regulator", function () {
}); });
it("should mark 3 authentications and regulate (reject)", 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) return regulator.mark(USER1, false)
.then(function () { .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 () { 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) return markAuthenticationAt(regulator, USER1, "1/2/2000 00:00:00", false)
.then(function () { .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 () { 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) return markAuthenticationAt(regulator, user, "1/2/2000 00:00:00", false)
.then(function () { .then(function () {
return markAuthenticationAt(regulator, user, "1/2/2000 00:00:45", false); 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 regulator1 = new Regulator(userDataStoreStub, 3, 60, 60);
const regulator2 = new AuthenticationRegulator(userDataStoreStub, 3, 2 * 60, 60); const regulator2 = new Regulator(userDataStoreStub, 3, 2 * 60, 60);
const p1 = markAuthentications(regulator1, USER1); const p1 = markAuthentications(regulator1, USER1);
const p2 = markAuthentications(regulator2, USER2); const p2 = markAuthentications(regulator2, USER2);
@ -138,7 +138,7 @@ describe("test authentication regulator", function () {
}); });
it("should user wait after regulation to authenticate again", 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) return markAuthenticationAt(regulator, user, "1/2/2000 00:00:00", false)
.then(function () { .then(function () {
return markAuthenticationAt(regulator, user, "1/2/2000 00:00:10", false); 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); return markAuthentications(regulator, USER1);
}); });
it("should disable regulation when max_retries is set to 0", function () { it("should disable regulation when max_retries is set to 0", function () {
const maxRetries = 0; 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) return markAuthenticationAt(regulator, USER1, "1/2/2000 00:00:00", false)
.then(function () { .then(function () {
return markAuthenticationAt(regulator, USER1, "1/2/2000 00:00:10", false); return markAuthenticationAt(regulator, USER1, "1/2/2000 00:00:10", false);

View File

@ -1,8 +1,8 @@
import sinon = require("sinon"); import Sinon = require("sinon");
import BluebirdPromise = require("bluebird"); import BluebirdPromise = require("bluebird");
import Assert = require("assert"); import Assert = require("assert");
import winston = require("winston"); import Winston = require("winston");
import FirstFactorPost = require("../../../src/lib/routes/firstfactor/post"); import FirstFactorPost = require("../../../src/lib/routes/firstfactor/post");
import exceptions = require("../../../src/lib/Exceptions"); import exceptions = require("../../../src/lib/Exceptions");
@ -12,7 +12,7 @@ import Endpoints = require("../../../../shared/api");
import AuthenticationRegulatorMock = require("../../mocks/AuthenticationRegulator"); import AuthenticationRegulatorMock = require("../../mocks/AuthenticationRegulator");
import { AccessControllerStub } from "../../mocks/AccessControllerStub"; import { AccessControllerStub } from "../../mocks/AccessControllerStub";
import ExpressMock = require("../../mocks/express"); import ExpressMock = require("../../mocks/express");
import ServerVariablesMock = require("../../mocks/ServerVariablesMock"); import { ServerVariablesMock, ServerVariablesMockBuilder } from "../../mocks/ServerVariablesMockBuilder";
import { ServerVariables } from "../../../src/lib/ServerVariables"; import { ServerVariables } from "../../../src/lib/ServerVariables";
describe("test the first factor validation route", function () { 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 res: ExpressMock.ResponseMock;
let emails: string[]; let emails: string[];
let groups: string[]; let groups: string[];
let configuration; let vars: ServerVariables;
let regulator: AuthenticationRegulatorMock.AuthenticationRegulatorMock; let mocks: ServerVariablesMock;
let accessController: AccessControllerStub;
let serverVariables: ServerVariables;
beforeEach(function () { beforeEach(function () {
configuration = {
ldap: {
base_dn: "ou=users,dc=example,dc=com",
user_name_attribute: "uid"
}
};
emails = ["test_ok@example.com"]; emails = ["test_ok@example.com"];
groups = ["group1", "group2" ]; groups = ["group1", "group2" ];
const s = ServerVariablesMockBuilder.build();
mocks = s.mocks;
vars = s.variables;
accessController = new AccessControllerStub(); mocks.accessController.isAccessAllowedMock.returns(true);
accessController.isAccessAllowedMock.returns(true); mocks.regulator.regulateStub.returns(BluebirdPromise.resolve());
mocks.regulator.markStub.returns(BluebirdPromise.resolve());
regulator = AuthenticationRegulatorMock.AuthenticationRegulatorMock();
regulator.regulate.returns(BluebirdPromise.resolve());
regulator.mark.returns(BluebirdPromise.resolve());
req = { req = {
app: { app: {
get: sinon.stub().returns({ logger: winston }) get: Sinon.stub().returns({ logger: Winston })
}, },
body: { body: {
username: "username", username: "username",
@ -62,20 +53,11 @@ describe("test the first factor validation route", function () {
}; };
AuthenticationSession.reset(req as any); 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(); res = ExpressMock.ResponseMock();
}); });
it("should reply with 204 if success", function () { 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({ .returns(BluebirdPromise.resolve({
emails: emails, emails: emails,
groups: groups groups: groups
@ -84,7 +66,7 @@ describe("test the first factor validation route", function () {
return AuthenticationSession.get(req as any) return AuthenticationSession.get(req as any)
.then(function (_authSession: AuthenticationSession.AuthenticationSession) { .then(function (_authSession: AuthenticationSession.AuthenticationSession) {
authSession = _authSession; authSession = _authSession;
return FirstFactorPost.default(req as any, res as any); return FirstFactorPost.default(vars)(req as any, res as any);
}) })
.then(function () { .then(function () {
Assert.equal("username", authSession.userid); Assert.equal("username", authSession.userid);
@ -93,15 +75,15 @@ describe("test the first factor validation route", function () {
}); });
it("should retrieve email from LDAP", 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"] }])); .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 () { it("should set first email address as user session variable", function () {
const emails = ["test_ok@example.com"]; const emails = ["test_ok@example.com"];
let authSession: AuthenticationSession.AuthenticationSession; let authSession: AuthenticationSession.AuthenticationSession;
(serverVariables.ldapAuthenticator as any).authenticate.withArgs("username", "password") mocks.ldapAuthenticator.authenticateStub.withArgs("username", "password")
.returns(BluebirdPromise.resolve({ .returns(BluebirdPromise.resolve({
emails: emails, emails: emails,
groups: groups groups: groups
@ -110,7 +92,7 @@ describe("test the first factor validation route", function () {
return AuthenticationSession.get(req as any) return AuthenticationSession.get(req as any)
.then(function (_authSession: AuthenticationSession.AuthenticationSession) { .then(function (_authSession: AuthenticationSession.AuthenticationSession) {
authSession = _authSession; authSession = _authSession;
return FirstFactorPost.default(req as any, res as any); return FirstFactorPost.default(vars)(req as any, res as any);
}) })
.then(function () { .then(function () {
Assert.equal("test_ok@example.com", authSession.email); 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 () { 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"))); .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 () { .then(function () {
Assert.equal(res.status.getCall(0).args[0], 200); 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], { Assert.deepEqual(res.send.getCall(0).args[0], {
error: "Operation failed." 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 () { it("should return error message when regulator rejects authentication", function () {
const err = new exceptions.AuthenticationRegulationError("Authentication regulation..."); const err = new exceptions.AuthenticationRegulationError("Authentication regulation...");
regulator.regulate.returns(BluebirdPromise.reject(err)); mocks.regulator.regulateStub.returns(BluebirdPromise.reject(err));
return FirstFactorPost.default(req as any, res as any) return FirstFactorPost.default(vars)(req as any, res as any)
.then(function () { .then(function () {
Assert.equal(res.status.getCall(0).args[0], 200); Assert.equal(res.status.getCall(0).args[0], 200);
Assert.deepEqual(res.send.getCall(0).args[0], { Assert.deepEqual(res.send.getCall(0).args[0], {

View File

@ -1,41 +1,44 @@
import BluebirdPromise = require("bluebird"); import BluebirdPromise = require("bluebird");
import sinon = require("sinon"); import Sinon = require("sinon");
import assert = require("assert"); import assert = require("assert");
import winston = require("winston"); import winston = require("winston");
import exceptions = require("../../../../../src/lib/Exceptions"); import exceptions = require("../../../../../src/lib/Exceptions");
import AuthenticationSession = require("../../../../../src/lib/AuthenticationSession"); import AuthenticationSession = require("../../../../../src/lib/AuthenticationSession");
import SignPost = require("../../../../../src/lib/routes/secondfactor/totp/sign/post"); import SignPost = require("../../../../../src/lib/routes/secondfactor/totp/sign/post");
import { ServerVariables } from "../../../../../src/lib/ServerVariables";
import ExpressMock = require("../../../../mocks/express"); import ExpressMock = require("../../../../mocks/express");
import TOTPValidatorMock = require("../../../../mocks/TOTPValidator");
import ServerVariablesMock = require("../../../../mocks/ServerVariablesMock");
import { UserDataStoreStub } from "../../../../mocks/storage/UserDataStoreStub"; import { UserDataStoreStub } from "../../../../mocks/storage/UserDataStoreStub";
import { ServerVariablesMock, ServerVariablesMockBuilder } from "../../../../mocks/ServerVariablesMockBuilder";
describe("test totp route", function () { describe("test totp route", function () {
let req: ExpressMock.RequestMock; let req: ExpressMock.RequestMock;
let res: ExpressMock.ResponseMock; let res: ExpressMock.ResponseMock;
let totpValidator: TOTPValidatorMock.TOTPValidatorMock;
let authSession: AuthenticationSession.AuthenticationSession; let authSession: AuthenticationSession.AuthenticationSession;
let vars: ServerVariables;
let mocks: ServerVariablesMock;
beforeEach(function () { beforeEach(function () {
const app_get = sinon.stub(); const s = ServerVariablesMockBuilder.build();
vars = s.variables;
mocks = s.mocks;
const app_get = Sinon.stub();
req = { req = {
app: { app: {
get: sinon.stub().returns({ logger: winston }) get: Sinon.stub().returns({ logger: winston })
}, },
body: { body: {
token: "abc" token: "abc"
}, },
session: {} session: {},
query: {
redirect: "http://redirect"
}
}; };
AuthenticationSession.reset(req as any);
const mocks = ServerVariablesMock.mock(req.app);
res = ExpressMock.ResponseMock(); res = ExpressMock.ResponseMock();
AuthenticationSession.reset(req as any);
const config = { totp_secret: "secret" };
totpValidator = TOTPValidatorMock.TOTPValidatorMock();
const doc = { const doc = {
userid: "user", userid: "user",
@ -44,9 +47,6 @@ describe("test totp route", function () {
} }
}; };
mocks.userDataStore.retrieveTOTPSecretStub.returns(BluebirdPromise.resolve(doc)); mocks.userDataStore.retrieveTOTPSecretStub.returns(BluebirdPromise.resolve(doc));
mocks.totpValidator = totpValidator;
mocks.config = config;
return AuthenticationSession.get(req as any) return AuthenticationSession.get(req as any)
.then(function (_authSession: AuthenticationSession.AuthenticationSession) { .then(function (_authSession: AuthenticationSession.AuthenticationSession) {
authSession = _authSession; authSession = _authSession;
@ -58,8 +58,8 @@ describe("test totp route", function () {
it("should send status code 200 when totp is valid", function () { it("should send status code 200 when totp is valid", function () {
totpValidator.validate.returns(BluebirdPromise.resolve("ok")); mocks.totpHandler.validateStub.returns(true);
return SignPost.default(req as any, res as any) return SignPost.default(vars)(req as any, res as any)
.then(function () { .then(function () {
assert.equal(true, authSession.second_factor); assert.equal(true, authSession.second_factor);
return BluebirdPromise.resolve(); return BluebirdPromise.resolve();
@ -67,8 +67,8 @@ describe("test totp route", function () {
}); });
it("should send error message when totp is not valid", function () { it("should send error message when totp is not valid", function () {
totpValidator.validate.returns(BluebirdPromise.reject(new exceptions.InvalidTOTPError("Bad TOTP token"))); mocks.totpHandler.validateStub.returns(false);
return SignPost.default(req as any, res as any) return SignPost.default(vars)(req as any, res as any)
.then(function () { .then(function () {
assert.equal(false, authSession.second_factor); assert.equal(false, authSession.second_factor);
assert.equal(res.status.getCall(0).args[0], 200); 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 () { 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 = {}; 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")); }) .then(function () { return BluebirdPromise.reject(new Error("It should fail")); })
.catch(function () { .catch(function () {
assert.equal(401, res.status.getCall(0).args[0]); assert.equal(401, res.status.getCall(0).args[0]);

View File

@ -4,27 +4,21 @@ import VerifyGet = require("../../../src/lib/routes/verify/get");
import AuthenticationSession = require("../../../src/lib/AuthenticationSession"); import AuthenticationSession = require("../../../src/lib/AuthenticationSession");
import { AuthenticationMethodCalculator } from "../../../src/lib/AuthenticationMethodCalculator"; import { AuthenticationMethodCalculator } from "../../../src/lib/AuthenticationMethodCalculator";
import { AuthenticationMethodsConfiguration } from "../../../src/lib/configuration/Configuration"; import { AuthenticationMethodsConfiguration } from "../../../src/lib/configuration/Configuration";
import Sinon = require("sinon"); import Sinon = require("sinon");
import winston = require("winston"); import winston = require("winston");
import BluebirdPromise = require("bluebird"); import BluebirdPromise = require("bluebird");
import express = require("express"); import express = require("express");
import ExpressMock = require("../../mocks/express"); import ExpressMock = require("../../mocks/express");
import { AccessControllerStub } from "../../mocks/AccessControllerStub"; import { ServerVariables } from "../../../src/lib/ServerVariables";
import ServerVariablesMock = require("../../mocks/ServerVariablesMock"); import { ServerVariablesMockBuilder, ServerVariablesMock } from "../../mocks/ServerVariablesMockBuilder";
describe("test authentication token verification", function () { describe("test /verify endpoint", function () {
let req: ExpressMock.RequestMock; let req: ExpressMock.RequestMock;
let res: ExpressMock.ResponseMock; let res: ExpressMock.ResponseMock;
let accessController: AccessControllerStub; let mocks: ServerVariablesMock;
let mocks: any; let vars: ServerVariables;
beforeEach(function () { beforeEach(function () {
accessController = new AccessControllerStub();
accessController.isAccessAllowedMock.returns(true);
req = ExpressMock.RequestMock(); req = ExpressMock.RequestMock();
res = ExpressMock.ResponseMock(); res = ExpressMock.ResponseMock();
req.session = {}; req.session = {};
@ -37,20 +31,14 @@ describe("test authentication token verification", function () {
AuthenticationSession.reset(req as any); AuthenticationSession.reset(req as any);
req.headers = {}; req.headers = {};
req.headers.host = "secret.example.com"; req.headers.host = "secret.example.com";
mocks = ServerVariablesMock.mock(req.app); const s = ServerVariablesMockBuilder.build();
mocks.config = {} as any; mocks = s.mocks;
mocks.accessController = accessController as any; vars = s.variables;
const options: AuthenticationMethodsConfiguration = {
default_method: "two_factor",
per_subdomain_methods: {
"redirect.url": "basic_auth"
}
};
mocks.authenticationMethodsCalculator = new AuthenticationMethodCalculator(options);
}); });
it("should be already authenticated", function () { it("should be already authenticated", function () {
req.session = {}; req.session = {};
mocks.accessController.isAccessAllowedMock.returns(true);
AuthenticationSession.reset(req as any); AuthenticationSession.reset(req as any);
return AuthenticationSession.get(req as any) return AuthenticationSession.get(req as any)
.then(function (authSession: AuthenticationSession.AuthenticationSession) { .then(function (authSession: AuthenticationSession.AuthenticationSession) {
@ -58,7 +46,7 @@ describe("test authentication token verification", function () {
authSession.second_factor = true; authSession.second_factor = true;
authSession.userid = "myuser"; authSession.userid = "myuser";
authSession.groups = ["mygroup", "othergroup"]; 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 () { .then(function () {
Sinon.assert.calledWithExactly(res.setHeader, "Remote-User", "myuser"); Sinon.assert.calledWithExactly(res.setHeader, "Remote-User", "myuser");
@ -71,26 +59,30 @@ describe("test authentication token verification", function () {
return AuthenticationSession.get(req as any) return AuthenticationSession.get(req as any)
.then(function (authSession) { .then(function (authSession) {
authSession = _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 () { .then(function () {
Assert.equal(status_code, res.status.getCall(0).args[0]); Assert.equal(status_code, res.status.getCall(0).args[0]);
}); });
} }
function test_non_authenticated_401(auth_session: AuthenticationSession.AuthenticationSession) { function test_non_authenticated_401(authSession: AuthenticationSession.AuthenticationSession) {
return test_session(auth_session, 401); return test_session(authSession, 401);
} }
function test_unauthorized_403(auth_session: AuthenticationSession.AuthenticationSession) { function test_unauthorized_403(authSession: AuthenticationSession.AuthenticationSession) {
return test_session(auth_session, 403); return test_session(authSession, 403);
} }
function test_authorized(auth_session: AuthenticationSession.AuthenticationSession) { function test_authorized(authSession: AuthenticationSession.AuthenticationSession) {
return test_session(auth_session, 204); return test_session(authSession, 204);
} }
describe("given user tries to access a 2-factor endpoint", function () { 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 () { describe("given different cases of session", function () {
it("should not be authenticated when second factor is missing", function () { it("should not be authenticated when second factor is missing", function () {
return test_non_authenticated_401({ return test_non_authenticated_401({
@ -99,6 +91,7 @@ describe("test authentication token verification", function () {
second_factor: false, second_factor: false,
email: undefined, email: undefined,
groups: [], groups: [],
last_activity_datetime: new Date()
}); });
}); });
@ -109,6 +102,7 @@ describe("test authentication token verification", function () {
second_factor: true, second_factor: true,
email: undefined, email: undefined,
groups: [], groups: [],
last_activity_datetime: new Date()
}); });
}); });
@ -119,6 +113,7 @@ describe("test authentication token verification", function () {
second_factor: false, second_factor: false,
email: undefined, email: undefined,
groups: [], groups: [],
last_activity_datetime: new Date()
}); });
}); });
@ -129,6 +124,7 @@ describe("test authentication token verification", function () {
second_factor: false, second_factor: false,
email: undefined, email: undefined,
groups: [], 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 () { it("should not be authenticated when domain is not allowed for user", function () {
return AuthenticationSession.get(req as any) return AuthenticationSession.get(req as any)
.then(function (authSession: AuthenticationSession.AuthenticationSession) { .then(function (authSession) {
authSession.first_factor = true; authSession.first_factor = true;
authSession.second_factor = true; authSession.second_factor = true;
authSession.userid = "myuser"; authSession.userid = "myuser";
req.headers.host = "test.example.com"; req.headers.host = "test.example.com";
mocks.accessController.isAccessAllowedMock.returns(false);
accessController.isAccessAllowedMock.returns(false);
accessController.isAccessAllowedMock.withArgs("test.example.com", "user", ["group1", "group2"]).returns(true);
return test_unauthorized_403({ return test_unauthorized_403({
first_factor: true, first_factor: true,
second_factor: true, second_factor: true,
userid: "user", userid: "user",
groups: ["group1", "group2"], 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" redirect: "http://redirect.url"
}; };
req.headers["host"] = "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) return AuthenticationSession.get(req as any)
.then(function (authSession: AuthenticationSession.AuthenticationSession) { .then(function (authSession) {
authSession.first_factor = true; authSession.first_factor = true;
authSession.userid = "user1"; 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 () { .then(function () {
Assert(res.status.calledWith(204)); 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 () { it("should be rejected with 401 when first factor is not validated", function () {
mocks.accessController.isAccessAllowedMock.returns(true);
return AuthenticationSession.get(req as any) return AuthenticationSession.get(req as any)
.then(function (authSession: AuthenticationSession.AuthenticationSession) { .then(function (authSession) {
authSession.first_factor = false; 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 () { .then(function () {
Assert(res.status.calledWith(401)); Assert(res.status.calledWith(401));