From 20536abf8bf8634599b2424d5f1083be6ac2418f Mon Sep 17 00:00:00 2001 From: Clement Michaud Date: Sat, 2 Sep 2017 22:38:26 +0200 Subject: [PATCH 1/4] Introduce LDAP filters to search users and groups for more flexibility. --- config.template.yml | 20 +- scripts/dc-dev.sh | 1 + src/server/lib/Server.ts | 4 +- src/server/lib/ServerVariablesHandler.ts | 24 ++- .../lib/configuration/Configuration.d.ts | 33 +++- .../lib/configuration/ConfigurationAdapter.ts | 37 +++- src/server/lib/ldap/Authenticator.ts | 46 ++--- src/server/lib/ldap/Client.ts | 106 ++++++----- src/server/lib/ldap/ClientFactory.ts | 27 +++ src/server/lib/ldap/EmailsRetriever.ts | 23 +-- src/server/lib/ldap/IAuthenticator.ts | 6 + src/server/lib/ldap/IClient.ts | 16 ++ src/server/lib/ldap/IClientFactory.ts | 6 + src/server/lib/ldap/IEmailsRetriever.ts | 5 + src/server/lib/ldap/IPasswordUpdater.ts | 5 + src/server/lib/ldap/PasswordUpdater.ts | 23 +-- src/server/lib/ldap/common.ts | 18 -- src/server/lib/routes/firstfactor/post.ts | 11 +- test/features/resilience.feature | 18 +- test/unit/server/DataPersistence.test.ts | 179 ------------------ .../SessionConfigurationBuilder.test.ts | 18 +- .../ConfigurationAdapter.test.ts | 42 ++-- .../LdapConfigurationAdaptation.test.ts | 93 +++++++++ test/unit/server/ldap/Authenticator.test.ts | 176 ++++++++--------- test/unit/server/ldap/EmailsRetriever.test.ts | 113 +++++------ test/unit/server/ldap/PasswordUpdater.test.ts | 96 +++++----- test/unit/server/mocks/ServerVariablesMock.ts | 2 +- .../server/mocks/ldap/ClientFactoryStub.ts | 16 ++ test/unit/server/mocks/ldap/ClientStub.ts | 46 +++++ test/unit/server/server/Server.test.ts | 94 --------- 30 files changed, 639 insertions(+), 665 deletions(-) create mode 100644 src/server/lib/ldap/ClientFactory.ts create mode 100644 src/server/lib/ldap/IAuthenticator.ts create mode 100644 src/server/lib/ldap/IClient.ts create mode 100644 src/server/lib/ldap/IClientFactory.ts create mode 100644 src/server/lib/ldap/IEmailsRetriever.ts create mode 100644 src/server/lib/ldap/IPasswordUpdater.ts delete mode 100644 src/server/lib/ldap/common.ts delete mode 100644 test/unit/server/DataPersistence.test.ts rename test/unit/server/{ => configuration}/ConfigurationAdapter.test.ts (65%) create mode 100644 test/unit/server/configuration/LdapConfigurationAdaptation.test.ts create mode 100644 test/unit/server/mocks/ldap/ClientFactoryStub.ts create mode 100644 test/unit/server/mocks/ldap/ClientStub.ts diff --git a/config.template.yml b/config.template.yml index 638b5920..1f8bf016 100644 --- a/config.template.yml +++ b/config.template.yml @@ -18,17 +18,27 @@ ldap: base_dn: dc=example,dc=com # An additional dn to define the scope to all users - additional_user_dn: ou=users + additional_users_dn: ou=users - # The user name attribute of users. Might uid for FreeIPA. 'cn' by default. - user_name_attribute: cn + # 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_group_dn: ou=groups + additional_groups_dn: ou=groups - # The group name attribute of group. 'cn' by default. + # 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 diff --git a/scripts/dc-dev.sh b/scripts/dc-dev.sh index b9cd0630..416a3ba2 100755 --- a/scripts/dc-dev.sh +++ b/scripts/dc-dev.sh @@ -9,4 +9,5 @@ docker-compose \ -f example/mongo/docker-compose.yml \ -f example/redis/docker-compose.yml \ -f example/nginx/docker-compose.yml \ + -f example/ldap/docker-compose.admin.yml \ -f example/ldap/docker-compose.yml $* diff --git a/src/server/lib/Server.ts b/src/server/lib/Server.ts index bc42e883..ecfa152a 100644 --- a/src/server/lib/Server.ts +++ b/src/server/lib/Server.ts @@ -47,7 +47,7 @@ export default class Server { RestApi.setup(app); } - private transformConfiguration(yamlConfiguration: UserConfiguration, deps: GlobalDependencies): AppConfiguration { + private adaptConfiguration(yamlConfiguration: UserConfiguration, deps: GlobalDependencies): AppConfiguration { const config = ConfigurationAdapter.adapt(yamlConfiguration); // by default the level of logs is info @@ -76,7 +76,7 @@ export default class Server { start(yamlConfiguration: UserConfiguration, deps: GlobalDependencies): BluebirdPromise { const that = this; const app = Express(); - const config = this.transformConfiguration(yamlConfiguration, deps); + const config = this.adaptConfiguration(yamlConfiguration, deps); return this.setup(config, app, deps) .then(function () { return that.startServer(app, config.port); diff --git a/src/server/lib/ServerVariablesHandler.ts b/src/server/lib/ServerVariablesHandler.ts index a7d6df7b..49fde6fa 100644 --- a/src/server/lib/ServerVariablesHandler.ts +++ b/src/server/lib/ServerVariablesHandler.ts @@ -1,9 +1,13 @@ import winston = require("winston"); import BluebirdPromise = require("bluebird"); +import { IAuthenticator } from "./ldap/IAuthenticator"; +import { IPasswordUpdater } from "./ldap/IPasswordUpdater"; +import { IEmailsRetriever } from "./ldap/IEmailsRetriever"; import { Authenticator } from "./ldap/Authenticator"; import { PasswordUpdater } from "./ldap/PasswordUpdater"; import { EmailsRetriever } from "./ldap/EmailsRetriever"; +import { ClientFactory } from "./ldap/ClientFactory"; import { TOTPValidator } from "./TOTPValidator"; import { TOTPGenerator } from "./TOTPGenerator"; @@ -29,9 +33,9 @@ export const VARIABLES_KEY = "authelia-variables"; export interface ServerVariables { logger: typeof winston; - ldapAuthenticator: Authenticator; - ldapPasswordUpdater: PasswordUpdater; - ldapEmailsRetriever: EmailsRetriever; + ldapAuthenticator: IAuthenticator; + ldapPasswordUpdater: IPasswordUpdater; + ldapEmailsRetriever: IEmailsRetriever; totpValidator: TOTPValidator; totpGenerator: TOTPGenerator; u2f: typeof U2F; @@ -71,9 +75,11 @@ export class ServerVariablesHandler { const five_minutes = 5 * 60; const notifier = NotifierFactory.build(config.notifier, deps.nodemailer); - const ldapAuthenticator = new Authenticator(config.ldap, deps.ldapjs, deps.winston); - const ldapPasswordUpdater = new PasswordUpdater(config.ldap, deps.ldapjs, deps.dovehash, deps.winston); - const ldapEmailsRetriever = new EmailsRetriever(config.ldap, deps.ldapjs, deps.winston); + const ldapClientFactory = new ClientFactory(config.ldap, deps.ldapjs, deps.dovehash, deps.winston); + + const ldapAuthenticator = new Authenticator(config.ldap, ldapClientFactory); + const ldapPasswordUpdater = new PasswordUpdater(config.ldap, ldapClientFactory); + const ldapEmailsRetriever = new EmailsRetriever(config.ldap, ldapClientFactory); const accessController = new AccessController(config.access_control, deps.winston); const totpValidator = new TOTPValidator(deps.speakeasy); const totpGenerator = new TOTPGenerator(deps.speakeasy); @@ -113,15 +119,15 @@ export class ServerVariablesHandler { return (app.get(VARIABLES_KEY) as ServerVariables).notifier; } - static getLdapAuthenticator(app: express.Application): Authenticator { + static getLdapAuthenticator(app: express.Application): IAuthenticator { return (app.get(VARIABLES_KEY) as ServerVariables).ldapAuthenticator; } - static getLdapPasswordUpdater(app: express.Application): PasswordUpdater { + static getLdapPasswordUpdater(app: express.Application): IPasswordUpdater { return (app.get(VARIABLES_KEY) as ServerVariables).ldapPasswordUpdater; } - static getLdapEmailsRetriever(app: express.Application): EmailsRetriever { + static getLdapEmailsRetriever(app: express.Application): IEmailsRetriever { return (app.get(VARIABLES_KEY) as ServerVariables).ldapEmailsRetriever; } diff --git a/src/server/lib/configuration/Configuration.d.ts b/src/server/lib/configuration/Configuration.d.ts index ec060873..8a98280e 100644 --- a/src/server/lib/configuration/Configuration.d.ts +++ b/src/server/lib/configuration/Configuration.d.ts @@ -1,11 +1,32 @@ +export interface UserLdapConfiguration { + url: string; + base_dn: string; + + additional_users_dn?: string; + users_filter?: string; + + additional_groups_dn?: string; + groups_filter?: string; + + group_name_attribute?: string; + mail_attribute?: string; + + user: string; // admin username + password: string; // admin password +} export interface LdapConfiguration { url: string; - base_dn: string; - additional_user_dn?: string; - user_name_attribute?: string; // cn by default - additional_group_dn?: string; - group_name_attribute?: string; // cn by default + + users_dn: string; + users_filter: string; + + groups_dn: string; + groups_filter: string; + + group_name_attribute: string; + mail_attribute: string; + user: string; // admin username password: string; // admin password } @@ -67,7 +88,7 @@ export interface StorageConfiguration { export interface UserConfiguration { port?: number; logs_level?: string; - ldap: LdapConfiguration; + ldap: UserLdapConfiguration; session: SessionCookieConfiguration; storage: StorageConfiguration; notifier: NotifierConfiguration; diff --git a/src/server/lib/configuration/ConfigurationAdapter.ts b/src/server/lib/configuration/ConfigurationAdapter.ts index fe4c33eb..cc5de6ef 100644 --- a/src/server/lib/configuration/ConfigurationAdapter.ts +++ b/src/server/lib/configuration/ConfigurationAdapter.ts @@ -3,7 +3,8 @@ import * as ObjectPath from "object-path"; import { AppConfiguration, UserConfiguration, NotifierConfiguration, ACLConfiguration, LdapConfiguration, SessionRedisOptions, - MongoStorageConfiguration, LocalStorageConfiguration + MongoStorageConfiguration, LocalStorageConfiguration, + UserLdapConfiguration } from "./Configuration"; const LDAP_URL_ENV_VARIABLE = "LDAP_URL"; @@ -23,15 +24,45 @@ function ensure_key_existence(config: object, path: string): void { } } +function adaptLdapConfiguration(userConfig: UserLdapConfiguration): LdapConfiguration { + const DEFAULT_USERS_FILTER = "cn={0}"; + const DEFAULT_GROUPS_FILTER = "member={0}"; + const DEFAULT_GROUP_NAME_ATTRIBUTE = "cn"; + const DEFAULT_MAIL_ATTRIBUTE = "mail"; + + let usersDN = userConfig.base_dn; + if (userConfig.additional_users_dn) + usersDN = userConfig.additional_users_dn + "," + usersDN; + + let groupsDN = userConfig.base_dn; + if (userConfig.additional_groups_dn) + groupsDN = userConfig.additional_groups_dn + "," + groupsDN; + + return { + url: userConfig.url, + users_dn: usersDN, + users_filter: userConfig.users_filter || DEFAULT_USERS_FILTER, + groups_dn: groupsDN, + groups_filter: userConfig.groups_filter || DEFAULT_GROUPS_FILTER, + group_name_attribute: userConfig.group_name_attribute || DEFAULT_GROUP_NAME_ATTRIBUTE, + mail_attribute: userConfig.mail_attribute || DEFAULT_MAIL_ATTRIBUTE, + password: userConfig.password, + user: userConfig.user + }; +} + function adaptFromUserConfiguration(userConfiguration: UserConfiguration): AppConfiguration { ensure_key_existence(userConfiguration, "ldap"); + // ensure_key_existence(userConfiguration, "ldap.url"); + // ensure_key_existence(userConfiguration, "ldap.base_dn"); ensure_key_existence(userConfiguration, "session.secret"); - const port = ObjectPath.get(userConfiguration, "port", 8080); + const port = userConfiguration.port || 8080; + const ldapConfiguration = adaptLdapConfiguration(userConfiguration.ldap); return { port: port, - ldap: ObjectPath.get(userConfiguration, "ldap"), + ldap: ldapConfiguration, session: { domain: ObjectPath.get(userConfiguration, "session.domain"), secret: ObjectPath.get(userConfiguration, "session.secret"), diff --git a/src/server/lib/ldap/Authenticator.ts b/src/server/lib/ldap/Authenticator.ts index 26d9b5a0..22c59520 100644 --- a/src/server/lib/ldap/Authenticator.ts +++ b/src/server/lib/ldap/Authenticator.ts @@ -1,36 +1,38 @@ import BluebirdPromise = require("bluebird"); import exceptions = require("../Exceptions"); import ldapjs = require("ldapjs"); -import { Client, Attributes } from "./Client"; -import { buildUserDN } from "./common"; +import { IClient } from "./IClient"; +import { IClientFactory } from "./IClientFactory"; +import { GroupsAndEmails } from "./IClient"; +import { IAuthenticator } from "./IAuthenticator"; import { LdapConfiguration } from "../configuration/Configuration"; import { Winston, Ldapjs, Dovehash } from "../../../types/Dependencies"; -export class Authenticator { +export class Authenticator implements IAuthenticator { private options: LdapConfiguration; - private ldapjs: Ldapjs; - private logger: Winston; + private clientFactory: IClientFactory; - constructor(options: LdapConfiguration, ldapjs: Ldapjs, logger: Winston) { + constructor(options: LdapConfiguration, clientFactory: IClientFactory) { this.options = options; - this.ldapjs = ldapjs; - this.logger = logger; + this.clientFactory = clientFactory; } - private createClient(userDN: string, password: string): Client { - return new Client(userDN, password, this.options, this.ldapjs, undefined, this.logger); - } + authenticate(username: string, password: string): BluebirdPromise { + const that = this; + let userClient: IClient; + const adminClient = this.clientFactory.create(this.options.user, this.options.password); + let groupsAndEmails: GroupsAndEmails; - authenticate(username: string, password: string): BluebirdPromise { - const self = this; - const userDN = buildUserDN(username, this.options); - const userClient = this.createClient(userDN, password); - const adminClient = this.createClient(this.options.user, this.options.password); - let attributes: Attributes; - - return userClient.open() + return adminClient.open() + .then(function () { + return adminClient.searchUserDn(username); + }) + .then(function (userDN: string) { + userClient = that.clientFactory.create(userDN, password); + return userClient.open(); + }) .then(function () { return userClient.close(); }) @@ -40,12 +42,12 @@ export class Authenticator { .then(function () { return adminClient.searchEmailsAndGroups(username); }) - .then(function (attr: Attributes) { - attributes = attr; + .then(function (gae: GroupsAndEmails) { + groupsAndEmails = gae; return adminClient.close(); }) .then(function () { - return BluebirdPromise.resolve(attributes); + return BluebirdPromise.resolve(groupsAndEmails); }) .error(function (err: Error) { return BluebirdPromise.reject(new exceptions.LdapError(err.message)); diff --git a/src/server/lib/ldap/Client.ts b/src/server/lib/ldap/Client.ts index d2a6fc6b..4d457ec0 100644 --- a/src/server/lib/ldap/Client.ts +++ b/src/server/lib/ldap/Client.ts @@ -2,33 +2,30 @@ import util = require("util"); import BluebirdPromise = require("bluebird"); import exceptions = require("../Exceptions"); -import ldapjs = require("ldapjs"); -import { buildUserDN } from "./common"; +import Ldapjs = require("ldapjs"); +import Dovehash = require("dovehash"); import { EventEmitter } from "events"; +import { IClient, GroupsAndEmails } from "./IClient"; import { LdapConfiguration } from "../configuration/Configuration"; -import { Winston, Ldapjs, Dovehash } from "../../../types/Dependencies"; +import { Winston } from "../../../types/Dependencies"; interface SearchEntry { object: any; } -export interface Attributes { - groups: string[]; - emails: string[]; -} - -export class Client { +export class Client implements IClient { private userDN: string; private password: string; - private client: ldapjs.ClientAsync; + private client: Ldapjs.ClientAsync; - private ldapjs: Ldapjs; + private ldapjs: typeof Ldapjs; private logger: Winston; - private dovehash: Dovehash; + private dovehash: typeof Dovehash; private options: LdapConfiguration; - constructor(userDN: string, password: string, options: LdapConfiguration, ldapjs: Ldapjs, dovehash: Dovehash, logger: Winston) { + constructor(userDN: string, password: string, options: LdapConfiguration, + ldapjs: typeof Ldapjs, dovehash: typeof Dovehash, logger: Winston) { this.options = options; this.ldapjs = ldapjs; this.dovehash = dovehash; @@ -46,7 +43,7 @@ export class Client { clientLogger.level("trace"); }*/ - this.client = BluebirdPromise.promisifyAll(ldapClient) as ldapjs.ClientAsync; + this.client = BluebirdPromise.promisifyAll(ldapClient) as Ldapjs.ClientAsync; } open(): BluebirdPromise { @@ -65,7 +62,7 @@ export class Client { }); } - private search(base: string, query: ldapjs.SearchOptions): BluebirdPromise { + private search(base: string, query: Ldapjs.SearchOptions): BluebirdPromise { const that = this; that.logger.debug("LDAP: Search for '%s' in '%s'", JSON.stringify(query), base); @@ -95,27 +92,18 @@ export class Client { private searchGroups(username: string): BluebirdPromise { const that = this; - const userDN = buildUserDN(username, this.options); - const password = this.options.password; - - let groupNameAttribute = this.options.group_name_attribute; - if (!groupNameAttribute) groupNameAttribute = "cn"; - - const additionalGroupDN = this.options.additional_group_dn; - const base_dn = this.options.base_dn; - - let groupDN = base_dn; - if (additionalGroupDN) - groupDN = util.format("%s,", additionalGroupDN) + groupDN; - - const query = { - scope: "sub", - attributes: [groupNameAttribute], - filter: "member=" + userDN - }; const groups: string[] = []; - return that.search(groupDN, query) + return that.searchUserDn(username) + .then(function (userDN: string) { + const filter = that.options.groups_filter.replace("{0}", userDN); + const query = { + scope: "sub", + attributes: [that.options.group_name_attribute], + filter: filter + }; + return that.search(that.options.groups_dn, query); + }) .then(function (docs) { for (let i = 0; i < docs.length; ++i) { groups.push(docs[i].cn); @@ -127,32 +115,49 @@ export class Client { }); } + searchUserDn(username: string): BluebirdPromise { + const that = this; + const filter = this.options.users_filter.replace("{0}", username); + const query = { + scope: "sub", + sizeLimit: 1, + attributes: ["dn"], + filter: filter + }; + + that.logger.debug("LDAP: searching for user dn of %s", username); + return that.search(this.options.users_dn, query) + .then(function (users: { dn: string }[]) { + that.logger.debug("LDAP: retrieved user dn is %s", users[0].dn); + return BluebirdPromise.resolve(users[0].dn); + }); + } + searchEmails(username: string): BluebirdPromise { const that = this; - const userDN = buildUserDN(username, this.options); - const query = { scope: "base", sizeLimit: 1, - attributes: ["mail"] + attributes: [this.options.mail_attribute] }; - return this.search(userDN, query) - .then(function (docs) { - const emails = []; - for (let i = 0; i < docs.length; ++i) { - if (typeof docs[i].mail === "string") - emails.push(docs[i].mail); - else { - emails.concat(docs[i].mail); - } + return this.searchUserDn(username) + .then(function (userDN) { + return that.search(userDN, query); + }) + .then(function (docs: { mail: string }[]) { + const emails: string[] = []; + if (typeof docs[0].mail === "string") + emails.push(docs[0].mail); + else { + emails.concat(docs[0].mail); } that.logger.debug("LDAP: emails of user '%s' are %s", username, emails); return BluebirdPromise.resolve(emails); }); } - searchEmailsAndGroups(username: string): BluebirdPromise { + searchEmailsAndGroups(username: string): BluebirdPromise { const that = this; let retrievedEmails: string[], retrievedGroups: string[]; @@ -172,8 +177,6 @@ export class Client { modifyPassword(username: string, newPassword: string): BluebirdPromise { const that = this; - const userDN = buildUserDN(username, this.options); - const encodedPassword = this.dovehash.encode("SSHA", newPassword); const change = { operation: "replace", @@ -183,7 +186,10 @@ export class Client { }; this.logger.debug("LDAP: update password of user '%s'", username); - return this.client.modifyAsync(userDN, change) + return this.searchUserDn(username) + .then(function (userDN: string) { + this.client.modifyAsync(userDN, change); + }) .then(function () { return that.client.unbindAsync(); }); diff --git a/src/server/lib/ldap/ClientFactory.ts b/src/server/lib/ldap/ClientFactory.ts new file mode 100644 index 00000000..850227e2 --- /dev/null +++ b/src/server/lib/ldap/ClientFactory.ts @@ -0,0 +1,27 @@ +import { IClientFactory } from "./IClientFactory"; +import { IClient } from "./IClient"; +import { Client } from "./Client"; +import { LdapConfiguration } from "../configuration/Configuration"; + +import Ldapjs = require("ldapjs"); +import Dovehash = require("dovehash"); +import Winston = require("winston"); + +export class ClientFactory implements IClientFactory { + private config: LdapConfiguration; + private ldapjs: typeof Ldapjs; + private dovehash: typeof Dovehash; + private logger: typeof Winston; + + constructor(ldapConfiguration: LdapConfiguration, ldapjs: typeof Ldapjs, + dovehash: typeof Dovehash, logger: typeof Winston) { + this.config = ldapConfiguration; + this.ldapjs = ldapjs; + this.dovehash = dovehash; + this.logger = logger; + } + + create(userDN: string, password: string): IClient { + return new Client(userDN, password, this.config, this.ldapjs, this.dovehash, this.logger); + } +} \ No newline at end of file diff --git a/src/server/lib/ldap/EmailsRetriever.ts b/src/server/lib/ldap/EmailsRetriever.ts index fe4c3e78..9cc5ff33 100644 --- a/src/server/lib/ldap/EmailsRetriever.ts +++ b/src/server/lib/ldap/EmailsRetriever.ts @@ -2,30 +2,23 @@ import BluebirdPromise = require("bluebird"); import exceptions = require("../Exceptions"); import ldapjs = require("ldapjs"); import { Client } from "./Client"; -import { buildUserDN } from "./common"; +import { IClientFactory } from "./IClientFactory"; +import { IEmailsRetriever } from "./IEmailsRetriever"; import { LdapConfiguration } from "../configuration/Configuration"; -import { Winston, Ldapjs, Dovehash } from "../../../types/Dependencies"; -export class EmailsRetriever { +export class EmailsRetriever implements IEmailsRetriever { private options: LdapConfiguration; - private ldapjs: Ldapjs; - private logger: Winston; + private clientFactory: IClientFactory; - constructor(options: LdapConfiguration, ldapjs: Ldapjs, logger: Winston) { + constructor(options: LdapConfiguration, clientFactory: IClientFactory) { this.options = options; - this.ldapjs = ldapjs; - this.logger = logger; - } - - private createClient(userDN: string, password: string): Client { - return new Client(userDN, password, this.options, this.ldapjs, undefined, this.logger); + this.clientFactory = clientFactory; } retrieve(username: string): BluebirdPromise { - const userDN = buildUserDN(username, this.options); - const adminClient = this.createClient(this.options.user, this.options.password); + const adminClient = this.clientFactory.create(this.options.user, this.options.password); let emails: string[]; return adminClient.open() @@ -36,7 +29,7 @@ export class EmailsRetriever { emails = emails_; return adminClient.close(); }) - .then(function() { + .then(function () { return BluebirdPromise.resolve(emails); }) .error(function (err: Error) { diff --git a/src/server/lib/ldap/IAuthenticator.ts b/src/server/lib/ldap/IAuthenticator.ts new file mode 100644 index 00000000..b1813ac2 --- /dev/null +++ b/src/server/lib/ldap/IAuthenticator.ts @@ -0,0 +1,6 @@ +import BluebirdPromise = require("bluebird"); +import { GroupsAndEmails } from "./IClient"; + +export interface IAuthenticator { + authenticate(username: string, password: string): BluebirdPromise; +} \ No newline at end of file diff --git a/src/server/lib/ldap/IClient.ts b/src/server/lib/ldap/IClient.ts new file mode 100644 index 00000000..741ebaf6 --- /dev/null +++ b/src/server/lib/ldap/IClient.ts @@ -0,0 +1,16 @@ + +import BluebirdPromise = require("bluebird"); + +export interface GroupsAndEmails { + groups: string[]; + emails: string[]; +} + +export interface IClient { + open(): BluebirdPromise; + close(): BluebirdPromise; + searchUserDn(username: string): BluebirdPromise; + searchEmails(username: string): BluebirdPromise; + searchEmailsAndGroups(username: string): BluebirdPromise; + modifyPassword(username: string, newPassword: string): BluebirdPromise; +} \ No newline at end of file diff --git a/src/server/lib/ldap/IClientFactory.ts b/src/server/lib/ldap/IClientFactory.ts new file mode 100644 index 00000000..19a6a656 --- /dev/null +++ b/src/server/lib/ldap/IClientFactory.ts @@ -0,0 +1,6 @@ + +import { IClient } from "./IClient"; + +export interface IClientFactory { + create(userDN: string, password: string): IClient; +} \ No newline at end of file diff --git a/src/server/lib/ldap/IEmailsRetriever.ts b/src/server/lib/ldap/IEmailsRetriever.ts new file mode 100644 index 00000000..608a2883 --- /dev/null +++ b/src/server/lib/ldap/IEmailsRetriever.ts @@ -0,0 +1,5 @@ +import BluebirdPromise = require("bluebird"); + +export interface IEmailsRetriever { + retrieve(username: string): BluebirdPromise; +} \ No newline at end of file diff --git a/src/server/lib/ldap/IPasswordUpdater.ts b/src/server/lib/ldap/IPasswordUpdater.ts new file mode 100644 index 00000000..ff8f3d2c --- /dev/null +++ b/src/server/lib/ldap/IPasswordUpdater.ts @@ -0,0 +1,5 @@ +import BluebirdPromise = require("bluebird"); + +export interface IPasswordUpdater { + updatePassword(username: string, newPassword: string): BluebirdPromise; +} \ No newline at end of file diff --git a/src/server/lib/ldap/PasswordUpdater.ts b/src/server/lib/ldap/PasswordUpdater.ts index c4e834e2..4e00034d 100644 --- a/src/server/lib/ldap/PasswordUpdater.ts +++ b/src/server/lib/ldap/PasswordUpdater.ts @@ -2,32 +2,23 @@ import BluebirdPromise = require("bluebird"); import exceptions = require("../Exceptions"); import ldapjs = require("ldapjs"); import { Client } from "./Client"; -import { buildUserDN } from "./common"; +import { IPasswordUpdater } from "./IPasswordUpdater"; import { LdapConfiguration } from "../configuration/Configuration"; -import { Winston, Ldapjs, Dovehash } from "../../../types/Dependencies"; +import { IClientFactory } from "./IClientFactory"; -export class PasswordUpdater { +export class PasswordUpdater implements IPasswordUpdater { private options: LdapConfiguration; - private ldapjs: Ldapjs; - private logger: Winston; - private dovehash: Dovehash; + private clientFactory: IClientFactory; - constructor(options: LdapConfiguration, ldapjs: Ldapjs, dovehash: Dovehash, logger: Winston) { + constructor(options: LdapConfiguration, clientFactory: IClientFactory) { this.options = options; - this.ldapjs = ldapjs; - this.logger = logger; - this.dovehash = dovehash; - } - - private createClient(userDN: string, password: string): Client { - return new Client(userDN, password, this.options, this.ldapjs, this.dovehash, this.logger); + this.clientFactory = clientFactory; } updatePassword(username: string, newPassword: string): BluebirdPromise { - const userDN = buildUserDN(username, this.options); - const adminClient = this.createClient(this.options.user, this.options.password); + const adminClient = this.clientFactory.create(this.options.user, this.options.password); return adminClient.open() .then(function () { diff --git a/src/server/lib/ldap/common.ts b/src/server/lib/ldap/common.ts deleted file mode 100644 index 6bc61833..00000000 --- a/src/server/lib/ldap/common.ts +++ /dev/null @@ -1,18 +0,0 @@ -import util = require("util"); - -import { LdapConfiguration } from "../configuration/Configuration"; - - -export function buildUserDN(username: string, options: LdapConfiguration): string { - let userNameAttribute = options.user_name_attribute; - // if not provided, default to cn - if (!userNameAttribute) userNameAttribute = "cn"; - - const additionalUserDN = options.additional_user_dn; - const base_dn = options.base_dn; - - let userDN = util.format("%s=%s", userNameAttribute, username); - if (additionalUserDN) userDN += util.format(",%s", additionalUserDN); - userDN += util.format(",%s", base_dn); - return userDN; -} \ No newline at end of file diff --git a/src/server/lib/routes/firstfactor/post.ts b/src/server/lib/routes/firstfactor/post.ts index 770c1c95..b5fa2df8 100644 --- a/src/server/lib/routes/firstfactor/post.ts +++ b/src/server/lib/routes/firstfactor/post.ts @@ -5,7 +5,7 @@ import BluebirdPromise = require("bluebird"); import express = require("express"); import { AccessController } from "../../access_control/AccessController"; import { AuthenticationRegulator } from "../../AuthenticationRegulator"; -import { Client, Attributes } from "../../ldap/Client"; +import { GroupsAndEmails } from "../../ldap/IClient"; import Endpoint = require("../../../endpoints"); import ErrorReplies = require("../../ErrorReplies"); import { ServerVariablesHandler } from "../../ServerVariablesHandler"; @@ -38,13 +38,14 @@ export default function (req: express.Request, res: express.Response): BluebirdP logger.info("1st factor: No regulation applied."); return ldap.authenticate(username, password); }) - .then(function (attributes: Attributes) { - logger.info("1st factor: LDAP binding successful. Retrieved information about user are %s", JSON.stringify(attributes)); + .then(function (groupsAndEmails: GroupsAndEmails) { + logger.info("1st factor: LDAP binding successful. Retrieved information about user are %s", + JSON.stringify(groupsAndEmails)); authSession.userid = username; authSession.first_factor = true; - const emails: string[] = attributes.emails; - const groups: string[] = attributes.groups; + const emails: string[] = groupsAndEmails.emails; + const groups: string[] = groupsAndEmails.groups; if (!emails || emails.length <= 0) { const errMessage = "No emails found. The user should have at least one email address to reset password."; diff --git a/test/features/resilience.feature b/test/features/resilience.feature index a580db4e..800ad015 100644 --- a/test/features/resilience.feature +++ b/test/features/resilience.feature @@ -5,9 +5,17 @@ Feature: Authelia keeps user sessions despite the application restart And the application restarts Then I have access to: | url | - | https://public.test.local:8080/secret.html | | https://secret.test.local:8080/secret.html | - | https://secret1.test.local:8080/secret.html | - | https://secret2.test.local:8080/secret.html | - | https://mx1.mail.test.local:8080/secret.html | - | https://mx2.mail.test.local:8080/secret.html | \ No newline at end of file + + Scenario: Secrets are stored even when Authelia restarts + Given I visit "https://auth.test.local:8080/" + And I login with user "john" and password "password" + And I register a TOTP secret called "Sec0" + When the application restarts + And I visit "https://secret.test.local:8080/secret.html" and get redirected "https://auth.test.local:8080/" + And I login with user "john" and password "password" + And I use "Sec0" as TOTP token handle + And I click on "TOTP" + Then I have access to: + | url | + | https://secret.test.local:8080/secret.html | \ No newline at end of file diff --git a/test/unit/server/DataPersistence.test.ts b/test/unit/server/DataPersistence.test.ts deleted file mode 100644 index 0737fd40..00000000 --- a/test/unit/server/DataPersistence.test.ts +++ /dev/null @@ -1,179 +0,0 @@ - -import * as BluebirdPromise from "bluebird"; -import * as request from "request"; - -import Server from "../../../src/server/lib/Server"; -import { UserConfiguration } from "../../../src/server/lib/configuration/Configuration"; -import { GlobalDependencies } from "../../../src/types/Dependencies"; -import * as tmp from "tmp"; -import U2FMock = require("./mocks/u2f"); -import { LdapjsClientMock } from "./mocks/ldapjs"; - - -const requestp = BluebirdPromise.promisifyAll(request) as request.Request; -const assert = require("assert"); -const speakeasy = require("speakeasy"); -const sinon = require("sinon"); -const nedb = require("nedb"); -const session = require("express-session"); -const winston = require("winston"); - -const PORT = 8050; -const requests = require("./requests")(PORT); - -describe("test data persistence", function () { - let u2f: U2FMock.U2FMock; - let tmpDir: tmp.SynchrounousResult; - const ldapClient = LdapjsClientMock(); - const ldap = { - createClient: sinon.spy(function () { - return ldapClient; - }) - }; - - let config: UserConfiguration; - - before(function () { - u2f = U2FMock.U2FMock(); - - const search_doc = { - object: { - mail: "test_ok@example.com" - } - }; - - const search_res = { - on: sinon.spy(function (event: string, fn: (s: object) => void) { - if (event != "error") fn(search_doc); - }) - }; - - ldapClient.bind.withArgs("cn=admin,dc=example,dc=com", - "password").yields(); - ldapClient.bind.withArgs("cn=test_ok,ou=users,dc=example,dc=com", - "password").yields(); - ldapClient.bind.withArgs("cn=test_nok,ou=users,dc=example,dc=com", - "password").yields("error"); - ldapClient.search.yields(undefined, search_res); - ldapClient.unbind.yields(); - - tmpDir = tmp.dirSync({ unsafeCleanup: true }); - 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: { - path: tmpDir.name - } - }, - notifier: { - gmail: { - username: "user@example.com", - password: "password" - } - } - }; - }); - - after(function () { - tmpDir.removeCallback(); - }); - - it("should save a u2f meta and reload it after a restart of the server", function () { - let server: Server; - const sign_request = {}; - const sign_status = {}; - const registration_status = {}; - u2f.request.returns(sign_request); - u2f.checkRegistration.returns(sign_status); - u2f.checkSignature.returns(registration_status); - - const nodemailer = { - createTransport: sinon.spy(function () { - return transporter; - }) - }; - const transporter = { - sendMail: sinon.stub().yields() - }; - - const deps: GlobalDependencies = { - u2f: u2f, - nedb: nedb, - nodemailer: nodemailer, - session: session, - winston: winston, - ldapjs: ldap, - speakeasy: speakeasy, - ConnectRedis: sinon.spy(), - dovehash: sinon.spy() - }; - - const j1 = request.jar(); - const j2 = request.jar(); - - return start_server(config, deps) - .then(function (s) { - server = s; - return requests.login(j1); - }) - .then(function (res) { - return requests.first_factor(j1); - }) - .then(function () { - return requests.u2f_registration(j1, transporter); - }) - .then(function () { - return requests.u2f_authentication(j1); - }) - .then(function () { - return stop_server(server); - }) - .then(function () { - return start_server(config, deps); - }) - .then(function (s) { - server = s; - return requests.login(j2); - }) - .then(function () { - return requests.first_factor(j2); - }) - .then(function () { - return requests.u2f_authentication(j2); - }) - .then(function (res) { - assert.equal(200, res.statusCode); - server.stop(); - return BluebirdPromise.resolve(); - }) - .catch(function (err) { - console.error(err); - return BluebirdPromise.reject(err); - }); - }); - - function start_server(config: UserConfiguration, deps: GlobalDependencies): BluebirdPromise { - return new BluebirdPromise(function (resolve, reject) { - const s = new Server(); - s.start(config, deps); - resolve(s); - }); - } - - function stop_server(s: Server) { - return new BluebirdPromise(function (resolve, reject) { - s.stop(); - resolve(); - }); - } -}); diff --git a/test/unit/server/SessionConfigurationBuilder.test.ts b/test/unit/server/SessionConfigurationBuilder.test.ts index 9cf071a4..73271695 100644 --- a/test/unit/server/SessionConfigurationBuilder.test.ts +++ b/test/unit/server/SessionConfigurationBuilder.test.ts @@ -17,9 +17,14 @@ describe("test session configuration builder", function () { }, ldap: { url: "ldap://ldap", - base_dn: "dc=example,dc=com", user: "user", - password: "password" + password: "password", + groups_dn: "ou=groups,dc=example,dc=com", + users_dn: "ou=users,dc=example,dc=com", + group_name_attribute: "", + groups_filter: "", + mail_attribute: "", + users_filter: "" }, logs_level: "debug", notifier: { @@ -77,9 +82,14 @@ describe("test session configuration builder", function () { }, ldap: { url: "ldap://ldap", - base_dn: "dc=example,dc=com", user: "user", - password: "password" + password: "password", + groups_dn: "ou=groups,dc=example,dc=com", + users_dn: "ou=users,dc=example,dc=com", + group_name_attribute: "", + groups_filter: "", + mail_attribute: "", + users_filter: "" }, logs_level: "debug", notifier: { diff --git a/test/unit/server/ConfigurationAdapter.test.ts b/test/unit/server/configuration/ConfigurationAdapter.test.ts similarity index 65% rename from test/unit/server/ConfigurationAdapter.test.ts rename to test/unit/server/configuration/ConfigurationAdapter.test.ts index 7e4b6b6c..e699a7b7 100644 --- a/test/unit/server/ConfigurationAdapter.test.ts +++ b/test/unit/server/configuration/ConfigurationAdapter.test.ts @@ -1,14 +1,16 @@ import * as Assert from "assert"; -import { UserConfiguration } from "../../../src/server/lib/configuration/Configuration"; -import { ConfigurationAdapter } from "../../../src/server/lib/configuration/ConfigurationAdapter"; +import { UserConfiguration, LdapConfiguration } from "../../../../src/server/lib/configuration/Configuration"; +import { ConfigurationAdapter } from "../../../../src/server/lib/configuration/ConfigurationAdapter"; -describe("test config adapter", function() { +describe("test config adapter", function () { function build_yaml_config(): UserConfiguration { const yaml_config = { port: 8080, ldap: { url: "http://ldap", - base_dn: "cn=test,dc=example,dc=com", + base_dn: "dc=example,dc=com", + additional_users_dn: "ou=users", + additional_groups_dn: "ou=groups", user: "user", password: "pass" }, @@ -33,41 +35,21 @@ describe("test config adapter", function() { return yaml_config; } - it("should read the port from the yaml file", function() { + it("should read the port from the yaml file", function () { const yaml_config = build_yaml_config(); yaml_config.port = 7070; const config = ConfigurationAdapter.adapt(yaml_config); Assert.equal(config.port, 7070); }); - it("should default the port to 8080 if not provided", function() { + it("should default the port to 8080 if not provided", function () { const yaml_config = build_yaml_config(); delete yaml_config.port; const config = ConfigurationAdapter.adapt(yaml_config); Assert.equal(config.port, 8080); }); - it("should get the ldap attributes", function() { - const yaml_config = build_yaml_config(); - yaml_config.ldap = { - url: "http://ldap", - base_dn: "cn=test,dc=example,dc=com", - additional_user_dn: "ou=users", - user_name_attribute: "uid", - user: "admin", - password: "pass" - }; - - const config = ConfigurationAdapter.adapt(yaml_config); - - Assert.equal(config.ldap.url, "http://ldap"); - Assert.equal(config.ldap.additional_user_dn, "ou=users"); - Assert.equal(config.ldap.user_name_attribute, "uid"); - Assert.equal(config.ldap.user, "admin"); - Assert.equal(config.ldap.password, "pass"); - }); - - it("should get the session attributes", function() { + it("should get the session attributes", function () { const yaml_config = build_yaml_config(); yaml_config.session = { domain: "example.com", @@ -80,14 +62,14 @@ describe("test config adapter", function() { Assert.equal(config.session.expiration, 3600); }); - it("should get the log level", function() { + it("should get the log level", function () { const yaml_config = build_yaml_config(); yaml_config.logs_level = "debug"; const config = ConfigurationAdapter.adapt(yaml_config); Assert.equal(config.logs_level, "debug"); }); - it("should get the notifier config", function() { + it("should get the notifier config", function () { const yaml_config = build_yaml_config(); yaml_config.notifier = { gmail: { @@ -104,7 +86,7 @@ describe("test config adapter", function() { }); }); - it("should get the access_control config", function() { + it("should get the access_control config", function () { const yaml_config = build_yaml_config(); yaml_config.access_control = { default: [], diff --git a/test/unit/server/configuration/LdapConfigurationAdaptation.test.ts b/test/unit/server/configuration/LdapConfigurationAdaptation.test.ts new file mode 100644 index 00000000..6a4a375f --- /dev/null +++ b/test/unit/server/configuration/LdapConfigurationAdaptation.test.ts @@ -0,0 +1,93 @@ +import * as Assert from "assert"; +import { UserConfiguration, LdapConfiguration } from "../../../../src/server/lib/configuration/Configuration"; +import { ConfigurationAdapter } from "../../../../src/server/lib/configuration/ConfigurationAdapter"; + +describe("test ldap configuration adaptation", function () { + function build_yaml_config(): UserConfiguration { + const yaml_config = { + port: 8080, + ldap: { + url: "http://ldap", + base_dn: "dc=example,dc=com", + additional_users_dn: "ou=users", + additional_groups_dn: "ou=groups", + user: "user", + password: "pass" + }, + session: { + domain: "example.com", + secret: "secret", + max_age: 40000 + }, + storage: { + local: { + path: "/mydirectory" + } + }, + logs_level: "debug", + notifier: { + gmail: { + username: "user", + password: "password" + } + } + }; + return yaml_config; + } + + it("should adapt correctly while user only specify mandatory fields", function () { + const yaml_config = build_yaml_config(); + yaml_config.ldap = { + url: "http://ldap", + base_dn: "dc=example,dc=com", + user: "admin", + password: "password" + }; + + const config = ConfigurationAdapter.adapt(yaml_config); + const expectedConfig: LdapConfiguration = { + url: "http://ldap", + users_dn: "dc=example,dc=com", + users_filter: "cn={0}", + groups_dn: "dc=example,dc=com", + groups_filter: "member={0}", + group_name_attribute: "cn", + mail_attribute: "mail", + user: "admin", + password: "password" + }; + + Assert.deepEqual(config.ldap, expectedConfig); + }); + + it("should adapt correctly while user specify every fields", function () { + const yaml_config = build_yaml_config(); + yaml_config.ldap = { + url: "http://ldap-server", + base_dn: "dc=example,dc=com", + additional_users_dn: "ou=users", + users_filter: "uid={0}", + additional_groups_dn: "ou=groups", + groups_filter: "uniqueMember={0}", + mail_attribute: "email", + group_name_attribute: "groupName", + user: "admin2", + password: "password2" + }; + + const config = ConfigurationAdapter.adapt(yaml_config); + const expectedConfig: LdapConfiguration = { + url: "http://ldap-server", + users_dn: "ou=users,dc=example,dc=com", + users_filter: "uid={0}", + groups_dn: "ou=groups,dc=example,dc=com", + groups_filter: "uniqueMember={0}", + mail_attribute: "email", + group_name_attribute: "groupName", + user: "admin2", + password: "password2" + }; + + Assert.deepEqual(config.ldap, expectedConfig); + }); +}); diff --git a/test/unit/server/ldap/Authenticator.test.ts b/test/unit/server/ldap/Authenticator.test.ts index 9753163c..08373d4c 100644 --- a/test/unit/server/ldap/Authenticator.test.ts +++ b/test/unit/server/ldap/Authenticator.test.ts @@ -2,121 +2,127 @@ import { Authenticator } from "../../../../src/server/lib/ldap/Authenticator"; import { LdapConfiguration } from "../../../../src/server/lib/configuration/Configuration"; -import sinon = require("sinon"); +import Sinon = require("sinon"); import BluebirdPromise = require("bluebird"); -import assert = require("assert"); -import ldapjs = require("ldapjs"); -import winston = require("winston"); -import { EventEmitter } from "events"; +import Assert = require("assert"); -import { LdapjsMock, LdapjsClientMock } from "../mocks/ldapjs"; +import { ClientFactoryStub } from "../mocks/ldap/ClientFactoryStub"; +import { ClientStub } from "../mocks/ldap/ClientStub"; describe("test ldap authentication", function () { + const USERNAME = "username"; + const PASSWORD = "password"; + + const ADMIN_USER_DN = "cn=admin,dc=example,dc=com"; + const ADMIN_PASSWORD = "admin_password"; + + let clientFactoryStub: ClientFactoryStub; + let adminClientStub: ClientStub; + let userClientStub: ClientStub; + let authenticator: Authenticator; - let ldapClient: LdapjsClientMock; - let ldapjs: LdapjsMock; let ldapConfig: LdapConfiguration; - let adminUserDN: string; - let adminPassword: string; - - function retrieveEmailsAndGroups(ldapClient: LdapjsClientMock) { - const email0 = { - object: { - mail: "user@example.com" - } - }; - - const email1 = { - object: { - mail: "user@example1.com" - } - }; - - const group0 = { - object: { - group: "group0" - } - }; - - const emailsEmitter = { - on: sinon.spy(function (event: string, fn: (doc: any) => void) { - if (event != "error") fn(email0); - if (event != "error") fn(email1); - }) - }; - - const groupsEmitter = { - on: sinon.spy(function (event: string, fn: (doc: any) => void) { - if (event != "error") fn(group0); - }) - }; - - ldapClient.search.onCall(0).yields(undefined, emailsEmitter); - ldapClient.search.onCall(1).yields(undefined, groupsEmitter); - } beforeEach(function () { - ldapClient = LdapjsClientMock(); - ldapjs = LdapjsMock(); - ldapjs.createClient.returns(ldapClient); - - // winston.level = "debug"; - - adminUserDN = "cn=admin,dc=example,dc=com"; - adminPassword = "password"; + clientFactoryStub = new ClientFactoryStub(); + adminClientStub = new ClientStub(); + userClientStub = new ClientStub(); ldapConfig = { url: "http://localhost:324", - user: adminUserDN, - password: adminPassword, - base_dn: "dc=example,dc=com", - additional_user_dn: "ou=users" + users_dn: "ou=users,dc=example,dc=com", + users_filter: "cn={0}", + groups_dn: "ou=groups,dc=example,dc=com", + groups_filter: "member={0}", + mail_attribute: "mail", + group_name_attribute: "cn", + user: ADMIN_USER_DN, + password: ADMIN_PASSWORD }; - authenticator = new Authenticator(ldapConfig, ldapjs, winston); + authenticator = new Authenticator(ldapConfig, clientFactoryStub); }); - function test_check_password_internal() { - const username = "username"; - const password = "password"; - return authenticator.authenticate(username, password); - } - describe("success", function () { - beforeEach(function () { - retrieveEmailsAndGroups(ldapClient); - ldapClient.bind.withArgs(adminUserDN, adminPassword).yields(); - ldapClient.unbind.yields(); - }); - it("should bind the user if good credentials provided", function () { - ldapClient.bind.withArgs("cn=username,ou=users,dc=example,dc=com", "password").yields(); - return test_check_password_internal(); - }); + clientFactoryStub.createStub.withArgs(ADMIN_USER_DN, ADMIN_PASSWORD) + .returns(adminClientStub); + clientFactoryStub.createStub.withArgs("cn=" + USERNAME + ",ou=users,dc=example,dc=com", PASSWORD) + .returns(userClientStub); - it("should bind the user with correct DN", function () { - ldapConfig.user_name_attribute = "uid"; - ldapClient.bind.withArgs("uid=username,ou=users,dc=example,dc=com", "password").yields(); - return test_check_password_internal(); + // admin connects successfully + adminClientStub.openStub.returns(BluebirdPromise.resolve()); + adminClientStub.closeStub.returns(BluebirdPromise.resolve()); + + // admin search for user dn of user + adminClientStub.searchUserDnStub.withArgs(USERNAME) + .returns(BluebirdPromise.resolve("cn=" + USERNAME + ",ou=users,dc=example,dc=com")); + + // user connects successfully + userClientStub.openStub.returns(BluebirdPromise.resolve()); + userClientStub.closeStub.returns(BluebirdPromise.resolve()); + + // admin retrieves emails and groups of user + adminClientStub.searchEmailsAndGroupsStub.returns(BluebirdPromise.resolve({ + groups: ["group1"], + emails: ["user@example.com"] + })); + + return authenticator.authenticate(USERNAME, PASSWORD); }); }); describe("failure", function () { it("should not bind the user if wrong credentials provided", function () { - ldapClient.bind.yields("wrong credentials"); - return test_check_password_internal() + clientFactoryStub.createStub.withArgs(ADMIN_USER_DN, ADMIN_PASSWORD) + .returns(adminClientStub); + clientFactoryStub.createStub.withArgs("cn=" + USERNAME + ",ou=users,dc=example,dc=com", PASSWORD) + .returns(userClientStub); + + // admin connects successfully + adminClientStub.openStub.returns(BluebirdPromise.resolve()); + adminClientStub.closeStub.returns(BluebirdPromise.resolve()); + + // admin search for user dn of user + adminClientStub.searchUserDnStub.withArgs(USERNAME) + .returns(BluebirdPromise.resolve("cn=" + USERNAME + ",ou=users,dc=example,dc=com")); + + // user connects successfully + userClientStub.openStub.returns(BluebirdPromise.reject(new Error("Error while binding"))); + userClientStub.closeStub.returns(BluebirdPromise.resolve()); + + return authenticator.authenticate(USERNAME, PASSWORD) + .then(function () { return BluebirdPromise.reject("Should not be here!"); }) .catch(function () { return BluebirdPromise.resolve(); }); }); it("should not bind the user if search of emails or group fails", function () { - ldapClient.bind.withArgs("cn=username,ou=users,dc=example,dc=com", "password").yields(); - ldapClient.bind.withArgs(adminUserDN, adminPassword).yields(); - ldapClient.unbind.yields(); - ldapClient.search.yields("wrong credentials"); - return test_check_password_internal() + clientFactoryStub.createStub.withArgs(ADMIN_USER_DN, ADMIN_PASSWORD) + .returns(adminClientStub); + clientFactoryStub.createStub.withArgs("cn=" + USERNAME + ",ou=users,dc=example,dc=com", PASSWORD) + .returns(userClientStub); + + // admin connects successfully + adminClientStub.openStub.returns(BluebirdPromise.resolve()); + adminClientStub.closeStub.returns(BluebirdPromise.resolve()); + + // admin search for user dn of user + adminClientStub.searchUserDnStub.withArgs(USERNAME) + .returns(BluebirdPromise.resolve("cn=" + USERNAME + ",ou=users,dc=example,dc=com")); + + // user connects successfully + userClientStub.openStub.returns(BluebirdPromise.resolve()); + userClientStub.closeStub.returns(BluebirdPromise.resolve()); + + // admin retrieves emails and groups of user + adminClientStub.searchEmailsAndGroupsStub + .returns(BluebirdPromise.reject(new Error("Error while retrieving emails and groups"))); + + return authenticator.authenticate(USERNAME, PASSWORD) + .then(function () { return BluebirdPromise.reject("Should not be here!"); }) .catch(function () { return BluebirdPromise.resolve(); }); diff --git a/test/unit/server/ldap/EmailsRetriever.test.ts b/test/unit/server/ldap/EmailsRetriever.test.ts index 1ad68768..2cfb7ad1 100644 --- a/test/unit/server/ldap/EmailsRetriever.test.ts +++ b/test/unit/server/ldap/EmailsRetriever.test.ts @@ -2,93 +2,74 @@ import { EmailsRetriever } from "../../../../src/server/lib/ldap/EmailsRetriever"; import { LdapConfiguration } from "../../../../src/server/lib/configuration/Configuration"; -import sinon = require("sinon"); +import Sinon = require("sinon"); import BluebirdPromise = require("bluebird"); -import assert = require("assert"); -import ldapjs = require("ldapjs"); -import winston = require("winston"); -import { EventEmitter } from "events"; - -import { LdapjsMock, LdapjsClientMock } from "../mocks/ldapjs"; +import Assert = require("assert"); +import { ClientFactoryStub } from "../mocks/ldap/ClientFactoryStub"; +import { ClientStub } from "../mocks/ldap/ClientStub"; describe("test emails retriever", function () { + const USERNAME = "username"; + const ADMIN_USER_DN = "cn=admin,dc=example,dc=com"; + const ADMIN_PASSWORD = "password"; + + let clientFactoryStub: ClientFactoryStub; + let adminClientStub: ClientStub; + let emailsRetriever: EmailsRetriever; - let ldapClient: LdapjsClientMock; - let ldapjs: LdapjsMock; let ldapConfig: LdapConfiguration; - let adminUserDN: string; - let adminPassword: string; beforeEach(function () { - ldapClient = LdapjsClientMock(); - ldapjs = LdapjsMock(); - ldapjs.createClient.returns(ldapClient); - - // winston.level = "debug"; - - adminUserDN = "cn=admin,dc=example,dc=com"; - adminPassword = "password"; + clientFactoryStub = new ClientFactoryStub(); + adminClientStub = new ClientStub(); ldapConfig = { - url: "http://localhost:324", - user: adminUserDN, - password: adminPassword, - base_dn: "dc=example,dc=com", - additional_user_dn: "ou=users" + url: "http://ldap", + user: ADMIN_USER_DN, + password: ADMIN_PASSWORD, + users_dn: "ou=users,dc=example,dc=com", + groups_dn: "ou=groups,dc=example,dc=com", + group_name_attribute: "cn", + groups_filter: "cn={0}", + mail_attribute: "mail", + users_filter: "cn={0}" }; - emailsRetriever = new EmailsRetriever(ldapConfig, ldapjs, winston); + emailsRetriever = new EmailsRetriever(ldapConfig, clientFactoryStub); }); - function retrieveEmails(ldapClient: LdapjsClientMock) { - const email0 = { - object: { - mail: "user@example.com" - } - }; - - const email1 = { - object: { - mail: "user@example1.com" - } - }; - - const emailsEmitter = { - on: sinon.spy(function (event: string, fn: (doc: any) => void) { - if (event != "error") fn(email0); - if (event != "error") fn(email1); - }) - }; - - ldapClient.search.onCall(0).yields(undefined, emailsEmitter); - } - - function test_emails_retrieval() { - const username = "username"; - return emailsRetriever.retrieve(username); - } - describe("success", function () { - beforeEach(function () { - ldapClient.bind.withArgs(adminUserDN, adminPassword).yields(); - ldapClient.unbind.yields(); - }); + it("should retrieve emails successfully", function () { + clientFactoryStub.createStub.withArgs(ADMIN_USER_DN, ADMIN_PASSWORD) + .returns(adminClientStub); - it("should update the password successfully", function () { - retrieveEmails(ldapClient); - return test_emails_retrieval(); + // admin connects successfully + adminClientStub.openStub.returns(BluebirdPromise.resolve()); + adminClientStub.closeStub.returns(BluebirdPromise.resolve()); + + adminClientStub.searchEmailsStub.withArgs(USERNAME) + .returns(BluebirdPromise.resolve(["user@example.com"])); + + return emailsRetriever.retrieve(USERNAME); }); }); describe("failure", function () { it("should fail retrieving emails when search operation fails", function () { - ldapClient.bind.withArgs(adminUserDN, adminPassword).yields(); - ldapClient.search.yields("wrong credentials"); - return test_emails_retrieval() - .catch(function () { - return BluebirdPromise.resolve(); - }); + clientFactoryStub.createStub.withArgs(ADMIN_USER_DN, ADMIN_PASSWORD) + .returns(adminClientStub); + + // admin connects successfully + adminClientStub.openStub.returns(BluebirdPromise.resolve()); + adminClientStub.closeStub.returns(BluebirdPromise.resolve()); + + adminClientStub.searchEmailsStub.withArgs(USERNAME) + .returns(BluebirdPromise.reject(new Error("Error while searching emails"))); + + return emailsRetriever.retrieve(USERNAME) + .then(function () { return BluebirdPromise.reject(new Error("Should not be here")); }) + .catch(function () { return BluebirdPromise.resolve(); }); }); }); }); \ No newline at end of file diff --git a/test/unit/server/ldap/PasswordUpdater.test.ts b/test/unit/server/ldap/PasswordUpdater.test.ts index 9cf318aa..514bf601 100644 --- a/test/unit/server/ldap/PasswordUpdater.test.ts +++ b/test/unit/server/ldap/PasswordUpdater.test.ts @@ -2,82 +2,78 @@ import { PasswordUpdater } from "../../../../src/server/lib/ldap/PasswordUpdater"; import { LdapConfiguration } from "../../../../src/server/lib/configuration/Configuration"; -import sinon = require("sinon"); +import Sinon = require("sinon"); import BluebirdPromise = require("bluebird"); -import assert = require("assert"); -import ldapjs = require("ldapjs"); -import winston = require("winston"); -import { EventEmitter } from "events"; - -import { LdapjsMock, LdapjsClientMock } from "../mocks/ldapjs"; +import Assert = require("assert"); +import { ClientFactoryStub } from "../mocks/ldap/ClientFactoryStub"; +import { ClientStub } from "../mocks/ldap/ClientStub"; describe("test password update", function () { + const USERNAME = "username"; + const NEW_PASSWORD = "new-password"; + + const ADMIN_USER_DN = "cn=admin,dc=example,dc=com"; + const ADMIN_PASSWORD = "password"; + + let clientFactoryStub: ClientFactoryStub; + let adminClientStub: ClientStub; + let passwordUpdater: PasswordUpdater; - let ldapClient: LdapjsClientMock; - let ldapjs: LdapjsMock; let ldapConfig: LdapConfiguration; - let adminUserDN: string; - let adminPassword: string; let dovehash: any; beforeEach(function () { - ldapClient = LdapjsClientMock(); - ldapjs = LdapjsMock(); - ldapjs.createClient.returns(ldapClient); - - // winston.level = "debug"; - - adminUserDN = "cn=admin,dc=example,dc=com"; - adminPassword = "password"; + clientFactoryStub = new ClientFactoryStub(); + adminClientStub = new ClientStub(); ldapConfig = { - url: "http://localhost:324", - user: adminUserDN, - password: adminPassword, - base_dn: "dc=example,dc=com", - additional_user_dn: "ou=users" + url: "http://ldap", + user: ADMIN_USER_DN, + password: ADMIN_PASSWORD, + users_dn: "ou=users,dc=example,dc=com", + groups_dn: "ou=groups,dc=example,dc=com", + group_name_attribute: "cn", + groups_filter: "cn={0}", + mail_attribute: "mail", + users_filter: "cn={0}" }; dovehash = { - encode: sinon.stub() + encode: Sinon.stub() }; - passwordUpdater = new PasswordUpdater(ldapConfig, ldapjs, dovehash, winston); + passwordUpdater = new PasswordUpdater(ldapConfig, clientFactoryStub); }); - function test_update_password() { - const username = "username"; - const newpassword = "newpassword"; - return passwordUpdater.updatePassword(username, newpassword); - } - describe("success", function () { - beforeEach(function () { - ldapClient.bind.withArgs(adminUserDN, adminPassword).yields(); - ldapClient.unbind.yields(); - }); - it("should update the password successfully", function () { + clientFactoryStub.createStub.withArgs(ADMIN_USER_DN, ADMIN_PASSWORD) + .returns(adminClientStub); + dovehash.encode.returns("{SSHA}AQmxaKfobGY9HSQa6aDYkAWOgPGNhGYn"); - ldapClient.modify.withArgs("cn=username,ou=users,dc=example,dc=com", { - operation: "replace", - modification: { - userPassword: "{SSHA}AQmxaKfobGY9HSQa6aDYkAWOgPGNhGYn" - } - }).yields(); - return test_update_password(); + adminClientStub.modifyPasswordStub.withArgs(USERNAME, NEW_PASSWORD).returns(BluebirdPromise.resolve()); + adminClientStub.openStub.returns(BluebirdPromise.resolve()); + adminClientStub.closeStub.returns(BluebirdPromise.resolve()); + + return passwordUpdater.updatePassword(USERNAME, NEW_PASSWORD); }); }); describe("failure", function () { it("should fail updating password when modify operation fails", function () { - ldapClient.bind.withArgs(adminUserDN, adminPassword).yields(); - ldapClient.modify.yields("wrong credentials"); - return test_update_password() - .catch(function () { - return BluebirdPromise.resolve(); - }); + clientFactoryStub.createStub.withArgs(ADMIN_USER_DN, ADMIN_PASSWORD) + .returns(adminClientStub); + + dovehash.encode.returns("{SSHA}AQmxaKfobGY9HSQa6aDYkAWOgPGNhGYn"); + adminClientStub.modifyPasswordStub.withArgs(USERNAME, NEW_PASSWORD) + .returns(BluebirdPromise.reject(new Error("Error while updating password"))); + adminClientStub.openStub.returns(BluebirdPromise.resolve()); + adminClientStub.closeStub.returns(BluebirdPromise.resolve()); + + return passwordUpdater.updatePassword(USERNAME, NEW_PASSWORD) + .then(function () { return BluebirdPromise.reject(new Error("should not be here")); }) + .catch(function () { return BluebirdPromise.resolve(); }); }); }); }); \ No newline at end of file diff --git a/test/unit/server/mocks/ServerVariablesMock.ts b/test/unit/server/mocks/ServerVariablesMock.ts index 6281b333..a59ab158 100644 --- a/test/unit/server/mocks/ServerVariablesMock.ts +++ b/test/unit/server/mocks/ServerVariablesMock.ts @@ -2,7 +2,7 @@ import sinon = require("sinon"); import express = require("express"); import winston = require("winston"); import { UserDataStoreStub } from "./storage/UserDataStoreStub"; -import { ServerVariables, VARIABLES_KEY }  from "../../../../src/server/lib/ServerVariablesHandler"; +import { VARIABLES_KEY }  from "../../../../src/server/lib/ServerVariablesHandler"; export interface ServerVariablesMock { logger: any; diff --git a/test/unit/server/mocks/ldap/ClientFactoryStub.ts b/test/unit/server/mocks/ldap/ClientFactoryStub.ts new file mode 100644 index 00000000..26d3909e --- /dev/null +++ b/test/unit/server/mocks/ldap/ClientFactoryStub.ts @@ -0,0 +1,16 @@ + +import { IClient } from "../../../../../src/server/lib/ldap/IClient"; +import { IClientFactory } from "../../../../../src/server/lib/ldap/IClientFactory"; +import Sinon = require("sinon"); + +export class ClientFactoryStub implements IClientFactory { + createStub: Sinon.SinonStub; + + constructor() { + this.createStub = Sinon.stub(); + } + + create(userDN: string, password: string): IClient { + return this.createStub(userDN, password); + } +} \ No newline at end of file diff --git a/test/unit/server/mocks/ldap/ClientStub.ts b/test/unit/server/mocks/ldap/ClientStub.ts new file mode 100644 index 00000000..faf3b74a --- /dev/null +++ b/test/unit/server/mocks/ldap/ClientStub.ts @@ -0,0 +1,46 @@ + +import BluebirdPromise = require("bluebird"); +import { IClient, GroupsAndEmails } from "../../../../../src/server/lib/ldap/IClient"; +import Sinon = require("sinon"); + +export class ClientStub implements IClient { + openStub: Sinon.SinonStub; + closeStub: Sinon.SinonStub; + searchUserDnStub: Sinon.SinonStub; + searchEmailsStub: Sinon.SinonStub; + searchEmailsAndGroupsStub: Sinon.SinonStub; + modifyPasswordStub: Sinon.SinonStub; + + constructor() { + this.openStub = Sinon.stub(); + this.closeStub = Sinon.stub(); + this.searchUserDnStub = Sinon.stub(); + this.searchEmailsStub = Sinon.stub(); + this.searchEmailsAndGroupsStub = Sinon.stub(); + this.modifyPasswordStub = Sinon.stub(); + } + + open(): BluebirdPromise { + return this.openStub(); + } + + close(): BluebirdPromise { + return this.closeStub(); + } + + searchUserDn(username: string): BluebirdPromise { + return this.searchUserDnStub(username); + } + + searchEmails(username: string): BluebirdPromise { + return this.searchEmailsStub(username); + } + + searchEmailsAndGroups(username: string): BluebirdPromise { + return this.searchEmailsAndGroupsStub(username); + } + + modifyPassword(username: string, newPassword: string): BluebirdPromise { + return this.modifyPasswordStub(username, newPassword); + } +} \ No newline at end of file diff --git a/test/unit/server/server/Server.test.ts b/test/unit/server/server/Server.test.ts index fcaf757a..f8d2c315 100644 --- a/test/unit/server/server/Server.test.ts +++ b/test/unit/server/server/Server.test.ts @@ -1,5 +1,3 @@ - - import Server from "../../../../src/server/lib/Server"; import { LdapjsClientMock } from "./../mocks/ldapjs"; @@ -36,7 +34,6 @@ describe("test the server", function () { 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", }, @@ -125,81 +122,10 @@ describe("test the server", function () { describe("test authentication and verification", function () { test_authentication(); - test_reset_password(); test_regulation(); }); function test_authentication() { - it("should return status code 401 when user is not authenticated", function () { - return requestp.getAsync({ url: BASE_URL + Endpoints.VERIFY_GET }) - .then(function (response: request.RequestResponse) { - Assert.equal(response.statusCode, 401); - return BluebirdPromise.resolve(); - }); - }); - - it("should return status code 204 when user is authenticated using totp", function () { - 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) { - Assert.equal(res.statusCode, 302, "first factor failed"); - return requests.register_totp(j, transporter); - }) - .then(function (base32_secret: string) { - const realToken = speakeasy.totp({ - secret: base32_secret, - encoding: "base32" - }); - return requests.totp(j, realToken); - }) - .then(function (res: request.RequestResponse) { - Assert.equal(res.statusCode, 200, "second factor failed"); - return requests.verify(j); - }) - .then(function (res: request.RequestResponse) { - Assert.equal(res.statusCode, 204, "verify failed"); - return BluebirdPromise.resolve(); - }) - .catch(function (err: Error) { return BluebirdPromise.reject(err); }); - }); - - it("should keep session variables when login page is reloaded", function () { - 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) { - Assert.equal(res.statusCode, 302, "first factor failed"); - return requests.register_totp(j, transporter); - }) - .then(function (base32_secret: string) { - const realToken = speakeasy.totp({ - secret: base32_secret, - encoding: "base32" - }); - return requests.totp(j, realToken); - }) - .then(function (res: request.RequestResponse) { - Assert.equal(res.statusCode, 200, "second factor failed"); - return requests.login(j); - }) - .then(function (res: request.RequestResponse) { - Assert.equal(res.statusCode, 200, "login page loading failed"); - return requests.verify(j); - }) - .then(function (res: request.RequestResponse) { - Assert.equal(res.statusCode, 204, "verify failed"); - return BluebirdPromise.resolve(); - }) - .catch(function (err: Error) { return BluebirdPromise.reject(err); }); - }); - it("should return status code 204 when user is authenticated using u2f", function () { const sign_request = {}; const sign_status = {}; @@ -236,26 +162,6 @@ describe("test the server", function () { }); } - function test_reset_password() { - it("should reset the password", function () { - 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) { - Assert.equal(res.headers.location, Endpoints.SECOND_FACTOR_GET); - Assert.equal(res.statusCode, 302, "first factor failed"); - return requests.reset_password(j, transporter, "user", "new-password"); - }) - .then(function (res: request.RequestResponse) { - Assert.equal(res.statusCode, 204, "second factor, finish register failed"); - return BluebirdPromise.resolve(); - }); - }); - } - function test_regulation() { it("should regulate authentication", function () { const j = requestp.jar(); From 64c06fd6b81331142a184faab8338fe22043f993 Mon Sep 17 00:00:00 2001 From: Clement Michaud Date: Sun, 3 Sep 2017 01:25:43 +0200 Subject: [PATCH 2/4] Parameterize authentication regulation via configuration file. Both for flexibility and for testing purposes. --- .gitignore | 1 - README.md | 6 +- config.template.yml | 57 ++++- config.test.yml | 147 ++++++++++++ docker-compose.test.yml | 5 + docker-compose.yml | 2 +- example/ldap/base.ldif | 8 + src/client/lib/Notifier.ts | 60 ++++- src/client/lib/firstfactor/index.ts | 6 +- src/server/index.ts | 2 +- src/server/lib/AuthenticationRegulator.ts | 48 ++-- src/server/lib/ServerVariablesHandler.ts | 5 +- .../lib/configuration/Configuration.d.ts | 8 + .../lib/configuration/ConfigurationAdapter.ts | 4 +- src/server/lib/storage/IUserDataStore.d.ts | 2 +- src/server/lib/storage/UserDataStore.ts | 5 +- test/features/authentication.feature | 2 +- test/features/regulation.feature | 52 +++++ test/features/step_definitions/after.ts | 7 - .../step_definitions/authentication.ts | 39 ++-- test/features/step_definitions/hooks.ts | 20 ++ .../step_definitions/notifications.ts | 4 +- test/features/step_definitions/regulation.ts | 11 + .../step_definitions/reset-password.ts | 2 +- test/features/support/world.ts | 5 +- test/unit/client/Notifier.test.ts | 2 +- .../server/AuthenticationRegulator.test.ts | 216 ++++++++++++------ test/unit/server/ServerConfiguration.test.ts | 5 + .../SessionConfigurationBuilder.test.ts | 10 + .../ConfigurationAdapter.test.ts | 9 +- .../LdapConfigurationAdaptation.test.ts | 9 +- .../server/mocks/storage/UserDataStoreStub.ts | 4 +- test/unit/server/server/PrivatePages.ts | 9 +- test/unit/server/server/PublicPages.ts | 9 +- test/unit/server/server/Server.test.ts | 198 ---------------- .../unit/server/storage/UserDataStore.test.ts | 11 +- 36 files changed, 615 insertions(+), 375 deletions(-) create mode 100644 config.test.yml create mode 100644 docker-compose.test.yml create mode 100644 test/features/regulation.feature delete mode 100644 test/features/step_definitions/after.ts create mode 100644 test/features/step_definitions/hooks.ts create mode 100644 test/features/step_definitions/regulation.ts delete mode 100644 test/unit/server/server/Server.test.ts diff --git a/.gitignore b/.gitignore index e391408e..8e737612 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 aa8d2d07..bd61219d 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 1f8bf016..2bcc1c92 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 00000000..557cf5d4 --- /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 00000000..53495d0f --- /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 ace1d5db..82e4e0a2 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 06e962c0..4c6c33c7 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 cc59ee88..e8052918 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('status %s\ - %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 23b4a40f..9725c2bb 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 621d28c1..732a180d 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 b3319b4e..a0454733 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 49fde6fa..e16172c7 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 8a98280e..5c3c8e32 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 cc5de6ef..8307ef07 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(userConfiguration, "notifier"), - access_control: ObjectPath.get(userConfiguration, "access_control") + access_control: ObjectPath.get(userConfiguration, "access_control"), + regulation: userConfiguration.regulation }; } diff --git a/src/server/lib/storage/IUserDataStore.d.ts b/src/server/lib/storage/IUserDataStore.d.ts index a03abb37..81df482a 100644 --- a/src/server/lib/storage/IUserDataStore.d.ts +++ b/src/server/lib/storage/IUserDataStore.d.ts @@ -11,7 +11,7 @@ export interface IUserDataStore { retrieveU2FRegistration(userId: string, appId: string): BluebirdPromise; saveAuthenticationTrace(userId: string, isAuthenticationSuccessful: boolean): BluebirdPromise; - retrieveLatestAuthenticationTraces(userId: string, isAuthenticationSuccessful: boolean, count: number): BluebirdPromise; + retrieveLatestAuthenticationTraces(userId: string, count: number): BluebirdPromise; produceIdentityValidationToken(userId: string, token: string, challenge: string, maxAge: number): BluebirdPromise; consumeIdentityValidationToken(token: string, challenge: string): BluebirdPromise; diff --git a/src/server/lib/storage/UserDataStore.ts b/src/server/lib/storage/UserDataStore.ts index 9297b559..27b0cddb 100644 --- a/src/server/lib/storage/UserDataStore.ts +++ b/src/server/lib/storage/UserDataStore.ts @@ -76,10 +76,9 @@ export class UserDataStore implements IUserDataStore { return this.authenticationTracesCollection.insert(newDocument); } - retrieveLatestAuthenticationTraces(userId: string, isAuthenticationSuccessful: boolean, count: number): BluebirdPromise { + retrieveLatestAuthenticationTraces(userId: string, count: number): BluebirdPromise { const q = { - userId: userId, - isAuthenticationSuccessful: isAuthenticationSuccessful + userId: userId }; return this.authenticationTracesCollection.find(q, { date: -1 }, count); diff --git a/test/features/authentication.feature b/test/features/authentication.feature index 028bffeb..cf5a531e 100644 --- a/test/features/authentication.feature +++ b/test/features/authentication.feature @@ -26,7 +26,7 @@ Feature: User validate first factor Scenario: User fails TOTP second factor When I visit "https://secret.test.local:8080/secret.html" and get redirected "https://auth.test.local:8080/" - And I login with user "john" and password "password" + And I login with user "john" and password "password" And I use "BADTOKEN" as TOTP token And I click on "TOTP" Then I get a notification of type "error" with message "Problem with TOTP validation." diff --git a/test/features/regulation.feature b/test/features/regulation.feature new file mode 100644 index 00000000..835d980d --- /dev/null +++ b/test/features/regulation.feature @@ -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 | \ No newline at end of file diff --git a/test/features/step_definitions/after.ts b/test/features/step_definitions/after.ts deleted file mode 100644 index c5132ddb..00000000 --- a/test/features/step_definitions/after.ts +++ /dev/null @@ -1,7 +0,0 @@ -import Cucumber = require("cucumber"); - -Cucumber.defineSupportCode(function({After}) { - After(function() { - return this.driver.quit(); - }); -}); \ No newline at end of file diff --git a/test/features/step_definitions/authentication.ts b/test/features/step_definitions/authentication.ts index 35983f7e..ba07f82f 100644 --- a/test/features/step_definitions/authentication.ts +++ b/test/features/step_definitions/authentication.ts @@ -22,9 +22,20 @@ Cucumber.defineSupportCode(function ({ Given, When, Then }) { return this.clickOnButton(text); }); - 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}", + 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,17 +49,19 @@ Cucumber.defineSupportCode(function ({ Given, When, Then }) { return this.useTotpTokenHandle(handle); }); - When("I visit {stringInDoubleQuotes} and get redirected {stringInDoubleQuotes}", function (url: string, redirectUrl: string) { - const that = this; - return this.driver.get(url) - .then(function () { - return that.driver.wait(seleniumWebdriver.until.urlIs(redirectUrl), 2000); - }); - }); + When("I visit {stringInDoubleQuotes} and get redirected {stringInDoubleQuotes}", + function (url: string, redirectUrl: string) { + const that = this; + return this.driver.get(url) + .then(function () { + return that.driver.wait(seleniumWebdriver.until.urlIs(redirectUrl), 2000); + }); + }); - Given("I register TOTP and login with user {stringInDoubleQuotes} and password {stringInDoubleQuotes}", function (username: string, password: string) { - return this.registerTotpAndSignin(username, password); - }); + Given("I register TOTP and login with user {stringInDoubleQuotes} and password {stringInDoubleQuotes}", + function (username: string, password: string) { + return this.registerTotpAndSignin(username, password); + }); function hasAccessToSecret(link: string, that: any) { return that.driver.get(link) diff --git a/test/features/step_definitions/hooks.ts b/test/features/step_definitions/hooks.ts new file mode 100644 index 00000000..708bd3ae --- /dev/null +++ b/test/features/step_definitions/hooks.ts @@ -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"); + }); +}); \ No newline at end of file diff --git a/test/features/step_definitions/notifications.ts b/test/features/step_definitions/notifications.ts index 9a13a64c..91658134 100644 --- a/test/features/step_definitions/notifications.ts +++ b/test/features/step_definitions/notifications.ts @@ -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); }); }); diff --git a/test/features/step_definitions/regulation.ts b/test/features/step_definitions/regulation.ts new file mode 100644 index 00000000..12766d49 --- /dev/null +++ b/test/features/step_definitions/regulation.ts @@ -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); + }); +}); \ No newline at end of file diff --git a/test/features/step_definitions/reset-password.ts b/test/features/step_definitions/reset-password.ts index e1b1c5e0..45c0a40c 100644 --- a/test/features/step_definitions/reset-password.ts +++ b/test/features/step_definitions/reset-password.ts @@ -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]; diff --git a/test/features/support/world.ts b/test/features/support/world.ts index d456b14b..698d76cb 100644 --- a/test/features/support/world.ts +++ b/test/features/support/world.ts @@ -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); diff --git a/test/unit/client/Notifier.test.ts b/test/unit/client/Notifier.test.ts index 17c8e246..de7ae06e 100644 --- a/test/unit/client/Notifier.test.ts +++ b/test/unit/client/Notifier.test.ts @@ -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 }; diff --git a/test/unit/server/AuthenticationRegulator.test.ts b/test/unit/server/AuthenticationRegulator.test.ts index 9b18623c..7f2c3aff 100644 --- a/test/unit/server/AuthenticationRegulator.test.ts +++ b/test/unit/server/AuthenticationRegulator.test.ts @@ -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(); + }); + + 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 2 authentication and regulate", function () { - const user = "USER"; + afterEach(function () { + MockDate.reset(); + }); - collection.insertStub.returns(BluebirdPromise.resolve()); - collection.findStub.returns(BluebirdPromise.resolve([{ - userId: user, - date: new Date(), - isAuthenticationSuccessful: false - }, { - userId: user, - date: new Date(), - isAuthenticationSuccessful: true - }])); + function markAuthenticationAt(regulator: AuthenticationRegulator, user: string, time: string, success: boolean) { + MockDate.set(time); + return regulator.mark(user, success); + } - const dataStore = new UserDataStore(collectionFactory); - const regulator = new AuthenticationRegulator(dataStore, 10); + it("should mark 2 authentication and regulate (accept)", function () { + const regulator = new AuthenticationRegulator(userDataStoreStub, 3, 10, 10); - return regulator.mark(user, false) + return regulator.mark(USER1, false) .then(function () { - return regulator.mark(user, true); + return regulator.mark(USER1, true); }) .then(function () { - return regulator.regulate(user); + return regulator.regulate(USER1); }); }); - 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 - }])); + it("should mark 3 authentications and regulate (reject)", function () { + const regulator = new AuthenticationRegulator(userDataStoreStub, 3, 10, 10); - const dataStore = new UserDataStore(collectionFactory); - const regulator = new AuthenticationRegulator(dataStore, 10); - - regulator.mark(user, false) + return regulator.mark(USER1, false) .then(function () { - return regulator.mark(user, false); + return regulator.mark(USER1, false); }) .then(function () { - return regulator.mark(user, false); + return regulator.mark(USER1, false); }) .then(function () { - return regulator.regulate(user); + 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(user); + return regulator.regulate(USER1); }) .then(function () { - done(); + 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 () { + 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); }); }); }); \ No newline at end of file diff --git a/test/unit/server/ServerConfiguration.test.ts b/test/unit/server/ServerConfiguration.test.ts index a4b26fce..cbc9d7ea 100644 --- a/test/unit/server/ServerConfiguration.test.ts +++ b/test/unit/server/ServerConfiguration.test.ts @@ -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 diff --git a/test/unit/server/SessionConfigurationBuilder.test.ts b/test/unit/server/SessionConfigurationBuilder.test.ts index 73271695..3e24452a 100644 --- a/test/unit/server/SessionConfigurationBuilder.test.ts +++ b/test/unit/server/SessionConfigurationBuilder.test.ts @@ -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 diff --git a/test/unit/server/configuration/ConfigurationAdapter.test.ts b/test/unit/server/configuration/ConfigurationAdapter.test.ts index e699a7b7..938654dc 100644 --- a/test/unit/server/configuration/ConfigurationAdapter.test.ts +++ b/test/unit/server/configuration/ConfigurationAdapter.test.ts @@ -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: { diff --git a/test/unit/server/configuration/LdapConfigurationAdaptation.test.ts b/test/unit/server/configuration/LdapConfigurationAdaptation.test.ts index 6a4a375f..0bf8df29 100644 --- a/test/unit/server/configuration/LdapConfigurationAdaptation.test.ts +++ b/test/unit/server/configuration/LdapConfigurationAdaptation.test.ts @@ -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: { diff --git a/test/unit/server/mocks/storage/UserDataStoreStub.ts b/test/unit/server/mocks/storage/UserDataStoreStub.ts index cb6aa1ff..cb709c4f 100644 --- a/test/unit/server/mocks/storage/UserDataStoreStub.ts +++ b/test/unit/server/mocks/storage/UserDataStoreStub.ts @@ -43,8 +43,8 @@ export class UserDataStoreStub implements IUserDataStore { return this.saveAuthenticationTraceStub(userId, isAuthenticationSuccessful); } - retrieveLatestAuthenticationTraces(userId: string, isAuthenticationSuccessful: boolean, count: number): BluebirdPromise { - return this.retrieveLatestAuthenticationTracesStub(userId, isAuthenticationSuccessful, count); + retrieveLatestAuthenticationTraces(userId: string, count: number): BluebirdPromise { + return this.retrieveLatestAuthenticationTracesStub(userId, count); } produceIdentityValidationToken(userId: string, token: string, challenge: string, maxAge: number): BluebirdPromise { diff --git a/test/unit/server/server/PrivatePages.ts b/test/unit/server/server/PrivatePages.ts index 2f8c4414..520b9f59 100644 --- a/test/unit/server/server/PrivatePages.ts +++ b/test/unit/server/server/PrivatePages.ts @@ -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 diff --git a/test/unit/server/server/PublicPages.ts b/test/unit/server/server/PublicPages.ts index f24c382e..44ede36a 100644 --- a/test/unit/server/server/PublicPages.ts +++ b/test/unit/server/server/PublicPages.ts @@ -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", diff --git a/test/unit/server/server/Server.test.ts b/test/unit/server/server/Server.test.ts deleted file mode 100644 index f8d2c315..00000000 --- a/test/unit/server/server/Server.test.ts +++ /dev/null @@ -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(); - }); - }); - } -}); - diff --git a/test/unit/server/storage/UserDataStore.test.ts b/test/unit/server/storage/UserDataStore.test.ts index e941ce65..44b1eaad 100644 --- a/test/unit/server/storage/UserDataStore.test.ts +++ b/test/unit/server/storage/UserDataStore.test.ts @@ -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); }); }); From 98aa23ed5e314ec74d594d91ba6f920427ae4102 Mon Sep 17 00:00:00 2001 From: Clement Michaud Date: Sun, 3 Sep 2017 11:34:19 +0200 Subject: [PATCH 3/4] Fix client notifications not fading out after few seconds --- src/client/lib/Notifier.ts | 8 +++++--- test/unit/client/Notifier.test.ts | 22 +++++++++++----------- test/unit/client/mocks/jquery.ts | 2 ++ 3 files changed, 18 insertions(+), 14 deletions(-) diff --git a/src/client/lib/Notifier.ts b/src/client/lib/Notifier.ts index e8052918..c0252b9b 100644 --- a/src/client/lib/Notifier.ts +++ b/src/client/lib/Notifier.ts @@ -28,13 +28,15 @@ class NotificationEvent { this.element.html(html); this.element.addClass(this.statusType); this.element.fadeIn(FADE_TIME, function () { - handlers.onFadedIn(); + if (handlers) + handlers.onFadedIn(); }); this.timeoutId = setTimeout(function () { that.element.fadeOut(FADE_TIME, function () { that.clearNotification(); - handlers.onFadedOut(); + if (handlers) + handlers.onFadedOut(); }); }, 4000); } @@ -60,7 +62,7 @@ export class Notifier implements INotifier { this.onGoingEvent.interrupt(); this.onGoingEvent = new NotificationEvent(this.element, msg, statusType); - this.onGoingEvent.start(); + this.onGoingEvent.start(handlers); } success(msg: string, handlers?: Handlers) { diff --git a/test/unit/client/Notifier.test.ts b/test/unit/client/Notifier.test.ts index de7ae06e..33e4331d 100644 --- a/test/unit/client/Notifier.test.ts +++ b/test/unit/client/Notifier.test.ts @@ -5,38 +5,38 @@ import JQueryMock = require("./mocks/jquery"); import { Notifier } from "../../../src/client/lib/Notifier"; -describe.skip("test notifier", function() { +describe("test notifier", function() { const SELECTOR = "dummy-selector"; const MESSAGE = "This is a message"; let jqueryMock: { jquery: JQueryMock.JQueryMock, element: JQueryMock.JQueryElementsMock }; + let clock: any; beforeEach(function() { jqueryMock = JQueryMock.JQueryMock(); + clock = Sinon.useFakeTimers(); + }); + + afterEach(function() { + clock.restore(); }); function should_fade_in_and_out_on_notification(notificationType: string): void { - const fadeInReturn = { - delay: Sinon.stub() - }; - const delayReturn = { fadeOut: Sinon.stub() }; - jqueryMock.element.fadeIn.returns(fadeInReturn); jqueryMock.element.fadeIn.yields(); - delayReturn.fadeOut.yields(); - - fadeInReturn.delay.returns(delayReturn); function onFadedInCallback() { Assert(jqueryMock.element.fadeIn.calledOnce); Assert(jqueryMock.element.addClass.calledWith(notificationType)); Assert(!jqueryMock.element.removeClass.calledWith(notificationType)); + clock.tick(10 * 1000); } function onFadedOutCallback() { Assert(jqueryMock.element.removeClass.calledWith(notificationType)); + Assert(jqueryMock.element.fadeOut.calledOnce); } const notifier = new Notifier(SELECTOR, jqueryMock.jquery as any); @@ -47,9 +47,9 @@ describe.skip("test notifier", function() { onFadedOut: onFadedOutCallback }); + clock.tick(510); + Assert(jqueryMock.element.fadeIn.calledOnce); - Assert(fadeInReturn.delay.calledOnce); - Assert(delayReturn.fadeOut.calledOnce); } diff --git a/test/unit/client/mocks/jquery.ts b/test/unit/client/mocks/jquery.ts index e21f0b8c..273f9086 100644 --- a/test/unit/client/mocks/jquery.ts +++ b/test/unit/client/mocks/jquery.ts @@ -18,6 +18,7 @@ export interface JQueryElementsMock { addClass: sinon.SinonStub; removeClass: sinon.SinonStub; fadeIn: sinon.SinonStub; + fadeOut: sinon.SinonStub; on: sinon.SinonStub; } @@ -36,6 +37,7 @@ export function JQueryMock(): { jquery: JQueryMock, element: JQueryElementsMock addClass: sinon.stub(), removeClass: sinon.stub(), fadeIn: sinon.stub(), + fadeOut: sinon.stub(), on: sinon.stub() }; jquery.ajax = sinon.stub(); From 85462be268e782e0444e8d0141d883896e7bdc22 Mon Sep 17 00:00:00 2001 From: Clement Michaud Date: Sun, 3 Sep 2017 15:02:38 +0200 Subject: [PATCH 4/4] Wait for notifications to fade out before going forward in integration test steps. --- test/features/redirection.feature | 2 +- test/features/step_definitions/notifications.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/test/features/redirection.feature b/test/features/redirection.feature index eaafa7d0..760f2343 100644 --- a/test/features/redirection.feature +++ b/test/features/redirection.feature @@ -1,4 +1,4 @@ -Feature: User is correctly redirected correctly +Feature: User is correctly redirected Scenario: User is redirected to authelia when he is not authenticated Given I'm on https://home.test.local:8080 diff --git a/test/features/step_definitions/notifications.ts b/test/features/step_definitions/notifications.ts index 91658134..3a068129 100644 --- a/test/features/step_definitions/notifications.ts +++ b/test/features/step_definitions/notifications.ts @@ -20,7 +20,7 @@ Cucumber.defineSupportCode(function ({ Given, When, Then }) { }) .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); + return that.driver.sleep(500); }); });