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/161/head
Clement Michaud 2017-10-17 00:35:34 +02:00
parent 5570ac3d84
commit 9e275441c9
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-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']);

View File

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

View File

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

View File

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

View File

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

View File

@ -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<AuthenticationSession> {

View File

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

View File

@ -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<void> {
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) {

View File

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

View File

@ -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<void> {
export class ServerVariablesInitializer {
static initialize(config: Configuration.AppConfiguration, requestLogger: IRequestLogger,
deps: GlobalDependencies): BluebirdPromise<ServerVariables> {
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;
}
}

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

View File

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

View File

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

View File

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

View File

@ -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<void> {
let authSession: AuthenticationSession.AuthenticationSession;
const token = req.body.token;
export function handler(req: express.Request, res: express.Response): BluebirdPromise<void> {
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);
}

View File

@ -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<void> {
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<void> {
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<express.Request, string>(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<void> {
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<void> {
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));
};
}

View File

@ -1,5 +1,5 @@
import assert = require("assert");
import Assert = require("assert");
import Sinon = require("sinon");
import nedb = require("nedb");
import express = require("express");
@ -72,9 +72,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");
});
});
});

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

View File

@ -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], {

View File

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

View File

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