Merge pull request #161 from clems4ever/inactivity_timeout

Inactivity timeout (soft session expiration timeout)
pull/162/head
Clément Michaud 2017-10-18 00:09:07 +02:00 committed by GitHub
commit 5300f67217
50 changed files with 922 additions and 471 deletions

1
.gitignore vendored
View File

@ -29,6 +29,7 @@ dist/
# Specific files
/config.yml
/config.test.yml
example/ldap/private.ldif

View File

@ -42,8 +42,8 @@ module.exports = function (grunt) {
args: ['--colors', '--compilers', 'ts:ts-node/register', '--recursive', 'client/test']
},
"test-int": {
cmd: "./node_modules/.bin/cucumber-js",
args: ["--colors", "--compiler", "ts:ts-node/register", "./test/features"]
cmd: "./scripts/run-cucumber.sh",
args: ["./test/features"]
},
"docker-build": {
cmd: "docker",
@ -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,11 @@ 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 inactivity time in ms before the session is reset.
inactivity: 300000 # 5 minutes
# The domain to protect.
# Note: the authenticator must also be in that domain. If empty, the cookie

View File

@ -23,8 +23,8 @@ ldap:
# An additional dn to define the scope to all users
additional_users_dn: ou=users
# The users filter.
# {0} is the matcher replaced by username.
# The users filter used to find the user DN
# {0} is a matcher replaced by username.
# 'cn={0}' by default.
users_filter: cn={0}
@ -47,6 +47,7 @@ ldap:
user: cn=admin,dc=example,dc=com
password: password
# Authentication methods
#
# Authentication methods can be defined per subdomain.
@ -65,17 +66,36 @@ authentication_methods:
# Access Control
#
# Access control is a set of rules you can use to restrict the user access.
# Default (anyone), per-user or per-group rules can be defined.
# Access control is a set of rules you can use to restrict user access to certain
# resources.
# Any (apply to anyone), per-user or per-group rules can be defined.
#
# If 'access_control' is not defined, ACL rules are disabled and a default policy
# is applied, i.e., access is allowed to anyone. Otherwise restrictions follow
# the rules defined below.
# If no rule is provided, all domains are denied.
# If 'access_control' is not defined, ACL rules are disabled and the `allow` default
# policy is applied, i.e., access is allowed to anyone. Otherwise restrictions follow
# the rules defined.
#
# Note: One can use the wildcard * to match any subdomain.
# It must stand at the beginning of the pattern. (example: *.mydomain.com)
#
# Note: You must put the pattern in simple quotes when using the wildcard for the YAML
# to be syntaxically correct.
#
# Definition: A `rule` is an object with the following keys: `domain`, `policy`
# and `resources`.
# - `domain` defines which domain or set of domains the rule applies to.
# - `policy` is the policy to apply to resources. It must be either `allow` or `deny`.
# - `resources` is a list of regular expressions that matches a set of resources to
# apply the policy to.
#
# Note: Rules follow an order of priority defined as follows:
# In each category (`any`, `groups`, `users`), the latest rules have the highest
# priority. In other words, it means that if a given resource matches two rules in the
# same category, the latest one overrides the first one.
# Each category has also its own priority. That is, `users` has the highest priority, then
# `groups` and `any` has the lowest priority. It means if two rules in different categories
# match a given resource, the one in the category with the highest priority overrides the
# other one.
#
# One can use the wildcard * to match any subdomain.
# Note 1: It must stand at the beginning of the pattern. (example: *.mydomain.com)
# Note 2: You must put the pattern in simple quotes when using the wildcard.
access_control:
# Default policy can either be `allow` or `deny`.
# It is the policy applied to any resource if it has not been overriden
@ -125,15 +145,19 @@ access_control:
resources:
- '^/users/bob/.*$'
# Configuration of session cookies
#
# 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
# The time in ms before the cookie expires and session is reset.
expiration: 3600000 # 1 hour
# The inactivity time in ms before the session is reset.
inactivity: 300000 # 5 minutes
# The domain to protect.
# Note: the authenticator must also be in that domain. If empty, the cookie
@ -178,6 +202,10 @@ storage:
# registration or a TOTP registration.
# Use only an available configuration: filesystem, gmail
notifier:
# For testing purpose, notifications can be sent in a file
# filesystem:
# filename: /tmp/authelia/notification.txt
# Use your gmail account to send the notifications. You can use an app password.
# gmail:
# username: user@example.com
@ -187,7 +215,7 @@ notifier:
# Use a SMTP server for sending notifications
smtp:
username: test
password: test
password: password
secure: false
host: 'smtp'
port: 1025

View File

@ -0,0 +1,3 @@
#!/bin/bash
./node_modules/.bin/cucumber-js --colors --compiler ts:ts-node/register $*

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: number;
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().getTime();
}
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,16 +96,19 @@ 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 {
return (app.get(VARIABLES_KEY) as ServerVariables).logger;
@ -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

@ -62,6 +62,7 @@ export interface SessionRedisOptions {
interface SessionCookieConfiguration {
secret: string;
expiration?: number;
inactivity?: number;
domain?: string;
redis?: SessionRedisOptions;
}

View File

@ -71,6 +71,7 @@ function adaptFromUserConfiguration(userConfiguration: UserConfiguration)
domain: ObjectPath.get<object, string>(userConfiguration, "session.domain"),
secret: ObjectPath.get<object, string>(userConfiguration, "session.secret"),
expiration: get_optional<number>(userConfiguration, "session.expiration", 3600000), // in ms
inactivity: get_optional<number>(userConfiguration, "session.inactivity", undefined),
redis: ObjectPath.get<object, SessionRedisOptions>(userConfiguration, "session.redis")
},
storage: {

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,18 +13,14 @@ 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> {
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;
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()
@ -32,19 +28,19 @@ export default function (req: express.Request, res: express.Response): BluebirdP
if (!username || !password) {
return BluebirdPromise.reject(new Error("No username or password."));
}
logger.info(req, "Starting authentication of user \"%s\"", username);
vars.logger.info(req, "Starting authentication of user \"%s\"", username);
return AuthenticationSession.get(req);
})
.then(function (_authSession: AuthenticationSession.AuthenticationSession) {
authSession = _authSession;
return regulator.regulate(username);
return vars.regulator.regulate(username);
})
.then(function () {
logger.info(req, "No regulation applied.");
return ldap.authenticate(username, password);
vars.logger.info(req, "No regulation applied.");
return vars.ldapAuthenticator.authenticate(username, password);
})
.then(function (groupsAndEmails: GroupsAndEmails) {
logger.info(req, "LDAP binding successful. Retrieved information about user are %s",
vars.logger.info(req, "LDAP binding successful. Retrieved information about user are %s",
JSON.stringify(groupsAndEmails));
authSession.userid = username;
authSession.first_factor = true;
@ -56,26 +52,29 @@ export default function (req: express.Request, res: express.Response): BluebirdP
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);
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.";
logger.error(req, "%s", errMessage);
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;
logger.debug(req, "Mark successful authentication to regulator.");
regulator.mark(username, true);
vars.logger.debug(req, "Mark successful authentication to regulator.");
vars.regulator.mark(username, true);
if (authMethod == "basic_auth") {
res.send({
redirect: redirectUrl
});
logger.debug(req, "Redirect to '%s'", redirectUrl);
vars.logger.debug(req, "Redirect to '%s'", redirectUrl);
}
else if (authMethod == "two_factor") {
let newRedirectUrl = Endpoint.SECOND_FACTOR_GET;
@ -83,7 +82,7 @@ export default function (req: express.Request, res: express.Response): BluebirdP
newRedirectUrl += "?" + Constants.REDIRECT_QUERY_PARAM + "="
+ encodeURIComponent(redirectUrl);
}
logger.debug(req, "Redirect to '%s'", newRedirectUrl, typeof redirectUrl);
vars.logger.debug(req, "Redirect to '%s'", newRedirectUrl, typeof redirectUrl);
res.send({
redirect: newRedirectUrl
});
@ -94,8 +93,9 @@ export default function (req: express.Request, res: express.Response): BluebirdP
return BluebirdPromise.resolve();
})
.catch(Exceptions.LdapBindError, function (err: Error) {
regulator.mark(username, false);
return ErrorReplies.replyWithError200(req, res, logger, UserMessages.OPERATION_FAILED)(err);
vars.regulator.mark(username, false);
return ErrorReplies.replyWithError200(req, res, vars.logger, UserMessages.OPERATION_FAILED)(err);
})
.catch(ErrorReplies.replyWithError200(req, res, logger, UserMessages.OPERATION_FAILED));
.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 function handler(req: express.Request, res: express.Response): BluebirdPromise<void> {
export default function (vars: ServerVariables) {
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;
logger.info(req, "Initiate TOTP validation for user '%s'.", authSession.userid);
return userDataStore.retrieveTOTPSecret(authSession.userid);
vars.logger.info(req, "Initiate TOTP validation for user '%s'.", authSession.userid);
return vars.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.");
vars.logger.debug(req, "TOTP secret is %s", JSON.stringify(doc));
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, logger,
.catch(ErrorReplies.replyWithError200(req, res, vars.logger,
UserMessages.OPERATION_FAILED));
}
return FirstFactorBlocker(handler);
}

View File

@ -6,28 +6,63 @@ 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";
import { IRequestLogger } from "../../logging/IRequestLogger";
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_inactivity(req: express.Request,
authSession: AuthenticationSession.AuthenticationSession,
configuration: AppConfiguration, logger: IRequestLogger)
: BluebirdPromise<void> {
const lastActivityTime = authSession.last_activity_datetime;
const currentTime = new Date().getTime();
authSession.last_activity_datetime = currentTime;
// If inactivity is not specified, then inactivity timeout does not apply
if (!configuration.session.inactivity) {
return BluebirdPromise.resolve();
}
const inactivityPeriodMs = currentTime - lastActivityTime;
logger.debug(req, "Inactivity period was %s s and max period was %s.",
inactivityPeriodMs / 1000, configuration.session.inactivity / 1000);
if (inactivityPeriodMs < configuration.session.inactivity) {
return BluebirdPromise.resolve();
}
logger.debug(req, "Session has been reset after too long inactivity period.");
AuthenticationSession.reset(req);
return BluebirdPromise.reject(new Error("Inactivity period exceeded."));
}
function verify_filter(req: express.Request, res: express.Response,
vars: ServerVariables): BluebirdPromise<void> {
let _authSession: AuthenticationSession.AuthenticationSession;
let username: string;
let groups: string[];
return AuthenticationSession.get(req)
.then(function (authSession) {
_authSession = authSession;
username = _authSession.userid;
groups = _authSession.groups;
res.set("Redirect", encodeURIComponent("https://" + req.headers["host"] +
req.headers["x-original-uri"]));
const username = authSession.userid;
const groups = authSession.groups;
if (!authSession.userid)
if (!_authSession.userid)
return BluebirdPromise.reject(
new exceptions.AccessDeniedError(FIRST_FACTOR_NOT_VALIDATED_MESSAGE));
@ -35,33 +70,40 @@ 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)
if (!_authSession.first_factor)
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(Util.format("User '%s' does not have access to '%s'",
username, domain)));
if (authenticationMethod == "two_factor" && !authSession.second_factor)
if (authenticationMethod == "two_factor" && !_authSession.second_factor)
return BluebirdPromise.reject(
new exceptions.AccessDeniedError(SECOND_FACTOR_NOT_VALIDATED_MESSAGE));
res.setHeader("Remote-User", username);
res.setHeader("Remote-Groups", groups.join(","));
const isAllowed = vars.accessController.isAccessAllowed(domain, path, username, groups);
if (!isAllowed) return BluebirdPromise.reject(
new exceptions.DomainAccessDenied(Util.format("User '%s' does not have access to '%s'",
username, domain)));
return BluebirdPromise.resolve();
})
.then(function () {
return verify_inactivity(req, _authSession,
vars.config, vars.logger);
})
.then(function () {
res.setHeader(REMOTE_USER, username);
res.setHeader(REMOTE_GROUPS, groups.join(","));
});
}
export default function (req: express.Request, res: express.Response): BluebirdPromise<void> {
const logger = ServerVariablesHandler.getLogger(req.app);
return verify_filter(req, res)
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();
@ -69,8 +111,9 @@ export default function (req: express.Request, res: express.Response): BluebirdP
})
// The user is authenticated but has restricted access -> 403
.catch(exceptions.DomainAccessDenied, ErrorReplies
.replyWithError403(req, res, logger))
.replyWithError403(req, res, vars.logger))
// The user is not yet authenticated -> 401
.catch(ErrorReplies.replyWithError401(req, res, logger));
.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

