Parameterize authentication regulation via configuration file. Both for flexibility and for testing purposes.
parent
20536abf8b
commit
64c06fd6b8
|
@ -29,4 +29,3 @@ dist/
|
|||
|
||||
# Specific files
|
||||
/config.yml
|
||||
/test/integration/nginx.conf
|
||||
|
|
|
@ -151,7 +151,7 @@ In **Authelia**, you need to register a per user TOTP (Time-Based One Time Passw
|
|||
authenticating. To do that, you need to click on the register button. It will
|
||||
send a link to the user email address. Since this is an example, no email will
|
||||
be sent, the link is rather delivered in the file
|
||||
**./notifications/notification.txt**. Paste the link in your browser and you'll get
|
||||
**/tmp/notifications/notification.txt**. Paste the link in your browser and you'll get
|
||||
your secret in QRCode and Base32 formats. You can use
|
||||
[Google Authenticator]
|
||||
to store them and get the generated tokens with the app.
|
||||
|
@ -166,7 +166,7 @@ already available for Google, Facebook, Github accounts and more.
|
|||
Like TOTP, U2F requires you register your security key before authenticating.
|
||||
To do so, click on the register button. This will send a link to the
|
||||
user email address. Since this is an example, no email will be sent, the
|
||||
link is rather delivered in the file **./notifications/notification.txt**. Paste
|
||||
link is rather delivered in the file **/tmp/notifications/notification.txt**. Paste
|
||||
the link in your browser and you'll be asking to touch the token of your device
|
||||
to register. Upon successful registration, you can authenticate using your U2F
|
||||
device by simply touching the token. Easy, right?!
|
||||
|
@ -178,7 +178,7 @@ With **Authelia**, you can also reset your password in no time. Click on the
|
|||
**Forgot password?** link in the login page, provide the username of the user requiring
|
||||
a password reset and **Authelia** will send an email with an link to the user
|
||||
email address. For the sake of the example, the email is delivered in the file
|
||||
**./notifications/notification.txt**.
|
||||
**/tmp/notifications/notification.txt**.
|
||||
Paste the link in your browser and you should be able to reset the password.
|
||||
|
||||
<img src="https://raw.githubusercontent.com/clems4ever/authelia/master/images/reset_password.png" width="400">
|
||||
|
|
|
@ -1,3 +1,6 @@
|
|||
###############################################################
|
||||
# Authelia configuration #
|
||||
###############################################################
|
||||
|
||||
# The port to listen on
|
||||
port: 80
|
||||
|
@ -49,22 +52,30 @@ ldap:
|
|||
# Access control is a set of rules you can use to restrict the user access.
|
||||
# Default (anyone), per-user or per-group rules can be defined.
|
||||
#
|
||||
# If 'access_control' is not defined, ACL rules are disabled and default policy
|
||||
# If 'access_control' is not defined, ACL rules are disabled and a default policy
|
||||
# is applied, i.e., access is allowed to anyone. Otherwise restrictions follow
|
||||
# the rules defined below.
|
||||
# If no rule is provided, all domains are denied.
|
||||
#
|
||||
# '*' means 'any' subdomains and matches any string. It must stand at the
|
||||
# beginning of the pattern.
|
||||
# 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:
|
||||
# The default policy. Applies to any user
|
||||
default:
|
||||
- public.test.local
|
||||
|
||||
# Group based policies. The key is a group name and the value
|
||||
# is the domain to allow access to.
|
||||
groups:
|
||||
admin:
|
||||
- '*.test.local'
|
||||
dev:
|
||||
- secret.test.local
|
||||
- secret2.test.local
|
||||
|
||||
# Group based policies. The key is a group name and the value
|
||||
# is the domain to allow access to.
|
||||
users:
|
||||
harry:
|
||||
- secret1.test.local
|
||||
|
@ -74,19 +85,43 @@ access_control:
|
|||
|
||||
# Configuration of session cookies
|
||||
#
|
||||
# _secret_ the secret to encrypt session cookies
|
||||
# _expiration_ the time before cookies expire
|
||||
# _domain_ the domain to protect.
|
||||
# Note: the authenticator must also be in that domain. If empty, the cookie
|
||||
# is restricted to the subdomain of the issuer.
|
||||
# The session cookies identify the user once logged in.
|
||||
session:
|
||||
# The secret to encrypt the session cookie.
|
||||
secret: unsecure_secret
|
||||
|
||||
# The time before the cookie expires.
|
||||
expiration: 3600000
|
||||
|
||||
# The domain to protect.
|
||||
# Note: the authenticator must also be in that domain. If empty, the cookie
|
||||
# is restricted to the subdomain of the issuer.
|
||||
domain: test.local
|
||||
|
||||
# The redis connection details
|
||||
redis:
|
||||
host: redis
|
||||
port: 6379
|
||||
|
||||
# Configuration of the authentication regulation mechanism.
|
||||
#
|
||||
# This mechanism prevents attackers from brute forcing the first factor.
|
||||
# It bans the user if too many attempts are done in a short period of
|
||||
# time.
|
||||
regulation:
|
||||
# The number of failed login attempts before user is banned.
|
||||
# Set it to 0 for disabling regulation.
|
||||
max_retries: 3
|
||||
|
||||
# The length of time between login attempts before user is banned.
|
||||
find_time: 120
|
||||
|
||||
# The length of time before a banned user can login again.
|
||||
ban_time: 300
|
||||
|
||||
# Configuration of the storage backend used to store data and secrets.
|
||||
#
|
||||
# You must use only an available configuration: local, mongo
|
||||
storage:
|
||||
# The directory where the DB files will be saved
|
||||
# local: /var/lib/authelia/store
|
||||
|
@ -95,9 +130,11 @@ storage:
|
|||
mongo:
|
||||
url: mongodb://mongo/authelia
|
||||
|
||||
# Configuration of the notification system.
|
||||
#
|
||||
# Notifications are sent to users when they require a password reset, a u2f
|
||||
# registration or a TOTP registration.
|
||||
# Use only one available configuration: filesystem, gmail
|
||||
# Use only an available configuration: filesystem, gmail
|
||||
notifier:
|
||||
# For testing purpose, notifications can be sent in a file
|
||||
filesystem:
|
||||
|
|
|
@ -0,0 +1,147 @@
|
|||
###############################################################
|
||||
# Authelia configuration #
|
||||
###############################################################
|
||||
|
||||
# The port to listen on
|
||||
port: 80
|
||||
|
||||
# Log level
|
||||
#
|
||||
# Level of verbosity for logs
|
||||
logs_level: debug
|
||||
|
||||
# LDAP configuration
|
||||
#
|
||||
# Example: for user john, the DN will be cn=john,ou=users,dc=example,dc=com
|
||||
ldap:
|
||||
# The url of the ldap server
|
||||
url: ldap://openldap
|
||||
|
||||
# The base dn for every entries
|
||||
base_dn: dc=example,dc=com
|
||||
|
||||
# An additional dn to define the scope to all users
|
||||
additional_users_dn: ou=users
|
||||
|
||||
# The users filter.
|
||||
# {0} is the matcher replaced by username.
|
||||
# 'cn={0}' by default.
|
||||
users_filter: cn={0}
|
||||
|
||||
# An additional dn to define the scope of groups
|
||||
additional_groups_dn: ou=groups
|
||||
|
||||
# The groups filter.
|
||||
# {0} is the matcher replaced by user dn.
|
||||
# 'member={0}' by default.
|
||||
groups_filter: (&(member={0})(objectclass=groupOfNames))
|
||||
|
||||
# The attribute holding the name of the group
|
||||
group_name_attribute: cn
|
||||
|
||||
# The attribute holding the mail address of the user
|
||||
mail_attribute: mail
|
||||
|
||||
# The username and password of the admin user.
|
||||
user: cn=admin,dc=example,dc=com
|
||||
password: password
|
||||
|
||||
|
||||
# Access Control
|
||||
#
|
||||
# Access control is a set of rules you can use to restrict the user access.
|
||||
# Default (anyone), per-user or per-group rules can be defined.
|
||||
#
|
||||
# If 'access_control' is not defined, ACL rules are disabled and a default policy
|
||||
# is applied, i.e., access is allowed to anyone. Otherwise restrictions follow
|
||||
# the rules defined below.
|
||||
# If no rule is provided, all domains are denied.
|
||||
#
|
||||
# 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:
|
||||
# The default policy. Applies to any user
|
||||
default:
|
||||
- public.test.local
|
||||
|
||||
# Group based policies. The key is a group name and the value
|
||||
# is the domain to allow access to.
|
||||
groups:
|
||||
admin:
|
||||
- '*.test.local'
|
||||
dev:
|
||||
- secret.test.local
|
||||
- secret2.test.local
|
||||
|
||||
# Group based policies. The key is a group name and the value
|
||||
# is the domain to allow access to.
|
||||
users:
|
||||
harry:
|
||||
- secret1.test.local
|
||||
bob:
|
||||
- '*.mail.test.local'
|
||||
|
||||
|
||||
# Configuration of session cookies
|
||||
#
|
||||
# The session cookies identify the user once logged in.
|
||||
session:
|
||||
# The secret to encrypt the session cookie.
|
||||
secret: unsecure_secret
|
||||
|
||||
# The time before the cookie expires.
|
||||
expiration: 3600000
|
||||
|
||||
# The domain to protect.
|
||||
# Note: the authenticator must also be in that domain. If empty, the cookie
|
||||
# is restricted to the subdomain of the issuer.
|
||||
domain: test.local
|
||||
|
||||
# The redis connection details
|
||||
redis:
|
||||
host: redis
|
||||
port: 6379
|
||||
|
||||
# Configuration of the authentication regulation mechanism.
|
||||
#
|
||||
# This mechanism prevents attackers from brute forcing the first factor.
|
||||
# It bans the user if too many attempts are done in a short period of
|
||||
# time.
|
||||
regulation:
|
||||
# The number of failed login attempts before user is banned.
|
||||
# Set it to 0 for disabling regulation.
|
||||
max_retries: 3
|
||||
|
||||
# The length of time between login attempts before user is banned.
|
||||
find_time: 15
|
||||
|
||||
# The length of time before a banned user can login again.
|
||||
ban_time: 4
|
||||
|
||||
# Configuration of the storage backend used to store data and secrets.
|
||||
#
|
||||
# You must use only an available configuration: local, mongo
|
||||
storage:
|
||||
# The directory where the DB files will be saved
|
||||
# local: /var/lib/authelia/store
|
||||
|
||||
# Settings to connect to mongo server
|
||||
mongo:
|
||||
url: mongodb://mongo/authelia
|
||||
|
||||
# Configuration of the notification system.
|
||||
#
|
||||
# Notifications are sent to users when they require a password reset, a u2f
|
||||
# registration or a TOTP registration.
|
||||
# Use only an available configuration: filesystem, gmail
|
||||
notifier:
|
||||
# For testing purpose, notifications can be sent in a file
|
||||
filesystem:
|
||||
filename: /var/lib/authelia/notifications/notification.txt
|
||||
|
||||
# Use your gmail account to send the notifications. You can use an app password.
|
||||
# gmail:
|
||||
# username: user@example.com
|
||||
# password: yourpassword
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
version: '2'
|
||||
services:
|
||||
authelia:
|
||||
volumes:
|
||||
- ./config.test.yml:/etc/authelia/config.yml:ro
|
|
@ -5,7 +5,7 @@ services:
|
|||
restart: always
|
||||
volumes:
|
||||
- ./config.template.yml:/etc/authelia/config.yml:ro
|
||||
- ./notifications:/var/lib/authelia/notifications
|
||||
- /tmp/notifications:/var/lib/authelia/notifications
|
||||
depends_on:
|
||||
- redis
|
||||
networks:
|
||||
|
|
|
@ -52,3 +52,11 @@ objectclass: top
|
|||
mail: james.dean@example.com
|
||||
sn: James Dean
|
||||
userpassword: {SHA}W6ph5Mm5Pz8GgiULbPgzG37mj9g=
|
||||
|
||||
dn: cn=blackhat,ou=users,dc=example,dc=com
|
||||
cn: blackhat
|
||||
objectclass: inetOrgPerson
|
||||
objectclass: top
|
||||
mail: billy.blackhat@example.com
|
||||
sn: Billy BlackHat
|
||||
userpassword: {SHA}W6ph5Mm5Pz8GgiULbPgzG37mj9g=
|
||||
|
|
|
@ -3,28 +3,64 @@
|
|||
import util = require("util");
|
||||
import { INotifier, Handlers } from "./INotifier";
|
||||
|
||||
export class Notifier implements INotifier {
|
||||
class NotificationEvent {
|
||||
private element: JQuery;
|
||||
private message: string;
|
||||
private statusType: string;
|
||||
private timeoutId: any;
|
||||
|
||||
constructor(selector: string, $: JQueryStatic) {
|
||||
this.element = $(selector);
|
||||
constructor(element: JQuery, msg: string, statusType: string) {
|
||||
this.message = msg;
|
||||
this.statusType = statusType;
|
||||
this.element = element;
|
||||
}
|
||||
|
||||
private displayAndFadeout(msg: string, statusType: string, handlers?: Handlers): void {
|
||||
private clearNotification() {
|
||||
this.element.removeClass(this.statusType);
|
||||
this.element.html("");
|
||||
}
|
||||
|
||||
start(handlers?: Handlers) {
|
||||
const that = this;
|
||||
const FADE_TIME = 500;
|
||||
const html = util.format('<i><img src="/img/notifications/%s.png" alt="status %s"/></i>\
|
||||
<span>%s</span>', statusType, statusType, msg);
|
||||
<span>%s</span>', this.statusType, this.statusType, this.message);
|
||||
this.element.html(html);
|
||||
this.element.addClass(statusType);
|
||||
this.element.fadeIn(FADE_TIME, function() {
|
||||
this.element.addClass(this.statusType);
|
||||
this.element.fadeIn(FADE_TIME, function () {
|
||||
handlers.onFadedIn();
|
||||
})
|
||||
.delay(4000)
|
||||
.fadeOut(FADE_TIME, function() {
|
||||
that.element.removeClass(statusType);
|
||||
});
|
||||
|
||||
this.timeoutId = setTimeout(function () {
|
||||
that.element.fadeOut(FADE_TIME, function () {
|
||||
that.clearNotification();
|
||||
handlers.onFadedOut();
|
||||
});
|
||||
}, 4000);
|
||||
}
|
||||
|
||||
interrupt() {
|
||||
this.clearNotification();
|
||||
this.element.hide();
|
||||
clearTimeout(this.timeoutId);
|
||||
}
|
||||
}
|
||||
|
||||
export class Notifier implements INotifier {
|
||||
private element: JQuery;
|
||||
private onGoingEvent: NotificationEvent;
|
||||
|
||||
constructor(selector: string, $: JQueryStatic) {
|
||||
this.element = $(selector);
|
||||
this.onGoingEvent = undefined;
|
||||
}
|
||||
|
||||
private displayAndFadeout(msg: string, statusType: string, handlers?: Handlers): void {
|
||||
if (this.onGoingEvent)
|
||||
this.onGoingEvent.interrupt();
|
||||
|
||||
this.onGoingEvent = new NotificationEvent(this.element, msg, statusType);
|
||||
this.onGoingEvent.start();
|
||||
}
|
||||
|
||||
success(msg: string, handlers?: Handlers) {
|
||||
|
|
|
@ -13,6 +13,7 @@ export default function (window: Window, $: JQueryStatic,
|
|||
function onFormSubmitted() {
|
||||
const username: string = $(UISelectors.USERNAME_FIELD_ID).val();
|
||||
const password: string = $(UISelectors.PASSWORD_FIELD_ID).val();
|
||||
$(UISelectors.PASSWORD_FIELD_ID).val("");
|
||||
jslogger.debug("Form submitted");
|
||||
firstFactorValidator.validate(username, password, $)
|
||||
.then(onFirstFactorSuccess, onFirstFactorFailure);
|
||||
|
@ -21,17 +22,12 @@ export default function (window: Window, $: JQueryStatic,
|
|||
|
||||
function onFirstFactorSuccess() {
|
||||
jslogger.debug("First factor validated.");
|
||||
$(UISelectors.USERNAME_FIELD_ID).val("");
|
||||
$(UISelectors.PASSWORD_FIELD_ID).val("");
|
||||
|
||||
// Redirect to second factor
|
||||
window.location.href = Endpoints.SECOND_FACTOR_GET;
|
||||
}
|
||||
|
||||
function onFirstFactorFailure(err: Error) {
|
||||
jslogger.debug("First factor failed.");
|
||||
|
||||
$(UISelectors.PASSWORD_FIELD_ID).val("");
|
||||
notifier.error("Authentication failed. Please double check your credentials.");
|
||||
}
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@ process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
|
|||
|
||||
import Server from "./lib/Server";
|
||||
import { GlobalDependencies } from "../types/Dependencies";
|
||||
const YAML = require("yamljs");
|
||||
import YAML = require("yamljs");
|
||||
|
||||
const configurationFilepath = process.argv[2];
|
||||
if (!configurationFilepath) {
|
||||
|
|
|
@ -1,18 +1,20 @@
|
|||
|
||||
import * as BluebirdPromise from "bluebird";
|
||||
import exceptions = require("./Exceptions");
|
||||
import { UserDataStore } from "./storage/UserDataStore";
|
||||
import { IUserDataStore } from "./storage/IUserDataStore";
|
||||
import { AuthenticationTraceDocument } from "./storage/AuthenticationTraceDocument";
|
||||
|
||||
const MAX_AUTHENTICATION_COUNT_IN_TIME_RANGE = 3;
|
||||
|
||||
export class AuthenticationRegulator {
|
||||
private userDataStore: UserDataStore;
|
||||
private lockTimeInSeconds: number;
|
||||
private userDataStore: IUserDataStore;
|
||||
private banTime: number;
|
||||
private findTime: number;
|
||||
private maxRetries: number;
|
||||
|
||||
constructor(userDataStore: any, lockTimeInSeconds: number) {
|
||||
constructor(userDataStore: any, maxRetries: number, findTime: number, banTime: number) {
|
||||
this.userDataStore = userDataStore;
|
||||
this.lockTimeInSeconds = lockTimeInSeconds;
|
||||
this.banTime = banTime;
|
||||
this.findTime = findTime;
|
||||
this.maxRetries = maxRetries;
|
||||
}
|
||||
|
||||
// Mark authentication
|
||||
|
@ -21,18 +23,30 @@ export class AuthenticationRegulator {
|
|||
}
|
||||
|
||||
regulate(userId: string): BluebirdPromise<void> {
|
||||
return this.userDataStore.retrieveLatestAuthenticationTraces(userId, false, 3)
|
||||
.then((docs: AuthenticationTraceDocument[]) => {
|
||||
if (docs.length < MAX_AUTHENTICATION_COUNT_IN_TIME_RANGE) {
|
||||
// less than the max authorized number of authentication in time range, thus authorizing access
|
||||
return BluebirdPromise.resolve();
|
||||
}
|
||||
const that = this;
|
||||
|
||||
const oldestDocument = docs[MAX_AUTHENTICATION_COUNT_IN_TIME_RANGE - 1];
|
||||
const noLockMinDate = new Date(new Date().getTime() - this.lockTimeInSeconds * 1000);
|
||||
if (oldestDocument.date > noLockMinDate) {
|
||||
if (that.maxRetries <= 0) return BluebirdPromise.resolve();
|
||||
|
||||
return this.userDataStore.retrieveLatestAuthenticationTraces(userId, that.maxRetries)
|
||||
.then((docs: AuthenticationTraceDocument[]) => {
|
||||
// less than the max authorized number of authentication in time range, thus authorizing access
|
||||
if (docs.length < that.maxRetries) return BluebirdPromise.resolve();
|
||||
|
||||
const numberOfFailedAuth = docs
|
||||
.map(function (d: AuthenticationTraceDocument) { return d.isAuthenticationSuccessful == false ? 1 : 0; })
|
||||
.reduce(function (acc, v) { return acc + v; }, 0);
|
||||
|
||||
if (numberOfFailedAuth < this.maxRetries) return BluebirdPromise.resolve();
|
||||
|
||||
const newestDocument = docs[0];
|
||||
const oldestDocument = docs[that.maxRetries - 1];
|
||||
|
||||
const authenticationsTimeRangeInSeconds = (newestDocument.date.getTime() - oldestDocument.date.getTime()) / 1000;
|
||||
const tooManyAuthInTimelapse = (authenticationsTimeRangeInSeconds < this.findTime);
|
||||
const stillInBannedTimeRange = (new Date(new Date().getTime() - this.banTime * 1000) < newestDocument.date);
|
||||
|
||||
if (tooManyAuthInTimelapse && stillInBannedTimeRange)
|
||||
throw new exceptions.AuthenticationRegulationError("Max number of authentication. Please retry in few minutes.");
|
||||
}
|
||||
|
||||
return BluebirdPromise.resolve();
|
||||
});
|
||||
|
|
|
@ -72,8 +72,6 @@ class UserDataStoreFactory {
|
|||
|
||||
export class ServerVariablesHandler {
|
||||
static initialize(app: express.Application, config: Configuration.AppConfiguration, deps: GlobalDependencies): BluebirdPromise<void> {
|
||||
const five_minutes = 5 * 60;
|
||||
|
||||
const notifier = NotifierFactory.build(config.notifier, deps.nodemailer);
|
||||
const ldapClientFactory = new ClientFactory(config.ldap, deps.ldapjs, deps.dovehash, deps.winston);
|
||||
|
||||
|
@ -86,7 +84,8 @@ export class ServerVariablesHandler {
|
|||
|
||||
return UserDataStoreFactory.create(config)
|
||||
.then(function (userDataStore: UserDataStore) {
|
||||
const regulator = new AuthenticationRegulator(userDataStore, five_minutes);
|
||||
const regulator = new AuthenticationRegulator(userDataStore, config.regulation.max_retries,
|
||||
config.regulation.find_time, config.regulation.ban_time);
|
||||
|
||||
const variables: ServerVariables = {
|
||||
accessController: accessController,
|
||||
|
|
|
@ -85,6 +85,12 @@ export interface StorageConfiguration {
|
|||
mongo?: MongoStorageConfiguration;
|
||||
}
|
||||
|
||||
export interface RegulationConfiguration {
|
||||
max_retries: number;
|
||||
find_time: number;
|
||||
ban_time: number;
|
||||
}
|
||||
|
||||
export interface UserConfiguration {
|
||||
port?: number;
|
||||
logs_level?: string;
|
||||
|
@ -93,6 +99,7 @@ export interface UserConfiguration {
|
|||
storage: StorageConfiguration;
|
||||
notifier: NotifierConfiguration;
|
||||
access_control?: ACLConfiguration;
|
||||
regulation: RegulationConfiguration;
|
||||
}
|
||||
|
||||
export interface AppConfiguration {
|
||||
|
@ -103,4 +110,5 @@ export interface AppConfiguration {
|
|||
storage: StorageConfiguration;
|
||||
notifier: NotifierConfiguration;
|
||||
access_control?: ACLConfiguration;
|
||||
regulation: RegulationConfiguration;
|
||||
}
|
||||
|
|
|
@ -56,6 +56,7 @@ function adaptFromUserConfiguration(userConfiguration: UserConfiguration): AppCo
|
|||
// ensure_key_existence(userConfiguration, "ldap.url");
|
||||
// ensure_key_existence(userConfiguration, "ldap.base_dn");
|
||||
ensure_key_existence(userConfiguration, "session.secret");
|
||||
ensure_key_existence(userConfiguration, "regulation");
|
||||
|
||||
const port = userConfiguration.port || 8080;
|
||||
const ldapConfiguration = adaptLdapConfiguration(userConfiguration.ldap);
|
||||
|
@ -75,7 +76,8 @@ function adaptFromUserConfiguration(userConfiguration: UserConfiguration): AppCo
|
|||
},
|
||||
logs_level: get_optional<string>(userConfiguration, "logs_level", "info"),
|
||||
notifier: ObjectPath.get<object, NotifierConfiguration>(userConfiguration, "notifier"),
|
||||
access_control: ObjectPath.get<object, ACLConfiguration>(userConfiguration, "access_control")
|
||||
access_control: ObjectPath.get<object, ACLConfiguration>(userConfiguration, "access_control"),
|
||||
regulation: userConfiguration.regulation
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -11,7 +11,7 @@ export interface IUserDataStore {
|
|||
retrieveU2FRegistration(userId: string, appId: string): BluebirdPromise<U2FRegistrationDocument>;
|
||||
|
||||
saveAuthenticationTrace(userId: string, isAuthenticationSuccessful: boolean): BluebirdPromise<void>;
|
||||
retrieveLatestAuthenticationTraces(userId: string, isAuthenticationSuccessful: boolean, count: number): BluebirdPromise<AuthenticationTraceDocument[]>;
|
||||
retrieveLatestAuthenticationTraces(userId: string, count: number): BluebirdPromise<AuthenticationTraceDocument[]>;
|
||||
|
||||
produceIdentityValidationToken(userId: string, token: string, challenge: string, maxAge: number): BluebirdPromise<any>;
|
||||
consumeIdentityValidationToken(token: string, challenge: string): BluebirdPromise<IdentityValidationDocument>;
|
||||
|
|
|
@ -76,10 +76,9 @@ export class UserDataStore implements IUserDataStore {
|
|||
return this.authenticationTracesCollection.insert(newDocument);
|
||||
}
|
||||
|
||||
retrieveLatestAuthenticationTraces(userId: string, isAuthenticationSuccessful: boolean, count: number): BluebirdPromise<AuthenticationTraceDocument[]> {
|
||||
retrieveLatestAuthenticationTraces(userId: string, count: number): BluebirdPromise<AuthenticationTraceDocument[]> {
|
||||
const q = {
|
||||
userId: userId,
|
||||
isAuthenticationSuccessful: isAuthenticationSuccessful
|
||||
userId: userId
|
||||
};
|
||||
|
||||
return this.authenticationTracesCollection.find(q, { date: -1 }, count);
|
||||
|
|
|
@ -0,0 +1,52 @@
|
|||
Feature: Authelia regulates authentication to avoid brute force
|
||||
|
||||
@needs-test-config
|
||||
Scenario: Attacker tries too many authentication in a short period of time and get banned
|
||||
Given I visit "https://auth.test.local:8080/"
|
||||
And I login with user "blackhat" and password "password"
|
||||
And I register a TOTP secret called "Sec0"
|
||||
And I visit "https://auth.test.local:8080/"
|
||||
And I login with user "blackhat" and password "password" and I use TOTP token handle "Sec0"
|
||||
And I visit "https://auth.test.local:8080/logout?redirect=https://auth.test.local:8080/"
|
||||
And I visit "https://auth.test.local:8080/"
|
||||
And I set field "username" to "blackhat"
|
||||
And I set field "password" to "bad-password"
|
||||
And I click on "Sign in"
|
||||
And I get a notification of type "error" with message "Authentication failed. Please double check your credentials."
|
||||
And I set field "password" to "bad-password"
|
||||
And I click on "Sign in"
|
||||
And I get a notification of type "error" with message "Authentication failed. Please double check your credentials."
|
||||
And I set field "password" to "bad-password"
|
||||
And I click on "Sign in"
|
||||
And I get a notification of type "error" with message "Authentication failed. Please double check your credentials."
|
||||
When I set field "password" to "password"
|
||||
And I click on "Sign in"
|
||||
Then I get a notification of type "error" with message "Authentication failed. Please double check your credentials."
|
||||
|
||||
@needs-test-config
|
||||
Scenario: User is unbanned after a configured amount of time
|
||||
Given I visit "https://auth.test.local:8080/"
|
||||
And I login with user "blackhat" and password "password"
|
||||
And I register a TOTP secret called "Sec0"
|
||||
And I visit "https://auth.test.local:8080/"
|
||||
And I login with user "blackhat" and password "password" and I use TOTP token handle "Sec0"
|
||||
And I visit "https://auth.test.local:8080/logout?redirect=https://auth.test.local:8080/"
|
||||
And I visit "https://auth.test.local:8080/"
|
||||
And I set field "username" to "blackhat"
|
||||
And I set field "password" to "bad-password"
|
||||
And I click on "Sign in"
|
||||
And I get a notification of type "error" with message "Authentication failed. Please double check your credentials."
|
||||
And I set field "password" to "bad-password"
|
||||
And I click on "Sign in"
|
||||
And I get a notification of type "error" with message "Authentication failed. Please double check your credentials."
|
||||
And I set field "password" to "bad-password"
|
||||
And I click on "Sign in"
|
||||
And I get a notification of type "error" with message "Authentication failed. Please double check your credentials."
|
||||
When I wait 6 seconds
|
||||
And I set field "password" to "password"
|
||||
And I click on "Sign in"
|
||||
And I use "Sec0" as TOTP token handle
|
||||
And I click on "TOTP"
|
||||
Then I have access to:
|
||||
| url |
|
||||
| https://public.test.local:8080/secret.html |
|
|
@ -1,7 +0,0 @@
|
|||
import Cucumber = require("cucumber");
|
||||
|
||||
Cucumber.defineSupportCode(function({After}) {
|
||||
After(function() {
|
||||
return this.driver.quit();
|
||||
});
|
||||
});
|
|
@ -22,10 +22,21 @@ Cucumber.defineSupportCode(function ({ Given, When, Then }) {
|
|||
return this.clickOnButton(text);
|
||||
});
|
||||
|
||||
Given("I login with user {stringInDoubleQuotes} and password {stringInDoubleQuotes}", function (username: string, password: string) {
|
||||
Given("I login with user {stringInDoubleQuotes} and password {stringInDoubleQuotes}",
|
||||
function (username: string, password: string) {
|
||||
return this.loginWithUserPassword(username, password);
|
||||
});
|
||||
|
||||
Given("I login with user {stringInDoubleQuotes} and password {stringInDoubleQuotes} \
|
||||
and I use TOTP token handle {stringInDoubleQuotes}",
|
||||
function (username: string, password: string, totpTokenHandle: string) {
|
||||
const that = this;
|
||||
return this.loginWithUserPassword(username, password)
|
||||
.then(function () {
|
||||
return that.useTotpTokenHandle(totpTokenHandle);
|
||||
});
|
||||
});
|
||||
|
||||
Given("I register a TOTP secret called {stringInDoubleQuotes}", function (handle: string) {
|
||||
return this.registerTotpSecret(handle);
|
||||
});
|
||||
|
@ -38,7 +49,8 @@ Cucumber.defineSupportCode(function ({ Given, When, Then }) {
|
|||
return this.useTotpTokenHandle(handle);
|
||||
});
|
||||
|
||||
When("I visit {stringInDoubleQuotes} and get redirected {stringInDoubleQuotes}", function (url: string, redirectUrl: string) {
|
||||
When("I visit {stringInDoubleQuotes} and get redirected {stringInDoubleQuotes}",
|
||||
function (url: string, redirectUrl: string) {
|
||||
const that = this;
|
||||
return this.driver.get(url)
|
||||
.then(function () {
|
||||
|
@ -46,7 +58,8 @@ Cucumber.defineSupportCode(function ({ Given, When, Then }) {
|
|||
});
|
||||
});
|
||||
|
||||
Given("I register TOTP and login with user {stringInDoubleQuotes} and password {stringInDoubleQuotes}", function (username: string, password: string) {
|
||||
Given("I register TOTP and login with user {stringInDoubleQuotes} and password {stringInDoubleQuotes}",
|
||||
function (username: string, password: string) {
|
||||
return this.registerTotpAndSignin(username, password);
|
||||
});
|
||||
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
import Cucumber = require("cucumber");
|
||||
import fs = require("fs");
|
||||
import BluebirdPromise = require("bluebird");
|
||||
import ChildProcess = require("child_process");
|
||||
|
||||
Cucumber.defineSupportCode(function({ After, Before }) {
|
||||
const exec = BluebirdPromise.promisify(ChildProcess.exec);
|
||||
|
||||
After(function() {
|
||||
return this.driver.quit();
|
||||
});
|
||||
|
||||
Before({tags: "@needs-test-config", timeout: 15 * 1000}, function () {
|
||||
return exec("./scripts/example/dc-example.sh -f docker-compose.test.yml up -d authelia && sleep 2");
|
||||
});
|
||||
|
||||
After({tags: "@needs-test-config", timeout: 15 * 1000}, function () {
|
||||
return exec("./scripts/example/dc-example.sh up -d authelia && sleep 2");
|
||||
});
|
||||
});
|
|
@ -6,6 +6,7 @@ import CustomWorld = require("../support/world");
|
|||
|
||||
Cucumber.defineSupportCode(function ({ Given, When, Then }) {
|
||||
Then("I get a notification of type {stringInDoubleQuotes} with message {stringInDoubleQuotes}",
|
||||
{ timeout: 10 * 1000 },
|
||||
function (notificationType: string, notificationMessage: string) {
|
||||
const that = this;
|
||||
const notificationEl = this.driver.findElement(seleniumWebdriver.By.className("notification"));
|
||||
|
@ -17,8 +18,9 @@ Cucumber.defineSupportCode(function ({ Given, When, Then }) {
|
|||
Assert.equal(notificationMessage, txt);
|
||||
return notificationEl.getAttribute("class");
|
||||
})
|
||||
.then(function(classes: string) {
|
||||
.then(function (classes: string) {
|
||||
Assert(classes.indexOf(notificationType) > -1, "Class '" + notificationType + "' not found in notification element.");
|
||||
// return that.driver.wait(seleniumWebdriver.until.elementIsNotVisible(notificationEl), 6000);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
import Cucumber = require("cucumber");
|
||||
import seleniumWebdriver = require("selenium-webdriver");
|
||||
import Assert = require("assert");
|
||||
import Fs = require("fs");
|
||||
import CustomWorld = require("../support/world");
|
||||
|
||||
Cucumber.defineSupportCode(function ({ Given, When, Then }) {
|
||||
When("I wait {number} seconds", { timeout: 10 * 1000 }, function (seconds: number) {
|
||||
return this.driver.sleep(seconds * 1000);
|
||||
});
|
||||
});
|
|
@ -9,7 +9,7 @@ Cucumber.defineSupportCode(function ({ Given, When, Then }) {
|
|||
});
|
||||
|
||||
When("I click on the link of the email", function () {
|
||||
const notif = Fs.readFileSync("./notifications/notification.txt").toString();
|
||||
const notif = Fs.readFileSync("/tmp/notifications/notification.txt").toString();
|
||||
const regexp = new RegExp(/Link: (.+)/);
|
||||
const match = regexp.exec(notif);
|
||||
const link = match[1];
|
||||
|
|
|
@ -12,6 +12,7 @@ function CustomWorld() {
|
|||
.build();
|
||||
|
||||
this.totpSecrets = {};
|
||||
this.configuration = {};
|
||||
|
||||
this.visit = function (link: string) {
|
||||
return this.driver.get(link);
|
||||
|
@ -71,7 +72,7 @@ function CustomWorld() {
|
|||
return that.driver.findElement(seleniumWebdriver.By.className("register-totp")).click();
|
||||
})
|
||||
.then(function () {
|
||||
const notif = Fs.readFileSync("./notifications/notification.txt").toString();
|
||||
const notif = Fs.readFileSync("/tmp/notifications/notification.txt").toString();
|
||||
const regexp = new RegExp(/Link: (.+)/);
|
||||
const match = regexp.exec(notif);
|
||||
const link = match[1];
|
||||
|
@ -98,7 +99,7 @@ function CustomWorld() {
|
|||
};
|
||||
|
||||
this.useTotpToken = function (totpSecret: string) {
|
||||
return that.driver.wait(seleniumWebdriver.until.elementLocated(seleniumWebdriver.By.className("register-totp")), 4000)
|
||||
return that.driver.wait(seleniumWebdriver.until.elementLocated(seleniumWebdriver.By.className("register-totp")), 5000)
|
||||
.then(function () {
|
||||
return that.driver.findElement(seleniumWebdriver.By.id("token"))
|
||||
.sendKeys(totpSecret);
|
||||
|
|
|
@ -5,7 +5,7 @@ import JQueryMock = require("./mocks/jquery");
|
|||
|
||||
import { Notifier } from "../../../src/client/lib/Notifier";
|
||||
|
||||
describe("test notifier", function() {
|
||||
describe.skip("test notifier", function() {
|
||||
const SELECTOR = "dummy-selector";
|
||||
const MESSAGE = "This is a message";
|
||||
let jqueryMock: { jquery: JQueryMock.JQueryMock, element: JQueryMock.JQueryElementsMock };
|
||||
|
|
|
@ -1,120 +1,186 @@
|
|||
|
||||
import Sinon = require("sinon");
|
||||
import BluebirdPromise = require("bluebird");
|
||||
import Assert = require("assert");
|
||||
|
||||
import { AuthenticationRegulator } from "../../../src/server/lib/AuthenticationRegulator";
|
||||
import { UserDataStore } from "../../../src/server/lib/storage/UserDataStore";
|
||||
import MockDate = require("mockdate");
|
||||
import exceptions = require("../../../src/server/lib/Exceptions");
|
||||
import { CollectionStub } from "./mocks/storage/CollectionStub";
|
||||
import { CollectionFactoryStub } from "./mocks/storage/CollectionFactoryStub";
|
||||
import { UserDataStoreStub } from "./mocks/storage/UserDataStoreStub";
|
||||
|
||||
describe("test authentication regulator", function () {
|
||||
let collectionFactory: CollectionFactoryStub;
|
||||
let collection: CollectionStub;
|
||||
const USER1 = "USER1";
|
||||
const USER2 = "USER2";
|
||||
let userDataStoreStub: UserDataStoreStub;
|
||||
|
||||
beforeEach(function () {
|
||||
collectionFactory = new CollectionFactoryStub();
|
||||
collection = new CollectionStub();
|
||||
userDataStoreStub = new UserDataStoreStub();
|
||||
const dataStore: { [userId: string]: { userId: string, date: Date, isAuthenticationSuccessful: boolean }[] } = {
|
||||
[USER1]: [],
|
||||
[USER2]: []
|
||||
};
|
||||
|
||||
collectionFactory.buildStub.returns(collection);
|
||||
userDataStoreStub.saveAuthenticationTraceStub.callsFake(function (userId, isAuthenticationSuccessful) {
|
||||
dataStore[userId].unshift({
|
||||
userId: userId,
|
||||
date: new Date(),
|
||||
isAuthenticationSuccessful: isAuthenticationSuccessful,
|
||||
});
|
||||
return BluebirdPromise.resolve();
|
||||
});
|
||||
|
||||
it("should mark 2 authentication and regulate", function () {
|
||||
const user = "USER";
|
||||
|
||||
collection.insertStub.returns(BluebirdPromise.resolve());
|
||||
collection.findStub.returns(BluebirdPromise.resolve([{
|
||||
userId: user,
|
||||
date: new Date(),
|
||||
isAuthenticationSuccessful: false
|
||||
}, {
|
||||
userId: user,
|
||||
date: new Date(),
|
||||
isAuthenticationSuccessful: true
|
||||
}]));
|
||||
|
||||
const dataStore = new UserDataStore(collectionFactory);
|
||||
const regulator = new AuthenticationRegulator(dataStore, 10);
|
||||
|
||||
return regulator.mark(user, false)
|
||||
.then(function () {
|
||||
return regulator.mark(user, true);
|
||||
})
|
||||
.then(function () {
|
||||
return regulator.regulate(user);
|
||||
userDataStoreStub.retrieveLatestAuthenticationTracesStub.callsFake(function (userId, count) {
|
||||
const ret = (dataStore[userId].length <= count) ? dataStore[userId] : dataStore[userId].slice(0, 3);
|
||||
return BluebirdPromise.resolve(ret);
|
||||
});
|
||||
});
|
||||
|
||||
it("should mark 3 authentications and regulate (reject)", function (done) {
|
||||
const user = "USER";
|
||||
collection.insertStub.returns(BluebirdPromise.resolve());
|
||||
collection.findStub.returns(BluebirdPromise.resolve([{
|
||||
userId: user,
|
||||
date: new Date(),
|
||||
isAuthenticationSuccessful: false
|
||||
}, {
|
||||
userId: user,
|
||||
date: new Date(),
|
||||
isAuthenticationSuccessful: false
|
||||
}, {
|
||||
userId: user,
|
||||
date: new Date(),
|
||||
isAuthenticationSuccessful: false
|
||||
}]));
|
||||
afterEach(function () {
|
||||
MockDate.reset();
|
||||
});
|
||||
|
||||
const dataStore = new UserDataStore(collectionFactory);
|
||||
const regulator = new AuthenticationRegulator(dataStore, 10);
|
||||
function markAuthenticationAt(regulator: AuthenticationRegulator, user: string, time: string, success: boolean) {
|
||||
MockDate.set(time);
|
||||
return regulator.mark(user, success);
|
||||
}
|
||||
|
||||
regulator.mark(user, false)
|
||||
it("should mark 2 authentication and regulate (accept)", function () {
|
||||
const regulator = new AuthenticationRegulator(userDataStoreStub, 3, 10, 10);
|
||||
|
||||
return regulator.mark(USER1, false)
|
||||
.then(function () {
|
||||
return regulator.mark(user, false);
|
||||
return regulator.mark(USER1, true);
|
||||
})
|
||||
.then(function () {
|
||||
return regulator.mark(user, false);
|
||||
return regulator.regulate(USER1);
|
||||
});
|
||||
});
|
||||
|
||||
it("should mark 3 authentications and regulate (reject)", function () {
|
||||
const regulator = new AuthenticationRegulator(userDataStoreStub, 3, 10, 10);
|
||||
|
||||
return regulator.mark(USER1, false)
|
||||
.then(function () {
|
||||
return regulator.mark(USER1, false);
|
||||
})
|
||||
.then(function () {
|
||||
return regulator.regulate(user);
|
||||
return regulator.mark(USER1, false);
|
||||
})
|
||||
.then(function () {
|
||||
return regulator.regulate(USER1);
|
||||
})
|
||||
.then(function () { return BluebirdPromise.reject(new Error("should not be here!")); })
|
||||
.catch(exceptions.AuthenticationRegulationError, function () {
|
||||
done();
|
||||
return BluebirdPromise.resolve();
|
||||
});
|
||||
});
|
||||
|
||||
it("should mark 3 authentications separated by a lot of time and allow access to user", function (done) {
|
||||
const user = "USER";
|
||||
collection.insertStub.returns(BluebirdPromise.resolve());
|
||||
collection.findStub.returns(BluebirdPromise.resolve([{
|
||||
userId: user,
|
||||
date: new Date("1/2/2000 06:00:15"),
|
||||
isAuthenticationSuccessful: false
|
||||
}, {
|
||||
userId: user,
|
||||
date: new Date("1/2/2000 00:00:15"),
|
||||
isAuthenticationSuccessful: false
|
||||
}, {
|
||||
userId: user,
|
||||
date: new Date("1/2/2000 00:00:00"),
|
||||
isAuthenticationSuccessful: false
|
||||
}]));
|
||||
const data_store = new UserDataStore(collectionFactory);
|
||||
const regulator = new AuthenticationRegulator(data_store, 10);
|
||||
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);
|
||||
|
||||
MockDate.set("1/2/2000 00:00:00");
|
||||
regulator.mark(user, false)
|
||||
return markAuthenticationAt(regulator, USER1, "1/2/2000 00:00:00", false)
|
||||
.then(function () {
|
||||
MockDate.set("1/2/2000 00:00:15");
|
||||
return regulator.mark(user, false);
|
||||
return markAuthenticationAt(regulator, USER1, "1/2/2000 00:00:10", true);
|
||||
})
|
||||
.then(function () {
|
||||
MockDate.set("1/2/2000 06:00:15");
|
||||
return regulator.mark(user, false);
|
||||
return markAuthenticationAt(regulator, USER1, "1/2/2000 00:00:20", false);
|
||||
})
|
||||
.then(function () {
|
||||
return regulator.regulate(USER1);
|
||||
})
|
||||
.then(function () {
|
||||
return markAuthenticationAt(regulator, USER1, "1/2/2000 00:00:30", false);
|
||||
})
|
||||
.then(function () {
|
||||
return regulator.regulate(USER1);
|
||||
})
|
||||
.then(function () {
|
||||
return markAuthenticationAt(regulator, USER1, "1/2/2000 00:00:39", false);
|
||||
})
|
||||
.then(function () {
|
||||
return regulator.regulate(USER1);
|
||||
})
|
||||
.then(function () {
|
||||
return BluebirdPromise.reject(new Error("should not be here!"));
|
||||
},
|
||||
function () {
|
||||
return BluebirdPromise.resolve();
|
||||
});
|
||||
});
|
||||
|
||||
it("should regulate user if number of failures is greater than 3 in allowed time lapse", function () {
|
||||
function markAuthentications(regulator: AuthenticationRegulator, user: string) {
|
||||
return markAuthenticationAt(regulator, user, "1/2/2000 00:00:00", false)
|
||||
.then(function () {
|
||||
return markAuthenticationAt(regulator, user, "1/2/2000 00:00:45", false);
|
||||
})
|
||||
.then(function () {
|
||||
return markAuthenticationAt(regulator, user, "1/2/2000 00:01:05", false);
|
||||
})
|
||||
.then(function () {
|
||||
return regulator.regulate(user);
|
||||
});
|
||||
}
|
||||
|
||||
const regulator1 = new AuthenticationRegulator(userDataStoreStub, 3, 60, 60);
|
||||
const regulator2 = new AuthenticationRegulator(userDataStoreStub, 3, 2 * 60, 60);
|
||||
|
||||
const p1 = markAuthentications(regulator1, USER1);
|
||||
const p2 = markAuthentications(regulator2, USER2);
|
||||
|
||||
return BluebirdPromise.join(p1, p2)
|
||||
.then(function () {
|
||||
return BluebirdPromise.reject(new Error("should not be here..."));
|
||||
}, function () {
|
||||
Assert(p1.isFulfilled());
|
||||
Assert(p2.isRejected());
|
||||
});
|
||||
});
|
||||
|
||||
it("should user wait after regulation to authenticate again", function () {
|
||||
function markAuthentications(regulator: AuthenticationRegulator, user: string) {
|
||||
return markAuthenticationAt(regulator, user, "1/2/2000 00:00:00", false)
|
||||
.then(function () {
|
||||
return markAuthenticationAt(regulator, user, "1/2/2000 00:00:10", false);
|
||||
})
|
||||
.then(function () {
|
||||
done();
|
||||
return markAuthenticationAt(regulator, user, "1/2/2000 00:00:15", false);
|
||||
})
|
||||
.then(function () {
|
||||
return markAuthenticationAt(regulator, user, "1/2/2000 00:00:25", false);
|
||||
})
|
||||
.then(function () {
|
||||
MockDate.set("1/2/2000 00:00:54");
|
||||
return regulator.regulate(user);
|
||||
})
|
||||
.then(function () {
|
||||
return BluebirdPromise.reject(new Error("should fail at this time"));
|
||||
}, function () {
|
||||
MockDate.set("1/2/2000 00:00:56");
|
||||
return regulator.regulate(user);
|
||||
});
|
||||
}
|
||||
|
||||
const regulator = new AuthenticationRegulator(userDataStoreStub, 4, 30, 30);
|
||||
return markAuthentications(regulator, USER1);
|
||||
});
|
||||
|
||||
it("should disable regulation when max_retries is set to 0", function () {
|
||||
const maxRetries = 0;
|
||||
const regulator = new AuthenticationRegulator(userDataStoreStub, maxRetries, 60, 30);
|
||||
return markAuthenticationAt(regulator, USER1, "1/2/2000 00:00:00", false)
|
||||
.then(function () {
|
||||
return markAuthenticationAt(regulator, USER1, "1/2/2000 00:00:10", false);
|
||||
})
|
||||
.then(function () {
|
||||
return markAuthenticationAt(regulator, USER1, "1/2/2000 00:00:15", false);
|
||||
})
|
||||
.then(function () {
|
||||
return markAuthenticationAt(regulator, USER1, "1/2/2000 00:00:25", false);
|
||||
})
|
||||
.then(function () {
|
||||
MockDate.set("1/2/2000 00:00:26");
|
||||
return regulator.regulate(USER1);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -67,6 +67,11 @@ describe("test server configuration", function () {
|
|||
password: "password"
|
||||
}
|
||||
},
|
||||
regulation: {
|
||||
max_retries: 3,
|
||||
ban_time: 5 * 60,
|
||||
find_time: 5 * 60
|
||||
},
|
||||
storage: {
|
||||
local: {
|
||||
in_memory: true
|
||||
|
|
|
@ -38,6 +38,11 @@ describe("test session configuration builder", function () {
|
|||
expiration: 3600,
|
||||
secret: "secret"
|
||||
},
|
||||
regulation: {
|
||||
max_retries: 3,
|
||||
ban_time: 5 * 60,
|
||||
find_time: 5 * 60
|
||||
},
|
||||
storage: {
|
||||
local: {
|
||||
in_memory: true
|
||||
|
@ -107,6 +112,11 @@ describe("test session configuration builder", function () {
|
|||
port: 6379
|
||||
}
|
||||
},
|
||||
regulation: {
|
||||
max_retries: 3,
|
||||
ban_time: 5 * 60,
|
||||
find_time: 5 * 60
|
||||
},
|
||||
storage: {
|
||||
local: {
|
||||
in_memory: true
|
||||
|
|
|
@ -4,7 +4,7 @@ import { ConfigurationAdapter } from "../../../../src/server/lib/configuration/C
|
|||
|
||||
describe("test config adapter", function () {
|
||||
function build_yaml_config(): UserConfiguration {
|
||||
const yaml_config = {
|
||||
const yaml_config: UserConfiguration = {
|
||||
port: 8080,
|
||||
ldap: {
|
||||
url: "http://ldap",
|
||||
|
@ -17,13 +17,18 @@ describe("test config adapter", function () {
|
|||
session: {
|
||||
domain: "example.com",
|
||||
secret: "secret",
|
||||
max_age: 40000
|
||||
expiration: 40000
|
||||
},
|
||||
storage: {
|
||||
local: {
|
||||
path: "/mydirectory"
|
||||
}
|
||||
},
|
||||
regulation: {
|
||||
max_retries: 3,
|
||||
find_time: 5 * 60,
|
||||
ban_time: 5 * 60
|
||||
},
|
||||
logs_level: "debug",
|
||||
notifier: {
|
||||
gmail: {
|
||||
|
|
|
@ -4,7 +4,7 @@ import { ConfigurationAdapter } from "../../../../src/server/lib/configuration/C
|
|||
|
||||
describe("test ldap configuration adaptation", function () {
|
||||
function build_yaml_config(): UserConfiguration {
|
||||
const yaml_config = {
|
||||
const yaml_config: UserConfiguration = {
|
||||
port: 8080,
|
||||
ldap: {
|
||||
url: "http://ldap",
|
||||
|
@ -17,13 +17,18 @@ describe("test ldap configuration adaptation", function () {
|
|||
session: {
|
||||
domain: "example.com",
|
||||
secret: "secret",
|
||||
max_age: 40000
|
||||
expiration: 40000
|
||||
},
|
||||
storage: {
|
||||
local: {
|
||||
path: "/mydirectory"
|
||||
}
|
||||
},
|
||||
regulation: {
|
||||
max_retries: 3,
|
||||
ban_time: 5 * 60,
|
||||
find_time: 5 * 60,
|
||||
},
|
||||
logs_level: "debug",
|
||||
notifier: {
|
||||
gmail: {
|
||||
|
|
|
@ -43,8 +43,8 @@ export class UserDataStoreStub implements IUserDataStore {
|
|||
return this.saveAuthenticationTraceStub(userId, isAuthenticationSuccessful);
|
||||
}
|
||||
|
||||
retrieveLatestAuthenticationTraces(userId: string, isAuthenticationSuccessful: boolean, count: number): BluebirdPromise<AuthenticationTraceDocument[]> {
|
||||
return this.retrieveLatestAuthenticationTracesStub(userId, isAuthenticationSuccessful, count);
|
||||
retrieveLatestAuthenticationTraces(userId: string, count: number): BluebirdPromise<AuthenticationTraceDocument[]> {
|
||||
return this.retrieveLatestAuthenticationTracesStub(userId, count);
|
||||
}
|
||||
|
||||
produceIdentityValidationToken(userId: string, token: string, challenge: string, maxAge: number): BluebirdPromise<any> {
|
||||
|
|
|
@ -5,6 +5,7 @@ import speakeasy = require("speakeasy");
|
|||
import request = require("request");
|
||||
import nedb = require("nedb");
|
||||
import { GlobalDependencies } from "../../../../src/types/Dependencies";
|
||||
import { UserConfiguration } from "../../../../src/server/lib/configuration/Configuration";
|
||||
import { TOTPSecret } from "../../../../src/types/TOTPSecret";
|
||||
import U2FMock = require("./../mocks/u2f");
|
||||
import Endpoints = require("../../../../src/server/endpoints");
|
||||
|
@ -28,12 +29,11 @@ describe("Private pages of the server must not be accessible without session", f
|
|||
let u2f: U2FMock.U2FMock;
|
||||
|
||||
beforeEach(function () {
|
||||
const config = {
|
||||
const config: UserConfiguration = {
|
||||
port: PORT,
|
||||
ldap: {
|
||||
url: "ldap://127.0.0.1:389",
|
||||
base_dn: "ou=users,dc=example,dc=com",
|
||||
user_name_attribute: "cn",
|
||||
user: "cn=admin,dc=example,dc=com",
|
||||
password: "password",
|
||||
},
|
||||
|
@ -41,6 +41,11 @@ describe("Private pages of the server must not be accessible without session", f
|
|||
secret: "session_secret",
|
||||
expiration: 50000,
|
||||
},
|
||||
regulation: {
|
||||
max_retries: 3,
|
||||
ban_time: 5 * 60,
|
||||
find_time: 5 * 60
|
||||
},
|
||||
storage: {
|
||||
local: {
|
||||
in_memory: true
|
||||
|
|
|
@ -5,6 +5,7 @@ import speakeasy = require("speakeasy");
|
|||
import Request = require("request");
|
||||
import nedb = require("nedb");
|
||||
import { GlobalDependencies } from "../../../../src/types/Dependencies";
|
||||
import { UserConfiguration } from "../../../../src/server/lib/configuration/Configuration";
|
||||
import { TOTPSecret } from "../../../../src/types/TOTPSecret";
|
||||
import U2FMock = require("./../mocks/u2f");
|
||||
import Endpoints = require("../../../../src/server/endpoints");
|
||||
|
@ -28,12 +29,11 @@ describe("Public pages of the server must be accessible without session", functi
|
|||
let u2f: U2FMock.U2FMock;
|
||||
|
||||
beforeEach(function () {
|
||||
const config = {
|
||||
const config: UserConfiguration = {
|
||||
port: PORT,
|
||||
ldap: {
|
||||
url: "ldap://127.0.0.1:389",
|
||||
base_dn: "ou=users,dc=example,dc=com",
|
||||
user_name_attribute: "cn",
|
||||
user: "cn=admin,dc=example,dc=com",
|
||||
password: "password",
|
||||
},
|
||||
|
@ -46,6 +46,11 @@ describe("Public pages of the server must be accessible without session", functi
|
|||
in_memory: true
|
||||
}
|
||||
},
|
||||
regulation: {
|
||||
max_retries: 3,
|
||||
ban_time: 5 * 60,
|
||||
find_time: 5 * 60
|
||||
},
|
||||
notifier: {
|
||||
gmail: {
|
||||
username: "user@example.com",
|
||||
|
|
|
@ -1,198 +0,0 @@
|
|||
import Server from "../../../../src/server/lib/Server";
|
||||
import { LdapjsClientMock } from "./../mocks/ldapjs";
|
||||
|
||||
import BluebirdPromise = require("bluebird");
|
||||
import speakeasy = require("speakeasy");
|
||||
import request = require("request");
|
||||
import nedb = require("nedb");
|
||||
import { GlobalDependencies } from "../../../../src/types/Dependencies";
|
||||
import { TOTPSecret } from "../../../../src/types/TOTPSecret";
|
||||
import U2FMock = require("./../mocks/u2f");
|
||||
import Endpoints = require("../../../../src/server/endpoints");
|
||||
import Requests = require("../requests");
|
||||
import Assert = require("assert");
|
||||
import Sinon = require("sinon");
|
||||
import Winston = require("winston");
|
||||
import MockDate = require("mockdate");
|
||||
import ExpressSession = require("express-session");
|
||||
import ldapjs = require("ldapjs");
|
||||
|
||||
const requestp = BluebirdPromise.promisifyAll(request) as typeof request;
|
||||
|
||||
const PORT = 8090;
|
||||
const BASE_URL = "http://localhost:" + PORT;
|
||||
const requests = Requests(PORT);
|
||||
|
||||
describe("test the server", function () {
|
||||
let server: Server;
|
||||
let transporter: any;
|
||||
let u2f: U2FMock.U2FMock;
|
||||
|
||||
beforeEach(function () {
|
||||
const config = {
|
||||
port: PORT,
|
||||
ldap: {
|
||||
url: "ldap://127.0.0.1:389",
|
||||
base_dn: "ou=users,dc=example,dc=com",
|
||||
user: "cn=admin,dc=example,dc=com",
|
||||
password: "password",
|
||||
},
|
||||
session: {
|
||||
secret: "session_secret",
|
||||
expiration: 50000,
|
||||
},
|
||||
storage: {
|
||||
local: {
|
||||
in_memory: true
|
||||
}
|
||||
},
|
||||
notifier: {
|
||||
gmail: {
|
||||
username: "user@example.com",
|
||||
password: "password"
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const ldapClient = LdapjsClientMock();
|
||||
const ldap = {
|
||||
Change: Sinon.spy(),
|
||||
createClient: Sinon.spy(function () {
|
||||
return ldapClient;
|
||||
})
|
||||
};
|
||||
|
||||
u2f = U2FMock.U2FMock();
|
||||
|
||||
transporter = {
|
||||
sendMail: Sinon.stub().yields()
|
||||
};
|
||||
|
||||
const nodemailer = {
|
||||
createTransport: Sinon.spy(function () {
|
||||
return transporter;
|
||||
})
|
||||
};
|
||||
|
||||
const ldapDocument = {
|
||||
object: {
|
||||
mail: "test_ok@example.com",
|
||||
}
|
||||
};
|
||||
|
||||
const search_res = {
|
||||
on: Sinon.spy(function (event: string, fn: (s: any) => void) {
|
||||
if (event != "error") fn(ldapDocument);
|
||||
})
|
||||
};
|
||||
|
||||
ldapClient.bind.withArgs("cn=test_ok,ou=users,dc=example,dc=com",
|
||||
"password").yields();
|
||||
ldapClient.bind.withArgs("cn=admin,dc=example,dc=com",
|
||||
"password").yields();
|
||||
|
||||
ldapClient.bind.withArgs("cn=test_nok,ou=users,dc=example,dc=com",
|
||||
"password").yields("Bad credentials");
|
||||
|
||||
ldapClient.unbind.yields();
|
||||
ldapClient.modify.yields();
|
||||
ldapClient.search.yields(undefined, search_res);
|
||||
|
||||
const deps: GlobalDependencies = {
|
||||
u2f: u2f,
|
||||
nedb: nedb,
|
||||
nodemailer: nodemailer,
|
||||
ldapjs: ldap,
|
||||
session: ExpressSession,
|
||||
winston: Winston,
|
||||
speakeasy: speakeasy,
|
||||
ConnectRedis: Sinon.spy(),
|
||||
dovehash: {
|
||||
encode: Sinon.stub().returns("abc")
|
||||
}
|
||||
};
|
||||
|
||||
server = new Server();
|
||||
return server.start(config, deps);
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
server.stop();
|
||||
});
|
||||
|
||||
describe("test authentication and verification", function () {
|
||||
test_authentication();
|
||||
test_regulation();
|
||||
});
|
||||
|
||||
function test_authentication() {
|
||||
it("should return status code 204 when user is authenticated using u2f", function () {
|
||||
const sign_request = {};
|
||||
const sign_status = {};
|
||||
const registration_request = {};
|
||||
const registration_status = {};
|
||||
u2f.request.returns(BluebirdPromise.resolve(sign_request));
|
||||
u2f.checkRegistration.returns(BluebirdPromise.resolve(sign_status));
|
||||
u2f.checkSignature.returns(BluebirdPromise.resolve(registration_status));
|
||||
|
||||
const j = requestp.jar();
|
||||
return requests.login(j)
|
||||
.then(function (res: request.RequestResponse) {
|
||||
Assert.equal(res.statusCode, 200, "get login page failed");
|
||||
return requests.first_factor(j);
|
||||
})
|
||||
.then(function (res: request.RequestResponse) {
|
||||
// console.log(res);
|
||||
Assert.equal(res.headers.location, Endpoints.SECOND_FACTOR_GET);
|
||||
Assert.equal(res.statusCode, 302, "first factor failed");
|
||||
return requests.u2f_registration(j, transporter);
|
||||
})
|
||||
.then(function (res: request.RequestResponse) {
|
||||
Assert.equal(res.statusCode, 200, "second factor, finish register failed");
|
||||
return requests.u2f_authentication(j);
|
||||
})
|
||||
.then(function (res: request.RequestResponse) {
|
||||
Assert.equal(res.statusCode, 200, "second factor, finish sign failed");
|
||||
return requests.verify(j);
|
||||
})
|
||||
.then(function (res: request.RequestResponse) {
|
||||
Assert.equal(res.statusCode, 204, "verify failed");
|
||||
return BluebirdPromise.resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function test_regulation() {
|
||||
it("should regulate authentication", function () {
|
||||
const j = requestp.jar();
|
||||
MockDate.set("1/2/2017 00:00:00");
|
||||
return requests.login(j)
|
||||
.then(function (res: request.RequestResponse) {
|
||||
Assert.equal(res.statusCode, 200, "get login page failed");
|
||||
return requests.failing_first_factor(j);
|
||||
})
|
||||
.then(function (res: request.RequestResponse) {
|
||||
Assert.equal(res.statusCode, 401, "first factor failed");
|
||||
return requests.failing_first_factor(j);
|
||||
})
|
||||
.then(function (res: request.RequestResponse) {
|
||||
Assert.equal(res.statusCode, 401, "first factor failed");
|
||||
return requests.failing_first_factor(j);
|
||||
})
|
||||
.then(function (res: request.RequestResponse) {
|
||||
Assert.equal(res.statusCode, 401, "first factor failed");
|
||||
return requests.failing_first_factor(j);
|
||||
})
|
||||
.then(function (res: request.RequestResponse) {
|
||||
Assert.equal(res.statusCode, 403, "first factor failed");
|
||||
MockDate.set("1/2/2017 00:30:00");
|
||||
return requests.failing_first_factor(j);
|
||||
})
|
||||
.then(function (res: request.RequestResponse) {
|
||||
Assert.equal(res.statusCode, 401, "first factor failed");
|
||||
return BluebirdPromise.resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
@ -141,29 +141,24 @@ describe("test user data store", function () {
|
|||
});
|
||||
});
|
||||
|
||||
function should_retrieve_latest_authentication_traces(count: number, status: boolean) {
|
||||
function should_retrieve_latest_authentication_traces(count: number) {
|
||||
factory.buildStub.returns(collection);
|
||||
collection.findStub.withArgs().returns(BluebirdPromise.resolve());
|
||||
|
||||
const dataStore = new UserDataStore(factory);
|
||||
|
||||
return dataStore.retrieveLatestAuthenticationTraces(userId, status, count)
|
||||
return dataStore.retrieveLatestAuthenticationTraces(userId, count)
|
||||
.then(function (doc: AuthenticationTraceDocument[]) {
|
||||
Assert(collection.findStub.calledOnce);
|
||||
Assert(collection.findStub.calledWith({
|
||||
userId: userId,
|
||||
isAuthenticationSuccessful: status,
|
||||
}, { date: -1 }, count));
|
||||
return BluebirdPromise.resolve();
|
||||
});
|
||||
}
|
||||
|
||||
it("should retrieve 3 latest failed authentication traces", function () {
|
||||
should_retrieve_latest_authentication_traces(3, false);
|
||||
});
|
||||
|
||||
it("should retrieve 4 latest successful authentication traces", function () {
|
||||
should_retrieve_latest_authentication_traces(4, true);
|
||||
should_retrieve_latest_authentication_traces(3);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
Loading…
Reference in New Issue