Introduce LDAP filters to search users and groups for more flexibility.

pull/75/head
Clement Michaud 2017-09-02 22:38:26 +02:00
parent 9403326226
commit 20536abf8b
30 changed files with 639 additions and 665 deletions

View File

@ -18,17 +18,27 @@ ldap:
base_dn: dc=example,dc=com base_dn: dc=example,dc=com
# An additional dn to define the scope to all users # 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. # The users filter.
user_name_attribute: cn # {0} is the matcher replaced by username.
# 'cn={0}' by default.
users_filter: cn={0}
# An additional dn to define the scope of groups # 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 group_name_attribute: cn
# The attribute holding the mail address of the user
mail_attribute: mail
# The username and password of the admin user. # The username and password of the admin user.
user: cn=admin,dc=example,dc=com user: cn=admin,dc=example,dc=com
password: password password: password

View File

@ -9,4 +9,5 @@ docker-compose \
-f example/mongo/docker-compose.yml \ -f example/mongo/docker-compose.yml \
-f example/redis/docker-compose.yml \ -f example/redis/docker-compose.yml \
-f example/nginx/docker-compose.yml \ -f example/nginx/docker-compose.yml \
-f example/ldap/docker-compose.admin.yml \
-f example/ldap/docker-compose.yml $* -f example/ldap/docker-compose.yml $*

View File