@ -113,6 +113,7 @@ describe("test session configuration builder", function () {
domain: "example.com",
expiration: 3600,
secret: "secret",
inactivity: 4000,
redis: {
host: "redis.example.com",
port: 6379

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

@ -60,7 +60,23 @@ describe("test config parser", function () {
});
});
describe("test session configuration", function() {
it("should get the session attributes", function () {
const yaml_config = buildYamlConfig();
yaml_config.session = {
domain: "example.com",
secret: "secret",
expiration: 3600,
inactivity: 4000
};
const config = ConfigurationParser.parse(yaml_config);
Assert.equal(config.session.domain, "example.com");
Assert.equal(config.session.secret, "secret");
Assert.equal(config.session.expiration, 3600);
Assert.equal(config.session.inactivity, 4000);
});
it("should be ok not specifying inactivity", function () {
const yaml_config = buildYamlConfig();
yaml_config.session = {
domain: "example.com",
@ -71,6 +87,8 @@ describe("test config parser", function () {
Assert.equal(config.session.domain, "example.com");
Assert.equal(config.session.secret, "secret");
Assert.equal(config.session.expiration, 3600);
Assert.equal(config.session.inactivity, undefined);
});
});
it("should get the log level", function () {

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().getTime()
});
});
@ -109,6 +102,7 @@ describe("test authentication token verification", function () {
second_factor: true,
email: undefined,
groups: [],
last_activity_datetime: new Date().getTime()
});
});
@ -119,6 +113,7 @@ describe("test authentication token verification", function () {
second_factor: false,
email: undefined,
groups: [],
last_activity_datetime: new Date().getTime()
});
});
@ -129,6 +124,7 @@ describe("test authentication token verification", function () {
second_factor: false,
email: undefined,
groups: [],
last_activity_datetime: new Date().getTime()
});
});
@ -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().getTime()
});
});
});
@ -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,15 +180,64 @@ 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));
});
});
});
describe("inactivity period", function () {
it("should update last inactivity period on requests on /verify", function () {
mocks.config.session.inactivity = 200000;
mocks.accessController.isAccessAllowedMock.returns(true);
const currentTime = new Date().getTime() - 1000;
AuthenticationSession.reset(req as any);
return AuthenticationSession.get(req as any)
.then(function (authSession: AuthenticationSession.AuthenticationSession) {
authSession.first_factor = true;
authSession.second_factor = true;
authSession.userid = "myuser";
authSession.groups = ["mygroup", "othergroup"];
authSession.last_activity_datetime = currentTime;
return VerifyGet.default(vars)(req as express.Request, res as any);
})
.then(function () {
return AuthenticationSession.get(req as any);
})
.then(function (authSession) {
Assert(authSession.last_activity_datetime > currentTime);
});
});
it("should reset session when max inactivity period has been reached", function () {
mocks.config.session.inactivity = 1;
mocks.accessController.isAccessAllowedMock.returns(true);
const currentTime = new Date().getTime() - 1000;
AuthenticationSession.reset(req as any);
return AuthenticationSession.get(req as any)
.then(function (authSession: AuthenticationSession.AuthenticationSession) {
authSession.first_factor = true;
authSession.second_factor = true;
authSession.userid = "myuser";
authSession.groups = ["mygroup", "othergroup"];
authSession.last_activity_datetime = currentTime;
return VerifyGet.default(vars)(req as express.Request, res as any);
})
.then(function () {
return AuthenticationSession.get(req as any);
})
.then(function (authSession) {
Assert.equal(authSession.first_factor, false);
Assert.equal(authSession.second_factor, false);
Assert.equal(authSession.userid, undefined);
});
});
});
});

