Merge pull request #45 from clems4ever/ldap-bind

Fix LDAP search operation when user has no rights to search attributes in DB.
This commit is contained in:
Clément Michaud 2017-07-13 21:47:09 +02:00 committed by GitHub
commit 888bdd2bf9
55 changed files with 910 additions and 510 deletions

4
.gitignore vendored
View File

@ -13,9 +13,7 @@ src/.baseDir.ts
*.swp
*.sh
config.yml
/config.yml
npm-debug.log

View File

@ -19,14 +19,7 @@ addons:
before_install: npm install -g npm@'>=2.13.5'
script:
- grunt build-dist
- grunt docker-build
- docker-compose build
- docker-compose up -d
- sleep 5
- ./scripts/check-services.sh
- npm run int-test
- ./scripts/npm-deployment-test.sh
- ./scripts/travis.sh
after_success:
- ./scripts/docker-publish.sh

View File

@ -10,7 +10,7 @@ COPY dist/src/server /usr/src
ENV PORT=80
EXPOSE 80
VOLUME /etc/auth-server
VOLUME /var/lib/auth-server
VOLUME /etc/authelia
VOLUME /var/lib/authelia
CMD ["node", "index.js", "/etc/auth-server/config.yml"]
CMD ["node", "index.js", "/etc/authelia/config.yml"]

View File

