Merge pull request #45 from clems4ever/ldap-bind

Fix LDAP search operation when user has no rights to search attributes in DB.
pull/50/head
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 *.swp
*.sh /config.yml
config.yml
npm-debug.log npm-debug.log

View File

@ -19,14 +19,7 @@ addons:
before_install: npm install -g npm@'>=2.13.5' before_install: npm install -g npm@'>=2.13.5'
script: script:
- grunt build-dist - ./scripts/travis.sh
- 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
after_success: after_success:
- ./scripts/docker-publish.sh - ./scripts/docker-publish.sh

View File

@ -10,7 +10,7 @@ COPY dist/src/server /usr/src
ENV PORT=80 ENV PORT=80
EXPOSE 80 EXPOSE 80
VOLUME /etc/auth-server VOLUME /etc/authelia
VOLUME /var/lib/auth-server 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: { run: {
options: {}, options: {},
"build": { "build": {
cmd: "npm", cmd: "./node_modules/.bin/tsc",
args: ['run', 'build'] args: ['-p', 'tsconfig.json']
}, },
"tslint": { "tslint": {
cmd: "npm", cmd: "./node_modules/.bin/tslint",
args: ['run', 'tslint'] args: ['-c', 'tslint.json', '-p', 'tsconfig.json']
}, },
"test": { "test": {
cmd: "npm", cmd: "npm",

View File

@ -4,7 +4,7 @@
[![Build](https://travis-ci.org/clems4ever/authelia.svg?branch=master)](https://travis-ci.org/clems4ever/authelia) [![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 **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. used in production to secure internal services in a small docker swarm cluster.
## Features ## Features
@ -17,25 +17,53 @@ address.
## Deployment ## Deployment
If you don't have any LDAP and nginx setup yet, I advise you to follow the If you don't have any LDAP and/or nginx setup yet, I advise you to follow the
Getting Started. That way, you will not require anything to start. [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 ### With NPM
npm install -g authelia npm install -g authelia
authelia /path/to/your/config.yml
### With Docker ### With Docker
docker pull clems4ever/authelia 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 ## Getting started
The provided example is docker-based so that you can deploy and test it very 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 quickly.
port 8080 before starting.
Add the following lines to your /etc/hosts to simulate multiple subdomains ### 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 secret.test.local
127.0.0.1 secret1.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 mx2.mail.test.local
127.0.0.1 auth.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 npm install --only=dev
grunt build-dist ./node_modules/.bin/grunt build-dist
docker-compose build ./scripts/deploy-example.sh
docker-compose up -d
After few seconds the services should be running and you should be able to visit 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/). [https://home.test.local:8080/](https://home.test.local:8080/).
Normally, a self-signed certificate exception should appear, it has to be When accessing the login page, a self-signed certificate exception should appear,
accepted before getting to the login page: 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"> <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 An LDAP server has been deployed for you with the following credentials and
access control list: access control list:
@ -76,54 +109,55 @@ any subdomain.
- [secret1.test.local](https://secret1.test.local:8080/secret.html) - [secret1.test.local](https://secret1.test.local:8080/secret.html)
- [home.test.local](https://home.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 You can use them in the login page. If everything is ok, the second factor
have appeared as shown below. 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"> <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 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 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 **./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](https://play.google.com/store/apps/details?id=com.google.android.apps.authenticator2&hl=en) [Google Authenticator]
to store them and get the generated tokens required during authentication. 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"> <img src="https://raw.githubusercontent.com/clems4ever/authelia/master/images/totp.png" width="400">
### 2nd factor: U2F (Universal 2-Factor) with security keys ### 2nd factor: U2F (Universal 2-Factor) with security keys
**Authelia** also offers authentication using U2F devices like [Yubikey](Yubikey) **Authelia** also offers authentication using U2F devices like [Yubikey](Yubikey)
USB security keys. U2F is one of the most secure authentication protocol and is 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 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 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 **./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 it. You can now authenticate using your U2F device by simply to register. Upon successful registration, you can authenticate using your U2F
touching the token. device by simply touching the token. Easy, right?!
<img src="https://raw.githubusercontent.com/clems4ever/authelia/master/images/u2f.png" width="400"> <img src="https://raw.githubusercontent.com/clems4ever/authelia/master/images/u2f.png" width="400">
### Password reset ### Password reset
With **Authelia**, you can also reset your password in no time. Click on the 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 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. **./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">
### Access Control ### Access Control
With **Authelia**, you can define your own access control rules for restricting 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 the user access to some subdomains. Those rules are defined in the
configuration file and can be either default, per-user or per-group policies. 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. Check out the *config.template.yml* to see how they are defined.
## Documentation ## Documentation
@ -172,4 +206,6 @@ Follow [contributing](CONTRIBUTORS.md) file.
[TOTP]: https://en.wikipedia.org/wiki/Time-based_One-time_Password_Algorithm [TOTP]: https://en.wikipedia.org/wiki/Time-based_One-time_Password_Algorithm
[U2F]: https://www.yubico.com/about/background/fido/ [U2F]: https://www.yubico.com/about/background/fido/
[Yubikey]: https://www.yubico.com/products/yubikey-hardware/yubikey4/ [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 # Example: for user john, the DN will be cn=john,ou=users,dc=example,dc=com
ldap: ldap:
# The url of the ldap server # The url of the ldap server
url: ldap://ldap url: ldap://openldap-restriction
# The base dn for every entries # The base dn for every entries
base_dn: dc=example,dc=com base_dn: dc=example,dc=com
@ -85,7 +85,7 @@ store_directory: /var/lib/authelia/store
notifier: notifier:
# For testing purpose, notifications can be sent in a file # For testing purpose, notifications can be sent in a file
filesystem: 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. # Use your gmail account to send the notifications. You can use an app password.
# gmail: # gmail:

View File

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

View File

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

View File

@ -1,37 +1,11 @@
version: '2' version: '2'
services: services:
auth: authelia:
build: . build: .
depends_on:
- ldap
restart: always restart: always
volumes: volumes:
- ./config.template.yml:/etc/auth-server/config.yml:ro - ./config.template.yml:/etc/authelia/config.yml:ro
- ./notifications:/var/lib/auth-server/notifications - ./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"

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 cn: john
objectclass: inetOrgPerson objectclass: inetOrgPerson
objectclass: top objectclass: top
mail: clement.michaud34@gmail.com mail: john.doe@example.com
sn: John Doe sn: John Doe
userpassword: {SHA}W6ph5Mm5Pz8GgiULbPgzG37mj9g= userpassword: {SHA}W6ph5Mm5Pz8GgiULbPgzG37mj9g=
@ -45,18 +45,3 @@ mail: bob.dylan@example.com
sn: Bob Dylan sn: Bob Dylan
userpassword: {SHA}W6ph5Mm5Pz8GgiULbPgzG37mj9g= 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 Host $http_host;
proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Real-IP $remote_addr;
proxy_pass http://auth/; proxy_pass http://authelia/;
proxy_intercept_errors on; proxy_intercept_errors on;
@ -68,7 +68,7 @@ http {
proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Host $http_host; proxy_set_header Host $http_host;
proxy_pass http://auth/verify; proxy_pass http://authelia/verify;
} }
location = /secret.html { location = /secret.html {

View File

@ -4,6 +4,6 @@
</head> </head>
<body> <body>
This is a very important secret!<br/> 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> </body>
</html> </html>

View File

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

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 $*

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 $*

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"; docker login -u="$DOCKER_USERNAME" -p="$DOCKER_PASSWORD";
echo "Docker image $IMAGE_WITH_TAG will be deployed on Dockerhub." echo "Docker image $IMAGE_WITH_TAG will be deployed on Dockerhub."
docker build -t $IMAGE_NAME .
docker tag $IMAGE_NAME $IMAGE_WITH_TAG; docker tag $IMAGE_NAME $IMAGE_WITH_TAG;
docker push $IMAGE_WITH_TAG; docker push $IMAGE_WITH_TAG;
echo "Docker image deployed successfully." echo "Docker image deployed successfully."

View File

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

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

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

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 * as ObjectPath from "object-path";
import { AppConfiguration, UserConfiguration, NotifierConfiguration, ACLConfiguration, LdapConfiguration } from "./../../types/Configuration"; 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 { function get_optional<T>(config: object, path: string, default_value: T): T {
let entry = default_value; let entry = default_value;
@ -17,26 +19,36 @@ function ensure_key_existence(config: object, path: string): void {
} }
} }
function adaptFromUserConfiguration(userConfiguration: UserConfiguration): AppConfiguration {
ensure_key_existence(userConfiguration, "ldap");
ensure_key_existence(userConfiguration, "session.secret");
const port = ObjectPath.get(userConfiguration, "port", 8080);
return {
port: port,
ldap: ObjectPath.get<object, LdapConfiguration>(userConfiguration, "ldap"),
session: {
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>(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 { export default class ConfigurationAdapter {
static adapt(yaml_config: UserConfiguration): AppConfiguration { static adapt(userConfiguration: UserConfiguration): AppConfiguration {
ensure_key_existence(yaml_config, "ldap"); const appConfiguration = adaptFromUserConfiguration(userConfiguration);
ensure_key_existence(yaml_config, "session.secret");
const port = ObjectPath.get(yaml_config, "port", 8080); const ldapUrl = process.env[LDAP_URL_ENV_VARIABLE];
if (ldapUrl)
appConfiguration.ldap.url = ldapUrl;
return { return appConfiguration;
port: port,
ldap: ObjectPath.get<object, LdapConfiguration>(yaml_config, "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
},
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")
};
} }
} }

View File

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

View File

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

View File

@ -50,6 +50,7 @@ export default class Server {
// by default the level of logs is info // by default the level of logs is info
deps.winston.level = config.logs_level; deps.winston.level = config.logs_level;
console.log("Log level = ", deps.winston.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); ServerVariables.fill(app, config, deps);

View File

@ -35,7 +35,7 @@ export class GMailNotifier extends INotifier {
}; };
const mailOptions = { const mailOptions = {
from: "auth-server@open-intent.io", from: "authelia@authelia.com",
to: identity.email, to: identity.email,
subject: subject, subject: subject,
html: ejs.render(email_template, d) 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) return regulator.regulate(username)
.then(function () { .then(function () {
return ldap.bind(username, password); logger.info("1st factor: No regulation applied.");
return ldap.checkPassword(username, password);
}) })
.then(function () { .then(function () {
logger.info("1st factor: LDAP binding successful");
authSession.userid = username; authSession.userid = username;
authSession.first_factor = true; authSession.first_factor = true;
logger.info("1st factor: LDAP binding successful");
logger.debug("1st factor: Retrieve email from LDAP"); 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[]]) { .then(function (data: [string[], string[]]) {
const emails: string[] = data[0]; const emails: string[] = data[0];
const groups: string[] = data[1]; 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 email are %s", emails);
logger.debug("1st factor: Retrieved groups are %s", groups);
authSession.email = emails[0]; authSession.email = emails[0];
authSession.groups = groups; 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.LdapSearchError, ErrorReplies.replyWithError500(res, logger))
.catch(exceptions.LdapBindError, function (err: Error) { .catch(exceptions.LdapBindError, function (err: Error) {
regulator.mark(username, false); regulator.mark(username, false);
ErrorReplies.replyWithError401(res, logger)(err); return ErrorReplies.replyWithError401(res, logger)(err);
}) })
.catch(exceptions.AuthenticationRegulationError, ErrorReplies.replyWithError403(res, logger)) .catch(exceptions.AuthenticationRegulationError, ErrorReplies.replyWithError403(res, logger))
.catch(exceptions.DomainAccessDenied, ErrorReplies.replyWithError401(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); 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 () { .then(function () {
logger.info("POST reset-password: Password reset for user '%s'", userid); logger.info("POST reset-password: Password reset for user '%s'", userid);
AuthenticationSession.reset(req); 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")); return BluebirdPromise.reject(new exceptions.AccessDeniedError("No user id provided"));
const ldap = ServerVariables.getLdapClient(req.app); const ldap = ServerVariables.getLdapClient(req.app);
return ldap.get_emails(userid) return ldap.retrieveEmails(userid)
.then(function (emails: string[]) { .then(function (emails: string[]) {
if (!emails && emails.length <= 0) throw new Error("No email found"); if (!emails && emails.length <= 0) throw new Error("No email found");

View File

@ -5,6 +5,7 @@ import { EventEmitter } from "events";
declare module "ldapjs" { declare module "ldapjs" {
export interface ClientAsync { export interface ClientAsync {
bindAsync(username: string, password: string): BluebirdPromise<void>; bindAsync(username: string, password: string): BluebirdPromise<void>;
unbindAsync(): BluebirdPromise<void>;
searchAsync(base: string, query: ldapjs.SearchOptions): BluebirdPromise<EventEmitter>; searchAsync(base: string, query: ldapjs.SearchOptions): BluebirdPromise<EventEmitter>;
modifyAsync(userdn: string, change: ldapjs.Change): BluebirdPromise<void>; 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 { GlobalDependencies } from "../../src/types/Dependencies";
import * as tmp from "tmp"; import * as tmp from "tmp";
import U2FMock = require("./mocks/u2f"); import U2FMock = require("./mocks/u2f");
import { LdapjsClientMock } from "./mocks/ldapjs";
const requestp = BluebirdPromise.promisifyAll(request) as request.Request; const requestp = BluebirdPromise.promisifyAll(request) as request.Request;
@ -23,14 +24,10 @@ const requests = require("./requests")(PORT);
describe("test data persistence", function () { describe("test data persistence", function () {
let u2f: U2FMock.U2FMock; let u2f: U2FMock.U2FMock;
let tmpDir: tmp.SynchrounousResult; let tmpDir: tmp.SynchrounousResult;
const ldap_client = { const ldapClient = LdapjsClientMock();
bind: sinon.stub(),
search: sinon.stub(),
on: sinon.spy()
};
const ldap = { const ldap = {
createClient: sinon.spy(function () { 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", ldapClient.bind.withArgs("cn=test_ok,ou=users,dc=example,dc=com",
"password").yields(undefined); "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"); "password").yields("error");
ldap_client.search.yields(undefined, search_res); ldapClient.search.yields(undefined, search_res);
ldapClient.unbind.yields();
tmpDir = tmp.dirSync({ unsafeCleanup: true }); tmpDir = tmp.dirSync({ unsafeCleanup: true });
config = { config = {

View File

@ -14,22 +14,16 @@ import { LdapjsMock, LdapjsClientMock } from "./mocks/ldapjs";
describe("test ldap validation", function () { describe("test ldap validation", function () {
let ldap: LdapClient.LdapClient; let ldap: LdapClient.LdapClient;
let ldap_client: LdapjsClientMock; let ldapClient: LdapjsClientMock;
let ldapjs: LdapjsMock; let ldapjs: LdapjsMock;
let ldap_config: LdapConfiguration; let ldapConfig: LdapConfiguration;
beforeEach(function () { beforeEach(function () {
ldap_client = { ldapClient = LdapjsClientMock();
bind: sinon.stub(),
search: sinon.stub(),
modify: sinon.stub(),
on: sinon.stub()
} as any;
ldapjs = LdapjsMock(); ldapjs = LdapjsMock();
ldapjs.createClient.returns(ldap_client); ldapjs.createClient.returns(ldapClient);
ldap_config = { ldapConfig = {
url: "http://localhost:324", url: "http://localhost:324",
user: "admin", user: "admin",
password: "password", password: "password",
@ -37,45 +31,47 @@ describe("test ldap validation", function () {
additional_user_dn: "ou=users" additional_user_dn: "ou=users"
}; };
ldap = new LdapClient.LdapClient(ldap_config, ldapjs, winston); ldap = new LdapClient.LdapClient(ldapConfig, ldapjs, winston);
return ldap.connect();
}); });
describe("test binding", test_binding); describe("test checking password", test_checking_password);
describe("test get emails from username", test_get_emails); describe("test get emails from username", test_get_emails);
describe("test get groups from username", test_get_groups); describe("test get groups from username", test_get_groups);
describe("test update password", test_update_password); describe("test update password", test_update_password);
function test_binding() { function test_checking_password() {
function test_bind() { function test_check_password_internal() {
const username = "username"; const username = "username";
const password = "password"; const password = "password";
return ldap.bind(username, password); return ldap.checkPassword(username, password);
} }
it("should bind the user if good credentials provided", function () { it("should bind the user if good credentials provided", function () {
ldap_client.bind.yields(); ldapClient.bind.yields();
return test_bind(); ldapClient.unbind.yields();
return test_check_password_internal();
}); });
it("should bind the user with correct DN", function () { it("should bind the user with correct DN", function () {
ldap_config.user_name_attribute = "uid"; ldapConfig.user_name_attribute = "uid";
const username = "user"; const username = "user";
const password = "password"; const password = "password";
ldap_client.bind.withArgs("uid=user,ou=users,dc=example,dc=com").yields(); ldapClient.bind.withArgs("uid=user,ou=users,dc=example,dc=com").yields();
return ldap.bind(username, password); ldapClient.unbind.yields();
return ldap.checkPassword(username, password);
}); });
it("should default to cn user search filter if no filter provided", function () { it("should default to cn user search filter if no filter provided", function () {
const username = "user"; const username = "user";
const password = "password"; const password = "password";
ldap_client.bind.withArgs("cn=user,ou=users,dc=example,dc=com").yields(); ldapClient.bind.withArgs("cn=user,ou=users,dc=example,dc=com").yields();
return ldap.bind(username, password); ldapClient.unbind.yields();
return ldap.checkPassword(username, password);
}); });
it("should not bind the user if wrong credentials provided", function () { it("should not bind the user if wrong credentials provided", function () {
ldap_client.bind.yields("wrong credentials"); ldapClient.bind.yields("wrong credentials");
const promise = test_bind(); const promise = test_check_password_internal();
return promise.catch(function () { return promise.catch(function () {
return BluebirdPromise.resolve(); return BluebirdPromise.resolve();
}); });
@ -101,9 +97,9 @@ describe("test ldap validation", function () {
}); });
it("should retrieve the email of an existing user", 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) { .then(function (emails) {
assert.deepEqual(emails, [expected_doc.object.mail]); assert.deepEqual(emails, [expected_doc.object.mail]);
return BluebirdPromise.resolve(); return BluebirdPromise.resolve();
@ -111,9 +107,9 @@ describe("test ldap validation", function () {
}); });
it("should retrieve email for user with uid name attribute", function () { it("should retrieve email for user with uid name attribute", function () {
ldap_config.user_name_attribute = "uid"; ldapConfig.user_name_attribute = "uid";
ldap_client.search.withArgs("uid=username,ou=users,dc=example,dc=com").yields(undefined, res_emitter); ldapClient.search.withArgs("uid=username,ou=users,dc=example,dc=com").yields(undefined, res_emitter);
return ldap.get_emails("username") return ldap.retrieveEmails("username")
.then(function (emails) { .then(function (emails) {
assert.deepEqual(emails, ["user@example.com"]); assert.deepEqual(emails, ["user@example.com"]);
return BluebirdPromise.resolve(); return BluebirdPromise.resolve();
@ -124,9 +120,9 @@ describe("test ldap validation", function () {
const expected_doc = { const expected_doc = {
mail: ["user@example.com"] 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 () { .catch(function () {
return BluebirdPromise.resolve(); return BluebirdPromise.resolve();
}); });
@ -159,8 +155,8 @@ describe("test ldap validation", function () {
}); });
it("should retrieve the groups of an existing user", function () { it("should retrieve the groups of an existing user", function () {
ldap_client.search.yields(undefined, res_emitter); ldapClient.search.yields(undefined, res_emitter);
return ldap.get_groups("user") return ldap.retrieveGroups("user")
.then(function (groups) { .then(function (groups) {
assert.deepEqual(groups, ["group1", "group2"]); assert.deepEqual(groups, ["group1", "group2"]);
return BluebirdPromise.resolve(); return BluebirdPromise.resolve();
@ -168,29 +164,29 @@ describe("test ldap validation", function () {
}); });
it("should reduce the scope to additional_group_dn", function (done) { it("should reduce the scope to additional_group_dn", function (done) {
ldap_config.additional_group_dn = "ou=groups"; ldapConfig.additional_group_dn = "ou=groups";
ldap_client.search.yields(undefined, res_emitter); ldapClient.search.yields(undefined, res_emitter);
ldap.get_groups("user") ldap.retrieveGroups("user")
.then(function() { .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(); done();
}); });
}); });
it("should use default group_name_attr if not provided", function (done) { it("should use default group_name_attr if not provided", function (done) {
ldap_client.search.yields(undefined, res_emitter); ldapClient.search.yields(undefined, res_emitter);
ldap.get_groups("user") ldap.retrieveGroups("user")
.then(function() { .then(function() {
assert.equal(ldap_client.search.getCall(0).args[0], "dc=example,dc=com"); assert.equal(ldapClient.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.equal(ldapClient.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.deepEqual(ldapClient.search.getCall(0).args[1].attributes, ["cn"]);
done(); done();
}); });
}); });
it("should fail on error with search method", function () { it("should fail on error with search method", function () {
ldap_client.search.yields("error"); ldapClient.search.yields("error");
return ldap.get_groups("user") return ldap.retrieveGroups("user")
.catch(function () { .catch(function () {
return BluebirdPromise.resolve(); return BluebirdPromise.resolve();
}); });
@ -207,36 +203,39 @@ describe("test ldap validation", function () {
}; };
const userdn = "cn=user,ou=users,dc=example,dc=com"; const userdn = "cn=user,ou=users,dc=example,dc=com";
ldap_client.bind.yields(undefined); ldapClient.bind.yields();
ldap_client.modify.yields(undefined); ldapClient.unbind.yields();
ldapClient.modify.yields();
return ldap.update_password("user", "new-password") return ldap.updatePassword("user", "new-password")
.then(function () { .then(function () {
assert.deepEqual(ldap_client.modify.getCall(0).args[0], userdn); assert.deepEqual(ldapClient.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[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)); assert(/{SSHA}/.test(userPassword));
return BluebirdPromise.resolve(); return BluebirdPromise.resolve();
}); })
.catch(function(err) { return BluebirdPromise.reject(new Error("It should fail")); });
}); });
it("should fail when ldap throws an error", function () { it("should fail when ldap throws an error", function () {
ldap_client.bind.yields(undefined); ldapClient.bind.yields(undefined);
ldap_client.modify.yields("Error"); ldapClient.modify.yields("Error");
return ldap.update_password("user", "new-password") return ldap.updatePassword("user", "new-password")
.catch(function () { .catch(function () {
return BluebirdPromise.resolve(); return BluebirdPromise.resolve();
}); });
}); });
it("should update password of user using particular user name attribute", function () { 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); ldapClient.bind.yields();
ldap_client.modify.withArgs("uid=username,ou=users,dc=example,dc=com").yields(); ldapClient.unbind.yields();
return ldap.update_password("username", "newpass"); 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 Server from "../../src/server/lib/Server";
import LdapClient = require("../../src/server/lib/LdapClient"); import LdapClient = require("../../src/server/lib/LdapClient");
import { LdapjsClientMock } from "./mocks/ldapjs";
import BluebirdPromise = require("bluebird"); import BluebirdPromise = require("bluebird");
import speakeasy = require("speakeasy"); import speakeasy = require("speakeasy");
@ -51,16 +52,11 @@ describe("test the server", function () {
} }
}; };
const ldap_client = { const ldapClient = LdapjsClientMock();
bind: sinon.stub(),
search: sinon.stub(),
modify: sinon.stub(),
on: sinon.spy()
};
const ldap = { const ldap = {
Change: sinon.spy(), Change: sinon.spy(),
createClient: sinon.spy(function () { createClient: sinon.spy(function () {
return ldap_client; return ldapClient;
}) })
}; };
@ -76,7 +72,7 @@ describe("test the server", function () {
}) })
}; };
const ldap_document = { const ldapDocument = {
object: { object: {
mail: "test_ok@example.com", mail: "test_ok@example.com",
} }
@ -84,20 +80,21 @@ describe("test the server", function () {
const search_res = { const search_res = {
on: sinon.spy(function (event: string, fn: (s: any) => void) { 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", ldapClient.bind.withArgs("cn=test_ok,ou=users,dc=example,dc=com",
"password").yields(undefined); "password").yields();
ldap_client.bind.withArgs("cn=admin,dc=example,dc=com", ldapClient.bind.withArgs("cn=admin,dc=example,dc=com",
"password").yields(undefined); "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"); "password").yields("error");
ldap_client.modify.yields(undefined); ldapClient.unbind.yields();
ldap_client.search.yields(undefined, search_res); ldapClient.modify.yields();
ldapClient.search.yields(undefined, search_res);
const deps = { const deps = {
u2f: u2f, u2f: u2f,
@ -241,11 +238,11 @@ describe("test the server", function () {
return requests.register_totp(j, transporter); return requests.register_totp(j, transporter);
}) })
.then(function (base32_secret: string) { .then(function (base32_secret: string) {
const real_token = speakeasy.totp({ const realToken = speakeasy.totp({
secret: base32_secret, secret: base32_secret,
encoding: "base32" encoding: "base32"
}); });
return requests.totp(j, real_token); return requests.totp(j, realToken);
}) })
.then(function (res: request.RequestResponse) { .then(function (res: request.RequestResponse) {
assert.equal(res.statusCode, 200, "second factor failed"); assert.equal(res.statusCode, 200, "second factor failed");
@ -254,14 +251,11 @@ describe("test the server", function () {
.then(function (res: request.RequestResponse) { .then(function (res: request.RequestResponse) {
assert.equal(res.statusCode, 204, "verify failed"); assert.equal(res.statusCode, 204, "verify failed");
return BluebirdPromise.resolve(); return BluebirdPromise.resolve();
}); })
.catch(function (err: Error) { return BluebirdPromise.reject(err); });
}); });
it("should keep session variables when login page is reloaded", function () { 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(); const j = requestp.jar();
return requests.login(j) return requests.login(j)
.then(function (res: request.RequestResponse) { .then(function (res: request.RequestResponse) {
@ -269,11 +263,18 @@ describe("test the server", function () {
return requests.first_factor(j); return requests.first_factor(j);
}) })
.then(function (res: request.RequestResponse) { .then(function (res: request.RequestResponse) {
assert.equal(res.statusCode, 204, "first factor failed"); assert.equal(res.statusCode, 302, "first factor failed");
return requests.totp(j, real_token); 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) { .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); return requests.login(j);
}) })
.then(function (res: request.RequestResponse) { .then(function (res: request.RequestResponse) {
@ -284,9 +285,7 @@ describe("test the server", function () {
assert.equal(res.statusCode, 204, "verify failed"); assert.equal(res.statusCode, 204, "verify failed");
return BluebirdPromise.resolve(); return BluebirdPromise.resolve();
}) })
.catch(function (err: Error) { .catch(function (err: Error) { return BluebirdPromise.reject(err); });
console.error(err);
});
}); });
it("should return status code 204 when user is authenticated using u2f", function () { it("should return status code 204 when user is authenticated using u2f", function () {

View File

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

View File

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

View File

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

View File

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

View File

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