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 # Specific files
/config.yml /config.yml
/config.test.yml
example/ldap/private.ldif example/ldap/private.ldif

View File

@ -42,8 +42,8 @@ module.exports = function (grunt) {
args: ['--colors', '--compilers', 'ts:ts-node/register', '--recursive', 'client/test'] args: ['--colors', '--compilers', 'ts:ts-node/register', '--recursive', 'client/test']
}, },
"test-int": { "test-int": {
cmd: "./node_modules/.bin/cucumber-js", cmd: "./scripts/run-cucumber.sh",
args: ["--colors", "--compiler", "ts:ts-node/register", "./test/features"] args: ["./test/features"]
}, },
"docker-build": { "docker-build": {
cmd: "docker", cmd: "docker",
@ -194,6 +194,8 @@ module.exports = function (grunt) {
grunt.registerTask('build', ['build-client', 'build-server']); grunt.registerTask('build', ['build-client', 'build-server']);
grunt.registerTask('build-dist', ['build', 'run:minify', 'cssmin', 'run:include-minified-script']); grunt.registerTask('build-dist', ['build', 'run:minify', 'cssmin', 'run:include-minified-script']);
grunt.registerTask('schema', ['run:generate-config-schema'])
grunt.registerTask('docker-build', ['run:docker-build']); grunt.registerTask('docker-build', ['run:docker-build']);
grunt.registerTask('default', ['build-dist']); grunt.registerTask('default', ['build-dist']);

View File

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

View File

@ -23,8 +23,8 @@ ldap:
# An additional dn to define the scope to all users # An additional dn to define the scope to all users
additional_users_dn: ou=users additional_users_dn: ou=users
# The users filter. # The users filter used to find the user DN
# {0} is the matcher replaced by username. # {0} is a matcher replaced by username.
# 'cn={0}' by default. # 'cn={0}' by default.
users_filter: cn={0} users_filter: cn={0}
@ -47,6 +47,7 @@ ldap:
user: cn=admin,dc=example,dc=com user: cn=admin,dc=example,dc=com
password: password password: password
# Authentication methods # Authentication methods
# #
# Authentication methods can be defined per subdomain. # Authentication methods can be defined per subdomain.
@ -65,17 +66,36 @@ authentication_methods:
# Access Control # Access Control
# #
# Access control is a set of rules you can use to restrict the user access. # Access control is a set of rules you can use to restrict user access to certain
# Default (anyone), per-user or per-group rules can be defined. # 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 # If 'access_control' is not defined, ACL rules are disabled and the `allow` default
# is applied, i.e., access is allowed to anyone. Otherwise restrictions follow # policy is applied, i.e., access is allowed to anyone. Otherwise restrictions follow
# the rules defined below. # the rules defined.
# If no rule is provided, all domains are denied. #
# 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: access_control:
# Default policy can either be `allow` or `deny`. # Default policy can either be `allow` or `deny`.
# It is the policy applied to any resource if it has not been overriden # It is the policy applied to any resource if it has not been overriden
@ -125,15 +145,19 @@ access_control:
resources: resources:
- '^/users/bob/.*$' - '^/users/bob/.*$'
# Configuration of session cookies # Configuration of session cookies
# #
# The session cookies identify the user once logged in. # The session cookies identify the user once logged in.
session: session:
# The secret to encrypt the session cookie. # The secret to encrypt the session cookie.
secret: unsecure_secret secret: unsecure_session_secret
# The time before the cookie expires. # The time in ms before the cookie expires and session is reset.
expiration: 3600000 expiration: 3600000 # 1 hour
# The inactivity time in ms before the session is reset.
inactivity: 300000 # 5 minutes
# The domain to protect. # The domain to protect.
# Note: the authenticator must also be in that domain. If empty, the cookie # Note: the authenticator must also be in that domain. If empty, the cookie
@ -156,10 +180,10 @@ regulation:
max_retries: 3 max_retries: 3
# The length of time between login attempts before user is banned. # The length of time between login attempts before user is banned.
find_time: 15 find_time: 15
# The length of time before a banned user can login again. # The length of time before a banned user can login again.
ban_time: 4 ban_time: 4
# Configuration of the storage backend used to store data and secrets. # Configuration of the storage backend used to store data and secrets.
# #
@ -178,6 +202,10 @@ storage:
# registration or a TOTP registration. # registration or a TOTP registration.
# Use only an available configuration: filesystem, gmail # Use only an available configuration: filesystem, gmail
notifier: 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. # Use your gmail account to send the notifications. You can use an app password.
# gmail: # gmail:
# username: user@example.com # username: user@example.com
@ -187,8 +215,8 @@ notifier:
# Use a SMTP server for sending notifications # Use a SMTP server for sending notifications
smtp: smtp:
username: test username: test
password: test password: password
secure: false secure: false
host: 'smtp' host: 'smtp'
port: 1025 port: 1025
sender: admin@example.com sender: admin@example.com

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 --version
docker-compose --version docker-compose --version
grunt run:generate-config-schema # Generate configuration schema
grunt schema
# Run unit tests # Run unit tests
grunt test-unit grunt test-unit

View File

@ -8,8 +8,11 @@ export class AuthenticationMethodCalculator {
} }
compute(subDomain: string): AuthenticationMethod { compute(subDomain: string): AuthenticationMethod {
if (subDomain in this.configuration.per_subdomain_methods) if (this.configuration
&& this.configuration.per_subdomain_methods
&& subDomain in this.configuration.per_subdomain_methods) {
return this.configuration.per_subdomain_methods[subDomain]; return this.configuration.per_subdomain_methods[subDomain];
}
return this.configuration.default_method; return this.configuration.default_method;
} }
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,24 +0,0 @@
import Speakeasy = require("speakeasy");
import BluebirdPromise = require("bluebird");
import { TOTPSecret } from "../../types/TOTPSecret";
interface GenerateSecretOptions {
length?: number;
symbols?: boolean;
otpauth_url?: boolean;
name?: string;
issuer?: string;
}
export class TOTPGenerator {
private speakeasy: typeof Speakeasy;
constructor(speakeasy: typeof Speakeasy) {
this.speakeasy = speakeasy;
}
generate(options?: GenerateSecretOptions): TOTPSecret {
return this.speakeasy.generateSecret(options);
}
}

View File

@ -1,27 +0,0 @@
import Speakeasy = require("speakeasy");
import BluebirdPromise = require("bluebird");
const TOTP_ENCODING = "base32";
const WINDOW: number = 1;
export class TOTPValidator {
private speakeasy: typeof Speakeasy;
constructor(speakeasy: typeof Speakeasy) {
this.speakeasy = speakeasy;
}
validate(token: string, secret: string): BluebirdPromise<void> {
const isValid = this.speakeasy.totp.verify({
secret: secret,
encoding: TOTP_ENCODING,
token: token,
window: WINDOW
} as any);
if (isValid)
return BluebirdPromise.resolve();
else
return BluebirdPromise.reject(new Error("Wrong TOTP token."));
}
}

View File

@ -0,0 +1,14 @@
import { TOTPSecret } from "../../../../types/TOTPSecret";
export interface GenerateSecretOptions {
length?: number;
symbols?: boolean;
otpauth_url?: boolean;
name?: string;
issuer?: string;
}
export interface ITotpHandler {
generate(options?: GenerateSecretOptions): TOTPSecret;
validate(token: string, secret: string): boolean;
}

View File

@ -0,0 +1,27 @@
import { ITotpHandler, GenerateSecretOptions } from "./ITotpHandler";
import { TOTPSecret } from "../../../../types/TOTPSecret";
import Speakeasy = require("speakeasy");
const TOTP_ENCODING = "base32";
const WINDOW: number = 1;
export class TotpHandler implements ITotpHandler {
private speakeasy: typeof Speakeasy;
constructor(speakeasy: typeof Speakeasy) {
this.speakeasy = speakeasy;
}
generate(options?: GenerateSecretOptions): TOTPSecret {
return this.speakeasy.generateSecret(options);
}
validate(token: string, secret: string): boolean {
return this.speakeasy.totp.verify({
secret: secret,
encoding: TOTP_ENCODING,
token: token,
window: WINDOW
} as any);
}
}

View File

@ -0,0 +1,9 @@
import U2f = require("u2f");
export interface IU2fHandler {
request(appId: string, keyHandle?: string): U2f.Request;
checkRegistration(registrationRequest: U2f.Request, registrationResponse: U2f.RegistrationData)
: U2f.RegistrationResult | U2f.Error;
checkSignature(signatureRequest: U2f.Request, signatureResponse: U2f.SignatureData, publicKey: string)
: U2f.SignatureResult | U2f.Error;
}

View File

@ -0,0 +1,24 @@
import { IU2fHandler } from "./IU2fHandler";
import U2f = require("u2f");
export class U2fHandler implements IU2fHandler {
private u2f: typeof U2f;
constructor(u2f: typeof U2f) {
this.u2f = u2f;
}
request(appId: string, keyHandle?: string): U2f.Request {
return this.u2f.request(appId, keyHandle);
}
checkRegistration(registrationRequest: U2f.Request, registrationResponse: U2f.RegistrationData)
: U2f.RegistrationResult | U2f.Error {
return this.u2f.checkRegistration(registrationRequest, registrationResponse);
}
checkSignature(signatureRequest: U2f.Request, signatureResponse: U2f.SignatureData, publicKey: string)
: U2f.SignatureResult | U2f.Error {
return this.u2f.checkSignature(signatureRequest, signatureResponse, publicKey);
}
}

View File

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

View File

@ -71,6 +71,7 @@ function adaptFromUserConfiguration(userConfiguration: UserConfiguration)
domain: ObjectPath.get<object, string>(userConfiguration, "session.domain"), domain: ObjectPath.get<object, string>(userConfiguration, "session.domain"),
secret: ObjectPath.get<object, string>(userConfiguration, "session.secret"), secret: ObjectPath.get<object, string>(userConfiguration, "session.secret"),
expiration: get_optional<number>(userConfiguration, "session.expiration", 3600000), // in ms 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") redis: ObjectPath.get<object, SessionRedisOptions>(userConfiguration, "session.redis")
}, },
storage: { 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 * as BluebirdPromise from "bluebird";
import exceptions = require("./Exceptions"); import exceptions = require("../Exceptions");
import { IUserDataStore } from "./storage/IUserDataStore"; import { IUserDataStore } from "../storage/IUserDataStore";
import { AuthenticationTraceDocument } from "./storage/AuthenticationTraceDocument"; import { AuthenticationTraceDocument } from "../storage/AuthenticationTraceDocument";
import { IRegulator } from "./IRegulator";
export class AuthenticationRegulator { export class Regulator implements IRegulator {
private userDataStore: IUserDataStore; private userDataStore: IUserDataStore;
private banTime: number; private banTime: number;
private findTime: number; private findTime: number;

View File

@ -4,7 +4,7 @@ import objectPath = require("object-path");
import BluebirdPromise = require("bluebird"); import BluebirdPromise = require("bluebird");
import express = require("express"); import express = require("express");
import { AccessController } from "../../access_control/AccessController"; import { AccessController } from "../../access_control/AccessController";
import { AuthenticationRegulator } from "../../AuthenticationRegulator"; import { Regulator } from "../../regulation/Regulator";
import { GroupsAndEmails } from "../../ldap/IClient"; import { GroupsAndEmails } from "../../ldap/IClient";
import Endpoint = require("../../../../../shared/api"); import Endpoint = require("../../../../../shared/api");
import ErrorReplies = require("../../ErrorReplies"); import ErrorReplies = require("../../ErrorReplies");
@ -13,89 +13,89 @@ import AuthenticationSession = require("../../AuthenticationSession");
import Constants = require("../../../../../shared/constants"); import Constants = require("../../../../../shared/constants");
import { DomainExtractor } from "../../utils/DomainExtractor"; import { DomainExtractor } from "../../utils/DomainExtractor";
import UserMessages = require("../../../../../shared/UserMessages"); import UserMessages = require("../../../../../shared/UserMessages");
import { AuthenticationMethodCalculator } from "../../AuthenticationMethodCalculator";
import { ServerVariables } from "../../ServerVariables";
export default function (req: express.Request, res: express.Response): BluebirdPromise<void> { export default function (vars: ServerVariables) {
const username: string = req.body.username; return function (req: express.Request, res: express.Response)
const password: string = req.body.password; : BluebirdPromise<void> {
const username: string = req.body.username;
const password: string = req.body.password;
let authSession: AuthenticationSession.AuthenticationSession;
const logger = ServerVariablesHandler.getLogger(req.app); return BluebirdPromise.resolve()
const ldap = ServerVariablesHandler.getLdapAuthenticator(req.app); .then(function () {
const config = ServerVariablesHandler.getConfiguration(req.app); if (!username || !password) {
const regulator = ServerVariablesHandler.getAuthenticationRegulator(req.app); return BluebirdPromise.reject(new Error("No username or password."));
const accessController = ServerVariablesHandler.getAccessController(req.app);
const authenticationMethodsCalculator =
ServerVariablesHandler.getAuthenticationMethodCalculator(req.app);
let authSession: AuthenticationSession.AuthenticationSession;
return BluebirdPromise.resolve()
.then(function () {
if (!username || !password) {
return BluebirdPromise.reject(new Error("No username or password."));
}
logger.info(req, "Starting authentication of user \"%s\"", username);
return AuthenticationSession.get(req);
})
.then(function (_authSession: AuthenticationSession.AuthenticationSession) {
authSession = _authSession;
return regulator.regulate(username);
})
.then(function () {
logger.info(req, "No regulation applied.");
return ldap.authenticate(username, password);
})
.then(function (groupsAndEmails: GroupsAndEmails) {
logger.info(req, "LDAP binding successful. Retrieved information about user are %s",
JSON.stringify(groupsAndEmails));
authSession.userid = username;
authSession.first_factor = true;
const redirectUrl = req.query[Constants.REDIRECT_QUERY_PARAM] !== "undefined"
// Fuck, don't know why it is a string!
? req.query[Constants.REDIRECT_QUERY_PARAM]
: undefined;
const emails: string[] = groupsAndEmails.emails;
const groups: string[] = groupsAndEmails.groups;
const redirectHost: string = DomainExtractor.fromUrl(redirectUrl);
const authMethod = authenticationMethodsCalculator.compute(redirectHost);
logger.debug(req, "Authentication method for \"%s\" is \"%s\"", redirectHost, authMethod);
if (!emails || emails.length <= 0) {
const errMessage = "No emails found. The user should have at least one email address to reset password.";
logger.error(req, "%s", errMessage);
return BluebirdPromise.reject(new Error(errMessage));
}
authSession.email = emails[0];
authSession.groups = groups;
logger.debug(req, "Mark successful authentication to regulator.");
regulator.mark(username, true);
if (authMethod == "basic_auth") {
res.send({
redirect: redirectUrl
});
logger.debug(req, "Redirect to '%s'", redirectUrl);
}
else if (authMethod == "two_factor") {
let newRedirectUrl = Endpoint.SECOND_FACTOR_GET;
if (redirectUrl) {
newRedirectUrl += "?" + Constants.REDIRECT_QUERY_PARAM + "="
+ encodeURIComponent(redirectUrl);
} }
logger.debug(req, "Redirect to '%s'", newRedirectUrl, typeof redirectUrl); vars.logger.info(req, "Starting authentication of user \"%s\"", username);
res.send({ return AuthenticationSession.get(req);
redirect: newRedirectUrl })
}); .then(function (_authSession: AuthenticationSession.AuthenticationSession) {
} authSession = _authSession;
else { return vars.regulator.regulate(username);
return BluebirdPromise.reject(new Error("Unknown authentication method for this domain.")); })
} .then(function () {
return BluebirdPromise.resolve(); vars.logger.info(req, "No regulation applied.");
}) return vars.ldapAuthenticator.authenticate(username, password);
.catch(Exceptions.LdapBindError, function (err: Error) { })
regulator.mark(username, false); .then(function (groupsAndEmails: GroupsAndEmails) {
return ErrorReplies.replyWithError200(req, res, logger, UserMessages.OPERATION_FAILED)(err); vars.logger.info(req, "LDAP binding successful. Retrieved information about user are %s",
}) JSON.stringify(groupsAndEmails));
.catch(ErrorReplies.replyWithError200(req, res, logger, UserMessages.OPERATION_FAILED)); authSession.userid = username;
} authSession.first_factor = true;
const redirectUrl = req.query[Constants.REDIRECT_QUERY_PARAM] !== "undefined"
// Fuck, don't know why it is a string!
? req.query[Constants.REDIRECT_QUERY_PARAM]
: undefined;
const emails: string[] = groupsAndEmails.emails;
const groups: string[] = groupsAndEmails.groups;
const redirectHost: string = DomainExtractor.fromUrl(redirectUrl);
const authMethod =
new AuthenticationMethodCalculator(vars.config.authentication_methods)
.compute(redirectHost);
vars.logger.debug(req, "Authentication method for \"%s\" is \"%s\"", redirectHost, authMethod);
if (!emails || emails.length <= 0) {
const errMessage =
"No emails found. The user should have at least one email address to reset password.";
vars.logger.error(req, "%s", errMessage);
return BluebirdPromise.reject(new Error(errMessage));
}
authSession.email = emails[0];
authSession.groups = groups;
vars.logger.debug(req, "Mark successful authentication to regulator.");
vars.regulator.mark(username, true);
if (authMethod == "basic_auth") {
res.send({
redirect: redirectUrl
});
vars.logger.debug(req, "Redirect to '%s'", redirectUrl);
}
else if (authMethod == "two_factor") {
let newRedirectUrl = Endpoint.SECOND_FACTOR_GET;
if (redirectUrl) {
newRedirectUrl += "?" + Constants.REDIRECT_QUERY_PARAM + "="
+ encodeURIComponent(redirectUrl);
}
vars.logger.debug(req, "Redirect to '%s'", newRedirectUrl, typeof redirectUrl);
res.send({
redirect: newRedirectUrl
});
}
else {
return BluebirdPromise.reject(new Error("Unknown authentication method for this domain."));
}
return BluebirdPromise.resolve();
})
.catch(Exceptions.LdapBindError, function (err: Error) {
vars.regulator.mark(username, false);
return ErrorReplies.replyWithError200(req, res, vars.logger, UserMessages.OPERATION_FAILED)(err);
})
.catch(ErrorReplies.replyWithError200(req, res, vars.logger, UserMessages.OPERATION_FAILED));
};
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -113,6 +113,7 @@ describe("test session configuration builder", function () {
domain: "example.com", domain: "example.com",
expiration: 3600, expiration: 3600,
secret: "secret", secret: "secret",
inactivity: 4000,
redis: { redis: {
host: "redis.example.com", host: "redis.example.com",
port: 6379 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,17 +60,35 @@ describe("test config parser", function () {
}); });
}); });
it("should get the session attributes", function () { describe("test session configuration", function() {
const yaml_config = buildYamlConfig(); it("should get the session attributes", function () {
yaml_config.session = { const yaml_config = buildYamlConfig();
domain: "example.com", yaml_config.session = {
secret: "secret", domain: "example.com",
expiration: 3600 secret: "secret",
}; expiration: 3600,
const config = ConfigurationParser.parse(yaml_config); inactivity: 4000
Assert.equal(config.session.domain, "example.com"); };
Assert.equal(config.session.secret, "secret"); const config = ConfigurationParser.parse(yaml_config);
Assert.equal(config.session.expiration, 3600); 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",
secret: "secret",
expiration: 3600
};
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, undefined);
});
}); });
it("should get the log level", function () { 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 BluebirdPromise = require("bluebird");
import Assert = require("assert"); import Assert = require("assert");
import { AuthenticationRegulator } from "../src/lib/AuthenticationRegulator"; import { Regulator } from "../../src/lib/regulation/Regulator";
import MockDate = require("mockdate"); import MockDate = require("mockdate");
import exceptions = require("../src/lib/Exceptions"); import exceptions = require("../../src/lib/Exceptions");
import { UserDataStoreStub } from "./mocks/storage/UserDataStoreStub"; import { UserDataStoreStub } from "../mocks/storage/UserDataStoreStub";
describe("test authentication regulator", function () { describe("test authentication regulator", function () {
const USER1 = "USER1"; const USER1 = "USER1";
@ -39,13 +39,13 @@ describe("test authentication regulator", function () {
MockDate.reset(); MockDate.reset();
}); });
function markAuthenticationAt(regulator: AuthenticationRegulator, user: string, time: string, success: boolean) { function markAuthenticationAt(regulator: Regulator, user: string, time: string, success: boolean) {
MockDate.set(time); MockDate.set(time);
return regulator.mark(user, success); return regulator.mark(user, success);
} }
it("should mark 2 authentication and regulate (accept)", function () { it("should mark 2 authentication and regulate (accept)", function () {
const regulator = new AuthenticationRegulator(userDataStoreStub, 3, 10, 10); const regulator = new Regulator(userDataStoreStub, 3, 10, 10);
return regulator.mark(USER1, false) return regulator.mark(USER1, false)
.then(function () { .then(function () {
@ -57,7 +57,7 @@ describe("test authentication regulator", function () {
}); });
it("should mark 3 authentications and regulate (reject)", function () { it("should mark 3 authentications and regulate (reject)", function () {
const regulator = new AuthenticationRegulator(userDataStoreStub, 3, 10, 10); const regulator = new Regulator(userDataStoreStub, 3, 10, 10);
return regulator.mark(USER1, false) return regulator.mark(USER1, false)
.then(function () { .then(function () {
@ -76,7 +76,7 @@ describe("test authentication regulator", function () {
}); });
it("should mark 1 failed, 1 successful and 1 failed authentications within minimum time and regulate (accept)", function () { it("should mark 1 failed, 1 successful and 1 failed authentications within minimum time and regulate (accept)", function () {
const regulator = new AuthenticationRegulator(userDataStoreStub, 3, 60, 30); const regulator = new Regulator(userDataStoreStub, 3, 60, 30);
return markAuthenticationAt(regulator, USER1, "1/2/2000 00:00:00", false) return markAuthenticationAt(regulator, USER1, "1/2/2000 00:00:00", false)
.then(function () { .then(function () {
@ -109,7 +109,7 @@ describe("test authentication regulator", function () {
}); });
it("should regulate user if number of failures is greater than 3 in allowed time lapse", function () { it("should regulate user if number of failures is greater than 3 in allowed time lapse", function () {
function markAuthentications(regulator: AuthenticationRegulator, user: string) { function markAuthentications(regulator: Regulator, user: string) {
return markAuthenticationAt(regulator, user, "1/2/2000 00:00:00", false) return markAuthenticationAt(regulator, user, "1/2/2000 00:00:00", false)
.then(function () { .then(function () {
return markAuthenticationAt(regulator, user, "1/2/2000 00:00:45", false); return markAuthenticationAt(regulator, user, "1/2/2000 00:00:45", false);
@ -122,8 +122,8 @@ describe("test authentication regulator", function () {
}); });
} }
const regulator1 = new AuthenticationRegulator(userDataStoreStub, 3, 60, 60); const regulator1 = new Regulator(userDataStoreStub, 3, 60, 60);
const regulator2 = new AuthenticationRegulator(userDataStoreStub, 3, 2 * 60, 60); const regulator2 = new Regulator(userDataStoreStub, 3, 2 * 60, 60);
const p1 = markAuthentications(regulator1, USER1); const p1 = markAuthentications(regulator1, USER1);
const p2 = markAuthentications(regulator2, USER2); const p2 = markAuthentications(regulator2, USER2);
@ -138,7 +138,7 @@ describe("test authentication regulator", function () {
}); });
it("should user wait after regulation to authenticate again", function () { it("should user wait after regulation to authenticate again", function () {
function markAuthentications(regulator: AuthenticationRegulator, user: string) { function markAuthentications(regulator: Regulator, user: string) {
return markAuthenticationAt(regulator, user, "1/2/2000 00:00:00", false) return markAuthenticationAt(regulator, user, "1/2/2000 00:00:00", false)
.then(function () { .then(function () {
return markAuthenticationAt(regulator, user, "1/2/2000 00:00:10", false); return markAuthenticationAt(regulator, user, "1/2/2000 00:00:10", false);
@ -161,13 +161,13 @@ describe("test authentication regulator", function () {
}); });
} }
const regulator = new AuthenticationRegulator(userDataStoreStub, 4, 30, 30); const regulator = new Regulator(userDataStoreStub, 4, 30, 30);
return markAuthentications(regulator, USER1); return markAuthentications(regulator, USER1);
}); });
it("should disable regulation when max_retries is set to 0", function () { it("should disable regulation when max_retries is set to 0", function () {
const maxRetries = 0; const maxRetries = 0;
const regulator = new AuthenticationRegulator(userDataStoreStub, maxRetries, 60, 30); const regulator = new Regulator(userDataStoreStub, maxRetries, 60, 30);
return markAuthenticationAt(regulator, USER1, "1/2/2000 00:00:00", false) return markAuthenticationAt(regulator, USER1, "1/2/2000 00:00:00", false)
.then(function () { .then(function () {
return markAuthenticationAt(regulator, USER1, "1/2/2000 00:00:10", false); return markAuthenticationAt(regulator, USER1, "1/2/2000 00:00:10", false);

View File

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

View File

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

View File

@ -4,27 +4,21 @@ import VerifyGet = require("../../../src/lib/routes/verify/get");
import AuthenticationSession = require("../../../src/lib/AuthenticationSession"); import AuthenticationSession = require("../../../src/lib/AuthenticationSession");
import { AuthenticationMethodCalculator } from "../../../src/lib/AuthenticationMethodCalculator"; import { AuthenticationMethodCalculator } from "../../../src/lib/AuthenticationMethodCalculator";
import { AuthenticationMethodsConfiguration } from "../../../src/lib/configuration/Configuration"; import { AuthenticationMethodsConfiguration } from "../../../src/lib/configuration/Configuration";
import Sinon = require("sinon"); import Sinon = require("sinon");
import winston = require("winston"); import winston = require("winston");
import BluebirdPromise = require("bluebird"); import BluebirdPromise = require("bluebird");
import express = require("express"); import express = require("express");
import ExpressMock = require("../../mocks/express"); import ExpressMock = require("../../mocks/express");
import { AccessControllerStub } from "../../mocks/AccessControllerStub"; import { ServerVariables } from "../../../src/lib/ServerVariables";
import ServerVariablesMock = require("../../mocks/ServerVariablesMock"); import { ServerVariablesMockBuilder, ServerVariablesMock } from "../../mocks/ServerVariablesMockBuilder";
describe("test authentication token verification", function () { describe("test /verify endpoint", function () {
let req: ExpressMock.RequestMock; let req: ExpressMock.RequestMock;
let res: ExpressMock.ResponseMock; let res: ExpressMock.ResponseMock;
let accessController: AccessControllerStub; let mocks: ServerVariablesMock;
let mocks: any; let vars: ServerVariables;
beforeEach(function () { beforeEach(function () {
accessController = new AccessControllerStub();
accessController.isAccessAllowedMock.returns(true);
req = ExpressMock.RequestMock(); req = ExpressMock.RequestMock();
res = ExpressMock.ResponseMock(); res = ExpressMock.ResponseMock();
req.session = {}; req.session = {};
@ -37,20 +31,14 @@ describe("test authentication token verification", function () {
AuthenticationSession.reset(req as any); AuthenticationSession.reset(req as any);
req.headers = {}; req.headers = {};
req.headers.host = "secret.example.com"; req.headers.host = "secret.example.com";
mocks = ServerVariablesMock.mock(req.app); const s = ServerVariablesMockBuilder.build();
mocks.config = {} as any; mocks = s.mocks;
mocks.accessController = accessController as any; vars = s.variables;
const options: AuthenticationMethodsConfiguration = {
default_method: "two_factor",
per_subdomain_methods: {
"redirect.url": "basic_auth"
}
};
mocks.authenticationMethodsCalculator = new AuthenticationMethodCalculator(options);
}); });
it("should be already authenticated", function () { it("should be already authenticated", function () {
req.session = {}; req.session = {};
mocks.accessController.isAccessAllowedMock.returns(true);
AuthenticationSession.reset(req as any); AuthenticationSession.reset(req as any);
return AuthenticationSession.get(req as any) return AuthenticationSession.get(req as any)
.then(function (authSession: AuthenticationSession.AuthenticationSession) { .then(function (authSession: AuthenticationSession.AuthenticationSession) {
@ -58,7 +46,7 @@ describe("test authentication token verification", function () {
authSession.second_factor = true; authSession.second_factor = true;
authSession.userid = "myuser"; authSession.userid = "myuser";
authSession.groups = ["mygroup", "othergroup"]; authSession.groups = ["mygroup", "othergroup"];
return VerifyGet.default(req as express.Request, res as any); return VerifyGet.default(vars)(req as express.Request, res as any);
}) })
.then(function () { .then(function () {
Sinon.assert.calledWithExactly(res.setHeader, "Remote-User", "myuser"); Sinon.assert.calledWithExactly(res.setHeader, "Remote-User", "myuser");
@ -71,26 +59,30 @@ describe("test authentication token verification", function () {
return AuthenticationSession.get(req as any) return AuthenticationSession.get(req as any)
.then(function (authSession) { .then(function (authSession) {
authSession = _authSession; authSession = _authSession;
return VerifyGet.default(req as express.Request, res as any); return VerifyGet.default(vars)(req as express.Request, res as any);
}) })
.then(function () { .then(function () {
Assert.equal(status_code, res.status.getCall(0).args[0]); Assert.equal(status_code, res.status.getCall(0).args[0]);
}); });
} }
function test_non_authenticated_401(auth_session: AuthenticationSession.AuthenticationSession) { function test_non_authenticated_401(authSession: AuthenticationSession.AuthenticationSession) {
return test_session(auth_session, 401); return test_session(authSession, 401);
} }
function test_unauthorized_403(auth_session: AuthenticationSession.AuthenticationSession) { function test_unauthorized_403(authSession: AuthenticationSession.AuthenticationSession) {
return test_session(auth_session, 403); return test_session(authSession, 403);
} }
function test_authorized(auth_session: AuthenticationSession.AuthenticationSession) { function test_authorized(authSession: AuthenticationSession.AuthenticationSession) {
return test_session(auth_session, 204); return test_session(authSession, 204);
} }
describe("given user tries to access a 2-factor endpoint", function () { describe("given user tries to access a 2-factor endpoint", function () {
before(function () {
mocks.accessController.isAccessAllowedMock.returns(true);
});
describe("given different cases of session", function () { describe("given different cases of session", function () {
it("should not be authenticated when second factor is missing", function () { it("should not be authenticated when second factor is missing", function () {
return test_non_authenticated_401({ return test_non_authenticated_401({
@ -99,6 +91,7 @@ describe("test authentication token verification", function () {
second_factor: false, second_factor: false,
email: undefined, email: undefined,
groups: [], groups: [],
last_activity_datetime: new Date().getTime()
}); });
}); });
@ -109,6 +102,7 @@ describe("test authentication token verification", function () {
second_factor: true, second_factor: true,
email: undefined, email: undefined,
groups: [], groups: [],
last_activity_datetime: new Date().getTime()
}); });
}); });
@ -119,6 +113,7 @@ describe("test authentication token verification", function () {
second_factor: false, second_factor: false,
email: undefined, email: undefined,
groups: [], groups: [],
last_activity_datetime: new Date().getTime()
}); });
}); });
@ -129,6 +124,7 @@ describe("test authentication token verification", function () {
second_factor: false, second_factor: false,
email: undefined, email: undefined,
groups: [], groups: [],
last_activity_datetime: new Date().getTime()
}); });
}); });
@ -138,22 +134,20 @@ describe("test authentication token verification", function () {
it("should not be authenticated when domain is not allowed for user", function () { it("should not be authenticated when domain is not allowed for user", function () {
return AuthenticationSession.get(req as any) return AuthenticationSession.get(req as any)
.then(function (authSession: AuthenticationSession.AuthenticationSession) { .then(function (authSession) {
authSession.first_factor = true; authSession.first_factor = true;
authSession.second_factor = true; authSession.second_factor = true;
authSession.userid = "myuser"; authSession.userid = "myuser";
req.headers.host = "test.example.com"; req.headers.host = "test.example.com";
mocks.accessController.isAccessAllowedMock.returns(false);
accessController.isAccessAllowedMock.returns(false);
accessController.isAccessAllowedMock.withArgs("test.example.com", "user", ["group1", "group2"]).returns(true);
return test_unauthorized_403({ return test_unauthorized_403({
first_factor: true, first_factor: true,
second_factor: true, second_factor: true,
userid: "user", userid: "user",
groups: ["group1", "group2"], groups: ["group1", "group2"],
email: undefined email: undefined,
last_activity_datetime: new Date().getTime()
}); });
}); });
}); });
@ -166,14 +160,18 @@ describe("test authentication token verification", function () {
redirect: "http://redirect.url" redirect: "http://redirect.url"
}; };
req.headers["host"] = "redirect.url"; req.headers["host"] = "redirect.url";
mocks.config.authentication_methods.per_subdomain_methods = {
"redirect.url": "basic_auth"
};
}); });
it("should be authenticated when first factor is validated and not second factor", function () { it("should be authenticated when first factor is validated and second factor is not", function () {
mocks.accessController.isAccessAllowedMock.returns(true);
return AuthenticationSession.get(req as any) return AuthenticationSession.get(req as any)
.then(function (authSession: AuthenticationSession.AuthenticationSession) { .then(function (authSession) {
authSession.first_factor = true; authSession.first_factor = true;
authSession.userid = "user1"; authSession.userid = "user1";
return VerifyGet.default(req as express.Request, res as any); return VerifyGet.default(vars)(req as express.Request, res as any);
}) })
.then(function () { .then(function () {
Assert(res.status.calledWith(204)); Assert(res.status.calledWith(204));
@ -182,15 +180,64 @@ describe("test authentication token verification", function () {
}); });
it("should be rejected with 401 when first factor is not validated", function () { it("should be rejected with 401 when first factor is not validated", function () {
mocks.accessController.isAccessAllowedMock.returns(true);
return AuthenticationSession.get(req as any) return AuthenticationSession.get(req as any)
.then(function (authSession: AuthenticationSession.AuthenticationSession) { .then(function (authSession) {
authSession.first_factor = false; authSession.first_factor = false;
return VerifyGet.default(req as express.Request, res as any); return VerifyGet.default(vars)(req as express.Request, res as any);
}) })
.then(function () { .then(function () {
Assert(res.status.calledWith(401)); Assert(res.status.calledWith(401));
}); });
}); });
}); });
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" And I visit "https://admin.test.local:8080/secret.html"
Then I get an error 403 Then I get an error 403
Scenario: Redirection URL is propagated from restricted page to first factor Scenario: Redirection URL is propagated from restricted page to first factor
When I visit "https://public.test.local:8080/secret.html" 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" 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 Feature: Authelia regulates authentication to avoid brute force
@needs-test-config
@need-registered-user-blackhat @need-registered-user-blackhat
Scenario: Attacker tries too many authentication in a short period of time and get banned Scenario: Attacker tries too many authentication in a short period of time and get banned
Given I visit "https://auth.test.local:8080/" 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" And I click on "Sign in"
Then I get a notification of type "error" with message "Authentication failed. Please check your credentials." Then I get a notification of type "error" with message "Authentication failed. Please check your credentials."
@needs-test-config
@need-registered-user-blackhat @need-registered-user-blackhat
Scenario: User is unbanned after a configured amount of time 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" 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 { CollectionFactoryFactory } from "../../../server/src/lib/storage/CollectionFactoryFactory";
import { MongoConnector } from "../../../server/src/lib/connectors/mongo/MongoConnector"; import { MongoConnector } from "../../../server/src/lib/connectors/mongo/MongoConnector";
import { IMongoClient } from "../../../server/src/lib/connectors/mongo/IMongoClient"; 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"); import Speakeasy = require("speakeasy");
Cucumber.defineSupportCode(function ({ setDefaultTimeout }) { Cucumber.defineSupportCode(function ({ setDefaultTimeout }) {
@ -14,19 +14,46 @@ Cucumber.defineSupportCode(function ({ setDefaultTimeout }) {
}); });
Cucumber.defineSupportCode(function ({ After, Before }) { Cucumber.defineSupportCode(function ({ After, Before }) {
const exec = BluebirdPromise.promisify(ChildProcess.exec); const exec = BluebirdPromise.promisify<any, any>(ChildProcess.exec);
After(function () { After(function () {
return this.driver.quit(); return this.driver.quit();
}); });
Before({ tags: "@needs-test-config", timeout: 20 * 1000 }, function () { function createRegulationConfiguration(): BluebirdPromise<void> {
return exec("./scripts/example-commit/dc-example.sh -f docker-compose.test.yml up -d authelia && sleep 2"); 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 \
");
}
After({ tags: "@needs-test-config", timeout: 20 * 1000 }, function () { function createInactivityConfiguration(): BluebirdPromise<void> {
return exec("./scripts/example-commit/dc-example.sh up -d authelia && sleep 2"); 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-" + 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) { function registerUser(context: any, username: string) {
let secret: Speakeasy.Key; let secret: Speakeasy.Key;
@ -36,7 +63,7 @@ Cucumber.defineSupportCode(function ({ After, Before }) {
const collectionFactory = CollectionFactoryFactory.createMongo(mongoClient); const collectionFactory = CollectionFactoryFactory.createMongo(mongoClient);
const userDataStore = new UserDataStore(collectionFactory); const userDataStore = new UserDataStore(collectionFactory);
const generator = new TOTPGenerator(Speakeasy); const generator = new TotpHandler(Speakeasy);
secret = generator.generate(); secret = generator.generate();
return userDataStore.saveTOTPSecret(username, secret); return userDataStore.saveTOTPSecret(username, secret);
}) })
@ -56,7 +83,10 @@ Cucumber.defineSupportCode(function ({ After, Before }) {
} }
function needAuthenticatedUser(context: any, username: string): BluebirdPromise<void> { 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 () { .then(function () {
return registerUser(context, username); return registerUser(context, username);
}) })
@ -66,7 +96,7 @@ Cucumber.defineSupportCode(function ({ After, Before }) {
.then(function () { .then(function () {
return context.useTotpTokenHandle("REGISTERED"); return context.useTotpTokenHandle("REGISTERED");
}) })
.then(function() { .then(function () {
return context.clickOnButton("TOTP"); return context.clickOnButton("TOTP");
}); });
} }

View File

@ -7,6 +7,6 @@ import BluebirdPromise = require("bluebird");
Cucumber.defineSupportCode(function ({ Given, When, Then }) { Cucumber.defineSupportCode(function ({ Given, When, Then }) {
When(/^the application restarts$/, {timeout: 15 * 1000}, function () { When(/^the application restarts$/, {timeout: 15 * 1000}, function () {
const exec = BluebirdPromise.promisify(ChildProcess.exec); 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);
});
});