Improve logging format for clarity

Previously, logs were not very friendly and it was hard to track
a request because of the lack of request ID.
Now every log message comes with a header containing: method, path
request ID, session ID, IP of the user, date.

Moreover, the configurations displayed in the logs have their secrets
hidden from this commit.
pull/125/head
Clement Michaud 2017-10-08 00:46:57 +02:00
parent 26418278bc
commit 78f6028c1b
60 changed files with 582 additions and 378 deletions

View File

@ -134,7 +134,7 @@ access_control:
# The session cookies identify the user once logged in.
session:
# The secret to encrypt the session cookie.
secret: unsecure_secret
secret: unsecure_session_secret
# The time before the cookie expires.
expiration: 3600000
@ -190,7 +190,7 @@ notifier:
# Use a SMTP server for sending notifications
smtp:
username: test
password: test
password: password
secure: false
host: 'smtp'
port: 1025

View File

@ -29,6 +29,7 @@
"dovehash": "0.0.5",
"ejs": "^2.5.5",
"express": "^4.14.0",
"express-request-id": "^1.4.0",
"express-session": "^1.14.2",
"ldapjs": "^1.0.1",
"mongodb": "^2.2.30",
@ -59,7 +60,7 @@
"@types/mockdate": "^2.0.0",
"@types/mongodb": "^2.2.7",
"@types/nedb": "^1.8.3",
"@types/nodemailer": "^1.3.32",
"@types/nodemailer": "^3.1.3",
"@types/object-path": "^0.9.28",
"@types/proxyquire": "^1.3.27",
"@types/query-string": "^4.3.1",

View File

