Merge pull request #75 from clems4ever/ldap-filters

Add LDAP filters to configuration file for flexibility and rework authentication regulation
pull/80/head
Clément Michaud 2017-09-03 15:14:50 +02:00 committed by GitHub
commit 84c13c71e2
60 changed files with 1267 additions and 1049 deletions

1
.gitignore vendored
View File

@ -29,4 +29,3 @@ dist/
# Specific files # Specific files
/config.yml /config.yml
/test/integration/nginx.conf

View File

@ -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 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 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 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 your secret in QRCode and Base32 formats. You can use
[Google Authenticator] [Google Authenticator]
to store them and get the generated tokens with the app. 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. 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 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 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 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 to register. Upon successful registration, you can authenticate using your U2F
device by simply touching the token. Easy, right?! 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 **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 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 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. Paste the link in your browser and you should be able to reset the password.
<img src="https://raw.githubusercontent.com/clems4ever/authelia/master/images/reset_password.png" width="400"> <img src="https://raw.githubusercontent.com/clems4ever/authelia/master/images/reset_password.png" width="400">

View File

@ -1,3 +1,6 @@
###############################################################
# Authelia configuration #
###############################################################
# The port to listen on # The port to listen on
port: 80 port: 80
@ -18,17 +21,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
@ -39,22 +52,30 @@ ldap:
# Access control is a set of rules you can use to restrict the user access. # Access control is a set of rules you can use to restrict the user access.
# Default (anyone), per-user or per-group rules can be defined. # 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 # is applied, i.e., access is allowed to anyone. Otherwise restrictions follow
# the rules defined below. # the rules defined below.
# If no rule is provided, all domains are denied. # If no rule is provided, all domains are denied.
# #
# '*' means 'any' subdomains and matches any string. It must stand at the # One can use the wildcard * to match any subdomain.
# beginning of the pattern. # Note 1: It must stand at the beginning of the pattern. (example: *.mydomain.com)
# Note 2: You must put the pattern in simple quotes when using the wildcard.
access_control: access_control:
# The default policy. Applies to any user
default: default:
- public.test.local - public.test.local
# Group based policies. The key is a group name and the value
# is the domain to allow access to.
groups: groups:
admin: admin:
- '*.test.local' - '*.test.local'
dev: dev:
- secret.test.local - secret.test.local
- secret2.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: users:
harry: harry:
- secret1.test.local - secret1.test.local
@ -64,19 +85,43 @@ access_control:
# Configuration of session cookies # Configuration of session cookies
# #
# _secret_ the secret to encrypt session cookies # The session cookies identify the user once logged in.
# _expiration_ the time before cookies expire session:
# _domain_ the domain to protect. # 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 # Note: the authenticator must also be in that domain. If empty, the cookie
# is restricted to the subdomain of the issuer. # is restricted to the subdomain of the issuer.
session:
secret: unsecure_secret
expiration: 3600000
domain: test.local domain: test.local
# The redis connection details
redis: redis:
host: redis host: redis
port: 6379 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: storage:
# The directory where the DB files will be saved # The directory where the DB files will be saved
# local: /var/lib/authelia/store # local: /var/lib/authelia/store
@ -85,9 +130,11 @@ storage:
mongo: mongo:
url: mongodb://mongo/authelia url: mongodb://mongo/authelia
# Configuration of the notification system.
#
# Notifications are sent to users when they require a password reset, a u2f # Notifications are sent to users when they require a password reset, a u2f
# registration or a TOTP registration. # registration or a TOTP registration.
# Use only one available configuration: filesystem, gmail # Use only an available configuration: filesystem, gmail
notifier: notifier:
# For testing purpose, notifications can be sent in a file # For testing purpose, notifications can be sent in a file
filesystem: filesystem:

147
config.test.yml 100644
View File

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

View File

@ -0,0 +1,5 @@
version: '2'
services:
authelia:
volumes:
- ./config.test.yml:/etc/authelia/config.yml:ro

View File

@ -5,7 +5,7 @@ services:
restart: always restart: always
volumes: volumes:
- ./config.template.yml:/etc/authelia/config.yml:ro - ./config.template.yml:/etc/authelia/config.yml:ro
- ./notifications:/var/lib/authelia/notifications - /tmp/notifications:/var/lib/authelia/notifications
depends_on: depends_on:
- redis - redis
networks: networks:

View File

@ -52,3 +52,11 @@ objectclass: top
mail: james.dean@example.com mail: james.dean@example.com
sn: James Dean sn: James Dean
userpassword: {SHA}W6ph5Mm5Pz8GgiULbPgzG37mj9g= 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=

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

