Merge pull request #161 from clems4ever/inactivity_timeout
Inactivity timeout (soft session expiration timeout)pull/162/head
commit
5300f67217
|
@ -29,6 +29,7 @@ dist/
|
||||||
|
|
||||||
# Specific files
|
# Specific files
|
||||||
/config.yml
|
/config.yml
|
||||||
|
/config.test.yml
|
||||||
|
|
||||||
example/ldap/private.ldif
|
example/ldap/private.ldif
|
||||||
|
|
||||||
|
|
|
@ -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']);
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
|
@ -0,0 +1,3 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
./node_modules/.bin/cucumber-js --colors --compiler ts:ts-node/register $*
|
|
@ -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
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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> {
|
||||||
|
|
|
@ -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));
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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;
|
|
||||||
}
|
}
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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."));
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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;
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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: {
|
||||||
|
|
|
@ -0,0 +1,6 @@
|
||||||
|
import BluebirdPromise = require("bluebird");
|
||||||
|
|
||||||
|
export interface IRegulator {
|
||||||
|
mark(userId: string, isAuthenticationSuccessful: boolean): BluebirdPromise<void>;
|
||||||
|
regulate(userId: string): BluebirdPromise<void>;
|
||||||
|
}
|
|
@ -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;
|
|
@ -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));
|
||||||
|
};
|
||||||
|
}
|
|
@ -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));
|
||||||
}
|
}
|
|
@ -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)
|
||||||
|
|
|
@ -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));
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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));
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
|
@ -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));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
|
@ -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 () {
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
|
@ -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], {
|
||||||
|
|
|
@ -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]);
|
||||||
|
|
|
@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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"
|
|
@ -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");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -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");
|
||||||
});
|
});
|
||||||
});
|
});
|
|
@ -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);
|
||||||
|
});
|
||||||
|
});
|
Loading…
Reference in New Issue