@ -5,12 +5,12 @@ module.exports = function (grunt) {
run: {
options: {},
"build": {
cmd: "npm",
args: ['run', 'build']
cmd: "./node_modules/.bin/tsc",
args: ['-p', 'tsconfig.json']
},
"tslint": {
cmd: "npm",
args: ['run', 'tslint']
cmd: "./node_modules/.bin/tslint",
args: ['-c', 'tslint.json', '-p', 'tsconfig.json']
},
"test": {
cmd: "npm",

View File

@ -4,7 +4,7 @@
[![Build](https://travis-ci.org/clems4ever/authelia.svg?branch=master)](https://travis-ci.org/clems4ever/authelia)
**Authelia** is a complete HTTP 2-factor authentication server for proxies like
nginx. It has been made to work with NGINX auth_request module and is currently
nginx. It has been made to work with nginx [auth_request] module and is currently
used in production to secure internal services in a small docker swarm cluster.
## Features
@ -17,25 +17,53 @@ address.
## Deployment
If you don't have any LDAP and nginx setup yet, I advise you to follow the
Getting Started. That way, you will not require anything to start.
If you don't have any LDAP and/or nginx setup yet, I advise you to follow the
[Getting Started](#Getting-started) section. That way, you can test it right away
without even configure anything.
Otherwise here are the available steps to deploy on your machine.
Otherwise here are the available steps to deploy **Authelia** on your machine given
your configuration file is **/path/to/your/config.yml**.
### With NPM
npm install -g authelia
authelia /path/to/your/config.yml
### With Docker
docker pull clems4ever/authelia
docker run -v /path/to/your/config.yml:/etc/authelia/config.yml -v /path/to/data/dir:/var/lib/authelia clems4ever/authelia
where **/path/to/data/dir** is the directory where all user data will be stored.
## Getting started
The provided example is docker-based so that you can deploy and test it very
quickly. First clone the repo make sure you don't have anything listening on
port 8080 before starting.
Add the following lines to your /etc/hosts to simulate multiple subdomains
quickly.
### Pre-requisites
#### npm
Make sure you have npm and node installed on your computer.
#### Docker
Make sure you have **docker** and **docker-compose** installed on your machine.
For your information, here are the versions that have been used for testing:
docker --version
gave *Docker version 17.03.1-ce, build c6d412e*.
docker-compose --version
gave *docker-compose version 1.14.0, build c7bdf9e*.
#### Available port
Make sure you don't have anything listening on port 8080.
#### Subdomain aliases
Add the following lines to your **/etc/hosts** to alias multiple subdomains so that nginx can redirect request to the correct virtual host.
127.0.0.1 secret.test.local
127.0.0.1 secret1.test.local
@ -45,22 +73,27 @@ Add the following lines to your /etc/hosts to simulate multiple subdomains
127.0.0.1 mx2.mail.test.local
127.0.0.1 auth.test.local
Then, type the following command to build and deploy the services:
### Deployment
Deploy **Authelia** example with the following command:
npm install --only=dev
grunt build-dist
docker-compose build
docker-compose up -d
./node_modules/.bin/grunt build-dist
./scripts/deploy-example.sh
After few seconds the services should be running and you should be able to visit
[https://home.test.local:8080/](https://home.test.local:8080/).
Normally, a self-signed certificate exception should appear, it has to be
accepted before getting to the login page:
When accessing the login page, a self-signed certificate exception should appear,
it has to be trusted before you can get to the target page. The certificate
must be trusted for each subdomain, therefore it is normal to see the exception
several times.
Below is what the login page looks like:
<img src="https://raw.githubusercontent.com/clems4ever/authelia/master/images/first_factor.png" width="400">
### 1st factor: LDAP and ACL
### First factor: LDAP and ACL
An LDAP server has been deployed for you with the following credentials and
access control list:
@ -76,54 +109,55 @@ any subdomain.
- [secret1.test.local](https://secret1.test.local:8080/secret.html)
- [home.test.local](https://home.test.local:8080/secret.html)
Type them in the login page and validate. Then, the second factor page should
have appeared as shown below.
You can use them in the login page. If everything is ok, the second factor
page should appear as shown below. Otherwise you'll get an error message notifying
your credentials are wrong.
<img src="https://raw.githubusercontent.com/clems4ever/authelia/master/images/second_factor.png" width="400">
### 2nd factor: TOTP (Time-Base One Time Password)
### Second factor: TOTP (Time-Base One Time Password)
In **Authelia**, you need to register a per user TOTP secret before
authenticating. To do that, you need to click on the register button. It will
send a link to the user email address. Since this is an example, no email will
be sent, the link is rather delivered in the file
./notifications/notification.txt. Paste the link in your browser and you'll get
**./notifications/notification.txt**. Paste the link in your browser and you'll get
your secret in QRCode and Base32 formats. You can use
[Google Authenticator](https://play.google.com/store/apps/details?id=com.google.android.apps.authenticator2&hl=en)
to store them and get the generated tokens required during authentication.
[Google Authenticator]
to store them and get the generated tokens with the app.
<img src="https://raw.githubusercontent.com/clems4ever/authelia/master/images/totp.png" width="400">
### 2nd factor: U2F (Universal 2-Factor) with security keys
**Authelia** also offers authentication using U2F devices like [Yubikey](Yubikey)
USB security keys. U2F is one of the most secure authentication protocol and is
already available for accounts on Google, Facebook, Github and more.
already available for Google, Facebook, Github accounts and more.
Like TOTP, U2F requires you register your security key before authenticating
with it. To do so, click on the register button. This will send a link to the
Like TOTP, U2F requires you register your security key before authenticating.
To do so, click on the register button. This will send a link to the
user email address. Since this is an example, no email will be sent, the
link is rather delivered in the file ./notifications/notification.txt. Paste
link is rather delivered in the file **./notifications/notification.txt**. Paste
the link in your browser and you'll be asking to touch the token of your device
to register it. You can now authenticate using your U2F device by simply
touching the token.
to register. Upon successful registration, you can authenticate using your U2F
device by simply touching the token. Easy, right?!
<img src="https://raw.githubusercontent.com/clems4ever/authelia/master/images/u2f.png" width="400">
### Password reset
With **Authelia**, you can also reset your password in no time. Click on the
according button 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
email address. For the sake of the example, the email is delivered in the file
./notifications/notification.txt.
**./notifications/notification.txt**.
Paste the link in your browser and you should be able to reset the password.
<img src="https://raw.githubusercontent.com/clems4ever/authelia/master/images/reset_password.png" width="400">
### Access Control
With **Authelia**, you can define your own access control rules for restricting
the access to certain subdomains to your users. Those rules are defined in the
configuration file and can be either default, per-user or per-group policies.
the user access to some subdomains. Those rules are defined in the
configuration file and can be set either for everyone, per-user or per-group policies.
Check out the *config.template.yml* to see how they are defined.
## Documentation
@ -172,4 +206,6 @@ Follow [contributing](CONTRIBUTORS.md) file.
[TOTP]: https://en.wikipedia.org/wiki/Time-based_One-time_Password_Algorithm
[U2F]: https://www.yubico.com/about/background/fido/
[Yubikey]: https://www.yubico.com/products/yubikey-hardware/yubikey4/
[auth_request]: http://nginx.org/en/docs/http/ngx_http_auth_request_module.html
[Google Authenticator]: https://play.google.com/store/apps/details?id=com.google.android.apps.authenticator2&hl=en

View File

@ -12,7 +12,7 @@ logs_level: info
# 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://ldap
url: ldap://openldap-restriction
# The base dn for every entries
base_dn: dc=example,dc=com
@ -85,7 +85,7 @@ store_directory: /var/lib/authelia/store
notifier:
# For testing purpose, notifications can be sent in a file
filesystem:
filename: /var/lib/auth-server/notifications/notification.txt
filename: /var/lib/authelia/notifications/notification.txt
# Use your gmail account to send the notifications. You can use an app password.
# gmail:

5
docker-compose.base.yml Normal file
View File

@ -0,0 +1,5 @@
version: '2'
networks:
example-network:
driver: bridge

View File

@ -1,17 +1,10 @@
version: '2'
services:
auth:
authelia:
volumes:
- ./test:/usr/src/test
- ./dist/src/server:/usr/src
- ./node_modules:/usr/src/node_modules
- ./config.yml:/etc/auth-server/config.yml:ro
ldap-admin:
image: osixia/phpldapadmin:0.6.11
ports:
- 9090:80
environment:
- PHPLDAPADMIN_LDAP_HOSTS=ldap
- PHPLDAPADMIN_HTTPS=false
- ./config.yml:/etc/authelia/config.yml:ro
networks:
- example-network

View File

@ -1,37 +1,11 @@
version: '2'
services:
auth:
authelia:
build: .
depends_on:
- ldap
restart: always
volumes:
- ./config.template.yml:/etc/auth-server/config.yml:ro
- ./notifications:/var/lib/auth-server/notifications
- ./config.template.yml:/etc/authelia/config.yml:ro
- ./notifications:/var/lib/authelia/notifications
networks:
- example-network
ldap:
image: dinkel/openldap
environment:
- SLAPD_ORGANISATION=MyCompany
- SLAPD_DOMAIN=example.com
- SLAPD_PASSWORD=password
- SLAPD_ADDITIONAL_MODULES=memberof
- SLAPD_ADDITIONAL_SCHEMAS=openldap
- SLAPD_FORCE_RECONFIGURE=true
expose:
- "389"
volumes:
- ./example/ldap:/etc/ldap.dist/prepopulate
nginx:
image: nginx:alpine
volumes:
- ./example/nginx_conf/nginx.conf:/etc/nginx/nginx.conf
- ./example/nginx_conf/index.html:/usr/share/nginx/html/index.html
- ./example/nginx_conf/secret.html:/usr/share/nginx/html/secret.html
- ./example/nginx_conf/ssl:/etc/ssl
depends_on:
- auth
ports:
- "8080:443"

9
example/ldap/Dockerfile Normal file
View File

@ -0,0 +1,9 @@
FROM clems4ever/openldap
ENV SLAPD_ORGANISATION=MyCompany
ENV SLAPD_DOMAIN=example.com
ENV SLAPD_PASSWORD=password
ENV SLAPD_CONFIG_PASSWORD=password
ENV SLAPD_ADDITIONAL_MODULES=memberof
ENV SLAPD_ADDITIONAL_SCHEMAS=openldap
ENV SLAPD_FORCE_RECONFIGURE=true

View File

@ -0,0 +1,4 @@
olcAccess: {0}to attrs=userPassword,shadowLastChange by self write by anonymou
s auth by * none
# olcAccess: {1}to dn.base="" by * read
# olcAccess: {2}to * by * read

View File

@ -25,7 +25,7 @@ dn: cn=john,ou=users,dc=example,dc=com
cn: john
objectclass: inetOrgPerson
objectclass: top
mail: clement.michaud34@gmail.com
mail: john.doe@example.com
sn: John Doe
userpassword: {SHA}W6ph5Mm5Pz8GgiULbPgzG37mj9g=
@ -45,18 +45,3 @@ mail: bob.dylan@example.com
sn: Bob Dylan
userpassword: {SHA}W6ph5Mm5Pz8GgiULbPgzG37mj9g=
# dn: uid=jack,ou=users,dc=example,dc=com
# cn: jack
# gidnumber: 501
# givenname: Jack
# homedirectory: /home/jack
# loginshell: /bin/sh
# objectclass: inetOrgPerson
# objectclass: posixAccount
# objectclass: top
# mail: jack.daniels@example.com
# sn: Jack Daniels
# uid: jack
# uidnumber: 1001
# userpassword: {SHA}W6ph5Mm5Pz8GgiULbPgzG37mj9g=
#

View File

@ -0,0 +1,11 @@
version: '2'
services:
openldap-admin:
image: osixia/phpldapadmin:0.6.11
ports:
- 9090:80
environment:
- PHPLDAPADMIN_LDAP_HOSTS=openldap
- PHPLDAPADMIN_HTTPS=false
networks:
- example-network

View File

@ -0,0 +1,10 @@
version: '2'
services:
openldap:
build: ./example/ldap
volumes:
- ./example/ldap/base.ldif:/etc/ldap.dist/prepopulate/base.ldif
- ./example/ldap/access.rules:/etc/ldap.dist/prepopulate/access.rules
networks:
- example-network

View File

@ -0,0 +1,24 @@
version: '2'
services:
nginx:
image: nginx:alpine
volumes:
- ./example/nginx/index.html:/usr/share/nginx/html/index.html
- ./example/nginx/secret.html:/usr/share/nginx/html/secret.html
- ./example/nginx/ssl:/etc/ssl
- ./example/nginx/nginx.conf:/etc/nginx/nginx.conf
ports:
- "8080:443"
depends_on:
- authelia
networks:
example-network:
aliases:
- home.test.local
- secret.test.local
- secret1.test.local
- secret2.test.local
- mx1.mail.test.local
- mx2.mail.test.local
- auth.test.local

View File

@ -36,7 +36,7 @@ http {
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_pass http://auth/;
proxy_pass http://authelia/;
proxy_intercept_errors on;
@ -68,7 +68,7 @@ http {
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Host $http_host;
proxy_pass http://auth/verify;
proxy_pass http://authelia/verify;
}
location = /secret.html {

View File

@ -4,6 +4,6 @@
</head>
<body>
This is a very important secret!<br/>
Go back to <a href="https://home.test.local:8080/">home page</a>.
Go back to <a href="https://home.test.local/">home page</a>.
</body>
</html>

View File

@ -8,10 +8,7 @@
},
"scripts": {
"test": "./node_modules/.bin/mocha --compilers ts:ts-node/register --recursive test/client test/server",
"int-test": "./node_modules/.bin/mocha --compilers ts:ts-node/register --recursive test/integration",
"cover": "NODE_ENV=test nyc npm t",
"build": "tsc",
"tslint": "tslint -c tslint.json -p tsconfig.json",
"serve": "node dist/server/index.js"
},
"repository": {

5
scripts/dc-example.sh Executable file
View File

@ -0,0 +1,5 @@
#!/bin/bash
set -e
docker-compose -f docker-compose.base.yml -f docker-compose.yml -f example/nginx/docker-compose.yml -f example/ldap/docker-compose.yml $*

5
scripts/dc-test.sh Executable file
View File

@ -0,0 +1,5 @@
#!/bin/bash
set -e
docker-compose -f docker-compose.base.yml -f example/ldap/docker-compose.yml -f test/integration/docker-compose.yml $*

4
scripts/deploy-example.sh Executable file
View File

@ -0,0 +1,4 @@
#!/bin/bash
./scripts/dc-example.sh build
./scripts/dc-example.sh up -d

View File

@ -16,6 +16,7 @@ function deploy_on_dockerhub {
docker login -u="$DOCKER_USERNAME" -p="$DOCKER_PASSWORD";
echo "Docker image $IMAGE_WITH_TAG will be deployed on Dockerhub."
docker build -t $IMAGE_NAME .
docker tag $IMAGE_NAME $IMAGE_WITH_TAG;
docker push $IMAGE_WITH_TAG;
echo "Docker image deployed successfully."

View File

@ -1,5 +1,7 @@
#!/bin/bash
set -e
NPM_UNPACK_DIR=/tmp/npm-unpack
echo "--- Packing npm package into a tarball"

22
scripts/run-int-test.sh Executable file
View File

@ -0,0 +1,22 @@
#!/bin/bash
set -e
echo "Build services images..."
./scripts/dc-test.sh build
echo "Start services..."
./scripts/dc-test.sh up -d authelia nginx openldap
sleep 3
docker ps -a
echo "Display services logs..."
./scripts/dc-test.sh logs authelia
./scripts/dc-test.sh logs nginx
./scripts/dc-test.sh logs openldap
echo "Run integration tests..."
./scripts/dc-test.sh run --rm --name int-test int-test
echo "Shutdown services..."
./scripts/dc-test.sh down

15
scripts/run-staging.sh Executable file
View File

@ -0,0 +1,15 @@
#!/bin/bash
set -e
# Build production environment and set it up
./scripts/dc-example.sh build
./scripts/dc-example.sh up -d
# Wait for services to be running
sleep 5
# Check if services are correctly running
./scripts/check-services.sh
./scripts/dc-example.sh down

21
scripts/travis.sh Executable file
View File

@ -0,0 +1,21 @@
#!/bin/bash
set -e
docker --version
docker-compose --version
# Run unit tests
grunt test
# Build the app from Typescript and package
grunt build-dist
# Run integration tests
./scripts/run-int-test.sh
# Test staging environment
./scripts/run-staging.sh
# Test npm deployment before actual deployment
./scripts/npm-deployment-test.sh

3
scripts/undeploy-example.sh Executable file
View File

@ -0,0 +1,3 @@
#!/bin/bash
./scripts/dc-example.sh down

View File

@ -2,6 +2,8 @@
import * as ObjectPath from "object-path";
import { AppConfiguration, UserConfiguration, NotifierConfiguration, ACLConfiguration, LdapConfiguration } from "./../../types/Configuration";
const LDAP_URL_ENV_VARIABLE = "LDAP_URL";
function get_optional<T>(config: object, path: string, default_value: T): T {
let entry = default_value;
@ -17,26 +19,36 @@ function ensure_key_existence(config: object, path: string): void {
}
}
export default class ConfigurationAdapter {
static adapt(yaml_config: UserConfiguration): AppConfiguration {
ensure_key_existence(yaml_config, "ldap");
ensure_key_existence(yaml_config, "session.secret");
function adaptFromUserConfiguration(userConfiguration: UserConfiguration): AppConfiguration {
ensure_key_existence(userConfiguration, "ldap");
ensure_key_existence(userConfiguration, "session.secret");
const port = ObjectPath.get(yaml_config, "port", 8080);
const port = ObjectPath.get(userConfiguration, "port", 8080);
return {
port: port,
ldap: ObjectPath.get<object, LdapConfiguration>(yaml_config, "ldap"),
ldap: ObjectPath.get<object, LdapConfiguration>(userConfiguration, "ldap"),
session: {
domain: ObjectPath.get<object, string>(yaml_config, "session.domain"),
secret: ObjectPath.get<object, string>(yaml_config, "session.secret"),
expiration: get_optional<number>(yaml_config, "session.expiration", 3600000), // in ms
domain: ObjectPath.get<object, string>(userConfiguration, "session.domain"),
secret: ObjectPath.get<object, string>(userConfiguration, "session.secret"),
expiration: get_optional<number>(userConfiguration, "session.expiration", 3600000), // in ms
},
store_directory: get_optional<string>(yaml_config, "store_directory", undefined),
logs_level: get_optional<string>(yaml_config, "logs_level", "info"),
notifier: ObjectPath.get<object, NotifierConfiguration>(yaml_config, "notifier"),
access_control: ObjectPath.get<object, ACLConfiguration>(yaml_config, "access_control")
store_directory: get_optional<string>(userConfiguration, "store_directory", undefined),
logs_level: get_optional<string>(userConfiguration, "logs_level", "info"),
notifier: ObjectPath.get<object, NotifierConfiguration>(userConfiguration, "notifier"),
access_control: ObjectPath.get<object, ACLConfiguration>(userConfiguration, "access_control")
};
}
export default class ConfigurationAdapter {
static adapt(userConfiguration: UserConfiguration): AppConfiguration {
const appConfiguration = adaptFromUserConfiguration(userConfiguration);
const ldapUrl = process.env[LDAP_URL_ENV_VARIABLE];
if (ldapUrl)
appConfiguration.ldap.url = ldapUrl;
return appConfiguration;
}
}

View File

@ -1,8 +1,9 @@
import express = require("express");
import { Winston } from "winston";
import BluebirdPromise = require("bluebird");
function replyWithError(res: express.Response, code: number, logger: Winston) {
return function (err: Error) {
function replyWithError(res: express.Response, code: number, logger: Winston): (err: Error) => void {
return function (err: Error): void {
logger.error("Reply with error %d: %s", code, err);
res.status(code);
res.send();

View File

@ -18,7 +18,7 @@ export class LdapClient {
private options: LdapConfiguration;
private ldapjs: Ldapjs;
private logger: Winston;
private client: ldapjs.ClientAsync;
private adminClient: ldapjs.ClientAsync;
constructor(options: LdapConfiguration, ldapjs: Ldapjs, logger: Winston) {
this.options = options;
@ -28,100 +28,123 @@ export class LdapClient {
this.connect();
}
connect(): void {
const ldap_client = this.ldapjs.createClient({
private createClient(): ldapjs.ClientAsync {
const ldapClient = this.ldapjs.createClient({
url: this.options.url,
reconnect: true
});
ldap_client.on("error", function (err: Error) {
ldapClient.on("error", function (err: Error) {
console.error("LDAP Error:", err.message);
});
this.client = BluebirdPromise.promisifyAll(ldap_client) as ldapjs.ClientAsync;
return BluebirdPromise.promisifyAll(ldapClient) as ldapjs.ClientAsync;
}
private build_user_dn(username: string): string {
let user_name_attr = this.options.user_name_attribute;
// if not provided, default to cn
if (!user_name_attr) user_name_attr = "cn";
connect(): BluebirdPromise<void> {
const userDN = this.options.user;
const password = this.options.password;
const additional_user_dn = this.options.additional_user_dn;
this.adminClient = this.createClient();
return this.adminClient.bindAsync(userDN, password);
}
private buildUserDN(username: string): string {
let userNameAttribute = this.options.user_name_attribute;
// if not provided, default to cn
if (!userNameAttribute) userNameAttribute = "cn";
const additionalUserDN = this.options.additional_user_dn;
const base_dn = this.options.base_dn;
let user_dn = util.format("%s=%s", user_name_attr, username);
if (additional_user_dn) user_dn += util.format(",%s", additional_user_dn);
user_dn += util.format(",%s", base_dn);
return user_dn;
let userDN = util.format("%s=%s", userNameAttribute, username);
if (additionalUserDN) userDN += util.format(",%s", additionalUserDN);
userDN += util.format(",%s", base_dn);
return userDN;
}
bind(username: string, password: string): BluebirdPromise<void> {
const user_dn = this.build_user_dn(username);
checkPassword(username: string, password: string): BluebirdPromise<void> {
const userDN = this.buildUserDN(username);
const that = this;
const ldapClient = this.createClient();
this.logger.debug("LDAP: Bind user %s", user_dn);
return this.client.bindAsync(user_dn, password)
this.logger.debug("LDAP: Check password by binding user '%s'", userDN);
return ldapClient.bindAsync(userDN, password)
.then(function () {
that.logger.debug("LDAP: Unbind user '%s'", userDN);
return ldapClient.unbindAsync();
})
.error(function (err: Error) {
throw new exceptions.LdapBindError(err.message);
return BluebirdPromise.reject(new exceptions.LdapBindError(err.message));
});
}
private search_in_ldap(base: string, query: ldapjs.SearchOptions): BluebirdPromise<any> {
this.logger.debug("LDAP: Search for %s in %s", JSON.stringify(query), base);
return new BluebirdPromise((resolve, reject) => {
this.client.searchAsync(base, query)
private search(base: string, query: ldapjs.SearchOptions): BluebirdPromise<any> {
const that = this;
that.logger.debug("LDAP: Search for '%s' in '%s'", JSON.stringify(query), base);
return that.adminClient.searchAsync(base, query)
.then(function (res: EventEmitter) {
const doc: SearchEntry[] = [];
return new BluebirdPromise((resolve, reject) => {
res.on("searchEntry", function (entry: SearchEntry) {
that.logger.debug("Entry retrieved from LDAP is '%s'", JSON.stringify(entry.object));
doc.push(entry.object);
});
res.on("error", function (err: Error) {
that.logger.error("LDAP: Error received during search '%s'.", JSON.stringify(err));
reject(new exceptions.LdapSearchError(err.message));
});
res.on("end", function () {
that.logger.debug("LDAP: Result of search is '%s'.", JSON.stringify(doc));
resolve(doc);
});
});
})
.catch(function (err: Error) {
reject(new exceptions.LdapSearchError(err.message));
});
return BluebirdPromise.reject(new exceptions.LdapSearchError(err.message));
});
}
get_groups(username: string): BluebirdPromise<string[]> {
const user_dn = this.build_user_dn(username);
retrieveGroups(username: string): BluebirdPromise<string[]> {
const userDN = this.buildUserDN(username);
const password = this.options.password;
let group_name_attr = this.options.group_name_attribute;
if (!group_name_attr) group_name_attr = "cn";
let groupNameAttribute = this.options.group_name_attribute;
if (!groupNameAttribute) groupNameAttribute = "cn";
const additional_group_dn = this.options.additional_group_dn;
const additionalGroupDN = this.options.additional_group_dn;
const base_dn = this.options.base_dn;
let group_dn = base_dn;
if (additional_group_dn)
group_dn = util.format("%s,", additional_group_dn) + group_dn;
let groupDN = base_dn;
if (additionalGroupDN)
groupDN = util.format("%s,", additionalGroupDN) + groupDN;
const query = {
scope: "sub",
attributes: [group_name_attr],
filter: "member=" + user_dn
attributes: [groupNameAttribute],
filter: "member=" + userDN
};
const that = this;
this.logger.debug("LDAP: get groups of user %s", username);
return this.search_in_ldap(group_dn, query)
const groups: string[] = [];
return that.search(groupDN, query)
.then(function (docs) {
const groups = [];
for (let i = 0; i < docs.length; ++i) {
groups.push(docs[i].cn);
}
that.logger.debug("LDAP: got groups %s", groups);
that.logger.debug("LDAP: got groups '%s'", groups);
})
.then(function () {
return BluebirdPromise.resolve(groups);
});
}
get_emails(username: string): BluebirdPromise<string[]> {
retrieveEmails(username: string): BluebirdPromise<string[]> {
const that = this;
const user_dn = this.build_user_dn(username);
const user_dn = this.buildUserDN(username);
const query = {
scope: "base",
@ -129,8 +152,8 @@ export class LdapClient {
attributes: ["mail"]
};
this.logger.debug("LDAP: get emails of user %s", username);
return this.search_in_ldap(user_dn, query)
this.logger.debug("LDAP: get emails of user '%s'", username);
return this.search(user_dn, query)
.then(function (docs) {
const emails = [];
for (let i = 0; i < docs.length; ++i) {
@ -140,15 +163,15 @@ export class LdapClient {
emails.concat(docs[i].mail);
}
}
that.logger.debug("LDAP: got emails %s", emails);
that.logger.debug("LDAP: got emails '%s'", emails);
return BluebirdPromise.resolve(emails);
});
}
update_password(username: string, new_password: string): BluebirdPromise<void> {
const user_dn = this.build_user_dn(username);
updatePassword(username: string, newPassword: string): BluebirdPromise<void> {
const user_dn = this.buildUserDN(username);
const encoded_password = Dovehash.encode("SSHA", new_password);
const encoded_password = Dovehash.encode("SSHA", newPassword);
const change = {
operation: "replace",
modification: {
@ -157,13 +180,12 @@ export class LdapClient {
};
const that = this;
this.logger.debug("LDAP: update password of user %s", username);
this.logger.debug("LDAP: update password of user '%s'", username);
this.logger.debug("LDAP: bind admin");
return this.client.bindAsync(this.options.user, this.options.password)
.then(function () {
that.logger.debug("LDAP: modify password");
return that.client.modifyAsync(user_dn, change);
return that.adminClient.modifyAsync(user_dn, change)
.then(function () {
return that.adminClient.unbindAsync();
});
}
}

View File

@ -50,6 +50,7 @@ export default class Server {
// by default the level of logs is info
deps.winston.level = config.logs_level;
console.log("Log level = ", deps.winston.level);
deps.winston.debug("Authelia configuration is %s", JSON.stringify(config, undefined, 2));
ServerVariables.fill(app, config, deps);

View File

@ -35,7 +35,7 @@ export class GMailNotifier extends INotifier {
};
const mailOptions = {
from: "auth-server@open-intent.io",
from: "authelia@authelia.com",
to: identity.email,
subject: subject,
html: ejs.render(email_template, d)

View File

@ -35,21 +35,28 @@ export default function (req: express.Request, res: express.Response): BluebirdP
return regulator.regulate(username)
.then(function () {
return ldap.bind(username, password);
logger.info("1st factor: No regulation applied.");
return ldap.checkPassword(username, password);
})
.then(function () {
logger.info("1st factor: LDAP binding successful");
authSession.userid = username;
authSession.first_factor = true;
logger.info("1st factor: LDAP binding successful");
logger.debug("1st factor: Retrieve email from LDAP");
return BluebirdPromise.join(ldap.get_emails(username), ldap.get_groups(username));
return BluebirdPromise.join(ldap.retrieveEmails(username), ldap.retrieveGroups(username));
})
.then(function (data: [string[], string[]]) {
const emails: string[] = data[0];
const groups: string[] = data[1];
if (!emails && emails.length <= 0) throw new Error("No email found");
if (!emails || emails.length <= 0) {
const errMessage = "No emails found. The user should have at least one email address to reset password.";
logger.error("1s factor: %s", errMessage);
return BluebirdPromise.reject(new Error(errMessage));
}
logger.debug("1st factor: Retrieved email are %s", emails);
logger.debug("1st factor: Retrieved groups are %s", groups);
authSession.email = emails[0];
authSession.groups = groups;
@ -61,7 +68,7 @@ export default function (req: express.Request, res: express.Response): BluebirdP
.catch(exceptions.LdapSearchError, ErrorReplies.replyWithError500(res, logger))
.catch(exceptions.LdapBindError, function (err: Error) {
regulator.mark(username, false);
ErrorReplies.replyWithError401(res, logger)(err);
return ErrorReplies.replyWithError401(res, logger)(err);
})
.catch(exceptions.AuthenticationRegulationError, ErrorReplies.replyWithError403(res, logger))
.catch(exceptions.DomainAccessDenied, ErrorReplies.replyWithError401(res, logger))

View File

@ -26,7 +26,7 @@ export default function (req: express.Request, res: express.Response): BluebirdP
logger.info("POST reset-password: User %s wants to reset his/her password", userid);
return ldap.update_password(userid, new_password)
return ldap.updatePassword(userid, new_password)
.then(function () {
logger.info("POST reset-password: Password reset for user '%s'", userid);
AuthenticationSession.reset(req);

View File

@ -26,7 +26,7 @@ export default class PasswordResetHandler implements IdentityValidable {
return BluebirdPromise.reject(new exceptions.AccessDeniedError("No user id provided"));
const ldap = ServerVariables.getLdapClient(req.app);
return ldap.get_emails(userid)
return ldap.retrieveEmails(userid)
.then(function (emails: string[]) {
if (!emails && emails.length <= 0) throw new Error("No email found");

View File

@ -5,6 +5,7 @@ import { EventEmitter } from "events";
declare module "ldapjs" {
export interface ClientAsync {
bindAsync(username: string, password: string): BluebirdPromise<void>;
unbindAsync(): BluebirdPromise<void>;
searchAsync(base: string, query: ldapjs.SearchOptions): BluebirdPromise<EventEmitter>;
modifyAsync(userdn: string, change: ldapjs.Change): BluebirdPromise<void>;
}

View File

@ -0,0 +1,5 @@
FROM node:7-alpine
WORKDIR /usr/src
CMD ["./node_modules/.bin/mocha", "--compilers", "ts:ts-node/register", "--recursive", "test/integration"]

View File

@ -0,0 +1,164 @@
import Request = require("request");
import Assert = require("assert");
import Speakeasy = require("speakeasy");
import BluebirdPromise = require("bluebird");
import Util = require("util");
import Sinon = require("sinon");
import Endpoints = require("../../src/server/endpoints");
const EXEC_PATH = "./dist/src/server/index.js";
const CONFIG_PATH = "./test/integration/config.yml";
const j = Request.jar();
const request: typeof Request = <typeof Request>BluebirdPromise.promisifyAll(Request.defaults({ jar: j }));
process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
const DOMAIN = "test.local";
const PORT = 8080;
const HOME_URL = Util.format("https://%s.%s:%d", "home", DOMAIN, PORT);
const SECRET_URL = Util.format("https://%s.%s:%d", "secret", DOMAIN, PORT);
const SECRET1_URL = Util.format("https://%s.%s:%d", "secret1", DOMAIN, PORT);
const SECRET2_URL = Util.format("https://%s.%s:%d", "secret2", DOMAIN, PORT);
const MX1_URL = Util.format("https://%s.%s:%d", "mx1.mail", DOMAIN, PORT);
const MX2_URL = Util.format("https://%s.%s:%d", "mx2.mail", DOMAIN, PORT);
const BASE_AUTH_URL = Util.format("https://%s.%s:%d", "auth", DOMAIN, PORT);
function waitFor(ms: number): BluebirdPromise<{}> {
return new BluebirdPromise(function (resolve, reject) {
setTimeout(function () {
resolve();
}, ms);
});
}
describe("test the server", function () {
let home_page: string;
let login_page: string;
before(function () {
const home_page_promise = getHomePage()
.then(function (data) {
home_page = data.body;
});
const login_page_promise = getLoginPage()
.then(function (data) {
login_page = data.body;
});
return BluebirdPromise.all([home_page_promise,
login_page_promise]);
});
after(function () {
});
function str_contains(str: string, pattern: string) {
return str.indexOf(pattern) != -1;
}
function home_page_contains(pattern: string) {
return str_contains(home_page, pattern);
}
it("should serve a correct home page", function () {
Assert(home_page_contains(BASE_AUTH_URL + Endpoints.LOGOUT_GET + "?redirect=" + HOME_URL + "/"));
Assert(home_page_contains(HOME_URL + "/secret.html"));
Assert(home_page_contains(SECRET_URL + "/secret.html"));
Assert(home_page_contains(SECRET1_URL + "/secret.html"));
Assert(home_page_contains(SECRET2_URL + "/secret.html"));
Assert(home_page_contains(MX1_URL + "/secret.html"));
Assert(home_page_contains(MX2_URL + "/secret.html"));
});
it("should serve the login page", function () {
return getPromised(BASE_AUTH_URL + Endpoints.FIRST_FACTOR_GET)
.then(function (data: Request.RequestResponse) {
Assert.equal(data.statusCode, 200);
});
});
it("should serve the homepage", function () {
return getPromised(HOME_URL + "/")
.then(function (data: Request.RequestResponse) {
Assert.equal(data.statusCode, 200);
});
});
it("should redirect when logout", function () {
return getPromised(BASE_AUTH_URL + Endpoints.LOGOUT_GET + "?redirect=" + HOME_URL)
.then(function (data: Request.RequestResponse) {
Assert.equal(data.statusCode, 200);
Assert.equal(data.body, home_page);
});
});
it("should be redirected to the login page when accessing secret while not authenticated", function () {
return getPromised(HOME_URL + "/secret.html")
.then(function (data: Request.RequestResponse) {
Assert.equal(data.statusCode, 200);
Assert.equal(data.body, login_page);
});
});
it.skip("should fail the first factor", function () {
return postPromised(BASE_AUTH_URL + Endpoints.FIRST_FACTOR_POST, {
form: {
username: "admin",
password: "password",
}
})
.then(function (data: Request.RequestResponse) {
Assert.equal(data.body, "Bad credentials");
});
});
function login_as(username: string, password: string) {
return postPromised(BASE_AUTH_URL + Endpoints.FIRST_FACTOR_POST, {
form: {
username: "john",
password: "password",
}
})
.then(function (data: Request.RequestResponse) {
Assert.equal(data.statusCode, 302);
return BluebirdPromise.resolve();
});
}
it("should succeed the first factor", function () {
return login_as("john", "password");
});
describe("test ldap connection", function () {
it("should not fail after inactivity", function () {
const clock = Sinon.useFakeTimers();
return login_as("john", "password")
.then(function () {
clock.tick(3600000 * 24); // 24 hour
return login_as("john", "password");
})
.then(function () {
clock.restore();
return BluebirdPromise.resolve();
});
});
});
});
function getPromised(url: string) {
return request.getAsync(url);
}
function postPromised(url: string, body: Object) {
return request.postAsync(url, body);
}
function getHomePage(): BluebirdPromise<Request.RequestResponse> {
return getPromised(HOME_URL + "/");
}
function getLoginPage(): BluebirdPromise<Request.RequestResponse> {
return getPromised(BASE_AUTH_URL + Endpoints.FIRST_FACTOR_GET);
}

View File

@ -0,0 +1,94 @@
# 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_user_dn: ou=users
# The user name attribute of users. Might uid for FreeIPA. 'cn' by default.
user_name_attribute: cn
# An additional dn to define the scope of groups
additional_group_dn: ou=groups
# The group name attribute of group. 'cn' by default.
group_name_attribute: cn
# 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 default policy
# is applied, i.e., access is allowed to anyone. Otherwise restrictions follow
# the rules defined below.
# If no rule is provided, all domains are denied.
#
# '*' means 'any' subdomains and matches any string. It must stand at the
# beginning of the pattern.
access_control:
default:
- home.test.local
groups:
admin:
- '*.test.local'
dev:
- secret.test.local
- secret2.test.local
users:
harry:
- secret1.test.local
bob:
- '*.mail.test.local'
# Configuration of session cookies
#
# _secret_ the secret to encrypt session cookies
# _expiration_ the time before cookies expire
# _domain_ the domain to protect.
# Note: the authenticator must also be in that domain. If empty, the cookie
# is restricted to the subdomain of the issuer.
session:
secret: unsecure_secret
expiration: 3600000
domain: test.local
# The directory where the DB files will be saved
store_directory: /var/lib/authelia/store
# Notifications are sent to users when they require a password reset, a u2f
# registration or a TOTP registration.
# Use only one available configuration: filesystem, gmail
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
# password: password

View File

@ -0,0 +1,41 @@
version: '2'
services:
authelia:
image: node:7-alpine
command: node /usr/src/dist/src/server/index.js /etc/authelia/config.yml
volumes:
- ./:/usr/src
- ./test/integration/config.yml:/etc/authelia/config.yml:ro
networks:
- example-network
int-test:
build: ./test/integration
volumes:
- ./:/usr/src
networks:
- example-network
nginx:
image: nginx:alpine
volumes:
- ./example/nginx/index.html:/usr/share/nginx/html/index.html
- ./example/nginx/secret.html:/usr/share/nginx/html/secret.html
- ./example/nginx/ssl:/etc/ssl
- ./test/integration/nginx.conf:/etc/nginx/nginx.conf
expose:
- "8080"
depends_on:
- authelia
networks:
example-network:
aliases:
- home.test.local
- secret.test.local
- secret1.test.local
- secret2.test.local
- mx1.mail.test.local
- mx2.mail.test.local
- auth.test.local

View File

@ -0,0 +1,86 @@
# nginx-sso - example nginx config
#
# (c) 2015 by Johannes Gilger <heipei@hackvalue.de>
#
# This is an example config for using nginx with the nginx-sso cookie system.
# For simplicity, this config sets up two fictional vhosts that you can use to
# test against both components of the nginx-sso system: ssoauth & ssologin.
# In a real deployment, these vhosts would be separate hosts.
#user nobody;
worker_processes 1;
#error_log logs/error.log;
#error_log logs/error.log notice;
#error_log logs/error.log info;
#pid logs/nginx.pid;
events {
worker_connections 1024;
}
http {
server {
listen 8080 ssl;
server_name auth.test.local localhost;
ssl on;
ssl_certificate /etc/ssl/server.crt;
ssl_certificate_key /etc/ssl/server.key;
location / {
proxy_set_header X-Original-URI $request_uri;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_pass http://authelia/;
proxy_intercept_errors on;
error_page 401 = /error/401;
error_page 403 = /error/403;
error_page 404 = /error/404;
}
}
server {
listen 8080 ssl;
root /usr/share/nginx/html;
server_name secret1.test.local secret2.test.local secret.test.local
home.test.local mx1.mail.test.local mx2.mail.test.local;
ssl on;
ssl_certificate /etc/ssl/server.crt;
ssl_certificate_key /etc/ssl/server.key;
error_page 401 = @error401;
location @error401 {
return 302 https://auth.test.local:8080;
}
location /auth_verify {
internal;
proxy_set_header X-Original-URI $request_uri;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Host $http_host;
proxy_pass http://authelia/verify;
}
location = /secret.html {
auth_request /auth_verify;
auth_request_set $user $upstream_http_x_remote_user;
proxy_set_header X-Forwarded-User $user;
auth_request_set $groups $upstream_http_remote_groups;
proxy_set_header Remote-Groups $groups;
auth_request_set $expiry $upstream_http_remote_expiry;
proxy_set_header Remote-Expiry $expiry;
}
}
}

View File

@ -1,157 +0,0 @@
import request_ = require("request");
import assert = require("assert");
import speakeasy = require("speakeasy");
import BluebirdPromise = require("bluebird");
import util = require("util");
import sinon = require("sinon");
import Endpoints = require("../../src/server/endpoints");
const j = request_.jar();
const request: typeof request_ = <typeof request_>BluebirdPromise.promisifyAll(request_.defaults({ jar: j }));
process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
const AUTHELIA_HOST = "nginx";
const DOMAIN = "test.local";
const PORT = 8080;
const HOME_URL = util.format("https://%s.%s:%d", "home", DOMAIN, PORT);
const SECRET_URL = util.format("https://%s.%s:%d", "secret", DOMAIN, PORT);
const SECRET1_URL = util.format("https://%s.%s:%d", "secret1", DOMAIN, PORT);
const SECRET2_URL = util.format("https://%s.%s:%d", "secret2", DOMAIN, PORT);
const MX1_URL = util.format("https://%s.%s:%d", "mx1.mail", DOMAIN, PORT);
const MX2_URL = util.format("https://%s.%s:%d", "mx2.mail", DOMAIN, PORT);
const BASE_AUTH_URL = util.format("https://%s.%s:%d", "auth", DOMAIN, PORT);
describe("test the server", function () {
let home_page: string;
let login_page: string;
before(function () {
const home_page_promise = getHomePage()
.then(function (data) {
home_page = data.body;
});
const login_page_promise = getLoginPage()
.then(function (data) {
login_page = data.body;
});
return BluebirdPromise.all([home_page_promise,
login_page_promise]);
});
function str_contains(str: string, pattern: string) {
return str.indexOf(pattern) != -1;
}
function home_page_contains(pattern: string) {
return str_contains(home_page, pattern);
}
it("should serve a correct home page", function () {
assert(home_page_contains(BASE_AUTH_URL + Endpoints.LOGOUT_GET + "?redirect=" + HOME_URL + "/"));
assert(home_page_contains(HOME_URL + "/secret.html"));
assert(home_page_contains(SECRET_URL + "/secret.html"));
assert(home_page_contains(SECRET1_URL + "/secret.html"));
assert(home_page_contains(SECRET2_URL + "/secret.html"));
assert(home_page_contains(MX1_URL + "/secret.html"));
assert(home_page_contains(MX2_URL + "/secret.html"));
});
it("should serve the login page", function (done) {
getPromised(BASE_AUTH_URL + Endpoints.FIRST_FACTOR_GET + "?redirect=/")
.then(function (data: request_.RequestResponse) {
assert.equal(data.statusCode, 200);
done();
});
});
it("should serve the homepage", function (done) {
getPromised(HOME_URL + "/")
.then(function (data: request_.RequestResponse) {
assert.equal(data.statusCode, 200);
done();
});
});
it("should redirect when logout", function (done) {
getPromised(BASE_AUTH_URL + Endpoints.LOGOUT_GET + "?redirect=" + HOME_URL)
.then(function (data: request_.RequestResponse) {
assert.equal(data.statusCode, 200);
assert.equal(data.body, home_page);
done();
});
});
it("should be redirected to the login page when accessing secret while not authenticated", function (done) {
const url = HOME_URL + "/secret.html";
getPromised(url)
.then(function (data: request_.RequestResponse) {
assert.equal(data.statusCode, 200);
assert.equal(data.body, login_page);
done();
});
});
it.skip("should fail the first factor", function (done) {
postPromised(BASE_AUTH_URL + Endpoints.FIRST_FACTOR_POST, {
form: {
username: "admin",
password: "password",
}
})
.then(function (data: request_.RequestResponse) {
assert.equal(data.body, "Bad credentials");
done();
});
});
function login_as(username: string, password: string) {
return postPromised(BASE_AUTH_URL + Endpoints.FIRST_FACTOR_POST, {
form: {
username: "john",
password: "password",
}
})
.then(function (data: request_.RequestResponse) {
assert.equal(data.statusCode, 302);
return BluebirdPromise.resolve();
});
}
it("should succeed the first factor", function () {
return login_as("john", "password");
});
describe("test ldap connection", function () {
it("should not fail after inactivity", function () {
const clock = sinon.useFakeTimers();
return login_as("john", "password")
.then(function () {
clock.tick(3600000 * 24); // 24 hour
return login_as("john", "password");
})
.then(function () {
clock.restore();
return BluebirdPromise.resolve();
});
});
});
});
function getPromised(url: string) {
return request.getAsync(url);
}
function postPromised(url: string, body: Object) {
return request.postAsync(url, body);
}
function getHomePage(): BluebirdPromise<request_.RequestResponse> {
return getPromised(HOME_URL + "/");
}
function getLoginPage(): BluebirdPromise<request_.RequestResponse> {
return getPromised(BASE_AUTH_URL + Endpoints.FIRST_FACTOR_GET);
}

View File

@ -7,6 +7,7 @@ import { UserConfiguration } from "../../src/types/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;
@ -23,14 +24,10 @@ const requests = require("./requests")(PORT);
describe("test data persistence", function () {
let u2f: U2FMock.U2FMock;
let tmpDir: tmp.SynchrounousResult;
const ldap_client = {
bind: sinon.stub(),
search: sinon.stub(),
on: sinon.spy()
};
const ldapClient = LdapjsClientMock();
const ldap = {
createClient: sinon.spy(function () {
return ldap_client;
return ldapClient;
})
};
@ -51,11 +48,12 @@ describe("test data persistence", function () {
})
};
ldap_client.bind.withArgs("cn=test_ok,ou=users,dc=example,dc=com",
"password").yields(undefined);
ldap_client.bind.withArgs("cn=test_nok,ou=users,dc=example,dc=com",
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");
ldap_client.search.yields(undefined, search_res);
ldapClient.search.yields(undefined, search_res);
ldapClient.unbind.yields();
tmpDir = tmp.dirSync({ unsafeCleanup: true });
config = {

View File

@ -14,22 +14,16 @@ import { LdapjsMock, LdapjsClientMock } from "./mocks/ldapjs";
describe("test ldap validation", function () {
let ldap: LdapClient.LdapClient;
let ldap_client: LdapjsClientMock;
let ldapClient: LdapjsClientMock;
let ldapjs: LdapjsMock;
let ldap_config: LdapConfiguration;
let ldapConfig: LdapConfiguration;
beforeEach(function () {
ldap_client = {
bind: sinon.stub(),
search: sinon.stub(),
modify: sinon.stub(),
on: sinon.stub()
} as any;
ldapClient = LdapjsClientMock();
ldapjs = LdapjsMock();
ldapjs.createClient.returns(ldap_client);
ldapjs.createClient.returns(ldapClient);
ldap_config = {
ldapConfig = {
url: "http://localhost:324",
user: "admin",
password: "password",
@ -37,45 +31,47 @@ describe("test ldap validation", function () {
additional_user_dn: "ou=users"
};
ldap = new LdapClient.LdapClient(ldap_config, ldapjs, winston);
return ldap.connect();
ldap = new LdapClient.LdapClient(ldapConfig, ldapjs, winston);
});
describe("test binding", test_binding);
describe("test checking password", test_checking_password);
describe("test get emails from username", test_get_emails);
describe("test get groups from username", test_get_groups);
describe("test update password", test_update_password);
function test_binding() {
function test_bind() {
function test_checking_password() {
function test_check_password_internal() {
const username = "username";
const password = "password";
return ldap.bind(username, password);
return ldap.checkPassword(username, password);
}
it("should bind the user if good credentials provided", function () {
ldap_client.bind.yields();
return test_bind();
ldapClient.bind.yields();
ldapClient.unbind.yields();
return test_check_password_internal();
});
it("should bind the user with correct DN", function () {
ldap_config.user_name_attribute = "uid";
ldapConfig.user_name_attribute = "uid";
const username = "user";
const password = "password";
ldap_client.bind.withArgs("uid=user,ou=users,dc=example,dc=com").yields();
return ldap.bind(username, password);
ldapClient.bind.withArgs("uid=user,ou=users,dc=example,dc=com").yields();
ldapClient.unbind.yields();
return ldap.checkPassword(username, password);
});
it("should default to cn user search filter if no filter provided", function () {
const username = "user";
const password = "password";
ldap_client.bind.withArgs("cn=user,ou=users,dc=example,dc=com").yields();
return ldap.bind(username, password);
ldapClient.bind.withArgs("cn=user,ou=users,dc=example,dc=com").yields();
ldapClient.unbind.yields();
return ldap.checkPassword(username, password);
});
it("should not bind the user if wrong credentials provided", function () {
ldap_client.bind.yields("wrong credentials");
const promise = test_bind();
ldapClient.bind.yields("wrong credentials");
const promise = test_check_password_internal();
return promise.catch(function () {
return BluebirdPromise.resolve();
});
@ -101,9 +97,9 @@ describe("test ldap validation", function () {
});
it("should retrieve the email of an existing user", function () {
ldap_client.search.yields(undefined, res_emitter);
ldapClient.search.yields(undefined, res_emitter);
return ldap.get_emails("user")
return ldap.retrieveEmails("user")
.then(function (emails) {
assert.deepEqual(emails, [expected_doc.object.mail]);
return BluebirdPromise.resolve();
@ -111,9 +107,9 @@ describe("test ldap validation", function () {
});
it("should retrieve email for user with uid name attribute", function () {
ldap_config.user_name_attribute = "uid";
ldap_client.search.withArgs("uid=username,ou=users,dc=example,dc=com").yields(undefined, res_emitter);
return ldap.get_emails("username")
ldapConfig.user_name_attribute = "uid";
ldapClient.search.withArgs("uid=username,ou=users,dc=example,dc=com").yields(undefined, res_emitter);
return ldap.retrieveEmails("username")
.then(function (emails) {
assert.deepEqual(emails, ["user@example.com"]);
return BluebirdPromise.resolve();
@ -124,9 +120,9 @@ describe("test ldap validation", function () {
const expected_doc = {
mail: ["user@example.com"]
};
ldap_client.search.yields("Error while searching mails");
ldapClient.search.yields("Error while searching mails");
return ldap.get_emails("user")
return ldap.retrieveEmails("user")
.catch(function () {
return BluebirdPromise.resolve();
});
@ -159,8 +155,8 @@ describe("test ldap validation", function () {
});
it("should retrieve the groups of an existing user", function () {
ldap_client.search.yields(undefined, res_emitter);
return ldap.get_groups("user")
ldapClient.search.yields(undefined, res_emitter);
return ldap.retrieveGroups("user")
.then(function (groups) {
assert.deepEqual(groups, ["group1", "group2"]);
return BluebirdPromise.resolve();
@ -168,29 +164,29 @@ describe("test ldap validation", function () {
});
it("should reduce the scope to additional_group_dn", function (done) {
ldap_config.additional_group_dn = "ou=groups";
ldap_client.search.yields(undefined, res_emitter);
ldap.get_groups("user")
ldapConfig.additional_group_dn = "ou=groups";
ldapClient.search.yields(undefined, res_emitter);
ldap.retrieveGroups("user")
.then(function() {
assert.equal(ldap_client.search.getCall(0).args[0], "ou=groups,dc=example,dc=com");
assert.equal(ldapClient.search.getCall(0).args[0], "ou=groups,dc=example,dc=com");
done();
});
});
it("should use default group_name_attr if not provided", function (done) {
ldap_client.search.yields(undefined, res_emitter);
ldap.get_groups("user")
ldapClient.search.yields(undefined, res_emitter);
ldap.retrieveGroups("user")
.then(function() {
assert.equal(ldap_client.search.getCall(0).args[0], "dc=example,dc=com");
assert.equal(ldap_client.search.getCall(0).args[1].filter, "member=cn=user,ou=users,dc=example,dc=com");
assert.deepEqual(ldap_client.search.getCall(0).args[1].attributes, ["cn"]);
assert.equal(ldapClient.search.getCall(0).args[0], "dc=example,dc=com");
assert.equal(ldapClient.search.getCall(0).args[1].filter, "member=cn=user,ou=users,dc=example,dc=com");
assert.deepEqual(ldapClient.search.getCall(0).args[1].attributes, ["cn"]);
done();
});
});
it("should fail on error with search method", function () {
ldap_client.search.yields("error");
return ldap.get_groups("user")
ldapClient.search.yields("error");
return ldap.retrieveGroups("user")
.catch(function () {
return BluebirdPromise.resolve();
});
@ -207,36 +203,39 @@ describe("test ldap validation", function () {
};
const userdn = "cn=user,ou=users,dc=example,dc=com";
ldap_client.bind.yields(undefined);
ldap_client.modify.yields(undefined);
ldapClient.bind.yields();
ldapClient.unbind.yields();
ldapClient.modify.yields();
return ldap.update_password("user", "new-password")
return ldap.updatePassword("user", "new-password")
.then(function () {
assert.deepEqual(ldap_client.modify.getCall(0).args[0], userdn);
assert.deepEqual(ldap_client.modify.getCall(0).args[1].operation, change.operation);
assert.deepEqual(ldapClient.modify.getCall(0).args[0], userdn);
assert.deepEqual(ldapClient.modify.getCall(0).args[1].operation, change.operation);
const userPassword = ldap_client.modify.getCall(0).args[1].modification.userPassword;
const userPassword = ldapClient.modify.getCall(0).args[1].modification.userPassword;
assert(/{SSHA}/.test(userPassword));
return BluebirdPromise.resolve();
});
})
.catch(function(err) { return BluebirdPromise.reject(new Error("It should fail")); });
});
it("should fail when ldap throws an error", function () {
ldap_client.bind.yields(undefined);
ldap_client.modify.yields("Error");
ldapClient.bind.yields(undefined);
ldapClient.modify.yields("Error");
return ldap.update_password("user", "new-password")
return ldap.updatePassword("user", "new-password")
.catch(function () {
return BluebirdPromise.resolve();
});
});
it("should update password of user using particular user name attribute", function () {
ldap_config.user_name_attribute = "uid";
ldapConfig.user_name_attribute = "uid";
ldap_client.bind.yields(undefined);
ldap_client.modify.withArgs("uid=username,ou=users,dc=example,dc=com").yields();
return ldap.update_password("username", "newpass");
ldapClient.bind.yields();
ldapClient.unbind.yields();
ldapClient.modify.withArgs("uid=username,ou=users,dc=example,dc=com").yields();
return ldap.updatePassword("username", "newpass");
});
}
});

View File

@ -1,6 +1,7 @@
import Server from "../../src/server/lib/Server";
import LdapClient = require("../../src/server/lib/LdapClient");
import { LdapjsClientMock } from "./mocks/ldapjs";
import BluebirdPromise = require("bluebird");
import speakeasy = require("speakeasy");
@ -51,16 +52,11 @@ describe("test the server", function () {
}
};
const ldap_client = {
bind: sinon.stub(),
search: sinon.stub(),
modify: sinon.stub(),
on: sinon.spy()
};
const ldapClient = LdapjsClientMock();
const ldap = {
Change: sinon.spy(),
createClient: sinon.spy(function () {
return ldap_client;
return ldapClient;
})
};
@ -76,7 +72,7 @@ describe("test the server", function () {
})
};
const ldap_document = {
const ldapDocument = {
object: {
mail: "test_ok@example.com",
}
@ -84,20 +80,21 @@ describe("test the server", function () {
const search_res = {
on: sinon.spy(function (event: string, fn: (s: any) => void) {
if (event != "error") fn(ldap_document);
if (event != "error") fn(ldapDocument);
})
};
ldap_client.bind.withArgs("cn=test_ok,ou=users,dc=example,dc=com",
"password").yields(undefined);
ldap_client.bind.withArgs("cn=admin,dc=example,dc=com",
"password").yields(undefined);
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();
ldap_client.bind.withArgs("cn=test_nok,ou=users,dc=example,dc=com",
ldapClient.bind.withArgs("cn=test_nok,ou=users,dc=example,dc=com",
"password").yields("error");
ldap_client.modify.yields(undefined);
ldap_client.search.yields(undefined, search_res);
ldapClient.unbind.yields();
ldapClient.modify.yields();
ldapClient.search.yields(undefined, search_res);
const deps = {
u2f: u2f,
@ -241,11 +238,11 @@ describe("test the server", function () {
return requests.register_totp(j, transporter);
})
.then(function (base32_secret: string) {
const real_token = speakeasy.totp({
const realToken = speakeasy.totp({
secret: base32_secret,
encoding: "base32"
});
return requests.totp(j, real_token);
return requests.totp(j, realToken);
})
.then(function (res: request.RequestResponse) {
assert.equal(res.statusCode, 200, "second factor failed");
@ -254,14 +251,11 @@ describe("test the server", function () {
.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 real_token = speakeasy.totp({
secret: "totp_secret",
encoding: "base32"
});
const j = requestp.jar();
return requests.login(j)
.then(function (res: request.RequestResponse) {
@ -269,11 +263,18 @@ describe("test the server", function () {
return requests.first_factor(j);
})
.then(function (res: request.RequestResponse) {
assert.equal(res.statusCode, 204, "first factor failed");
return requests.totp(j, real_token);
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, 204, "second factor failed");
assert.equal(res.statusCode, 200, "second factor failed");
return requests.login(j);
})
.then(function (res: request.RequestResponse) {
@ -284,9 +285,7 @@ describe("test the server", function () {
assert.equal(res.statusCode, 204, "verify failed");
return BluebirdPromise.resolve();
})
.catch(function (err: Error) {
console.error(err);
});
.catch(function (err: Error) { return BluebirdPromise.reject(err); });
});
it("should return status code 204 when user is authenticated using u2f", function () {

View File

@ -1,6 +1,6 @@
import assert = require("assert");
import sinon = require ("sinon");
import sinon = require("sinon");
import nedb = require("nedb");
import express = require("express");
import winston = require("winston");
@ -36,7 +36,10 @@ describe("test server configuration", function () {
winston: winston,
ldapjs: {
createClient: sinon.spy(function () {
return { on: sinon.spy() };
return {
on: sinon.spy(),
bind: sinon.spy()
};
})
},
session: sessionMock as any

View File

@ -2,19 +2,19 @@
import sinon = require("sinon");
export interface LdapClientMock {
bind: sinon.SinonStub;
get_emails: sinon.SinonStub;
get_groups: sinon.SinonStub;
search_in_ldap: sinon.SinonStub;
update_password: sinon.SinonStub;
checkPassword: sinon.SinonStub;
retrieveEmails: sinon.SinonStub;
retrieveGroups: sinon.SinonStub;
search: sinon.SinonStub;
updatePassword: sinon.SinonStub;
}
export function LdapClientMock(): LdapClientMock {
return {
bind: sinon.stub(),
get_emails: sinon.stub(),
get_groups: sinon.stub(),
search_in_ldap: sinon.stub(),
update_password: sinon.stub()
checkPassword: sinon.stub(),
retrieveEmails: sinon.stub(),
retrieveGroups: sinon.stub(),
search: sinon.stub(),
updatePassword: sinon.stub()
};
}

View File

@ -7,6 +7,7 @@ export interface LdapjsMock {
export interface LdapjsClientMock {
bind: sinon.SinonStub;
unbind: sinon.SinonStub;
search: sinon.SinonStub;
modify: sinon.SinonStub;
on: sinon.SinonStub;
@ -21,6 +22,7 @@ export function LdapjsMock(): LdapjsMock {
export function LdapjsClientMock(): LdapjsClientMock {
return {
bind: sinon.stub(),
unbind: sinon.stub(),
search: sinon.stub(),
modify: sinon.stub(),
on: sinon.stub()

View File

@ -72,8 +72,8 @@ describe("test the first factor validation route", function () {
});
it("should redirect client to second factor page", function () {
ldapMock.bind.withArgs("username").returns(BluebirdPromise.resolve());
ldapMock.get_emails.returns(BluebirdPromise.resolve(emails));
ldapMock.checkPassword.withArgs("username").returns(BluebirdPromise.resolve());
ldapMock.retrieveEmails.returns(BluebirdPromise.resolve(emails));
const authSession = AuthenticationSession.get(req as any);
return FirstFactorPost.default(req as any, res as any)
.then(function () {
@ -82,55 +82,60 @@ describe("test the first factor validation route", function () {
});
});
it("should retrieve email from LDAP", function (done) {
res.redirect = sinon.spy(function () { done(); });
ldapMock.bind.returns(BluebirdPromise.resolve());
ldapMock.get_emails = sinon.stub().withArgs("username").returns(BluebirdPromise.resolve([{ mail: ["test@example.com"] }]));
FirstFactorPost.default(req as any, res as any);
it("should retrieve email from LDAP", function () {
ldapMock.checkPassword.returns(BluebirdPromise.resolve());
ldapMock.retrieveEmails = sinon.stub().withArgs("username").returns(BluebirdPromise.resolve([{ mail: ["test@example.com"] }]));
return FirstFactorPost.default(req as any, res as any);
});
it("should set email as session variables", function () {
it("should set first email address as user session variable", function () {
const emails = ["test_ok@example.com"];
const authSession = AuthenticationSession.get(req as any);
ldapMock.bind.returns(BluebirdPromise.resolve());
ldapMock.get_emails.returns(BluebirdPromise.resolve(emails));
ldapMock.checkPassword.returns(BluebirdPromise.resolve());
ldapMock.retrieveEmails.returns(BluebirdPromise.resolve(emails));
return FirstFactorPost.default(req as any, res as any)
.then(function () {
assert.equal("test_ok@example.com", authSession.email);
});
});
it("should return status code 401 when LDAP binding throws", function (done) {
res.send = sinon.spy(function () {
it("should return status code 401 when LDAP binding throws", function () {
ldapMock.checkPassword.returns(BluebirdPromise.reject(new exceptions.LdapBindError("Bad credentials")));
return FirstFactorPost.default(req as any, res as any)
.then(function () {
assert.equal(401, res.status.getCall(0).args[0]);
assert.equal(regulator.mark.getCall(0).args[0], "username");
done();
});
ldapMock.bind.returns(BluebirdPromise.reject(new exceptions.LdapBindError("Bad credentials")));
FirstFactorPost.default(req as any, res as any);
});
it("should return status code 500 when LDAP search throws", function (done) {
res.send = sinon.spy(function () {
it("should return status code 500 when LDAP search throws", function () {
ldapMock.checkPassword.returns(BluebirdPromise.resolve());
ldapMock.retrieveEmails.returns(BluebirdPromise.reject(new exceptions.LdapSearchError("error while retrieving emails")));
return FirstFactorPost.default(req as any, res as any)
.then(function () {
assert.equal(500, res.status.getCall(0).args[0]);
done();
});
ldapMock.bind.returns(BluebirdPromise.resolve());
ldapMock.get_emails.returns(BluebirdPromise.reject(new exceptions.LdapSearchError("error while retrieving emails")));
FirstFactorPost.default(req as any, res as any);
});
it("should return status code 403 when regulator rejects authentication", function (done) {
it("should return status code 403 when regulator rejects authentication", function () {
const err = new exceptions.AuthenticationRegulationError("Authentication regulation...");
regulator.regulate.returns(BluebirdPromise.reject(err));
res.send = sinon.spy(function () {
return FirstFactorPost.default(req as any, res as any)
.then(function () {
assert.equal(403, res.status.getCall(0).args[0]);
done();
assert.equal(1, res.send.callCount);
});
});
it("should fail when admin user does not have rights to retrieve attribute mail", function () {
ldapMock.checkPassword.returns(BluebirdPromise.resolve());
ldapMock.retrieveEmails = sinon.stub().withArgs("username").returns(BluebirdPromise.resolve([]));
ldapMock.retrieveGroups = sinon.stub().withArgs("username").returns(BluebirdPromise.resolve(["group1"]));
return FirstFactorPost.default(req as any, res as any)
.then(function () {
assert.equal(500, res.status.getCall(0).args[0]);
assert.equal(1, res.send.callCount);
});
ldapMock.bind.returns(BluebirdPromise.resolve());
ldapMock.get_emails.returns(BluebirdPromise.resolve());
FirstFactorPost.default(req as any, res as any);
});
});

View File

@ -82,7 +82,7 @@ describe("test reset password identity check", function () {
});
it("should fail if ldap fail", function (done) {
ldap_client.get_emails.returns(BluebirdPromise.reject("Internal error"));
ldap_client.retrieveEmails.returns(BluebirdPromise.reject("Internal error"));
new PasswordResetHandler().preValidationInit(req as any)
.catch(function (err: Error) {
done();
@ -91,16 +91,16 @@ describe("test reset password identity check", function () {
it("should perform a search in ldap to find email address", function (done) {
configuration.ldap.user_name_attribute = "uid";
ldap_client.get_emails.returns(BluebirdPromise.resolve([]));
ldap_client.retrieveEmails.returns(BluebirdPromise.resolve([]));
new PasswordResetHandler().preValidationInit(req as any)
.then(function () {
assert.equal("user", ldap_client.get_emails.getCall(0).args[0]);
assert.equal("user", ldap_client.retrieveEmails.getCall(0).args[0]);
done();
});
});
it("should returns identity when ldap replies", function (done) {
ldap_client.get_emails.returns(BluebirdPromise.resolve(["test@example.com"]));
ldap_client.retrieveEmails.returns(BluebirdPromise.resolve(["test@example.com"]));
new PasswordResetHandler().preValidationInit(req as any)
.then(function () {
done();

View File

@ -16,7 +16,7 @@ describe("test reset password route", function () {
let req: ExpressMock.RequestMock;
let res: ExpressMock.ResponseMock;
let user_data_store: UserDataStore;
let ldap_client: LdapClientMock;
let ldapClient: LdapClientMock;
let configuration: any;
let authSession: AuthenticationSession.AuthenticationSession;
@ -64,8 +64,8 @@ describe("test reset password route", function () {
mocks.logger = winston;
mocks.config = configuration;
ldap_client = LdapClientMock();
mocks.ldap = ldap_client;
ldapClient = LdapClientMock();
mocks.ldap = ldapClient;
res = ExpressMock.ResponseMock();
});
@ -79,8 +79,8 @@ describe("test reset password route", function () {
req.body = {};
req.body.password = "new-password";
ldap_client.update_password.returns(BluebirdPromise.resolve());
ldap_client.bind.returns(BluebirdPromise.resolve());
ldapClient.updatePassword.returns(BluebirdPromise.resolve());
ldapClient.checkPassword.returns(BluebirdPromise.resolve());
return PasswordResetFormPost.default(req as any, res as any)
.then(function () {
const authSession = AuthenticationSession.get(req as any);
@ -111,8 +111,8 @@ describe("test reset password route", function () {
req.body = {};
req.body.password = "new-password";
ldap_client.bind.yields(undefined);
ldap_client.update_password.returns(BluebirdPromise.reject("Internal error with LDAP"));
ldapClient.checkPassword.yields(undefined);
ldapClient.updatePassword.returns(BluebirdPromise.reject("Internal error with LDAP"));
res.send = sinon.spy(function () {
assert.equal(res.status.getCall(0).args[0], 500);
done();