@ -13,14 +13,11 @@ if (!configurationFilepath) {
process.exit(0);
}
console.log("Parse configuration file: %s", configurationFilepath);
const yamlContent = YAML.load(configurationFilepath);
const deps: GlobalDependencies = {
u2f: require("u2f"),
dovehash: require("dovehash"),
nodemailer: require("nodemailer"),
ldapjs: require("ldapjs"),
session: require("express-session"),
winston: require("winston"),
@ -29,8 +26,5 @@ const deps: GlobalDependencies = {
ConnectRedis: require("connect-redis")
};
const server = new Server();
server.start(yamlContent, deps)
.then(() => {
console.log("The server is started!");
});
const server = new Server(deps);
server.start(yamlContent, deps);

View File

@ -34,7 +34,7 @@ const INITIAL_AUTHENTICATION_SESSION: AuthenticationSession = {
export function reset(req: express.Request): void {
const logger = ServerVariablesHandler.getLogger(req.app);
logger.debug("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, {});
}
@ -42,12 +42,12 @@ export function get(req: express.Request): BluebirdPromise<AuthenticationSession
const logger = ServerVariablesHandler.getLogger(req.app);
if (!req.session) {
const errorMsg = "Something is wrong with session cookies. Please check Redis is running and Authelia can contact it.";
logger.error(errorMsg);
logger.error(req, errorMsg);
return BluebirdPromise.reject(new Error(errorMsg));
}
if (!req.session.auth) {
logger.debug("Authentication session %s was undefined. Resetting.", req.sessionID);
logger.debug(req, "Authentication session %s was undefined. Resetting.", req.sessionID);
reset(req);
}

View File

@ -1,27 +1,33 @@
import express = require("express");
import { Winston } from "winston";
import BluebirdPromise = require("bluebird");
import { IRequestLogger } from "./logging/IRequestLogger";
function replyWithError(res: express.Response, code: number, logger: Winston): (err: Error) => void {
function replyWithError(req: express.Request, res: express.Response,
code: number, logger: IRequestLogger): (err: Error) => void {
return function (err: Error): void {
logger.error("Reply with error %d: %s", code, err.stack);
logger.error(req, "Reply with error %d: %s", code, err.message);
logger.debug(req, "%s", err.stack);
res.status(code);
res.send();
};
}
export function replyWithError400(res: express.Response, logger: Winston) {
return replyWithError(res, 400, logger);
export function replyWithError400(req: express.Request,
res: express.Response, logger: IRequestLogger) {
return replyWithError(req, res, 400, logger);
}
export function replyWithError401(res: express.Response, logger: Winston) {
return replyWithError(res, 401, logger);
export function replyWithError401(req: express.Request,
res: express.Response, logger: IRequestLogger) {
return replyWithError(req, res, 401, logger);
}
export function replyWithError403(res: express.Response, logger: Winston) {
return replyWithError(res, 403, logger);
export function replyWithError403(req: express.Request,
res: express.Response, logger: IRequestLogger) {
return replyWithError(req, res, 403, logger);
}
export function replyWithError500(res: express.Response, logger: Winston) {
return replyWithError(res, 500, logger);
export function replyWithError500(req: express.Request,
res: express.Response, logger: IRequestLogger) {
return replyWithError(req, res, 500, logger);
}

View File

@ -33,20 +33,20 @@ export interface IdentityValidable {
mailSubject(): string;
}
function createAndSaveToken(userid: string, challenge: string, userDataStore: IUserDataStore, logger: Winston): BluebirdPromise<string> {
function createAndSaveToken(userid: string, challenge: string, userDataStore: IUserDataStore)
: BluebirdPromise<string> {
const five_minutes = 4 * 60 * 1000;
const token = randomstring.generate({ length: 64 });
const that = this;
logger.debug("identity_check: issue identity token %s for 5 minutes", token);
return userDataStore.produceIdentityValidationToken(userid, token, challenge, five_minutes)
.then(function () {
return BluebirdPromise.resolve(token);
});
}
function consumeToken(token: string, challenge: string, userDataStore: IUserDataStore, logger: Winston): BluebirdPromise<IdentityValidationDocument> {
logger.debug("identity_check: consume token %s", token);
function consumeToken(token: string, challenge: string, userDataStore: IUserDataStore)
: BluebirdPromise<IdentityValidationDocument> {
return userDataStore.consumeIdentityValidationToken(token, challenge);
}
@ -68,7 +68,7 @@ export function get_finish_validation(handler: IdentityValidable): express.Reque
let authSession: AuthenticationSession.AuthenticationSession;
const identityToken = objectPath.get<express.Request, string>(req, "query.identity_token");
logger.info("GET identity_check: identity token provided is %s", identityToken);
logger.debug(req, "Identity token provided is %s", identityToken);
return checkIdentityToken(req, identityToken)
.then(function () {
@ -81,7 +81,7 @@ export function get_finish_validation(handler: IdentityValidable): express.Reque
authSession = _authSession;
})
.then(function () {
return consumeToken(identityToken, handler.challenge(), userDataStore, logger);
return consumeToken(identityToken, handler.challenge(), userDataStore);
})
.then(function (doc: IdentityValidationDocument) {
authSession.identity_check = {
@ -91,9 +91,9 @@ export function get_finish_validation(handler: IdentityValidable): express.Reque
handler.postValidationResponse(req, res);
return BluebirdPromise.resolve();
})
.catch(Exceptions.FirstFactorValidationError, ErrorReplies.replyWithError401(res, logger))
.catch(Exceptions.AccessDeniedError, ErrorReplies.replyWithError403(res, logger))
.catch(ErrorReplies.replyWithError500(res, logger));
.catch(Exceptions.FirstFactorValidationError, ErrorReplies.replyWithError401(req, res, logger))
.catch(Exceptions.AccessDeniedError, ErrorReplies.replyWithError403(req, res, logger))
.catch(ErrorReplies.replyWithError500(req, res, logger));
};
}
@ -104,33 +104,32 @@ export function get_start_validation(handler: IdentityValidable, postValidationE
const notifier = ServerVariablesHandler.getNotifier(req.app);
const userDataStore = ServerVariablesHandler.getUserDataStore(req.app);
let identity: Identity.Identity;
logger.info("Identity Validation: Start identity validation");
return handler.preValidationInit(req)
.then(function (id: Identity.Identity) {
logger.debug("Identity Validation: retrieved identity is %s", JSON.stringify(id));
identity = id;
const email = identity.email;
const userid = identity.userid;
logger.info(req, "Start identity validation of user \"%s\"", userid);
if (!(email && userid))
return BluebirdPromise.reject(new Exceptions.IdentityError("Missing user id or email address"));
return createAndSaveToken(userid, handler.challenge(), userDataStore, logger);
return createAndSaveToken(userid, handler.challenge(), userDataStore);
})
.then(function (token: string) {
const host = req.get("Host");
const link_url = util.format("https://%s%s?identity_token=%s", host, postValidationEndpoint, token);
logger.info("POST identity_check: notification sent to user %s", identity.userid);
logger.info(req, "Notification sent to user \"%s\"", identity.userid);
return notifier.notify(identity, handler.mailSubject(), link_url);
})
.then(function () {
handler.preValidationResponse(req, res);
return BluebirdPromise.resolve();
})
.catch(Exceptions.FirstFactorValidationError, ErrorReplies.replyWithError401(res, logger))
.catch(Exceptions.IdentityError, ErrorReplies.replyWithError400(res, logger))
.catch(Exceptions.AccessDeniedError, ErrorReplies.replyWithError403(res, logger))
.catch(ErrorReplies.replyWithError500(res, logger));
.catch(Exceptions.FirstFactorValidationError, ErrorReplies.replyWithError401(req, res, logger))
.catch(Exceptions.IdentityError, ErrorReplies.replyWithError400(req, res, logger))
.catch(Exceptions.AccessDeniedError, ErrorReplies.replyWithError403(req, res, logger))
.catch(ErrorReplies.replyWithError500(req, res, logger));
};
}

View File

@ -36,7 +36,7 @@ import Endpoints = require("../../../shared/api");
function withLog(fn: (req: Express.Request, res: Express.Response) => void) {
return function(req: Express.Request, res: Express.Response) {
const logger = ServerVariablesHandler.getLogger(req.app);
logger.info("Request %s handled on %s", req.method, req.originalUrl);
logger.debug(req, "Headers = %s", JSON.stringify(req.headers));
fn(req, res);
};
}

View File

@ -1,4 +1,5 @@
import BluebirdPromise = require("bluebird");
import ObjectPath = require("object-path");
import { AccessController } from "./access_control/AccessController";
import { AppConfiguration, UserConfiguration } from "./configuration/Configuration";
@ -12,12 +13,16 @@ import { RestApi } from "./RestApi";
import { Client } from "./ldap/Client";
import { ServerVariablesHandler } from "./ServerVariablesHandler";
import { SessionConfigurationBuilder } from "./configuration/SessionConfigurationBuilder";
import { GlobalLogger } from "./logging/GlobalLogger";
import { RequestLogger } from "./logging/RequestLogger";
import * as Express from "express";
import * as BodyParser from "body-parser";
import * as Path from "path";
import * as http from "http";
const addRequestId = require("express-request-id")();
// Constants
const TRUST_PROXY = "trust proxy";
@ -25,9 +30,19 @@ const VIEWS = "views";
const VIEW_ENGINE = "view engine";
const PUG = "pug";
function clone(obj: any) {
return JSON.parse(JSON.stringify(obj));
}
export default class Server {
private httpServer: http.Server;
private globalLogger: GlobalLogger;
private requestLogger: RequestLogger;
constructor(deps: GlobalDependencies) {
this.globalLogger = new GlobalLogger(deps.winston);
this.requestLogger = new RequestLogger(deps.winston);
}
private setupExpressApplication(config: AppConfiguration, app: Express.Application, deps: GlobalDependencies): void {
const viewsDirectory = Path.resolve(__dirname, "../views");
@ -39,6 +54,7 @@ export default class Server {
app.use(BodyParser.urlencoded({ extended: false }));
app.use(BodyParser.json());
app.use(deps.session(expressSessionOptions));
app.use(addRequestId);
app.disable("x-powered-by");
app.set(TRUST_PROXY, 1);
@ -48,39 +64,61 @@ export default class Server {
RestApi.setup(app);
}
private adaptConfiguration(yamlConfiguration: UserConfiguration, deps: GlobalDependencies): AppConfiguration {
const config = ConfigurationAdapter.adapt(yamlConfiguration);
private displayConfigurations(userConfiguration: UserConfiguration,
appConfiguration: AppConfiguration) {
const displayableUserConfiguration = clone(userConfiguration);
const displayableAppConfiguration = clone(appConfiguration);
const STARS = "*****";
// by default the level of logs is info
deps.winston.level = config.logs_level;
console.log("Log level = ", deps.winston.level);
displayableUserConfiguration.ldap.password = STARS;
displayableUserConfiguration.session.secret = STARS;
if (displayableUserConfiguration.notifier && displayableUserConfiguration.notifier.gmail)
displayableUserConfiguration.notifier.gmail.password = STARS;
if (displayableUserConfiguration.notifier && displayableUserConfiguration.notifier.smtp)
displayableUserConfiguration.notifier.smtp.password = STARS;
deps.winston.debug("Content of YAML configuration file is %s", JSON.stringify(yamlConfiguration, undefined, 2));
deps.winston.debug("Authelia configuration is %s", JSON.stringify(config, undefined, 2));
return config;
displayableAppConfiguration.ldap.password = STARS;
displayableAppConfiguration.session.secret = STARS;
if (displayableAppConfiguration.notifier && displayableAppConfiguration.notifier.gmail)
displayableAppConfiguration.notifier.gmail.password = STARS;
if (displayableAppConfiguration.notifier && displayableAppConfiguration.notifier.smtp)
displayableAppConfiguration.notifier.smtp.password = STARS;
this.globalLogger.debug("User configuration is %s",
JSON.stringify(displayableUserConfiguration, undefined, 2));
this.globalLogger.debug("Adapted configuration is %s",
JSON.stringify(displayableAppConfiguration, undefined, 2));
}
private setup(config: AppConfiguration, app: Express.Application, deps: GlobalDependencies): BluebirdPromise<void> {
this.setupExpressApplication(config, app, deps);
return ServerVariablesHandler.initialize(app, config, deps);
return ServerVariablesHandler.initialize(app, config, this.requestLogger, deps);
}
private startServer(app: Express.Application, port: number) {
const that = this;
return new BluebirdPromise<void>((resolve, reject) => {
this.httpServer = app.listen(port, function (err: string) {
console.log("Listening on %d...", port);
that.globalLogger.info("Listening on port %d...", port);
resolve();
});
});
}
start(yamlConfiguration: UserConfiguration, deps: GlobalDependencies): BluebirdPromise<void> {
start(userConfiguration: UserConfiguration, deps: GlobalDependencies)
: BluebirdPromise<void> {
const that = this;
const app = Express();
const config = this.adaptConfiguration(yamlConfiguration, deps);
return this.setup(config, app, deps)
const appConfiguration = ConfigurationAdapter.adapt(userConfiguration);
// by default the level of logs is info
deps.winston.level = userConfiguration.logs_level;
this.displayConfigurations(userConfiguration, appConfiguration);
return this.setup(appConfiguration, app, deps)
.then(function () {
return that.startServer(app, config.port);
return that.startServer(app, appConfiguration.port);
});
}

View File

@ -0,0 +1,32 @@
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 { IUserDataStore } from "./storage/IUserDataStore";
import { INotifier } from "./notifiers/INotifier";
import { AuthenticationRegulator } from "./AuthenticationRegulator";
import Configuration = require("./configuration/Configuration");
import { AccessController } from "./access_control/AccessController";
export interface ServerVariables {
logger: IRequestLogger;
ldapAuthenticator: IAuthenticator;
ldapPasswordUpdater: IPasswordUpdater;
ldapEmailsRetriever: IEmailsRetriever;
totpValidator: TOTPValidator;
totpGenerator: TOTPGenerator;
u2f: typeof U2F;
userDataStore: IUserDataStore;
notifier: INotifier;
regulator: AuthenticationRegulator;
config: Configuration.AppConfiguration;
accessController: AccessController;
}

View File

@ -2,6 +2,10 @@
import winston = require("winston");
import BluebirdPromise = require("bluebird");
import U2F = require("u2f");
import Nodemailer = require("nodemailer");
import { IRequestLogger } from "./logging/IRequestLogger";
import { RequestLogger } from "./logging/RequestLogger";
import { IAuthenticator } from "./ldap/IAuthenticator";
import { IPasswordUpdater } from "./ldap/IPasswordUpdater";
@ -14,40 +18,28 @@ import { LdapClientFactory } from "./ldap/LdapClientFactory";
import { TOTPValidator } from "./TOTPValidator";
import { TOTPGenerator } from "./TOTPGenerator";
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 Configuration = require("./configuration/Configuration");
import { AccessController } from "./access_control/AccessController";
import { NotifierFactory } from "./notifiers/NotifierFactory";
import { CollectionFactoryFactory } from "./storage/CollectionFactoryFactory";
import { ICollectionFactory } from "./storage/ICollectionFactory";
import { MongoCollectionFactory } from "./storage/mongo/MongoCollectionFactory";
import { MongoConnectorFactory } from "./connectors/mongo/MongoConnectorFactory";
import { IMongoClient } from "./connectors/mongo/IMongoClient";
import { GlobalDependencies } from "../../types/Dependencies";
import { ServerVariables } from "./ServerVariables";
import express = require("express");
export const VARIABLES_KEY = "authelia-variables";
export interface ServerVariables {
logger: typeof winston;
ldapAuthenticator: IAuthenticator;
ldapPasswordUpdater: IPasswordUpdater;
ldapEmailsRetriever: IEmailsRetriever;
totpValidator: TOTPValidator;
totpGenerator: TOTPGenerator;
u2f: typeof U2F;
userDataStore: IUserDataStore;
notifier: INotifier;
regulator: AuthenticationRegulator;
config: Configuration.AppConfiguration;
accessController: AccessController;
}
class UserDataStoreFactory {
static create(config: Configuration.AppConfiguration): BluebirdPromise<UserDataStore> {
if (config.storage.local) {
@ -73,8 +65,10 @@ class UserDataStoreFactory {
}
export class ServerVariablesHandler {
static initialize(app: express.Application, config: Configuration.AppConfiguration, deps: GlobalDependencies): BluebirdPromise<void> {
const notifier = NotifierFactory.build(config.notifier, deps.nodemailer);
static initialize(app: express.Application, config: Configuration.AppConfiguration, requestLogger: IRequestLogger,
deps: GlobalDependencies): BluebirdPromise<void> {
const mailSenderBuilder = new MailSenderBuilder(Nodemailer);
const notifier = NotifierFactory.build(config.notifier, mailSenderBuilder);
const ldapClientFactory = new LdapClientFactory(config.ldap, deps.ldapjs);
const clientFactory = new ClientFactory(config.ldap, ldapClientFactory, deps.dovehash, deps.winston);
@ -96,20 +90,20 @@ export class ServerVariablesHandler {
ldapAuthenticator: ldapAuthenticator,
ldapPasswordUpdater: ldapPasswordUpdater,
ldapEmailsRetriever: ldapEmailsRetriever,
logger: deps.winston,
logger: requestLogger,
notifier: notifier,
regulator: regulator,
totpGenerator: totpGenerator,
totpValidator: totpValidator,
u2f: deps.u2f,
userDataStore: userDataStore
userDataStore: userDataStore,
};
app.set(VARIABLES_KEY, variables);
});
}
static getLogger(app: express.Application): typeof winston {
static getLogger(app: express.Application): IRequestLogger {
return (app.get(VARIABLES_KEY) as ServerVariables).logger;
}

View File

@ -0,0 +1,34 @@
import { IGlobalLogger } from "./IGlobalLogger";
import Util = require("util");
import Express = require("express");
import Winston = require("winston");
declare module "express" {
interface Request {
id: string;
}
}
export class GlobalLogger implements IGlobalLogger {
private winston: typeof Winston;
constructor(winston: typeof Winston) {
this.winston = winston;
}
private buildMessage(message: string, ...args: any[]): string {
return Util.format("date='%s' message='%s'", new Date(),
Util.format(message, ...args));
}
info(message: string, ...args: any[]): void {
this.winston.info(this.buildMessage(message, ...args));
}
debug(message: string, ...args: any[]): void {
this.winston.debug(this.buildMessage(message, ...args));
}
error(message: string, ...args: any[]): void {
this.winston.debug(this.buildMessage(message, ...args));
}
}

View File

@ -0,0 +1,5 @@
export interface IGlobalLogger {
info(message: string, ...args: any[]): void;
debug(message: string, ...args: any[]): void;
error(message: string, ...args: any[]): void;
}

View File

@ -0,0 +1,7 @@
import Express = require("express");
export interface IRequestLogger {
info(req: Express.Request, message: string, ...args: any[]): void;
debug(req: Express.Request, message: string, ...args: any[]): void;
error(req: Express.Request, message: string, ...args: any[]): void;
}

View File

@ -0,0 +1,45 @@
import { IRequestLogger } from "./IRequestLogger";
import Util = require("util");
import Express = require("express");
import Winston = require("winston");
declare module "express" {
interface Request {
id: string;
}
}
export class RequestLogger implements IRequestLogger {
private winston: typeof Winston;
constructor(winston: typeof Winston) {
this.winston = winston;
}
private formatHeader(req: Express.Request) {
const ip = req.headers["x-forwarded-for"] || req.connection.remoteAddress;
return Util.format("date='%s' method='%s', path='%s' requestId='%s' sessionId='%s' ip='%s'",
new Date(), req.method, req.path, req.id, req.sessionID, ip);
}
private formatBody(message: string) {
return Util.format("message='%s'", message);
}
private formatMessage(req: Express.Request, message: string) {
return Util.format("%s %s", this.formatHeader(req),
this.formatBody(message));
}
info(req: Express.Request, message: string, ...args: any[]): void {
this.winston.info(this.formatMessage(req, message), ...args);
}
debug(req: Express.Request, message: string, ...args: any[]): void {
this.winston.debug(this.formatMessage(req, message), ...args);
}
error(req: Express.Request, message: string, ...args: any[]): void {
this.winston.error(this.formatMessage(req, message), ...args);
}
}

View File

@ -1,24 +1,16 @@
import * as BluebirdPromise from "bluebird";
import nodemailer = require("nodemailer");
import { Nodemailer } from "../../../types/Dependencies";
import { AbstractEmailNotifier } from "../notifiers/AbstractEmailNotifier";
import { GmailNotifierConfiguration } from "../configuration/Configuration";
import { IMailSender } from "./IMailSender";
export class GMailNotifier extends AbstractEmailNotifier {
private transporter: any;
private mailSender: IMailSender;
constructor(options: GmailNotifierConfiguration, nodemailer: Nodemailer) {
constructor(options: GmailNotifierConfiguration, mailSender: IMailSender) {
super();
const transporter = nodemailer.createTransport({
service: "gmail",
auth: {
user: options.username,
pass: options.password
}
});
this.transporter = BluebirdPromise.promisifyAll(transporter);
this.mailSender = mailSender;
}
sendEmail(email: string, subject: string, content: string) {
@ -28,6 +20,6 @@ export class GMailNotifier extends AbstractEmailNotifier {
subject: subject,
html: content
};
return this.transporter.sendMailAsync(mailOptions);
return this.mailSender.send(mailOptions);
}
}

View File

@ -0,0 +1,6 @@
import BluebirdPromise = require("bluebird");
import Nodemailer = require("nodemailer");
export interface IMailSender {
send(mailOptions: Nodemailer.SendMailOptions): BluebirdPromise<void>;
}

View File

@ -0,0 +1,7 @@
import { IMailSender } from "./IMailSender";
import { SmtpNotifierConfiguration, GmailNotifierConfiguration } from "../configuration/Configuration";
export interface IMailSenderBuilder {
buildGmail(options: GmailNotifierConfiguration): IMailSender;
buildSmtp(options: SmtpNotifierConfiguration): IMailSender;
}

View File

@ -0,0 +1,42 @@
import { IMailSender } from "./IMailSender";
import Nodemailer = require("nodemailer");
import NodemailerDirectTransport = require("nodemailer-direct-transport");
import NodemailerSmtpTransport = require("nodemailer-smtp-transport");
import BluebirdPromise = require("bluebird");
export class MailSender implements IMailSender {
private transporter: Nodemailer.Transporter;
constructor(options: NodemailerDirectTransport.DirectOptions |
NodemailerSmtpTransport.SmtpOptions, nodemailer: typeof Nodemailer) {
this.transporter = nodemailer.createTransport(options);
}
verify(): BluebirdPromise<void> {
const that = this;
return new BluebirdPromise(function (resolve, reject) {
that.transporter.verify(function (error: Error, success: any) {
if (error) {
reject(new Error("Unable to connect to SMTP server. \
Please check the service is running and your credentials are correct."));
return;
}
resolve();
});
});
}
send(mailOptions: Nodemailer.SendMailOptions): BluebirdPromise<void> {
const that = this;
return new BluebirdPromise(function (resolve, reject) {
that.transporter.sendMail(mailOptions, (error: Error,
data: Nodemailer.SentMessageInfo) => {
if (error) {
reject(new Error("Error while sending email: " + error.message));
return;
}
resolve();
});
});
}
}

View File

@ -0,0 +1,38 @@
import { IMailSender } from "./IMailSender";
import { IMailSenderBuilder } from "./IMailSenderBuilder";
import { MailSender } from "./MailSender";
import Nodemailer = require("nodemailer");
import NodemailerSmtpTransport = require("nodemailer-smtp-transport");
import { SmtpNotifierConfiguration, GmailNotifierConfiguration } from "../configuration/Configuration";
export class MailSenderBuilder implements IMailSenderBuilder {
private nodemailer: typeof Nodemailer;
constructor(nodemailer: typeof Nodemailer) {
this.nodemailer = nodemailer;
}
buildGmail(options: GmailNotifierConfiguration): IMailSender {
const gmailOptions = {
service: "gmail",
auth: {
user: options.username,
pass: options.password
}
};
return new MailSender(gmailOptions, this.nodemailer);
}
buildSmtp(options: SmtpNotifierConfiguration): IMailSender {
const smtpOptions: NodemailerSmtpTransport.SmtpOptions = {
host: options.host,
port: options.port,
secure: options.secure, // upgrade later with STARTTLS
auth: {
user: options.username,
pass: options.password
}
};
return new MailSender(smtpOptions, this.nodemailer);
}
}

View File

@ -1,18 +1,22 @@
import { NotifierConfiguration } from "../configuration/Configuration";
import { Nodemailer } from "../../../types/Dependencies";
import Nodemailer = require("nodemailer");
import { INotifier } from "./INotifier";
import { GMailNotifier } from "./GMailNotifier";
import { SmtpNotifier } from "./SmtpNotifier";
import { IMailSender } from "./IMailSender";
import { IMailSenderBuilder } from "./IMailSenderBuilder";
export class NotifierFactory {
static build(options: NotifierConfiguration, nodemailer: Nodemailer): INotifier {
static build(options: NotifierConfiguration, mailSenderBuilder: IMailSenderBuilder): INotifier {
if ("gmail" in options) {
return new GMailNotifier(options.gmail, nodemailer);
const mailSender = mailSenderBuilder.buildGmail(options.gmail);
return new GMailNotifier(options.gmail, mailSender);
}
else if ("smtp" in options) {
return new SmtpNotifier(options.smtp, nodemailer);
const mailSender = mailSenderBuilder.buildSmtp(options.smtp);
return new SmtpNotifier(options.smtp, mailSender);
}
else {
throw new Error("No available notifier option detected.");

View File

@ -1,37 +1,18 @@
import * as BluebirdPromise from "bluebird";
import Nodemailer = require("nodemailer");
import { IMailSender } from "./IMailSender";
import { AbstractEmailNotifier } from "../notifiers/AbstractEmailNotifier";
import { SmtpNotifierConfiguration } from "../configuration/Configuration";
export class SmtpNotifier extends AbstractEmailNotifier {
private transporter: any;
private mailSender: IMailSender;
constructor(options: SmtpNotifierConfiguration, nodemailer: typeof Nodemailer) {
constructor(options: SmtpNotifierConfiguration,
mailSender: IMailSender) {
super();
const smtpOptions = {
host: options.host,
port: options.port,
secure: options.secure, // upgrade later with STARTTLS
auth: {
user: options.username,
pass: options.password
}
};
const transporter = nodemailer.createTransport(smtpOptions);
this.transporter = BluebirdPromise.promisifyAll(transporter);
// verify connection configuration
transporter.verify(function (error, success) {
if (error) {
throw new Error("Unable to connect to SMTP server. \
Please check the service is running and your credentials are correct.");
} else {
console.log("SMTP Server is ready to take our messages");
}
});
this.mailSender = mailSender;
}
sendEmail(email: string, subject: string, content: string) {
@ -41,11 +22,7 @@ Please check the service is running and your credentials are correct.");
subject: subject,
html: content
};
return this.transporter.sendMail(mailOptions, (error: Error, data: string) => {
if (error) {
return console.log(error);
}
console.log("Message sent: %s", JSON.stringify(data));
});
const that = this;
return this.mailSender.send(mailOptions);
}
}

View File

@ -21,6 +21,6 @@ export default function (callback: Handler): Handler {
.then(function () {
return callback(req, res);
})
.catch(Exceptions.FirstFactorValidationError, ErrorReplies.replyWithError401(res, logger));
.catch(Exceptions.FirstFactorValidationError, ErrorReplies.replyWithError401(req, res, logger));
};
}

View File

@ -8,10 +8,6 @@ import { ServerVariablesHandler } from "../../ServerVariablesHandler";
import BluebirdPromise = require("bluebird");
export default function (req: express.Request, res: express.Response): BluebirdPromise<void> {
const logger = ServerVariablesHandler.getLogger(req.app);
logger.debug("First factor: headers are %s", JSON.stringify(req.headers));
res.render("firstfactor", {
first_factor_post_endpoint: Endpoints.FIRST_FACTOR_POST,
reset_password_request_endpoint: Endpoints.RESET_PASSWORD_REQUEST_GET

View File

@ -22,7 +22,7 @@ export default function (req: express.Request, res: express.Response): BluebirdP
if (!username || !password) {
const err = new Error("No username or password");
ErrorReplies.replyWithError401(res, logger)(err);
ErrorReplies.replyWithError401(req, res, logger)(err);
return BluebirdPromise.reject(err);
}
@ -30,21 +30,18 @@ export default function (req: express.Request, res: express.Response): BluebirdP
const accessController = ServerVariablesHandler.getAccessController(req.app);
let authSession: AuthenticationSession.AuthenticationSession;
logger.info("1st factor: Starting authentication of user \"%s\"", username);
logger.debug("1st factor: Start bind operation against LDAP");
logger.debug("1st factor: username=%s", username);
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("1st factor: No regulation applied.");
logger.info(req, "No regulation applied.");
return ldap.authenticate(username, password);
})
.then(function (groupsAndEmails: GroupsAndEmails) {
logger.info("1st factor: LDAP binding successful. Retrieved information about user are %s",
logger.info(req, "LDAP binding successful. Retrieved information about user are %s",
JSON.stringify(groupsAndEmails));
authSession.userid = username;
authSession.first_factor = true;
@ -56,43 +53,40 @@ export default function (req: express.Request, res: express.Response): BluebirdP
if (!emails || emails.length <= 0) {
const errMessage = "No emails found. The user should have at least one email address to reset password.";
logger.error("1s factor: %s", errMessage);
logger.error(req, "%s", errMessage);
return BluebirdPromise.reject(new Error(errMessage));
}
authSession.email = emails[0];
authSession.groups = groups;
logger.debug("1st factor: Mark successful authentication to regulator.");
logger.debug(req, "Mark successful authentication to regulator.");
regulator.mark(username, true);
logger.debug("1st factor: Redirect URL is %s", redirectUrl);
logger.debug("1st factor: %s? %s", Constants.ONLY_BASIC_AUTH_QUERY_PARAM, onlyBasicAuth);
if (onlyBasicAuth) {
res.send({
redirect: redirectUrl
});
logger.debug("1st factor: redirect to '%s'", redirectUrl);
logger.debug(req, "Redirect to '%s'", redirectUrl);
}
else {
let newRedirectUrl = Endpoint.SECOND_FACTOR_GET;
if (redirectUrl !== "undefined") {
newRedirectUrl += "?redirect=" + encodeURIComponent(redirectUrl);
}
logger.debug("1st factor: redirect to '%s'", newRedirectUrl, typeof redirectUrl);
logger.debug(req, "Redirect to '%s'", newRedirectUrl, typeof redirectUrl);
res.send({
redirect: newRedirectUrl
});
}
return BluebirdPromise.resolve();
})
.catch(exceptions.LdapSearchError, ErrorReplies.replyWithError500(res, logger))
.catch(exceptions.LdapSearchError, ErrorReplies.replyWithError500(req, res, logger))
.catch(exceptions.LdapBindError, function (err: Error) {
regulator.mark(username, false);
return ErrorReplies.replyWithError401(res, logger)(err);
return ErrorReplies.replyWithError401(req, res, logger)(err);
})
.catch(exceptions.AuthenticationRegulationError, ErrorReplies.replyWithError403(res, logger))
.catch(exceptions.DomainAccessDenied, ErrorReplies.replyWithError401(res, logger))
.catch(ErrorReplies.replyWithError500(res, logger));
.catch(exceptions.AuthenticationRegulationError, ErrorReplies.replyWithError403(req, res, logger))
.catch(exceptions.DomainAccessDenied, ErrorReplies.replyWithError401(req, res, logger))
.catch(ErrorReplies.replyWithError500(req, res, logger));
}

View File

@ -18,9 +18,9 @@ export default function (req: express.Request, res: express.Response): BluebirdP
return AuthenticationSession.get(req)
.then(function (_authSession) {
authSession = _authSession;
logger.info("POST reset-password: User %s wants to reset his/her password.",
logger.info(req, "User %s wants to reset his/her password.",
authSession.identity_check.userid);
logger.info("POST reset-password: Challenge %s", authSession.identity_check.challenge);
logger.debug(req, "Challenge %s", authSession.identity_check.challenge);
if (authSession.identity_check.challenge != Constants.CHALLENGE) {
res.status(403);
@ -30,12 +30,12 @@ export default function (req: express.Request, res: express.Response): BluebirdP
return ldapPasswordUpdater.updatePassword(authSession.identity_check.userid, newPassword);
})
.then(function () {
logger.info("POST reset-password: Password reset for user '%s'",
logger.info(req, "Password reset for user '%s'",
authSession.identity_check.userid);
AuthenticationSession.reset(req);
res.status(204);
res.send();
return BluebirdPromise.resolve();
})
.catch(ErrorReplies.replyWithError500(res, logger));
.catch(ErrorReplies.replyWithError500(req, res, logger));
}

View File

@ -21,7 +21,7 @@ export default class PasswordResetHandler implements IdentityValidable {
const logger = ServerVariablesHandler.getLogger(req.app);
const userid: string = objectPath.get<express.Request, string>(req, "query.userid");
logger.debug("Reset Password: user '%s' requested a password reset", userid);
logger.debug(req, "User '%s' requested a password reset", userid);
if (!userid)
return BluebirdPromise.reject(new exceptions.AccessDeniedError("No user id provided"));
@ -29,7 +29,6 @@ export default class PasswordResetHandler implements IdentityValidable {
return emailsRetriever.retrieve(userid)
.then(function (emails: string[]) {
if (!emails && emails.length <= 0) throw new Error("No email found");
const identity = {
email: emails[0],
userid: userid

View File

@ -10,8 +10,6 @@ const TEMPLATE_NAME = "secondfactor";
export default FirstFactorBlocker.default(handler);
function handler(req: Express.Request, res: Express.Response): BluebirdPromise<void> {
const logger = ServerVariablesHandler.getLogger(req.app);
logger.debug("secondfactor request is coming from %s", req.originalUrl);
res.render(TEMPLATE_NAME, {
totp_identity_start_endpoint: Endpoints.SECOND_FACTOR_TOTP_IDENTITY_START_GET,
u2f_identity_start_endpoint: Endpoints.SECOND_FACTOR_U2F_IDENTITY_START_GET

View File

@ -6,8 +6,10 @@ import Endpoints = require("../../../../../shared/api");
import { ServerVariablesHandler } from "../../ServerVariablesHandler";
import AuthenticationSession = require("../../AuthenticationSession");
import BluebirdPromise = require("bluebird");
import ErrorReplies = require("../../ErrorReplies");
export default function (req: express.Request, res: express.Response): BluebirdPromise<void> {
const logger = ServerVariablesHandler.getLogger(req.app);
return AuthenticationSession.get(req)
.then(function (authSession: AuthenticationSession.AuthenticationSession) {
const redirectUrl = req.query.redirect || Endpoints.FIRST_FACTOR_GET;
@ -15,5 +17,6 @@ export default function (req: express.Request, res: express.Response): BluebirdP
redirection_url: redirectUrl
});
return BluebirdPromise.resolve();
});
})
.catch(ErrorReplies.replyWithError500(req, res, logger));
}

View File

@ -71,10 +71,10 @@ export default class RegistrationHandler implements IdentityValidable {
const totpGenerator = ServerVariablesHandler.getTOTPGenerator(req.app);
const secret = totpGenerator.generate();
logger.debug("POST new-totp-secret: save the TOTP secret in DB");
logger.debug(req, "Save the TOTP secret in DB");
return userDataStore.saveTOTPSecret(userid, secret)
.then(function () {
objectPath.set(req, "session", undefined);
AuthenticationSession.reset(req);
res.render(Constants.TEMPLATE_NAME, {
base32_secret: secret.base32,
@ -83,7 +83,7 @@ export default class RegistrationHandler implements IdentityValidable {
});
});
})
.catch(ErrorReplies.replyWithError500(res, logger));
.catch(ErrorReplies.replyWithError500(req, res, logger));
}
mailSubject(): string {

View File

@ -25,19 +25,19 @@ export function handler(req: express.Request, res: express.Response): BluebirdPr
return AuthenticationSession.get(req)
.then(function (_authSession: AuthenticationSession.AuthenticationSession) {
authSession = _authSession;
logger.info("POST 2ndfactor totp: Initiate TOTP validation for user %s", authSession.userid);
logger.info(req, "Initiate TOTP validation for user '%s'", authSession.userid);
return userDataStore.retrieveTOTPSecret(authSession.userid);
})
.then(function (doc: TOTPSecretDocument) {
logger.debug("POST 2ndfactor totp: TOTP secret is %s", JSON.stringify(doc));
logger.debug(req, "TOTP secret is %s", JSON.stringify(doc));
return totpValidator.validate(token, doc.secret.base32);
})
.then(function () {
logger.debug("POST 2ndfactor totp: TOTP validation succeeded");
logger.debug(req, "TOTP validation succeeded");
authSession.second_factor = true;
redirect(req, res);
return BluebirdPromise.resolve();
})
.catch(exceptions.InvalidTOTPError, ErrorReplies.replyWithError401(res, logger))
.catch(ErrorReplies.replyWithError500(res, logger));
.catch(exceptions.InvalidTOTPError, ErrorReplies.replyWithError401(req, res, logger))
.catch(ErrorReplies.replyWithError500(req, res, logger));
}

View File

@ -43,9 +43,9 @@ function handler(req: express.Request, res: express.Response): BluebirdPromise<v
return BluebirdPromise.reject(new Error("Bad challenge for registration request"));
}
logger.info("U2F register: Finishing registration");
logger.debug("U2F register: registrationRequest = %s", JSON.stringify(registrationRequest));
logger.debug("U2F register: registrationResponse = %s", JSON.stringify(registrationResponse));
logger.info(req, "Finishing registration");
logger.debug(req, "RegistrationRequest = %s", JSON.stringify(registrationRequest));
logger.debug(req, "RegistrationResponse = %s", JSON.stringify(registrationResponse));
return BluebirdPromise.resolve(u2f.checkRegistration(registrationRequest, registrationResponse));
})
@ -54,8 +54,8 @@ function handler(req: express.Request, res: express.Response): BluebirdPromise<v
return BluebirdPromise.reject(new Error("Error while registering."));
const registrationResult: U2f.RegistrationResult = u2fResult as U2f.RegistrationResult;
logger.info("U2F register: Store regisutration and reply");
logger.debug("U2F register: registration = %s", JSON.stringify(registrationResult));
logger.info(req, "Store registration and reply");
logger.debug(req, "RegistrationResult = %s", JSON.stringify(registrationResult));
const registration: U2FRegistration = {
keyHandle: registrationResult.keyHandle,
publicKey: registrationResult.publicKey
@ -67,5 +67,5 @@ function handler(req: express.Request, res: express.Response): BluebirdPromise<v
redirect(req, res);
return BluebirdPromise.resolve();
})
.catch(ErrorReplies.replyWithError500(res, logger));
.catch(ErrorReplies.replyWithError500(req, res, logger));
}

View File

@ -31,16 +31,15 @@ function handler(req: express.Request, res: express.Response): BluebirdPromise<v
const u2f = ServerVariablesHandler.getU2F(req.app);
const appid: string = u2f_common.extract_app_id(req);
logger.debug("U2F register_request: headers=%s", JSON.stringify(req.headers));
logger.info("U2F register_request: Starting registration for appId %s", appid);
logger.info(req, "Starting registration for appId '%s'", appid);
return BluebirdPromise.resolve(u2f.request(appid));
})
.then(function (registrationRequest: U2f.Request) {
logger.debug("U2F register_request: registrationRequest = %s", JSON.stringify(registrationRequest));
logger.debug(req, "RegistrationRequest = %s", JSON.stringify(registrationRequest));
authSession.register_request = registrationRequest;
res.json(registrationRequest);
return BluebirdPromise.resolve();
})
.catch(ErrorReplies.replyWithError500(res, logger));
.catch(ErrorReplies.replyWithError500(req, res, logger));
}

View File

@ -26,7 +26,7 @@ export function handler(req: express.Request, res: express.Response): BluebirdPr
authSession = _authSession;
if (!authSession.sign_request) {
const err = new Error("No sign request");
ErrorReplies.replyWithError401(res, logger)(err);
ErrorReplies.replyWithError401(req, res, logger)(err);
return BluebirdPromise.reject(err);
}
@ -39,17 +39,17 @@ export function handler(req: express.Request, res: express.Response): BluebirdPr
const u2f = ServerVariablesHandler.getU2F(req.app);
const signRequest = authSession.sign_request;
const signData: U2f.SignatureData = req.body;
logger.info("U2F sign: Finish authentication");
logger.info(req, "Finish authentication");
return BluebirdPromise.resolve(u2f.checkSignature(signRequest, signData, doc.registration.publicKey));
})
.then(function (result: U2f.SignatureResult | U2f.Error): BluebirdPromise<void> {
if (objectPath.has(result, "errorCode"))
return BluebirdPromise.reject(new Error("Error while signing"));
logger.info("U2F sign: Authentication successful");
logger.info(req, "Successful authentication");
authSession.second_factor = true;
redirect(req, res);
return BluebirdPromise.resolve();
})
.catch(ErrorReplies.replyWithError500(res, logger));
.catch(ErrorReplies.replyWithError500(req, res, logger));
}

View File

@ -33,8 +33,8 @@ export function handler(req: express.Request, res: express.Response): BluebirdPr
const u2f = ServerVariablesHandler.getU2F(req.app);
const appId: string = u2f_common.extract_app_id(req);
logger.info("U2F sign_request: Start authentication to app %s", appId);
logger.debug("U2F sign_request: appId=%s, keyHandle=%s", appId, JSON.stringify(doc.registration.keyHandle));
logger.info(req, "Start authentication of app '%s'", appId);
logger.debug(req, "AppId = %s, keyHandle = %s", appId, JSON.stringify(doc.registration.keyHandle));
const request = u2f.request(appId, doc.registration.keyHandle);
const authenticationMessage: SignMessage = {
@ -44,13 +44,13 @@ export function handler(req: express.Request, res: express.Response): BluebirdPr
return BluebirdPromise.resolve(authenticationMessage);
})
.then(function (authenticationMessage: SignMessage) {
logger.info("U2F sign_request: Store authentication request and reply");
logger.debug("U2F sign_request: authenticationRequest=%s", authenticationMessage);
logger.info(req, "Store authentication request and reply");
logger.debug(req, "AuthenticationRequest = %s", authenticationMessage);
authSession.sign_request = authenticationMessage.request;
res.json(authenticationMessage);
return BluebirdPromise.resolve();
})
.catch(exceptions.AccessDeniedError, ErrorReplies.replyWithError401(res, logger))
.catch(ErrorReplies.replyWithError500(res, logger));
.catch(exceptions.AccessDeniedError, ErrorReplies.replyWithError401(req, res, logger))
.catch(ErrorReplies.replyWithError500(req, res, logger));
}

View File

@ -9,6 +9,10 @@ import ErrorReplies = require("../../ErrorReplies");
import { ServerVariablesHandler } from "../../ServerVariablesHandler";
import AuthenticationSession = require("../../AuthenticationSession");
import Constants = require("../../../../../shared/constants");
import Util = require("util");
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);
@ -16,30 +20,36 @@ function verify_filter(req: express.Request, res: express.Response): BluebirdPro
return AuthenticationSession.get(req)
.then(function (authSession) {
logger.debug("Verify: headers are %s", JSON.stringify(req.headers));
res.set("Redirect", encodeURIComponent("https://" + req.headers["host"] + req.headers["x-original-uri"]));
res.set("Redirect", encodeURIComponent("https://" + req.headers["host"] +
req.headers["x-original-uri"]));
const username = authSession.userid;
const groups = authSession.groups;
if (!authSession.userid)
return BluebirdPromise.reject(
new exceptions.AccessDeniedError(FIRST_FACTOR_NOT_VALIDATED_MESSAGE));
const onlyBasicAuth = req.query[Constants.ONLY_BASIC_AUTH_QUERY_PARAM] === "true";
logger.debug("Verify: %s=%s", Constants.ONLY_BASIC_AUTH_QUERY_PARAM, onlyBasicAuth);
const host = objectPath.get<express.Request, string>(req, "headers.host");
const path = objectPath.get<express.Request, string>(req, "headers.x-original-uri");
const domain = host.split(":")[0];
logger.debug("Verify: domain=%s, path=%s", domain, path);
logger.debug("Verify: user=%s, groups=%s", username, groups.join(","));
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."));
return BluebirdPromise.reject(
new exceptions.AccessDeniedError(FIRST_FACTOR_NOT_VALIDATED_MESSAGE));
const isAllowed = accessController.isAccessAllowed(domain, path, username, groups);
if (!isAllowed) return BluebirdPromise.reject(
new exceptions.DomainAccessDenied("User '" + username + "' does not have access to " + domain));
new exceptions.DomainAccessDenied(Util.format("User '%s' does not have access to '%'",
username, domain)));
if (!onlyBasicAuth && !authSession.second_factor)
return BluebirdPromise.reject(new exceptions.AccessDeniedError("Second factor not validated."));
return BluebirdPromise.reject(
new exceptions.AccessDeniedError(SECOND_FACTOR_NOT_VALIDATED_MESSAGE));
res.setHeader("Remote-User", username);
res.setHeader("Remote-Groups", groups.join(","));
@ -56,7 +66,7 @@ export default function (req: express.Request, res: express.Response): BluebirdP
res.send();
return BluebirdPromise.resolve();
})
.catch(exceptions.DomainAccessDenied, ErrorReplies.replyWithError403(res, logger))
.catch(ErrorReplies.replyWithError401(res, logger));
.catch(exceptions.DomainAccessDenied, ErrorReplies.replyWithError403(req, res, logger))
.catch(ErrorReplies.replyWithError401(req, res, logger));
}

View File

@ -6,11 +6,10 @@ import express = require("express");
import winston = require("winston");
import speakeasy = require("speakeasy");
import u2f = require("u2f");
import nodemailer = require("nodemailer");
import session = require("express-session");
import { AppConfiguration, UserConfiguration } from "../src/lib/configuration/Configuration";
import { GlobalDependencies, Nodemailer } from "../types/Dependencies";
import { GlobalDependencies } from "../types/Dependencies";
import Server from "../src/lib/Server";
@ -19,17 +18,9 @@ describe("test server configuration", function () {
let sessionMock: Sinon.SinonSpy;
before(function () {
const transporter = {
sendMail: Sinon.stub().yields()
};
const createTransport = Sinon.stub(nodemailer, "createTransport");
createTransport.returns(transporter);
sessionMock = Sinon.spy(session);
deps = {
nodemailer: nodemailer,
speakeasy: speakeasy,
u2f: u2f,
nedb: nedb,
@ -79,7 +70,7 @@ describe("test server configuration", function () {
}
};
const server = new Server();
const server = new Server(deps);
server.start(config, deps);
assert(sessionMock.calledOnce);

View File

@ -55,7 +55,6 @@ describe("test session configuration builder", function () {
ConnectRedis: Sinon.spy() as any,
ldapjs: Sinon.spy() as any,
nedb: Sinon.spy() as any,
nodemailer: Sinon.spy() as any,
session: Sinon.spy() as any,
speakeasy: Sinon.spy() as any,
u2f: Sinon.spy() as any,
@ -132,7 +131,6 @@ describe("test session configuration builder", function () {
ConnectRedis: Sinon.stub().returns(RedisStoreMock) as any,
ldapjs: Sinon.spy() as any,
nedb: Sinon.spy() as any,
nodemailer: Sinon.spy() as any,
session: Sinon.spy() as any,
speakeasy: Sinon.spy() as any,
u2f: Sinon.spy() as any,

View File

@ -0,0 +1,26 @@
import { IRequestLogger } from "../../src/lib/logging/IRequestLogger";
import Sinon = require("sinon");
export class RequestLoggerStub implements IRequestLogger {
infoStub: Sinon.SinonStub;
debugStub: Sinon.SinonStub;
errorStub: Sinon.SinonStub;
constructor() {
this.infoStub = Sinon.stub();
this.debugStub = Sinon.stub();
this.errorStub = Sinon.stub();
}
info(req: Express.Request, message: string, ...args: any[]): void {
return this.infoStub(req, message, ...args);
}
debug(req: Express.Request, message: string, ...args: any[]): void {
return this.debugStub(req, message, ...args);
}
error(req: Express.Request, message: string, ...args: any[]): void {
return this.errorStub(req, message, ...args);
}
}

View File

@ -1,6 +1,6 @@
import Sinon = require("sinon");
import express = require("express");
import winston = require("winston");
import { RequestLoggerStub } from "./RequestLoggerStub";
import { UserDataStoreStub } from "./storage/UserDataStoreStub";
import { VARIABLES_KEY } from "../../src/lib/ServerVariablesHandler";
@ -27,7 +27,7 @@ export function mock(app: express.Application): ServerVariablesMock {
ldapAuthenticator: Sinon.stub() as any,
ldapEmailsRetriever: Sinon.stub() as any,
ldapPasswordUpdater: Sinon.stub() as any,
logger: winston,
logger: new RequestLoggerStub(),
notifier: Sinon.stub(),
regulator: Sinon.stub(),
totpGenerator: Sinon.stub(),

View File

@ -1,24 +0,0 @@
import sinon = require("sinon");
export interface NodemailerMock {
createTransport: sinon.SinonStub;
}
export function NodemailerMock(): NodemailerMock {
return {
createTransport: sinon.stub()
};
}
export interface NodemailerTransporterMock {
sendMail: sinon.SinonStub;
verify: sinon.SinonStub;
}
export function NodemailerTransporterMock() {
return {
sendMail: sinon.stub(),
verify: sinon.stub()
};
}

View File

@ -0,0 +1,25 @@
import { IMailSenderBuilder } from "../../../src/lib/notifiers/IMailSenderBuilder";
import BluebirdPromise = require("bluebird");
import Nodemailer = require("nodemailer");
import Sinon = require("sinon");
import { IMailSender } from "../../../src/lib/notifiers/IMailSender";
import { SmtpNotifierConfiguration, GmailNotifierConfiguration } from "../../../src/lib/configuration/Configuration";
export class MailSenderBuilderStub implements IMailSenderBuilder {
buildGmailStub: Sinon.SinonStub;
buildSmtpStub: Sinon.SinonStub;
constructor() {
this.buildGmailStub = Sinon.stub();
this.buildSmtpStub = Sinon.stub();
}
buildGmail(options: GmailNotifierConfiguration): IMailSender {
return this.buildGmailStub(options);
}
buildSmtp(options: SmtpNotifierConfiguration): IMailSender {
return this.buildSmtpStub(options);
}
}

View File

@ -0,0 +1,16 @@
import { IMailSender } from "../../../src/lib/notifiers/IMailSender";
import BluebirdPromise = require("bluebird");
import Nodemailer = require("nodemailer");
import Sinon = require("sinon");
export class MailSenderStub implements IMailSender {
sendStub: Sinon.SinonStub;
constructor() {
this.sendStub = Sinon.stub();
}
send(mailOptions: Nodemailer.SendMailOptions): BluebirdPromise<void> {
return this.sendStub(mailOptions);
}
}

View File

@ -2,24 +2,20 @@ import * as sinon from "sinon";
import * as assert from "assert";
import BluebirdPromise = require("bluebird");
import NodemailerMock = require("../mocks/nodemailer");
import { MailSenderStub } from "../mocks/notifiers/MailSenderStub";
import GMailNotifier = require("../../src/lib/notifiers/GMailNotifier");
describe("test gmail notifier", function () {
it("should send an email", function () {
const transporter = {
sendMail: sinon.stub().yields()
};
const nodemailerMock = NodemailerMock.NodemailerMock();
nodemailerMock.createTransport.returns(transporter);
it("should send an email to given user", function () {
const mailSender = new MailSenderStub();
const options = {
username: "user_gmail",
password: "pass_gmail"
};
const sender = new GMailNotifier.GMailNotifier(options, nodemailerMock);
mailSender.sendStub.returns(BluebirdPromise.resolve());
const sender = new GMailNotifier.GMailNotifier(options, mailSender);
const subject = "subject";
const identity = {
@ -31,10 +27,34 @@ describe("test gmail notifier", function () {
return sender.notify(identity, subject, url)
.then(function () {
assert.equal(nodemailerMock.createTransport.getCall(0).args[0].auth.user, "user_gmail");
assert.equal(nodemailerMock.createTransport.getCall(0).args[0].auth.pass, "pass_gmail");
assert.equal(transporter.sendMail.getCall(0).args[0].to, "user@example.com");
assert.equal(transporter.sendMail.getCall(0).args[0].subject, "subject");
assert.equal(mailSender.sendStub.getCall(0).args[0].to, "user@example.com");
assert.equal(mailSender.sendStub.getCall(0).args[0].subject, "subject");
return BluebirdPromise.resolve();
});
});
it("should fail while sending an email", function () {
const mailSender = new MailSenderStub();
const options = {
username: "user_gmail",
password: "pass_gmail"
};
mailSender.sendStub.returns(BluebirdPromise.reject(new Error("Failed to send mail")));
const sender = new GMailNotifier.GMailNotifier(options, mailSender);
const subject = "subject";
const identity = {
userid: "user",
email: "user@example.com"
};
const url = "http://test.com";
return sender.notify(identity, subject, url)
.then(function () {
return BluebirdPromise.reject(new Error());
}, function() {
return BluebirdPromise.resolve();
});
});

View File

@ -0,0 +1,46 @@
import { MailSenderBuilder } from "../../src/lib/notifiers/MailSenderBuilder";
import Nodemailer = require("nodemailer");
import Sinon = require("sinon");
import Assert = require("assert");
describe("test MailSenderBuilder", function() {
let createTransportStub: Sinon.SinonStub;
beforeEach(function() {
createTransportStub = Sinon.stub(Nodemailer, "createTransport");
});
afterEach(function() {
createTransportStub.restore();
});
it("should create a gmail mail sender", function() {
const mailSenderBuilder = new MailSenderBuilder(Nodemailer);
mailSenderBuilder.buildGmail({
username: "user_gmail",
password: "pass_gmail"
});
Assert.equal(createTransportStub.getCall(0).args[0].auth.user, "user_gmail");
Assert.equal(createTransportStub.getCall(0).args[0].auth.pass, "pass_gmail");
});
it("should create a smtp mail sender", function() {
const mailSenderBuilder = new MailSenderBuilder(Nodemailer);
mailSenderBuilder.buildSmtp({
host: "mail.example.com",
password: "password",
port: 25,
secure: true,
username: "user"
});
Assert.deepStrictEqual(createTransportStub.getCall(0).args[0], {
host: "mail.example.com",
auth: {
pass: "password",
user: "user"
},
port: 25,
secure: true,
});
});
});

View File

@ -6,26 +6,23 @@ import * as assert from "assert";
import { NotifierFactory } from "../../src/lib/notifiers/NotifierFactory";
import { GMailNotifier } from "../../src/lib/notifiers/GMailNotifier";
import { SmtpNotifier } from "../../src/lib/notifiers/SmtpNotifier";
import NodemailerMock = require("../mocks/nodemailer");
import { MailSenderBuilderStub } from "../mocks/notifiers/MailSenderBuilderStub";
describe("test notifier factory", function() {
let nodemailerMock: NodemailerMock.NodemailerMock;
it("should build a Gmail Notifier", function() {
describe("test notifier factory", function () {
let mailSenderBuilderStub: MailSenderBuilderStub;
it("should build a Gmail Notifier", function () {
const options = {
gmail: {
username: "abc",
password: "password"
}
};
nodemailerMock = NodemailerMock.NodemailerMock();
const transporterMock = NodemailerMock.NodemailerTransporterMock();
nodemailerMock.createTransport.returns(transporterMock);
assert(NotifierFactory.build(options, nodemailerMock) instanceof GMailNotifier);
mailSenderBuilderStub = new MailSenderBuilderStub();
assert(NotifierFactory.build(options, mailSenderBuilderStub) instanceof GMailNotifier);
});
it("should build a SMTP Notifier", function() {
it("should build a SMTP Notifier", function () {
const options = {
smtp: {
username: "user",
@ -36,9 +33,7 @@ describe("test notifier factory", function() {
}
};
nodemailerMock = NodemailerMock.NodemailerMock();
const transporterMock = NodemailerMock.NodemailerTransporterMock();
nodemailerMock.createTransport.returns(transporterMock);
assert(NotifierFactory.build(options, nodemailerMock) instanceof SmtpNotifier);
mailSenderBuilderStub = new MailSenderBuilderStub();
assert(NotifierFactory.build(options, mailSenderBuilderStub) instanceof SmtpNotifier);
});
});

View File

@ -6,8 +6,6 @@ import express = require("express");
import nodemailer = require("nodemailer");
import Endpoints = require("../../shared/api");
import NodemailerMock = require("./mocks/nodemailer");
declare module "request" {
export interface RequestAPI<TRequest extends Request,
TOptions extends CoreOptions,
@ -28,58 +26,6 @@ export = function (port: number) {
const PORT = port;
const BASE_URL = "http://localhost:" + PORT;
function execute_reset_password(jar: request.CookieJar, transporter: NodemailerMock.NodemailerTransporterMock, user: string, new_password: string) {
return requestAsync.getAsync({
url: BASE_URL + Endpoints.RESET_PASSWORD_IDENTITY_START_GET,
jar: jar,
qs: { userid: user }
})
.then(function (res: request.RequestResponse) {
assert.equal(res.statusCode, 200);
const html_content = transporter.sendMail.getCall(0).args[0].html;
const regexp = /identity_token=([a-zA-Z0-9]+)/;
const token = regexp.exec(html_content)[1];
// console.log(html_content, token);
return requestAsync.getAsync({
url: BASE_URL + Endpoints.RESET_PASSWORD_IDENTITY_FINISH_GET + "?identity_token=" + token,
jar: jar
});
})
.then(function (res: request.RequestResponse) {
assert.equal(res.statusCode, 200);
return requestAsync.postAsync({
url: BASE_URL + Endpoints.RESET_PASSWORD_FORM_POST,
jar: jar,
form: {
password: new_password
}
});
});
}
function execute_register_totp(jar: request.CookieJar, transporter: NodemailerMock.NodemailerTransporterMock) {
return requestAsync.getAsync({
url: BASE_URL + Endpoints.SECOND_FACTOR_TOTP_IDENTITY_START_GET,
jar: jar
})
.then(function (res: request.RequestResponse) {
assert.equal(res.statusCode, 200);
const html_content = transporter.sendMail.getCall(0).args[0].html;
const regexp = /identity_token=([a-zA-Z0-9]+)/;
const token = regexp.exec(html_content)[1];
return requestAsync.getAsync({
url: BASE_URL + Endpoints.SECOND_FACTOR_TOTP_IDENTITY_FINISH_GET + "?identity_token=" + token,
jar: jar
});
})
.then(function (res: request.RequestResponse) {
assert.equal(res.statusCode, 200);
const regex = /<p id="secret">([A-Z0-9]+)<\/p>/g;
const secret = regex.exec(res.body);
return BluebirdPromise.resolve(secret[1]);
});
}
function execute_totp(jar: request.CookieJar, token: string) {
return requestAsync.postAsync({
url: BASE_URL + Endpoints.SECOND_FACTOR_TOTP_POST,
@ -114,41 +60,6 @@ export = function (port: number) {
return requestAsync.getAsync({ url: BASE_URL + Endpoints.FIRST_FACTOR_GET, jar: jar });
}
function execute_u2f_registration(jar: request.CookieJar, transporter: NodemailerMock.NodemailerTransporterMock) {
return requestAsync.getAsync({
url: BASE_URL + Endpoints.SECOND_FACTOR_U2F_IDENTITY_START_GET,
jar: jar
})
.then(function (res: request.RequestResponse) {
assert.equal(res.statusCode, 200);
const html_content = transporter.sendMail.getCall(0).args[0].html;
const regexp = /identity_token=([a-zA-Z0-9]+)/;
const token = regexp.exec(html_content)[1];
// console.log(html_content, token);
return requestAsync.getAsync({
url: BASE_URL + Endpoints.SECOND_FACTOR_U2F_IDENTITY_FINISH_GET + "?identity_token=" + token,
jar: jar
});
})
.then(function (res: request.RequestResponse) {
assert.equal(res.statusCode, 200);
return requestAsync.getAsync({
url: BASE_URL + Endpoints.SECOND_FACTOR_U2F_REGISTER_REQUEST_GET,
jar: jar,
});
})
.then(function (res: request.RequestResponse) {
assert.equal(res.statusCode, 200);
return requestAsync.postAsync({
url: BASE_URL + Endpoints.SECOND_FACTOR_U2F_REGISTER_POST,
jar: jar,
form: {
s: "test"
}
});
});
}
function execute_first_factor(jar: request.CookieJar) {
return requestAsync.postAsync({
url: BASE_URL + Endpoints.FIRST_FACTOR_POST,
@ -174,13 +85,10 @@ export = function (port: number) {
return {
login: execute_login,
verify: execute_verification,
reset_password: execute_reset_password,
u2f_authentication: execute_u2f_authentication,
u2f_registration: execute_u2f_registration,
first_factor: execute_first_factor,
failing_first_factor: execute_failing_first_factor,
totp: execute_totp,
register_totp: execute_register_totp,
};
};

View File

@ -13,7 +13,7 @@ import AuthenticationRegulatorMock = require("../../mocks/AuthenticationRegulato
import { AccessControllerStub } from "../../mocks/AccessControllerStub";
import ExpressMock = require("../../mocks/express");
import ServerVariablesMock = require("../../mocks/ServerVariablesMock");
import { ServerVariables } from "../../../src/lib/ServerVariablesHandler";
import { ServerVariables } from "../../../src/lib/ServerVariables";
describe("test the first factor validation route", function () {
let req: ExpressMock.RequestMock;
@ -68,7 +68,6 @@ describe("test the first factor validation route", function () {
authenticate: sinon.stub()
} as any;
serverVariables.config = configuration as any;
serverVariables.logger = winston as any;
serverVariables.regulator = regulator as any;
serverVariables.accessController = accessController as any;

View File

@ -56,7 +56,6 @@ describe("test reset password identity check", function () {
}
};
serverVariables.logger = winston;
serverVariables.config = configuration;
serverVariables.ldapEmailsRetriever = {
retrieve: Sinon.stub()

View File

@ -51,7 +51,6 @@ describe("test reset password route", function () {
}
};
serverVariables.logger = winston;
serverVariables.config = configuration;
serverVariables.ldapPasswordUpdater = {

View File

@ -19,7 +19,6 @@ describe("test totp register", function () {
beforeEach(function () {
req = ExpressMock.RequestMock();
const mocks = ServerVariablesMock.mock(req.app);
mocks.logger = winston;
req.session = {};
AuthenticationSession.reset(req as any);

View File

@ -44,8 +44,6 @@ describe("test totp route", function () {
}
};
mocks.userDataStore.retrieveTOTPSecretStub.returns(BluebirdPromise.resolve(doc));
mocks.logger = winston;
mocks.totpValidator = totpValidator;
mocks.config = config;

View File

@ -20,7 +20,6 @@ describe("test register handler", function () {
req = ExpressMock.RequestMock();
req.app = {};
const mocks = ServerVariablesMock.mock(req.app);
mocks.logger = winston;
req.session = {};
AuthenticationSession.reset(req as any);
req.headers = {};

View File

@ -22,8 +22,6 @@ describe("test u2f routes: register", function () {
req = ExpressMock.RequestMock();
req.app = {};
mocks = ServerVariablesMock.mock(req.app);
mocks.logger = winston;
req.session = {};
req.headers = {};
req.headers.host = "localhost";

View File

@ -23,7 +23,6 @@ describe("test u2f routes: register_request", function () {
req = ExpressMock.RequestMock();
req.app = {};
mocks = ServerVariablesMock.mock(req.app);
mocks.logger = winston;
req.session = {};
AuthenticationSession.reset(req as any);

View File

@ -23,8 +23,6 @@ describe("test u2f routes: sign", function () {
req.app = {};
mocks = ServerVariablesMock.mock(req.app);
mocks.logger = winston;
req.session = {};
AuthenticationSession.reset(req as any);
req.headers = {};

View File

@ -26,7 +26,6 @@ describe("test u2f routes: sign_request", function () {
req.app = {};
mocks = ServerVariablesMock.mock(req.app);
mocks.logger = winston;
req.session = {};

View File

@ -36,7 +36,6 @@ describe("test authentication token verification", function () {
req.headers.host = "secret.example.com";
const mocks = ServerVariablesMock.mock(req.app);
mocks.config = {} as any;
mocks.logger = winston;
mocks.accessController = accessController as any;
});
@ -163,6 +162,7 @@ describe("test authentication token verification", function () {
return AuthenticationSession.get(req as any)
.then(function (authSession: AuthenticationSession.AuthenticationSession) {
authSession.first_factor = true;
authSession.userid = "user1";
return VerifyGet.default(req as express.Request, res as any);
})
.then(function () {

View File

@ -108,9 +108,8 @@ describe("Private pages of the server must not be accessible without session", f
ldap_client.search.yields(undefined, search_res);
const deps: GlobalDependencies = {
u2f: u2f,
u2f: u2f as any,
nedb: nedb,
nodemailer: nodemailer,
ldapjs: ldap,
session: ExpressSession,
winston: Winston,
@ -119,7 +118,7 @@ describe("Private pages of the server must not be accessible without session", f
dovehash: Sinon.spy() as any
};
server = new Server();
server = new Server(deps);
return server.start(config, deps);
});

View File

@ -108,9 +108,8 @@ describe("Public pages of the server must be accessible without session", functi
ldap_client.search.yields(undefined, search_res);
const deps: GlobalDependencies = {
u2f: u2f,
u2f: u2f as any,
nedb: nedb,
nodemailer: nodemailer,
ldapjs: ldap,
session: ExpressSession,
winston: Winston,
@ -119,7 +118,7 @@ describe("Public pages of the server must be accessible without session", functi
dovehash: Sinon.spy() as any
};
server = new Server();
server = new Server(deps);
return server.start(config, deps);
});

View File

@ -9,7 +9,6 @@ import RedisSession = require("connect-redis");
import dovehash = require("dovehash");
export type Dovehash = typeof dovehash;
export type Nodemailer = typeof nodemailer;
export type Speakeasy = typeof speakeasy;
export type Winston = typeof winston;
export type Session = typeof session;
@ -21,7 +20,6 @@ export type ConnectRedis = typeof RedisSession;
export interface GlobalDependencies {
u2f: U2f;
dovehash: Dovehash;
nodemailer: Nodemailer;
ldapjs: Ldapjs;
session: Session;
ConnectRedis: ConnectRedis;