diff --git a/.gitignore b/.gitignore
index e391408ef..8e7376122 100644
--- a/.gitignore
+++ b/.gitignore
@@ -29,4 +29,3 @@ dist/
# Specific files
/config.yml
-/test/integration/nginx.conf
diff --git a/README.md b/README.md
index aa8d2d07f..bd61219d4 100644
--- a/README.md
+++ b/README.md
@@ -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.
diff --git a/config.template.yml b/config.template.yml
index 1f8bf0160..2bcc1c923 100644
--- a/config.template.yml
+++ b/config.template.yml
@@ -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
@@ -73,20 +84,44 @@ 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:
diff --git a/config.test.yml b/config.test.yml
new file mode 100644
index 000000000..557cf5d4f
--- /dev/null
+++ b/config.test.yml
@@ -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
+
diff --git a/docker-compose.test.yml b/docker-compose.test.yml
new file mode 100644
index 000000000..53495d0f1
--- /dev/null
+++ b/docker-compose.test.yml
@@ -0,0 +1,5 @@
+version: '2'
+services:
+ authelia:
+ volumes:
+ - ./config.test.yml:/etc/authelia/config.yml:ro
diff --git a/docker-compose.yml b/docker-compose.yml
index ace1d5db6..82e4e0a27 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -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:
diff --git a/example/ldap/base.ldif b/example/ldap/base.ldif
index 06e962c04..4c6c33c76 100644
--- a/example/ldap/base.ldif
+++ b/example/ldap/base.ldif
@@ -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=
diff --git a/src/client/lib/Notifier.ts b/src/client/lib/Notifier.ts
index cc59ee885..e80529182 100644
--- a/src/client/lib/Notifier.ts
+++ b/src/client/lib/Notifier.ts
@@ -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('\
- %s', statusType, statusType, msg);
+ %s', 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);
- handlers.onFadedOut();
});
+
+ 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) {
diff --git a/src/client/lib/firstfactor/index.ts b/src/client/lib/firstfactor/index.ts
index 23b4a40f8..9725c2bb0 100644
--- a/src/client/lib/firstfactor/index.ts
+++ b/src/client/lib/firstfactor/index.ts
@@ -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.");
}
diff --git a/src/server/index.ts b/src/server/index.ts
index 621d28c12..732a180da 100755
--- a/src/server/index.ts
+++ b/src/server/index.ts
@@ -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) {
diff --git a/src/server/lib/AuthenticationRegulator.ts b/src/server/lib/AuthenticationRegulator.ts
index b3319b4e9..a04547335 100644
--- a/src/server/lib/AuthenticationRegulator.ts
+++ b/src/server/lib/AuthenticationRegulator.ts
@@ -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 {
- 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();
});
diff --git a/src/server/lib/ServerVariablesHandler.ts b/src/server/lib/ServerVariablesHandler.ts
index 49fde6fa5..e16172c78 100644
--- a/src/server/lib/ServerVariablesHandler.ts
+++ b/src/server/lib/ServerVariablesHandler.ts
@@ -72,8 +72,6 @@ class UserDataStoreFactory {
export class ServerVariablesHandler {
static initialize(app: express.Application, config: Configuration.AppConfiguration, deps: GlobalDependencies): BluebirdPromise {
- 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,
diff --git a/src/server/lib/configuration/Configuration.d.ts b/src/server/lib/configuration/Configuration.d.ts
index 8a98280e7..5c3c8e32b 100644
--- a/src/server/lib/configuration/Configuration.d.ts
+++ b/src/server/lib/configuration/Configuration.d.ts
@@ -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;
}
diff --git a/src/server/lib/configuration/ConfigurationAdapter.ts b/src/server/lib/configuration/ConfigurationAdapter.ts
index cc5de6ef7..8307ef076 100644
--- a/src/server/lib/configuration/ConfigurationAdapter.ts
+++ b/src/server/lib/configuration/ConfigurationAdapter.ts
@@ -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(userConfiguration, "logs_level", "info"),
notifier: ObjectPath.get