@ -47,7 +47,7 @@ export default class Server {
RestApi.setup(app); RestApi.setup(app);
} }
private transformConfiguration(yamlConfiguration: UserConfiguration, deps: GlobalDependencies): AppConfiguration { private adaptConfiguration(yamlConfiguration: UserConfiguration, deps: GlobalDependencies): AppConfiguration {
const config = ConfigurationAdapter.adapt(yamlConfiguration); const config = ConfigurationAdapter.adapt(yamlConfiguration);
// by default the level of logs is info // by default the level of logs is info
@ -76,7 +76,7 @@ export default class Server {
start(yamlConfiguration: UserConfiguration, deps: GlobalDependencies): BluebirdPromise<void> { start(yamlConfiguration: UserConfiguration, deps: GlobalDependencies): BluebirdPromise<void> {
const that = this; const that = this;
const app = Express(); const app = Express();
const config = this.transformConfiguration(yamlConfiguration, deps); const config = this.adaptConfiguration(yamlConfiguration, deps);
return this.setup(config, app, deps) return this.setup(config, app, deps)
.then(function () { .then(function () {
return that.startServer(app, config.port); return that.startServer(app, config.port);

View File

@ -1,9 +1,13 @@
import winston = require("winston"); import winston = require("winston");
import BluebirdPromise = require("bluebird"); 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 { Authenticator } from "./ldap/Authenticator";
import { PasswordUpdater } from "./ldap/PasswordUpdater"; import { PasswordUpdater } from "./ldap/PasswordUpdater";
import { EmailsRetriever } from "./ldap/EmailsRetriever"; import { EmailsRetriever } from "./ldap/EmailsRetriever";
import { ClientFactory } from "./ldap/ClientFactory";
import { TOTPValidator } from "./TOTPValidator"; import { TOTPValidator } from "./TOTPValidator";
import { TOTPGenerator } from "./TOTPGenerator"; import { TOTPGenerator } from "./TOTPGenerator";
@ -29,9 +33,9 @@ export const VARIABLES_KEY = "authelia-variables";
export interface ServerVariables { export interface ServerVariables {
logger: typeof winston; logger: typeof winston;
ldapAuthenticator: Authenticator; ldapAuthenticator: IAuthenticator;
ldapPasswordUpdater: PasswordUpdater; ldapPasswordUpdater: IPasswordUpdater;
ldapEmailsRetriever: EmailsRetriever; ldapEmailsRetriever: IEmailsRetriever;
totpValidator: TOTPValidator; totpValidator: TOTPValidator;
totpGenerator: TOTPGenerator; totpGenerator: TOTPGenerator;
u2f: typeof U2F; u2f: typeof U2F;
@ -71,9 +75,11 @@ export class ServerVariablesHandler {
const five_minutes = 5 * 60; const five_minutes = 5 * 60;
const notifier = NotifierFactory.build(config.notifier, deps.nodemailer); const notifier = NotifierFactory.build(config.notifier, deps.nodemailer);
const ldapAuthenticator = new Authenticator(config.ldap, deps.ldapjs, deps.winston); const ldapClientFactory = new ClientFactory(config.ldap, deps.ldapjs, deps.dovehash, 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 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 accessController = new AccessController(config.access_control, deps.winston);
const totpValidator = new TOTPValidator(deps.speakeasy); const totpValidator = new TOTPValidator(deps.speakeasy);
const totpGenerator = new TOTPGenerator(deps.speakeasy); const totpGenerator = new TOTPGenerator(deps.speakeasy);
@ -113,15 +119,15 @@ export class ServerVariablesHandler {
return (app.get(VARIABLES_KEY) as ServerVariables).notifier; 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; 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; 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; return (app.get(VARIABLES_KEY) as ServerVariables).ldapEmailsRetriever;
} }

View File

@ -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 { export interface LdapConfiguration {
url: string; url: string;
base_dn: string;
additional_user_dn?: string; users_dn: string;
user_name_attribute?: string; // cn by default users_filter: string;
additional_group_dn?: string;
group_name_attribute?: string; // cn by default groups_dn: string;
groups_filter: string;
group_name_attribute: string;
mail_attribute: string;
user: string; // admin username user: string; // admin username
password: string; // admin password password: string; // admin password
} }
@ -67,7 +88,7 @@ export interface StorageConfiguration {
export interface UserConfiguration { export interface UserConfiguration {
port?: number; port?: number;
logs_level?: string; logs_level?: string;
ldap: LdapConfiguration; ldap: UserLdapConfiguration;
session: SessionCookieConfiguration; session: SessionCookieConfiguration;
storage: StorageConfiguration; storage: StorageConfiguration;
notifier: NotifierConfiguration; notifier: NotifierConfiguration;

View File

@ -3,7 +3,8 @@ import * as ObjectPath from "object-path";
import { import {
AppConfiguration, UserConfiguration, NotifierConfiguration, AppConfiguration, UserConfiguration, NotifierConfiguration,
ACLConfiguration, LdapConfiguration, SessionRedisOptions, ACLConfiguration, LdapConfiguration, SessionRedisOptions,
MongoStorageConfiguration, LocalStorageConfiguration MongoStorageConfiguration, LocalStorageConfiguration,
UserLdapConfiguration
} from "./Configuration"; } from "./Configuration";
const LDAP_URL_ENV_VARIABLE = "LDAP_URL"; 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 { function adaptFromUserConfiguration(userConfiguration: UserConfiguration): AppConfiguration {
ensure_key_existence(userConfiguration, "ldap"); ensure_key_existence(userConfiguration, "ldap");
// ensure_key_existence(userConfiguration, "ldap.url");
// ensure_key_existence(userConfiguration, "ldap.base_dn");
ensure_key_existence(userConfiguration, "session.secret"); ensure_key_existence(userConfiguration, "session.secret");
const port = ObjectPath.get(userConfiguration, "port", 8080); const port = userConfiguration.port || 8080;
const ldapConfiguration = adaptLdapConfiguration(userConfiguration.ldap);
return { return {
port: port, port: port,
ldap: ObjectPath.get<object, LdapConfiguration>(userConfiguration, "ldap"), ldap: ldapConfiguration,
session: { session: {
domain: ObjectPath.get<object, string>(userConfiguration, "session.domain"), domain: ObjectPath.get<object, string>(userConfiguration, "session.domain"),
secret: ObjectPath.get<object, string>(userConfiguration, "session.secret"), secret: ObjectPath.get<object, string>(userConfiguration, "session.secret"),

View File

@ -1,36 +1,38 @@
import BluebirdPromise = require("bluebird"); import BluebirdPromise = require("bluebird");
import exceptions = require("../Exceptions"); import exceptions = require("../Exceptions");
import ldapjs = require("ldapjs"); import ldapjs = require("ldapjs");
import { Client, Attributes } from "./Client"; import { IClient } from "./IClient";
import { buildUserDN } from "./common"; import { IClientFactory } from "./IClientFactory";
import { GroupsAndEmails } from "./IClient";
import { IAuthenticator } from "./IAuthenticator";
import { LdapConfiguration } from "../configuration/Configuration"; import { LdapConfiguration } from "../configuration/Configuration";
import { Winston, Ldapjs, Dovehash } from "../../../types/Dependencies"; import { Winston, Ldapjs, Dovehash } from "../../../types/Dependencies";
export class Authenticator { export class Authenticator implements IAuthenticator {
private options: LdapConfiguration; private options: LdapConfiguration;
private ldapjs: Ldapjs; private clientFactory: IClientFactory;
private logger: Winston;
constructor(options: LdapConfiguration, ldapjs: Ldapjs, logger: Winston) { constructor(options: LdapConfiguration, clientFactory: IClientFactory) {
this.options = options; this.options = options;
this.ldapjs = ldapjs; this.clientFactory = clientFactory;
this.logger = logger;
} }
private createClient(userDN: string, password: string): Client { authenticate(username: string, password: string): BluebirdPromise<GroupsAndEmails> {
return new Client(userDN, password, this.options, this.ldapjs, undefined, this.logger); 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<Attributes> { return adminClient.open()
const self = this; .then(function () {
const userDN = buildUserDN(username, this.options); return adminClient.searchUserDn(username);
const userClient = this.createClient(userDN, password); })
const adminClient = this.createClient(this.options.user, this.options.password); .then(function (userDN: string) {
let attributes: Attributes; userClient = that.clientFactory.create(userDN, password);
return userClient.open();
return userClient.open() })
.then(function () { .then(function () {
return userClient.close(); return userClient.close();
}) })
@ -40,12 +42,12 @@ export class Authenticator {
.then(function () { .then(function () {
return adminClient.searchEmailsAndGroups(username); return adminClient.searchEmailsAndGroups(username);
}) })
.then(function (attr: Attributes) { .then(function (gae: GroupsAndEmails) {
attributes = attr; groupsAndEmails = gae;
return adminClient.close(); return adminClient.close();
}) })
.then(function () { .then(function () {
return BluebirdPromise.resolve(attributes); return BluebirdPromise.resolve(groupsAndEmails);
}) })
.error(function (err: Error) { .error(function (err: Error) {
return BluebirdPromise.reject(new exceptions.LdapError(err.message)); return BluebirdPromise.reject(new exceptions.LdapError(err.message));

View File

@ -2,33 +2,30 @@
import util = require("util"); import util = require("util");
import BluebirdPromise = require("bluebird"); import BluebirdPromise = require("bluebird");
import exceptions = require("../Exceptions"); import exceptions = require("../Exceptions");
import ldapjs = require("ldapjs"); import Ldapjs = require("ldapjs");
import { buildUserDN } from "./common"; import Dovehash = require("dovehash");
import { EventEmitter } from "events"; import { EventEmitter } from "events";
import { IClient, GroupsAndEmails } from "./IClient";
import { LdapConfiguration } from "../configuration/Configuration"; import { LdapConfiguration } from "../configuration/Configuration";
import { Winston, Ldapjs, Dovehash } from "../../../types/Dependencies"; import { Winston } from "../../../types/Dependencies";
interface SearchEntry { interface SearchEntry {
object: any; object: any;
} }
export interface Attributes { export class Client implements IClient {
groups: string[];
emails: string[];
}
export class Client {
private userDN: string; private userDN: string;
private password: string; private password: string;
private client: ldapjs.ClientAsync; private client: Ldapjs.ClientAsync;
private ldapjs: Ldapjs; private ldapjs: typeof Ldapjs;
private logger: Winston; private logger: Winston;
private dovehash: Dovehash; private dovehash: typeof Dovehash;
private options: LdapConfiguration; 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.options = options;
this.ldapjs = ldapjs; this.ldapjs = ldapjs;
this.dovehash = dovehash; this.dovehash = dovehash;
@ -46,7 +43,7 @@ export class Client {
clientLogger.level("trace"); clientLogger.level("trace");
}*/ }*/
this.client = BluebirdPromise.promisifyAll(ldapClient) as ldapjs.ClientAsync; this.client = BluebirdPromise.promisifyAll(ldapClient) as Ldapjs.ClientAsync;
} }
open(): BluebirdPromise<void> { open(): BluebirdPromise<void> {
@ -65,7 +62,7 @@ export class Client {
}); });
} }
private search(base: string, query: ldapjs.SearchOptions): BluebirdPromise<any> { private search(base: string, query: Ldapjs.SearchOptions): BluebirdPromise<any> {
const that = this; const that = this;
that.logger.debug("LDAP: Search for '%s' in '%s'", JSON.stringify(query), base); 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<string[]> { private searchGroups(username: string): BluebirdPromise<string[]> {
const that = this; 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[] = []; 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) { .then(function (docs) {
for (let i = 0; i < docs.length; ++i) { for (let i = 0; i < docs.length; ++i) {
groups.push(docs[i].cn); groups.push(docs[i].cn);
@ -127,32 +115,49 @@ export class Client {
}); });
} }
searchUserDn(username: string): BluebirdPromise<string> {
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<string[]> { searchEmails(username: string): BluebirdPromise<string[]> {
const that = this; const that = this;
const userDN = buildUserDN(username, this.options);
const query = { const query = {
scope: "base", scope: "base",
sizeLimit: 1, sizeLimit: 1,
attributes: ["mail"] attributes: [this.options.mail_attribute]
}; };
return this.search(userDN, query) return this.searchUserDn(username)
.then(function (docs) { .then(function (userDN) {
const emails = []; return that.search(userDN, query);
for (let i = 0; i < docs.length; ++i) { })
if (typeof docs[i].mail === "string") .then(function (docs: { mail: string }[]) {
emails.push(docs[i].mail); const emails: string[] = [];
else { if (typeof docs[0].mail === "string")
emails.concat(docs[i].mail); emails.push(docs[0].mail);
} else {
emails.concat(docs[0].mail);
} }
that.logger.debug("LDAP: emails of user '%s' are %s", username, emails); that.logger.debug("LDAP: emails of user '%s' are %s", username, emails);
return BluebirdPromise.resolve(emails); return BluebirdPromise.resolve(emails);
}); });
} }
searchEmailsAndGroups(username: string): BluebirdPromise<Attributes> { searchEmailsAndGroups(username: string): BluebirdPromise<GroupsAndEmails> {
const that = this; const that = this;
let retrievedEmails: string[], retrievedGroups: string[]; let retrievedEmails: string[], retrievedGroups: string[];
@ -172,8 +177,6 @@ export class Client {
modifyPassword(username: string, newPassword: string): BluebirdPromise<void> { modifyPassword(username: string, newPassword: string): BluebirdPromise<void> {
const that = this; const that = this;
const userDN = buildUserDN(username, this.options);
const encodedPassword = this.dovehash.encode("SSHA", newPassword); const encodedPassword = this.dovehash.encode("SSHA", newPassword);
const change = { const change = {
operation: "replace", operation: "replace",
@ -183,7 +186,10 @@ export class Client {
}; };
this.logger.debug("LDAP: update password of user '%s'", username); 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 () { .then(function () {
return that.client.unbindAsync(); return that.client.unbindAsync();
}); });

View File

@ -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);
}
}

View File

@ -2,30 +2,23 @@ import BluebirdPromise = require("bluebird");
import exceptions = require("../Exceptions"); import exceptions = require("../Exceptions");
import ldapjs = require("ldapjs"); import ldapjs = require("ldapjs");
import { Client } from "./Client"; import { Client } from "./Client";
import { buildUserDN } from "./common";
import { IClientFactory } from "./IClientFactory";
import { IEmailsRetriever } from "./IEmailsRetriever";
import { LdapConfiguration } from "../configuration/Configuration"; import { LdapConfiguration } from "../configuration/Configuration";
import { Winston, Ldapjs, Dovehash } from "../../../types/Dependencies";
export class EmailsRetriever { export class EmailsRetriever implements IEmailsRetriever {
private options: LdapConfiguration; private options: LdapConfiguration;
private ldapjs: Ldapjs; private clientFactory: IClientFactory;
private logger: Winston;
constructor(options: LdapConfiguration, ldapjs: Ldapjs, logger: Winston) { constructor(options: LdapConfiguration, clientFactory: IClientFactory) {
this.options = options; this.options = options;
this.ldapjs = ldapjs; this.clientFactory = clientFactory;
this.logger = logger;
}
private createClient(userDN: string, password: string): Client {
return new Client(userDN, password, this.options, this.ldapjs, undefined, this.logger);
} }
retrieve(username: string): BluebirdPromise<string[]> { retrieve(username: string): BluebirdPromise<string[]> {
const userDN = buildUserDN(username, this.options); const adminClient = this.clientFactory.create(this.options.user, this.options.password);
const adminClient = this.createClient(this.options.user, this.options.password);
let emails: string[]; let emails: string[];
return adminClient.open() return adminClient.open()
@ -36,7 +29,7 @@ export class EmailsRetriever {
emails = emails_; emails = emails_;
return adminClient.close(); return adminClient.close();
}) })
.then(function() { .then(function () {
return BluebirdPromise.resolve(emails); return BluebirdPromise.resolve(emails);
}) })
.error(function (err: Error) { .error(function (err: Error) {

View File

@ -0,0 +1,6 @@
import BluebirdPromise = require("bluebird");
import { GroupsAndEmails } from "./IClient";
export interface IAuthenticator {
authenticate(username: string, password: string): BluebirdPromise<GroupsAndEmails>;
}

View File

@ -0,0 +1,16 @@
import BluebirdPromise = require("bluebird");
export interface GroupsAndEmails {
groups: string[];
emails: string[];
}
export interface IClient {
open(): BluebirdPromise<void>;
close(): BluebirdPromise<void>;
searchUserDn(username: string): BluebirdPromise<string>;
searchEmails(username: string): BluebirdPromise<string[]>;
searchEmailsAndGroups(username: string): BluebirdPromise<GroupsAndEmails>;
modifyPassword(username: string, newPassword: string): BluebirdPromise<void>;
}

View File

@ -0,0 +1,6 @@
import { IClient } from "./IClient";
export interface IClientFactory {
create(userDN: string, password: string): IClient;
}

View File

@ -0,0 +1,5 @@
import BluebirdPromise = require("bluebird");
export interface IEmailsRetriever {
retrieve(username: string): BluebirdPromise<string[]>;
}

View File

@ -0,0 +1,5 @@
import BluebirdPromise = require("bluebird");
export interface IPasswordUpdater {
updatePassword(username: string, newPassword: string): BluebirdPromise<void>;
}

View File

@ -2,32 +2,23 @@ import BluebirdPromise = require("bluebird");
import exceptions = require("../Exceptions"); import exceptions = require("../Exceptions");
import ldapjs = require("ldapjs"); import ldapjs = require("ldapjs");
import { Client } from "./Client"; import { Client } from "./Client";
import { buildUserDN } from "./common";
import { IPasswordUpdater } from "./IPasswordUpdater";
import { LdapConfiguration } from "../configuration/Configuration"; 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 options: LdapConfiguration;
private ldapjs: Ldapjs; private clientFactory: IClientFactory;
private logger: Winston;
private dovehash: Dovehash;
constructor(options: LdapConfiguration, ldapjs: Ldapjs, dovehash: Dovehash, logger: Winston) { constructor(options: LdapConfiguration, clientFactory: IClientFactory) {
this.options = options; this.options = options;
this.ldapjs = ldapjs; this.clientFactory = clientFactory;
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);
} }
updatePassword(username: string, newPassword: string): BluebirdPromise<void> { updatePassword(username: string, newPassword: string): BluebirdPromise<void> {
const userDN = buildUserDN(username, this.options); const adminClient = this.clientFactory.create(this.options.user, this.options.password);
const adminClient = this.createClient(this.options.user, this.options.password);
return adminClient.open() return adminClient.open()
.then(function () { .then(function () {

View File

@ -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;
}

View File

@ -5,7 +5,7 @@ import BluebirdPromise = require("bluebird");
import express = require("express"); import express = require("express");
import { AccessController } from "../../access_control/AccessController"; import { AccessController } from "../../access_control/AccessController";
import { AuthenticationRegulator } from "../../AuthenticationRegulator"; import { AuthenticationRegulator } from "../../AuthenticationRegulator";
import { Client, Attributes } from "../../ldap/Client"; import { GroupsAndEmails } from "../../ldap/IClient";
import Endpoint = require("../../../endpoints"); import Endpoint = require("../../../endpoints");
import ErrorReplies = require("../../ErrorReplies"); import ErrorReplies = require("../../ErrorReplies");
import { ServerVariablesHandler } from "../../ServerVariablesHandler"; 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."); logger.info("1st factor: No regulation applied.");
return ldap.authenticate(username, password); return ldap.authenticate(username, password);
}) })
.then(function (attributes: Attributes) { .then(function (groupsAndEmails: GroupsAndEmails) {
logger.info("1st factor: LDAP binding successful. Retrieved information about user are %s", JSON.stringify(attributes)); logger.info("1st factor: LDAP binding successful. Retrieved information about user are %s",
JSON.stringify(groupsAndEmails));
authSession.userid = username; authSession.userid = username;
authSession.first_factor = true; authSession.first_factor = true;
const emails: string[] = attributes.emails; const emails: string[] = groupsAndEmails.emails;
const groups: string[] = attributes.groups; const groups: string[] = groupsAndEmails.groups;
if (!emails || emails.length <= 0) { if (!emails || emails.length <= 0) {
const errMessage = "No emails found. The user should have at least one email address to reset password."; const errMessage = "No emails found. The user should have at least one email address to reset password.";

View File

@ -5,9 +5,17 @@ Feature: Authelia keeps user sessions despite the application restart
And the application restarts And the application restarts
Then I have access to: Then I have access to:
| url | | url |
| https://public.test.local:8080/secret.html |
| https://secret.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 | Scenario: Secrets are stored even when Authelia restarts
| https://mx1.mail.test.local:8080/secret.html | Given I visit "https://auth.test.local:8080/"
| https://mx2.mail.test.local:8080/secret.html | 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 |

View File

@ -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<Server> {
return new BluebirdPromise<Server>(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();
});
}
});

View File

@ -17,9 +17,14 @@ describe("test session configuration builder", function () {
}, },
ldap: { ldap: {
url: "ldap://ldap", url: "ldap://ldap",
base_dn: "dc=example,dc=com",
user: "user", 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", logs_level: "debug",
notifier: { notifier: {
@ -77,9 +82,14 @@ describe("test session configuration builder", function () {
}, },
ldap: { ldap: {
url: "ldap://ldap", url: "ldap://ldap",
base_dn: "dc=example,dc=com",
user: "user", 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", logs_level: "debug",
notifier: { notifier: {

View File

@ -1,14 +1,16 @@
import * as Assert from "assert"; import * as Assert from "assert";
import { UserConfiguration } from "../../../src/server/lib/configuration/Configuration"; import { UserConfiguration, LdapConfiguration } from "../../../../src/server/lib/configuration/Configuration";
import { ConfigurationAdapter } from "../../../src/server/lib/configuration/ConfigurationAdapter"; import { ConfigurationAdapter } from "../../../../src/server/lib/configuration/ConfigurationAdapter";
describe("test config adapter", function() { describe("test config adapter", function () {
function build_yaml_config(): UserConfiguration { function build_yaml_config(): UserConfiguration {
const yaml_config = { const yaml_config = {
port: 8080, port: 8080,
ldap: { ldap: {
url: "http://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", user: "user",
password: "pass" password: "pass"
}, },
@ -33,41 +35,21 @@ describe("test config adapter", function() {
return yaml_config; 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(); const yaml_config = build_yaml_config();
yaml_config.port = 7070; yaml_config.port = 7070;
const config = ConfigurationAdapter.adapt(yaml_config); const config = ConfigurationAdapter.adapt(yaml_config);
Assert.equal(config.port, 7070); 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(); const yaml_config = build_yaml_config();
delete yaml_config.port; delete yaml_config.port;
const config = ConfigurationAdapter.adapt(yaml_config); const config = ConfigurationAdapter.adapt(yaml_config);
Assert.equal(config.port, 8080); Assert.equal(config.port, 8080);
}); });
it("should get the ldap attributes", function() { it("should get the session 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() {
const yaml_config = build_yaml_config(); const yaml_config = build_yaml_config();
yaml_config.session = { yaml_config.session = {
domain: "example.com", domain: "example.com",
@ -80,14 +62,14 @@ describe("test config adapter", function() {
Assert.equal(config.session.expiration, 3600); 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(); const yaml_config = build_yaml_config();
yaml_config.logs_level = "debug"; yaml_config.logs_level = "debug";
const config = ConfigurationAdapter.adapt(yaml_config); const config = ConfigurationAdapter.adapt(yaml_config);
Assert.equal(config.logs_level, "debug"); 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(); const yaml_config = build_yaml_config();
yaml_config.notifier = { yaml_config.notifier = {
gmail: { 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(); const yaml_config = build_yaml_config();
yaml_config.access_control = { yaml_config.access_control = {
default: [], default: [],

View File

@ -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);
});
});

View File

@ -2,121 +2,127 @@
import { Authenticator } from "../../../../src/server/lib/ldap/Authenticator"; import { Authenticator } from "../../../../src/server/lib/ldap/Authenticator";
import { LdapConfiguration } from "../../../../src/server/lib/configuration/Configuration"; import { LdapConfiguration } from "../../../../src/server/lib/configuration/Configuration";
import sinon = require("sinon"); import Sinon = require("sinon");
import BluebirdPromise = require("bluebird"); import BluebirdPromise = require("bluebird");
import assert = require("assert"); import Assert = require("assert");
import ldapjs = require("ldapjs");
import winston = require("winston");
import { EventEmitter } from "events";
import { LdapjsMock, LdapjsClientMock } from "../mocks/ldapjs"; import { ClientFactoryStub } from "../mocks/ldap/ClientFactoryStub";
import { ClientStub } from "../mocks/ldap/ClientStub";
describe("test ldap authentication", function () { 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 authenticator: Authenticator;
let ldapClient: LdapjsClientMock;
let ldapjs: LdapjsMock;
let ldapConfig: LdapConfiguration; 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 () { beforeEach(function () {
ldapClient = LdapjsClientMock(); clientFactoryStub = new ClientFactoryStub();
ldapjs = LdapjsMock(); adminClientStub = new ClientStub();
ldapjs.createClient.returns(ldapClient); userClientStub = new ClientStub();
// winston.level = "debug";
adminUserDN = "cn=admin,dc=example,dc=com";
adminPassword = "password";
ldapConfig = { ldapConfig = {
url: "http://localhost:324", url: "http://localhost:324",
user: adminUserDN, users_dn: "ou=users,dc=example,dc=com",
password: adminPassword, users_filter: "cn={0}",
base_dn: "dc=example,dc=com", groups_dn: "ou=groups,dc=example,dc=com",
additional_user_dn: "ou=users" 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 () { 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 () { it("should bind the user if good credentials provided", function () {
ldapClient.bind.withArgs("cn=username,ou=users,dc=example,dc=com", "password").yields(); clientFactoryStub.createStub.withArgs(ADMIN_USER_DN, ADMIN_PASSWORD)
return test_check_password_internal(); .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 () { // admin connects successfully
ldapConfig.user_name_attribute = "uid"; adminClientStub.openStub.returns(BluebirdPromise.resolve());
ldapClient.bind.withArgs("uid=username,ou=users,dc=example,dc=com", "password").yields(); adminClientStub.closeStub.returns(BluebirdPromise.resolve());
return test_check_password_internal();
// 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 () { describe("failure", function () {
it("should not bind the user if wrong credentials provided", function () { it("should not bind the user if wrong credentials provided", function () {
ldapClient.bind.yields("wrong credentials"); clientFactoryStub.createStub.withArgs(ADMIN_USER_DN, ADMIN_PASSWORD)
return test_check_password_internal() .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 () { .catch(function () {
return BluebirdPromise.resolve(); return BluebirdPromise.resolve();
}); });
}); });
it("should not bind the user if search of emails or group fails", function () { 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(); clientFactoryStub.createStub.withArgs(ADMIN_USER_DN, ADMIN_PASSWORD)
ldapClient.bind.withArgs(adminUserDN, adminPassword).yields(); .returns(adminClientStub);
ldapClient.unbind.yields(); clientFactoryStub.createStub.withArgs("cn=" + USERNAME + ",ou=users,dc=example,dc=com", PASSWORD)
ldapClient.search.yields("wrong credentials"); .returns(userClientStub);
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.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 () { .catch(function () {
return BluebirdPromise.resolve(); return BluebirdPromise.resolve();
}); });

View File

@ -2,93 +2,74 @@
import { EmailsRetriever } from "../../../../src/server/lib/ldap/EmailsRetriever"; import { EmailsRetriever } from "../../../../src/server/lib/ldap/EmailsRetriever";
import { LdapConfiguration } from "../../../../src/server/lib/configuration/Configuration"; import { LdapConfiguration } from "../../../../src/server/lib/configuration/Configuration";
import sinon = require("sinon"); import Sinon = require("sinon");
import BluebirdPromise = require("bluebird"); import BluebirdPromise = require("bluebird");
import assert = require("assert"); import Assert = require("assert");
import ldapjs = require("ldapjs");
import winston = require("winston");
import { EventEmitter } from "events";
import { LdapjsMock, LdapjsClientMock } from "../mocks/ldapjs";
import { ClientFactoryStub } from "../mocks/ldap/ClientFactoryStub";
import { ClientStub } from "../mocks/ldap/ClientStub";
describe("test emails retriever", function () { 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 emailsRetriever: EmailsRetriever;
let ldapClient: LdapjsClientMock;
let ldapjs: LdapjsMock;
let ldapConfig: LdapConfiguration; let ldapConfig: LdapConfiguration;
let adminUserDN: string;
let adminPassword: string;
beforeEach(function () { beforeEach(function () {
ldapClient = LdapjsClientMock(); clientFactoryStub = new ClientFactoryStub();
ldapjs = LdapjsMock(); adminClientStub = new ClientStub();
ldapjs.createClient.returns(ldapClient);
// winston.level = "debug";
adminUserDN = "cn=admin,dc=example,dc=com";
adminPassword = "password";
ldapConfig = { ldapConfig = {
url: "http://localhost:324", url: "http://ldap",
user: adminUserDN, user: ADMIN_USER_DN,
password: adminPassword, password: ADMIN_PASSWORD,
base_dn: "dc=example,dc=com", users_dn: "ou=users,dc=example,dc=com",
additional_user_dn: "ou=users" 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 () { describe("success", function () {
beforeEach(function () { it("should retrieve emails successfully", function () {
ldapClient.bind.withArgs(adminUserDN, adminPassword).yields(); clientFactoryStub.createStub.withArgs(ADMIN_USER_DN, ADMIN_PASSWORD)
ldapClient.unbind.yields(); .returns(adminClientStub);
});
it("should update the password successfully", function () { // admin connects successfully
retrieveEmails(ldapClient); adminClientStub.openStub.returns(BluebirdPromise.resolve());
return test_emails_retrieval(); adminClientStub.closeStub.returns(BluebirdPromise.resolve());
adminClientStub.searchEmailsStub.withArgs(USERNAME)
.returns(BluebirdPromise.resolve(["user@example.com"]));
return emailsRetriever.retrieve(USERNAME);
}); });
}); });
describe("failure", function () { describe("failure", function () {
it("should fail retrieving emails when search operation fails", function () { it("should fail retrieving emails when search operation fails", function () {
ldapClient.bind.withArgs(adminUserDN, adminPassword).yields(); clientFactoryStub.createStub.withArgs(ADMIN_USER_DN, ADMIN_PASSWORD)
ldapClient.search.yields("wrong credentials"); .returns(adminClientStub);
return test_emails_retrieval()
.catch(function () { // admin connects successfully
return BluebirdPromise.resolve(); 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(); });
}); });
}); });
}); });

View File

@ -2,82 +2,78 @@
import { PasswordUpdater } from "../../../../src/server/lib/ldap/PasswordUpdater"; import { PasswordUpdater } from "../../../../src/server/lib/ldap/PasswordUpdater";
import { LdapConfiguration } from "../../../../src/server/lib/configuration/Configuration"; import { LdapConfiguration } from "../../../../src/server/lib/configuration/Configuration";
import sinon = require("sinon"); import Sinon = require("sinon");
import BluebirdPromise = require("bluebird"); import BluebirdPromise = require("bluebird");
import assert = require("assert"); import Assert = require("assert");
import ldapjs = require("ldapjs");
import winston = require("winston");
import { EventEmitter } from "events";
import { LdapjsMock, LdapjsClientMock } from "../mocks/ldapjs";
import { ClientFactoryStub } from "../mocks/ldap/ClientFactoryStub";
import { ClientStub } from "../mocks/ldap/ClientStub";
describe("test password update", function () { 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 passwordUpdater: PasswordUpdater;
let ldapClient: LdapjsClientMock;
let ldapjs: LdapjsMock;
let ldapConfig: LdapConfiguration; let ldapConfig: LdapConfiguration;
let adminUserDN: string;
let adminPassword: string;
let dovehash: any; let dovehash: any;
beforeEach(function () { beforeEach(function () {
ldapClient = LdapjsClientMock(); clientFactoryStub = new ClientFactoryStub();
ldapjs = LdapjsMock(); adminClientStub = new ClientStub();
ldapjs.createClient.returns(ldapClient);
// winston.level = "debug";
adminUserDN = "cn=admin,dc=example,dc=com";
adminPassword = "password";
ldapConfig = { ldapConfig = {
url: "http://localhost:324", url: "http://ldap",
user: adminUserDN, user: ADMIN_USER_DN,
password: adminPassword, password: ADMIN_PASSWORD,
base_dn: "dc=example,dc=com", users_dn: "ou=users,dc=example,dc=com",
additional_user_dn: "ou=users" groups_dn: "ou=groups,dc=example,dc=com",
group_name_attribute: "cn",
groups_filter: "cn={0}",
mail_attribute: "mail",
users_filter: "cn={0}"
}; };
dovehash = { 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 () { describe("success", function () {
beforeEach(function () {
ldapClient.bind.withArgs(adminUserDN, adminPassword).yields();
ldapClient.unbind.yields();
});
it("should update the password successfully", function () { it("should update the password successfully", function () {
clientFactoryStub.createStub.withArgs(ADMIN_USER_DN, ADMIN_PASSWORD)
.returns(adminClientStub);
dovehash.encode.returns("{SSHA}AQmxaKfobGY9HSQa6aDYkAWOgPGNhGYn"); dovehash.encode.returns("{SSHA}AQmxaKfobGY9HSQa6aDYkAWOgPGNhGYn");
ldapClient.modify.withArgs("cn=username,ou=users,dc=example,dc=com", { adminClientStub.modifyPasswordStub.withArgs(USERNAME, NEW_PASSWORD).returns(BluebirdPromise.resolve());
operation: "replace", adminClientStub.openStub.returns(BluebirdPromise.resolve());
modification: { adminClientStub.closeStub.returns(BluebirdPromise.resolve());
userPassword: "{SSHA}AQmxaKfobGY9HSQa6aDYkAWOgPGNhGYn"
} return passwordUpdater.updatePassword(USERNAME, NEW_PASSWORD);
}).yields();
return test_update_password();
}); });
}); });
describe("failure", function () { describe("failure", function () {
it("should fail updating password when modify operation fails", function () { it("should fail updating password when modify operation fails", function () {
ldapClient.bind.withArgs(adminUserDN, adminPassword).yields(); clientFactoryStub.createStub.withArgs(ADMIN_USER_DN, ADMIN_PASSWORD)
ldapClient.modify.yields("wrong credentials"); .returns(adminClientStub);
return test_update_password()
.catch(function () { dovehash.encode.returns("{SSHA}AQmxaKfobGY9HSQa6aDYkAWOgPGNhGYn");
return BluebirdPromise.resolve(); 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(); });
}); });
}); });
}); });

View File

@ -2,7 +2,7 @@ import sinon = require("sinon");
import express = require("express"); import express = require("express");
import winston = require("winston"); import winston = require("winston");
import { UserDataStoreStub } from "./storage/UserDataStoreStub"; 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 { export interface ServerVariablesMock {
logger: any; logger: any;

View File

@ -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);
}
}

View File

@ -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<void> {
return this.openStub();
}
close(): BluebirdPromise<void> {
return this.closeStub();
}
searchUserDn(username: string): BluebirdPromise<string> {
return this.searchUserDnStub(username);
}
searchEmails(username: string): BluebirdPromise<string[]> {
return this.searchEmailsStub(username);
}
searchEmailsAndGroups(username: string): BluebirdPromise<GroupsAndEmails> {
return this.searchEmailsAndGroupsStub(username);
}
modifyPassword(username: string, newPassword: string): BluebirdPromise<void> {
return this.modifyPasswordStub(username, newPassword);
}
}

View File

@ -1,5 +1,3 @@
import Server from "../../../../src/server/lib/Server"; import Server from "../../../../src/server/lib/Server";
import { LdapjsClientMock } from "./../mocks/ldapjs"; import { LdapjsClientMock } from "./../mocks/ldapjs";
@ -36,7 +34,6 @@ describe("test the server", function () {
ldap: { ldap: {
url: "ldap://127.0.0.1:389", url: "ldap://127.0.0.1:389",
base_dn: "ou=users,dc=example,dc=com", base_dn: "ou=users,dc=example,dc=com",
user_name_attribute: "cn",
user: "cn=admin,dc=example,dc=com", user: "cn=admin,dc=example,dc=com",
password: "password", password: "password",
}, },
@ -125,81 +122,10 @@ describe("test the server", function () {
describe("test authentication and verification", function () { describe("test authentication and verification", function () {
test_authentication(); test_authentication();
test_reset_password();
test_regulation(); test_regulation();
}); });
function test_authentication() { 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 () { it("should return status code 204 when user is authenticated using u2f", function () {
const sign_request = {}; const sign_request = {};
const sign_status = {}; 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() { function test_regulation() {
it("should regulate authentication", function () { it("should regulate authentication", function () {
const j = requestp.jar(); const j = requestp.jar();