View File

@ -20,8 +20,6 @@ Feature: User is correctly redirected
And I visit "https://admin.test.local:8080/secret.html"
Then I get an error 403
Scenario: Redirection URL is propagated from restricted page to first factor
When I visit "https://public.test.local:8080/secret.html"
Then I'm redirected to "https://auth.test.local:8080/?redirect=https%3A%2F%2Fpublic.test.local%3A8080%2Fsecret.html"

View File

@ -1,6 +1,6 @@
@needs-regulation-config
Feature: Authelia regulates authentication to avoid brute force
@needs-test-config
@need-registered-user-blackhat
Scenario: Attacker tries too many authentication in a short period of time and get banned
Given I visit "https://auth.test.local:8080/"
@ -18,7 +18,6 @@ Feature: Authelia regulates authentication to avoid brute force
And I click on "Sign in"
Then I get a notification of type "error" with message "Authentication failed. Please check your credentials."
@needs-test-config
@need-registered-user-blackhat
Scenario: User is unbanned after a configured amount of time
Given I visit "https://auth.test.local:8080/?redirect=https%3A%2F%2Fpublic.test.local%3A8080%2Fsecret.html"

View File

@ -0,0 +1,24 @@
@needs-inactivity-config
Feature: Session is closed after a certain amount of time
@need-authenticated-user-john
Scenario: An authenticated user is disconnected after a certain inactivity period
Given I have access to:
| url |
| https://public.test.local:8080/secret.html |
When I sleep for 6 seconds
And I visit "https://public.test.local:8080/secret.html"
Then I'm redirected to "https://auth.test.local:8080/?redirect=https%3A%2F%2Fpublic.test.local%3A8080%2Fsecret.html"
@need-authenticated-user-john
Scenario: An authenticated user is disconnected after session expiration period
Given I have access to:
| url |
| https://public.test.local:8080/secret.html |
When I sleep for 4 seconds
And I visit "https://public.test.local:8080/secret.html"
And I sleep for 4 seconds
And I visit "https://public.test.local:8080/secret.html"
And I sleep for 4 seconds
And I visit "https://public.test.local:8080/secret.html"
Then I'm redirected to "https://auth.test.local:8080/?redirect=https%3A%2F%2Fpublic.test.local%3A8080%2Fsecret.html"