@ -3,28 +3,66 @@
import util = require("util"); import util = require("util");
import { INotifier, Handlers } from "./INotifier"; import { INotifier, Handlers } from "./INotifier";
export class Notifier implements INotifier { class NotificationEvent {
private element: JQuery; private element: JQuery;
private message: string;
private statusType: string;
private timeoutId: any;
constructor(selector: string, $: JQueryStatic) { constructor(element: JQuery, msg: string, statusType: string) {
this.element = $(selector); 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 that = this;
const FADE_TIME = 500; const FADE_TIME = 500;
const html = util.format('<i><img src="/img/notifications/%s.png" alt="status %s"/></i>\ const html = util.format('<i><img src="/img/notifications/%s.png" alt="status %s"/></i>\
<span>%s</span>', statusType, statusType, msg); <span>%s</span>', this.statusType, this.statusType, this.message);
this.element.html(html); this.element.html(html);
this.element.addClass(statusType); this.element.addClass(this.statusType);
this.element.fadeIn(FADE_TIME, function () { this.element.fadeIn(FADE_TIME, function () {
if (handlers)
handlers.onFadedIn(); handlers.onFadedIn();
}) });
.delay(4000)
.fadeOut(FADE_TIME, function() { this.timeoutId = setTimeout(function () {
that.element.removeClass(statusType); that.element.fadeOut(FADE_TIME, function () {
that.clearNotification();
if (handlers)
handlers.onFadedOut(); 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(handlers);
} }
success(msg: string, handlers?: Handlers) { success(msg: string, handlers?: Handlers) {

View File

@ -13,6 +13,7 @@ export default function (window: Window, $: JQueryStatic,
function onFormSubmitted() { function onFormSubmitted() {
const username: string = $(UISelectors.USERNAME_FIELD_ID).val(); const username: string = $(UISelectors.USERNAME_FIELD_ID).val();
const password: string = $(UISelectors.PASSWORD_FIELD_ID).val(); const password: string = $(UISelectors.PASSWORD_FIELD_ID).val();
$(UISelectors.PASSWORD_FIELD_ID).val("");
jslogger.debug("Form submitted"); jslogger.debug("Form submitted");
firstFactorValidator.validate(username, password, $) firstFactorValidator.validate(username, password, $)
.then(onFirstFactorSuccess, onFirstFactorFailure); .then(onFirstFactorSuccess, onFirstFactorFailure);
@ -21,17 +22,12 @@ export default function (window: Window, $: JQueryStatic,
function onFirstFactorSuccess() { function onFirstFactorSuccess() {
jslogger.debug("First factor validated."); jslogger.debug("First factor validated.");
$(UISelectors.USERNAME_FIELD_ID).val("");
$(UISelectors.PASSWORD_FIELD_ID).val("");
// Redirect to second factor // Redirect to second factor
window.location.href = Endpoints.SECOND_FACTOR_GET; window.location.href = Endpoints.SECOND_FACTOR_GET;
} }
function onFirstFactorFailure(err: Error) { function onFirstFactorFailure(err: Error) {
jslogger.debug("First factor failed."); jslogger.debug("First factor failed.");
$(UISelectors.PASSWORD_FIELD_ID).val("");
notifier.error("Authentication failed. Please double check your credentials."); notifier.error("Authentication failed. Please double check your credentials.");
} }

View File

@ -4,7 +4,7 @@ process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
import Server from "./lib/Server"; import Server from "./lib/Server";
import { GlobalDependencies } from "../types/Dependencies"; import { GlobalDependencies } from "../types/Dependencies";
const YAML = require("yamljs"); import YAML = require("yamljs");
const configurationFilepath = process.argv[2]; const configurationFilepath = process.argv[2];
if (!configurationFilepath) { if (!configurationFilepath) {

View File

@ -1,18 +1,20 @@
import * as BluebirdPromise from "bluebird"; import * as BluebirdPromise from "bluebird";
import exceptions = require("./Exceptions"); import exceptions = require("./Exceptions");
import { UserDataStore } from "./storage/UserDataStore"; import { IUserDataStore } from "./storage/IUserDataStore";
import { AuthenticationTraceDocument } from "./storage/AuthenticationTraceDocument"; import { AuthenticationTraceDocument } from "./storage/AuthenticationTraceDocument";
const MAX_AUTHENTICATION_COUNT_IN_TIME_RANGE = 3;
export class AuthenticationRegulator { export class AuthenticationRegulator {
private userDataStore: UserDataStore; private userDataStore: IUserDataStore;
private lockTimeInSeconds: number; 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.userDataStore = userDataStore;
this.lockTimeInSeconds = lockTimeInSeconds; this.banTime = banTime;
this.findTime = findTime;
this.maxRetries = maxRetries;
} }
// Mark authentication // Mark authentication
@ -21,18 +23,30 @@ export class AuthenticationRegulator {
} }
regulate(userId: string): BluebirdPromise<void> { regulate(userId: string): BluebirdPromise<void> {
return this.userDataStore.retrieveLatestAuthenticationTraces(userId, false, 3) const that = this;
.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 oldestDocument = docs[MAX_AUTHENTICATION_COUNT_IN_TIME_RANGE - 1]; if (that.maxRetries <= 0) return BluebirdPromise.resolve();
const noLockMinDate = new Date(new Date().getTime() - this.lockTimeInSeconds * 1000);
if (oldestDocument.date > noLockMinDate) { 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."); throw new exceptions.AuthenticationRegulationError("Max number of authentication. Please retry in few minutes.");
}
return BluebirdPromise.resolve(); return BluebirdPromise.resolve();
}); });

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;
@ -68,19 +72,20 @@ class UserDataStoreFactory {
export class ServerVariablesHandler { export class ServerVariablesHandler {
static initialize(app: express.Application, config: Configuration.AppConfiguration, deps: GlobalDependencies): BluebirdPromise<void> { static initialize(app: express.Application, config: Configuration.AppConfiguration, deps: GlobalDependencies): BluebirdPromise<void> {
const five_minutes = 5 * 60;
const notifier = NotifierFactory.build(config.notifier, deps.nodemailer); const 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);
return UserDataStoreFactory.create(config) return UserDataStoreFactory.create(config)
.then(function (userDataStore: UserDataStore) { .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 = { const variables: ServerVariables = {
accessController: accessController, accessController: accessController,
@ -113,15 +118,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
} }
@ -64,14 +85,21 @@ export interface StorageConfiguration {
mongo?: MongoStorageConfiguration; mongo?: MongoStorageConfiguration;
} }
export interface RegulationConfiguration {
max_retries: number;
find_time: number;
ban_time: number;
}
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;
access_control?: ACLConfiguration; access_control?: ACLConfiguration;
regulation: RegulationConfiguration;
} }
export interface AppConfiguration { export interface AppConfiguration {
@ -82,4 +110,5 @@ export interface AppConfiguration {
storage: StorageConfiguration; storage: StorageConfiguration;
notifier: NotifierConfiguration; notifier: NotifierConfiguration;
access_control?: ACLConfiguration; access_control?: ACLConfiguration;
regulation: RegulationConfiguration;
} }

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,46 @@ 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");
ensure_key_existence(userConfiguration, "regulation");
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"),
@ -44,7 +76,8 @@ function adaptFromUserConfiguration(userConfiguration: UserConfiguration): AppCo
}, },
logs_level: get_optional<string>(userConfiguration, "logs_level", "info"), logs_level: get_optional<string>(userConfiguration, "logs_level", "info"),
notifier: ObjectPath.get<object, NotifierConfiguration>(userConfiguration, "notifier"), notifier: ObjectPath.get<object, NotifierConfiguration>(userConfiguration, "notifier"),
access_control: ObjectPath.get<object, ACLConfiguration>(userConfiguration, "access_control") access_control: ObjectPath.get<object, ACLConfiguration>(userConfiguration, "access_control"),
regulation: userConfiguration.regulation
}; };
} }

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[] = [];
if (typeof docs[0].mail === "string")
emails.push(docs[0].mail);
else { else {
emails.concat(docs[i].mail); 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()

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

@ -11,7 +11,7 @@ export interface IUserDataStore {
retrieveU2FRegistration(userId: string, appId: string): BluebirdPromise<U2FRegistrationDocument>; retrieveU2FRegistration(userId: string, appId: string): BluebirdPromise<U2FRegistrationDocument>;
saveAuthenticationTrace(userId: string, isAuthenticationSuccessful: boolean): BluebirdPromise<void>; saveAuthenticationTrace(userId: string, isAuthenticationSuccessful: boolean): BluebirdPromise<void>;
retrieveLatestAuthenticationTraces(userId: string, isAuthenticationSuccessful: boolean, count: number): BluebirdPromise<AuthenticationTraceDocument[]>; retrieveLatestAuthenticationTraces(userId: string, count: number): BluebirdPromise<AuthenticationTraceDocument[]>;
produceIdentityValidationToken(userId: string, token: string, challenge: string, maxAge: number): BluebirdPromise<any>; produceIdentityValidationToken(userId: string, token: string, challenge: string, maxAge: number): BluebirdPromise<any>;
consumeIdentityValidationToken(token: string, challenge: string): BluebirdPromise<IdentityValidationDocument>; consumeIdentityValidationToken(token: string, challenge: string): BluebirdPromise<IdentityValidationDocument>;

View File

@ -76,10 +76,9 @@ export class UserDataStore implements IUserDataStore {
return this.authenticationTracesCollection.insert(newDocument); return this.authenticationTracesCollection.insert(newDocument);
} }
retrieveLatestAuthenticationTraces(userId: string, isAuthenticationSuccessful: boolean, count: number): BluebirdPromise<AuthenticationTraceDocument[]> { retrieveLatestAuthenticationTraces(userId: string, count: number): BluebirdPromise<AuthenticationTraceDocument[]> {
const q = { const q = {
userId: userId, userId: userId
isAuthenticationSuccessful: isAuthenticationSuccessful
}; };
return this.authenticationTracesCollection.find(q, { date: -1 }, count); return this.authenticationTracesCollection.find(q, { date: -1 }, count);

View File

@ -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 Scenario: User is redirected to authelia when he is not authenticated
Given I'm on https://home.test.local:8080 Given I'm on https://home.test.local:8080

View File

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

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,7 +0,0 @@
import Cucumber = require("cucumber");
Cucumber.defineSupportCode(function({After}) {
After(function() {
return this.driver.quit();
});
});

View File

@ -22,10 +22,21 @@ Cucumber.defineSupportCode(function ({ Given, When, Then }) {
return this.clickOnButton(text); return this.clickOnButton(text);
}); });
Given("I login with user {stringInDoubleQuotes} and password {stringInDoubleQuotes}", function (username: string, password: string) { Given("I login with user {stringInDoubleQuotes} and password {stringInDoubleQuotes}",
function (username: string, password: string) {
return this.loginWithUserPassword(username, password); 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) { Given("I register a TOTP secret called {stringInDoubleQuotes}", function (handle: string) {
return this.registerTotpSecret(handle); return this.registerTotpSecret(handle);
}); });
@ -38,7 +49,8 @@ Cucumber.defineSupportCode(function ({ Given, When, Then }) {
return this.useTotpTokenHandle(handle); return this.useTotpTokenHandle(handle);
}); });
When("I visit {stringInDoubleQuotes} and get redirected {stringInDoubleQuotes}", function (url: string, redirectUrl: string) { When("I visit {stringInDoubleQuotes} and get redirected {stringInDoubleQuotes}",
function (url: string, redirectUrl: string) {
const that = this; const that = this;
return this.driver.get(url) return this.driver.get(url)
.then(function () { .then(function () {
@ -46,7 +58,8 @@ Cucumber.defineSupportCode(function ({ Given, When, Then }) {
}); });
}); });
Given("I register TOTP and login with user {stringInDoubleQuotes} and password {stringInDoubleQuotes}", function (username: string, password: string) { Given("I register TOTP and login with user {stringInDoubleQuotes} and password {stringInDoubleQuotes}",
function (username: string, password: string) {
return this.registerTotpAndSignin(username, password); return this.registerTotpAndSignin(username, password);
}); });

View File

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

View File

@ -6,6 +6,7 @@ import CustomWorld = require("../support/world");
Cucumber.defineSupportCode(function ({ Given, When, Then }) { Cucumber.defineSupportCode(function ({ Given, When, Then }) {
Then("I get a notification of type {stringInDoubleQuotes} with message {stringInDoubleQuotes}", Then("I get a notification of type {stringInDoubleQuotes} with message {stringInDoubleQuotes}",
{ timeout: 10 * 1000 },
function (notificationType: string, notificationMessage: string) { function (notificationType: string, notificationMessage: string) {
const that = this; const that = this;
const notificationEl = this.driver.findElement(seleniumWebdriver.By.className("notification")); const notificationEl = this.driver.findElement(seleniumWebdriver.By.className("notification"));
@ -19,6 +20,7 @@ Cucumber.defineSupportCode(function ({ Given, When, Then }) {
}) })
.then(function (classes: string) { .then(function (classes: string) {
Assert(classes.indexOf(notificationType) > -1, "Class '" + notificationType + "' not found in notification element."); Assert(classes.indexOf(notificationType) > -1, "Class '" + notificationType + "' not found in notification element.");
return that.driver.sleep(500);
}); });
}); });

View File

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

View File

@ -9,7 +9,7 @@ Cucumber.defineSupportCode(function ({ Given, When, Then }) {
}); });
When("I click on the link of the email", function () { 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 regexp = new RegExp(/Link: (.+)/);
const match = regexp.exec(notif); const match = regexp.exec(notif);
const link = match[1]; const link = match[1];

View File

@ -12,6 +12,7 @@ function CustomWorld() {
.build(); .build();
this.totpSecrets = {}; this.totpSecrets = {};
this.configuration = {};
this.visit = function (link: string) { this.visit = function (link: string) {
return this.driver.get(link); return this.driver.get(link);
@ -71,7 +72,7 @@ function CustomWorld() {
return that.driver.findElement(seleniumWebdriver.By.className("register-totp")).click(); return that.driver.findElement(seleniumWebdriver.By.className("register-totp")).click();
}) })
.then(function () { .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 regexp = new RegExp(/Link: (.+)/);
const match = regexp.exec(notif); const match = regexp.exec(notif);
const link = match[1]; const link = match[1];
@ -98,7 +99,7 @@ function CustomWorld() {
}; };
this.useTotpToken = function (totpSecret: string) { 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 () { .then(function () {
return that.driver.findElement(seleniumWebdriver.By.id("token")) return that.driver.findElement(seleniumWebdriver.By.id("token"))
.sendKeys(totpSecret); .sendKeys(totpSecret);

View File

@ -9,34 +9,34 @@ describe("test notifier", function() {
const SELECTOR = "dummy-selector"; const SELECTOR = "dummy-selector";
const MESSAGE = "This is a message"; const MESSAGE = "This is a message";
let jqueryMock: { jquery: JQueryMock.JQueryMock, element: JQueryMock.JQueryElementsMock }; let jqueryMock: { jquery: JQueryMock.JQueryMock, element: JQueryMock.JQueryElementsMock };
let clock: any;
beforeEach(function() { beforeEach(function() {
jqueryMock = JQueryMock.JQueryMock(); jqueryMock = JQueryMock.JQueryMock();
clock = Sinon.useFakeTimers();
});
afterEach(function() {
clock.restore();
}); });
function should_fade_in_and_out_on_notification(notificationType: string): void { function should_fade_in_and_out_on_notification(notificationType: string): void {
const fadeInReturn = {
delay: Sinon.stub()
};
const delayReturn = { const delayReturn = {
fadeOut: Sinon.stub() fadeOut: Sinon.stub()
}; };
jqueryMock.element.fadeIn.returns(fadeInReturn);
jqueryMock.element.fadeIn.yields(); jqueryMock.element.fadeIn.yields();
delayReturn.fadeOut.yields();
fadeInReturn.delay.returns(delayReturn);
function onFadedInCallback() { function onFadedInCallback() {
Assert(jqueryMock.element.fadeIn.calledOnce); Assert(jqueryMock.element.fadeIn.calledOnce);
Assert(jqueryMock.element.addClass.calledWith(notificationType)); Assert(jqueryMock.element.addClass.calledWith(notificationType));
Assert(!jqueryMock.element.removeClass.calledWith(notificationType)); Assert(!jqueryMock.element.removeClass.calledWith(notificationType));
clock.tick(10 * 1000);
} }
function onFadedOutCallback() { function onFadedOutCallback() {
Assert(jqueryMock.element.removeClass.calledWith(notificationType)); Assert(jqueryMock.element.removeClass.calledWith(notificationType));
Assert(jqueryMock.element.fadeOut.calledOnce);
} }
const notifier = new Notifier(SELECTOR, jqueryMock.jquery as any); const notifier = new Notifier(SELECTOR, jqueryMock.jquery as any);
@ -47,9 +47,9 @@ describe("test notifier", function() {
onFadedOut: onFadedOutCallback onFadedOut: onFadedOutCallback
}); });
clock.tick(510);
Assert(jqueryMock.element.fadeIn.calledOnce); Assert(jqueryMock.element.fadeIn.calledOnce);
Assert(fadeInReturn.delay.calledOnce);
Assert(delayReturn.fadeOut.calledOnce);
} }

View File

@ -18,6 +18,7 @@ export interface JQueryElementsMock {
addClass: sinon.SinonStub; addClass: sinon.SinonStub;
removeClass: sinon.SinonStub; removeClass: sinon.SinonStub;
fadeIn: sinon.SinonStub; fadeIn: sinon.SinonStub;
fadeOut: sinon.SinonStub;
on: sinon.SinonStub; on: sinon.SinonStub;
} }
@ -36,6 +37,7 @@ export function JQueryMock(): { jquery: JQueryMock, element: JQueryElementsMock
addClass: sinon.stub(), addClass: sinon.stub(),
removeClass: sinon.stub(), removeClass: sinon.stub(),
fadeIn: sinon.stub(), fadeIn: sinon.stub(),
fadeOut: sinon.stub(),
on: sinon.stub() on: sinon.stub()
}; };
jquery.ajax = sinon.stub(); jquery.ajax = sinon.stub();

View File

@ -1,120 +1,186 @@
import Sinon = require("sinon"); import Sinon = require("sinon");
import BluebirdPromise = require("bluebird"); import BluebirdPromise = require("bluebird");
import Assert = require("assert");
import { AuthenticationRegulator } from "../../../src/server/lib/AuthenticationRegulator"; import { AuthenticationRegulator } from "../../../src/server/lib/AuthenticationRegulator";
import { UserDataStore } from "../../../src/server/lib/storage/UserDataStore";
import MockDate = require("mockdate"); import MockDate = require("mockdate");
import exceptions = require("../../../src/server/lib/Exceptions"); import exceptions = require("../../../src/server/lib/Exceptions");
import { CollectionStub } from "./mocks/storage/CollectionStub"; import { UserDataStoreStub } from "./mocks/storage/UserDataStoreStub";
import { CollectionFactoryStub } from "./mocks/storage/CollectionFactoryStub";
describe("test authentication regulator", function () { describe("test authentication regulator", function () {
let collectionFactory: CollectionFactoryStub; const USER1 = "USER1";
let collection: CollectionStub; const USER2 = "USER2";
let userDataStoreStub: UserDataStoreStub;
beforeEach(function () { beforeEach(function () {
collectionFactory = new CollectionFactoryStub(); userDataStoreStub = new UserDataStoreStub();
collection = new CollectionStub(); const dataStore: { [userId: string]: { userId: string, date: Date, isAuthenticationSuccessful: boolean }[] } = {
[USER1]: [],
[USER2]: []
};
collectionFactory.buildStub.returns(collection); userDataStoreStub.saveAuthenticationTraceStub.callsFake(function (userId, isAuthenticationSuccessful) {
dataStore[userId].unshift({
userId: userId,
date: new Date(),
isAuthenticationSuccessful: isAuthenticationSuccessful,
});
return BluebirdPromise.resolve();
}); });
it("should mark 2 authentication and regulate", function () { userDataStoreStub.retrieveLatestAuthenticationTracesStub.callsFake(function (userId, count) {
const user = "USER"; const ret = (dataStore[userId].length <= count) ? dataStore[userId] : dataStore[userId].slice(0, 3);
return BluebirdPromise.resolve(ret);
collection.insertStub.returns(BluebirdPromise.resolve());
collection.findStub.returns(BluebirdPromise.resolve([{
userId: user,
date: new Date(),
isAuthenticationSuccessful: false
}, {
userId: user,
date: new Date(),
isAuthenticationSuccessful: true
}]));
const dataStore = new UserDataStore(collectionFactory);
const regulator = new AuthenticationRegulator(dataStore, 10);
return regulator.mark(user, false)
.then(function () {
return regulator.mark(user, true);
})
.then(function () {
return regulator.regulate(user);
}); });
}); });
it("should mark 3 authentications and regulate (reject)", function (done) { afterEach(function () {
const user = "USER"; 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: false
}, {
userId: user,
date: new Date(),
isAuthenticationSuccessful: false
}]));
const dataStore = new UserDataStore(collectionFactory); function markAuthenticationAt(regulator: AuthenticationRegulator, user: string, time: string, success: boolean) {
const regulator = new AuthenticationRegulator(dataStore, 10); MockDate.set(time);
return regulator.mark(user, success);
}
regulator.mark(user, false) it("should mark 2 authentication and regulate (accept)", function () {
const regulator = new AuthenticationRegulator(userDataStoreStub, 3, 10, 10);
return regulator.mark(USER1, false)
.then(function () { .then(function () {
return regulator.mark(user, false); return regulator.mark(USER1, true);
}) })
.then(function () { .then(function () {
return regulator.mark(user, false); return regulator.regulate(USER1);
});
});
it("should mark 3 authentications and regulate (reject)", function () {
const regulator = new AuthenticationRegulator(userDataStoreStub, 3, 10, 10);
return regulator.mark(USER1, false)
.then(function () {
return regulator.mark(USER1, false);
}) })
.then(function () { .then(function () {
return regulator.regulate(user); return regulator.mark(USER1, false);
}) })
.then(function () {
return regulator.regulate(USER1);
})
.then(function () { return BluebirdPromise.reject(new Error("should not be here!")); })
.catch(exceptions.AuthenticationRegulationError, function () { .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) { it("should mark 1 failed, 1 successful and 1 failed authentications within minimum time and regulate (accept)", function () {
const user = "USER"; const regulator = new AuthenticationRegulator(userDataStoreStub, 3, 60, 30);
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);
MockDate.set("1/2/2000 00:00:00"); return markAuthenticationAt(regulator, USER1, "1/2/2000 00:00:00", false)
regulator.mark(user, false)
.then(function () { .then(function () {
MockDate.set("1/2/2000 00:00:15"); return markAuthenticationAt(regulator, USER1, "1/2/2000 00:00:10", true);
return regulator.mark(user, false);
}) })
.then(function () { .then(function () {
MockDate.set("1/2/2000 06:00:15"); return markAuthenticationAt(regulator, USER1, "1/2/2000 00:00:20", false);
return regulator.mark(user, false); })
.then(function () {
return regulator.regulate(USER1);
})
.then(function () {
return markAuthenticationAt(regulator, USER1, "1/2/2000 00:00:30", false);
})
.then(function () {
return regulator.regulate(USER1);
})
.then(function () {
return markAuthenticationAt(regulator, USER1, "1/2/2000 00:00:39", false);
})
.then(function () {
return regulator.regulate(USER1);
})
.then(function () {
return BluebirdPromise.reject(new Error("should not be here!"));
},
function () {
return BluebirdPromise.resolve();
});
});
it("should regulate user if number of failures is greater than 3 in allowed time lapse", function () {
function markAuthentications(regulator: AuthenticationRegulator, user: string) {
return markAuthenticationAt(regulator, user, "1/2/2000 00:00:00", false)
.then(function () {
return markAuthenticationAt(regulator, user, "1/2/2000 00:00:45", false);
})
.then(function () {
return markAuthenticationAt(regulator, user, "1/2/2000 00:01:05", false);
}) })
.then(function () { .then(function () {
return regulator.regulate(user); 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 () { .then(function () {
done(); return markAuthenticationAt(regulator, user, "1/2/2000 00:00:15", false);
})
.then(function () {
return markAuthenticationAt(regulator, user, "1/2/2000 00:00:25", false);
})
.then(function () {
MockDate.set("1/2/2000 00:00:54");
return regulator.regulate(user);
})
.then(function () {
return BluebirdPromise.reject(new Error("should fail at this time"));
}, function () {
MockDate.set("1/2/2000 00:00:56");
return regulator.regulate(user);
});
}
const regulator = new AuthenticationRegulator(userDataStoreStub, 4, 30, 30);
return markAuthentications(regulator, USER1);
});
it("should disable regulation when max_retries is set to 0", function () {
const maxRetries = 0;
const regulator = new AuthenticationRegulator(userDataStoreStub, maxRetries, 60, 30);
return markAuthenticationAt(regulator, USER1, "1/2/2000 00:00:00", false)
.then(function () {
return markAuthenticationAt(regulator, USER1, "1/2/2000 00:00:10", false);
})
.then(function () {
return markAuthenticationAt(regulator, USER1, "1/2/2000 00:00:15", false);
})
.then(function () {
return markAuthenticationAt(regulator, USER1, "1/2/2000 00:00:25", false);
})
.then(function () {
MockDate.set("1/2/2000 00:00:26");
return regulator.regulate(USER1);
}); });
}); });
}); });

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

@ -67,6 +67,11 @@ describe("test server configuration", function () {
password: "password" password: "password"
} }
}, },
regulation: {
max_retries: 3,
ban_time: 5 * 60,
find_time: 5 * 60
},
storage: { storage: {
local: { local: {
in_memory: true in_memory: true

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: {
@ -33,6 +38,11 @@ describe("test session configuration builder", function () {
expiration: 3600, expiration: 3600,
secret: "secret" secret: "secret"
}, },
regulation: {
max_retries: 3,
ban_time: 5 * 60,
find_time: 5 * 60
},
storage: { storage: {
local: { local: {
in_memory: true in_memory: true
@ -77,9 +87,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: {
@ -97,6 +112,11 @@ describe("test session configuration builder", function () {
port: 6379 port: 6379
} }
}, },
regulation: {
max_retries: 3,
ban_time: 5 * 60,
find_time: 5 * 60
},
storage: { storage: {
local: { local: {
in_memory: true in_memory: true

View File

@ -1,27 +1,34 @@
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: UserConfiguration = {
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"
}, },
session: { session: {
domain: "example.com", domain: "example.com",
secret: "secret", secret: "secret",
max_age: 40000 expiration: 40000
}, },
storage: { storage: {
local: { local: {
path: "/mydirectory" path: "/mydirectory"
} }
}, },
regulation: {
max_retries: 3,
find_time: 5 * 60,
ban_time: 5 * 60
},
logs_level: "debug", logs_level: "debug",
notifier: { notifier: {
gmail: { gmail: {
@ -47,26 +54,6 @@ describe("test config adapter", function() {
Assert.equal(config.port, 8080); 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(); const yaml_config = build_yaml_config();
yaml_config.session = { yaml_config.session = {

View File

@ -0,0 +1,98 @@
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: UserConfiguration = {
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",
expiration: 40000
},
storage: {
local: {
path: "/mydirectory"
}
},
regulation: {
max_retries: 3,
ban_time: 5 * 60,
find_time: 5 * 60,
},
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

@ -43,8 +43,8 @@ export class UserDataStoreStub implements IUserDataStore {
return this.saveAuthenticationTraceStub(userId, isAuthenticationSuccessful); return this.saveAuthenticationTraceStub(userId, isAuthenticationSuccessful);
} }
retrieveLatestAuthenticationTraces(userId: string, isAuthenticationSuccessful: boolean, count: number): BluebirdPromise<AuthenticationTraceDocument[]> { retrieveLatestAuthenticationTraces(userId: string, count: number): BluebirdPromise<AuthenticationTraceDocument[]> {
return this.retrieveLatestAuthenticationTracesStub(userId, isAuthenticationSuccessful, count); return this.retrieveLatestAuthenticationTracesStub(userId, count);
} }
produceIdentityValidationToken(userId: string, token: string, challenge: string, maxAge: number): BluebirdPromise<any> { produceIdentityValidationToken(userId: string, token: string, challenge: string, maxAge: number): BluebirdPromise<any> {

View File

@ -5,6 +5,7 @@ import speakeasy = require("speakeasy");
import request = require("request"); import request = require("request");
import nedb = require("nedb"); import nedb = require("nedb");
import { GlobalDependencies } from "../../../../src/types/Dependencies"; import { GlobalDependencies } from "../../../../src/types/Dependencies";
import { UserConfiguration } from "../../../../src/server/lib/configuration/Configuration";
import { TOTPSecret } from "../../../../src/types/TOTPSecret"; import { TOTPSecret } from "../../../../src/types/TOTPSecret";
import U2FMock = require("./../mocks/u2f"); import U2FMock = require("./../mocks/u2f");
import Endpoints = require("../../../../src/server/endpoints"); 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; let u2f: U2FMock.U2FMock;
beforeEach(function () { beforeEach(function () {
const config = { const config: UserConfiguration = {
port: PORT, port: PORT,
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",
}, },
@ -41,6 +41,11 @@ describe("Private pages of the server must not be accessible without session", f
secret: "session_secret", secret: "session_secret",
expiration: 50000, expiration: 50000,
}, },
regulation: {
max_retries: 3,
ban_time: 5 * 60,
find_time: 5 * 60
},
storage: { storage: {
local: { local: {
in_memory: true in_memory: true

View File

@ -5,6 +5,7 @@ import speakeasy = require("speakeasy");
import Request = require("request"); import Request = require("request");
import nedb = require("nedb"); import nedb = require("nedb");
import { GlobalDependencies } from "../../../../src/types/Dependencies"; import { GlobalDependencies } from "../../../../src/types/Dependencies";
import { UserConfiguration } from "../../../../src/server/lib/configuration/Configuration";
import { TOTPSecret } from "../../../../src/types/TOTPSecret"; import { TOTPSecret } from "../../../../src/types/TOTPSecret";
import U2FMock = require("./../mocks/u2f"); import U2FMock = require("./../mocks/u2f");
import Endpoints = require("../../../../src/server/endpoints"); 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; let u2f: U2FMock.U2FMock;
beforeEach(function () { beforeEach(function () {
const config = { const config: UserConfiguration = {
port: PORT, port: PORT,
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",
}, },
@ -46,6 +46,11 @@ describe("Public pages of the server must be accessible without session", functi
in_memory: true in_memory: true
} }
}, },
regulation: {
max_retries: 3,
ban_time: 5 * 60,
find_time: 5 * 60
},
notifier: { notifier: {
gmail: { gmail: {
username: "user@example.com", username: "user@example.com",

View File

@ -1,292 +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_name_attribute: "cn",
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_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 = {};
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_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();
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();
});
});
}
});

View File

@ -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); factory.buildStub.returns(collection);
collection.findStub.withArgs().returns(BluebirdPromise.resolve()); collection.findStub.withArgs().returns(BluebirdPromise.resolve());
const dataStore = new UserDataStore(factory); const dataStore = new UserDataStore(factory);
return dataStore.retrieveLatestAuthenticationTraces(userId, status, count) return dataStore.retrieveLatestAuthenticationTraces(userId, count)
.then(function (doc: AuthenticationTraceDocument[]) { .then(function (doc: AuthenticationTraceDocument[]) {
Assert(collection.findStub.calledOnce); Assert(collection.findStub.calledOnce);
Assert(collection.findStub.calledWith({ Assert(collection.findStub.calledWith({
userId: userId, userId: userId,
isAuthenticationSuccessful: status,
}, { date: -1 }, count)); }, { date: -1 }, count));
return BluebirdPromise.resolve(); return BluebirdPromise.resolve();
}); });
} }
it("should retrieve 3 latest failed authentication traces", function () { it("should retrieve 3 latest failed authentication traces", function () {
should_retrieve_latest_authentication_traces(3, false); should_retrieve_latest_authentication_traces(3);
});
it("should retrieve 4 latest successful authentication traces", function () {
should_retrieve_latest_authentication_traces(4, true);
}); });
}); });