View File

@ -6,7 +6,7 @@ import { UserDataStore } from "../../../server/src/lib/storage/UserDataStore";
import { CollectionFactoryFactory } from "../../../server/src/lib/storage/CollectionFactoryFactory";
import { MongoConnector } from "../../../server/src/lib/connectors/mongo/MongoConnector";
import { IMongoClient } from "../../../server/src/lib/connectors/mongo/IMongoClient";
import { TOTPGenerator } from "../../../server/src/lib/TOTPGenerator";
import { TotpHandler } from "../../../server/src/lib/authentication/totp/TotpHandler";
import Speakeasy = require("speakeasy");
Cucumber.defineSupportCode(function ({ setDefaultTimeout }) {
@ -14,19 +14,46 @@ Cucumber.defineSupportCode(function ({ setDefaultTimeout }) {
});
Cucumber.defineSupportCode(function ({ After, Before }) {
const exec = BluebirdPromise.promisify(ChildProcess.exec);
const exec = BluebirdPromise.promisify<any, any>(ChildProcess.exec);
After(function () {
return this.driver.quit();
});
Before({ tags: "@needs-test-config", timeout: 20 * 1000 }, function () {
return exec("./scripts/example-commit/dc-example.sh -f docker-compose.test.yml up -d authelia && sleep 2");
function createRegulationConfiguration(): BluebirdPromise<void> {
return exec("\
cat config.template.yml | \
sed 's/find_time: [0-9]\\+/find_time: 15/' | \
sed 's/ban_time: [0-9]\\+/ban_time: 4/' > config.test.yml \
");
}
function createInactivityConfiguration(): BluebirdPromise<void> {
return exec("\
cat config.template.yml | \
sed 's/expiration: [0-9]\\+/expiration: 10000/' | \
sed 's/inactivity: [0-9]\\+/inactivity: 5000/' > config.test.yml \
");
}
function declareNeedsConfiguration(tag: string, cb: () => BluebirdPromise<void>) {
Before({ tags: "@needs-" + tag + "-config", timeout: 20 * 1000 }, function () {
return cb()
.then(function () {
return exec("./scripts/example-commit/dc-example.sh -f docker-compose.test.yml up -d authelia && sleep 1");
})
});
After({ tags: "@needs-test-config", timeout: 20 * 1000 }, function () {
return exec("./scripts/example-commit/dc-example.sh up -d authelia && sleep 2");
After({ tags: "@needs-" + tag + "-config", timeout: 20 * 1000 }, function () {
return exec("rm config.test.yml")
.then(function () {
return exec("./scripts/example-commit/dc-example.sh up -d authelia && sleep 1");
});
});
}
declareNeedsConfiguration("regulation", createRegulationConfiguration);
declareNeedsConfiguration("inactivity", createInactivityConfiguration);
function registerUser(context: any, username: string) {
let secret: Speakeasy.Key;
@ -36,7 +63,7 @@ Cucumber.defineSupportCode(function ({ After, Before }) {
const collectionFactory = CollectionFactoryFactory.createMongo(mongoClient);
const userDataStore = new UserDataStore(collectionFactory);
const generator = new TOTPGenerator(Speakeasy);
const generator = new TotpHandler(Speakeasy);
secret = generator.generate();
return userDataStore.saveTOTPSecret(username, secret);
})
@ -56,7 +83,10 @@ Cucumber.defineSupportCode(function ({ After, Before }) {
}
function needAuthenticatedUser(context: any, username: string): BluebirdPromise<void> {
return context.visit("https://auth.test.local:8080/")
return context.visit("https://auth.test.local:8080/logout")
.then(function () {
return context.visit("https://auth.test.local:8080/");
})
.then(function () {
return registerUser(context, username);
})

View File

@ -7,6 +7,6 @@ import BluebirdPromise = require("bluebird");
Cucumber.defineSupportCode(function ({ Given, When, Then }) {
When(/^the application restarts$/, {timeout: 15 * 1000}, function () {
const exec = BluebirdPromise.promisify(ChildProcess.exec);
return exec("./scripts/example-commit/dc-example.sh restart authelia && sleep 2");
return exec("./scripts/example-commit/dc-example.sh restart authelia && sleep 1");
});
});

View File

@ -0,0 +1,8 @@
import Cucumber = require("cucumber");
import seleniumWebdriver = require("selenium-webdriver");
Cucumber.defineSupportCode(function ({ Given, When, Then }) {
When("I sleep for {number} seconds", function (seconds: number) {
return this.driver.sleep(seconds * 1000);
});
});