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
commit
888bdd2bf9
|
@ -13,9 +13,7 @@ src/.baseDir.ts
|
||||||
|
|
||||||
*.swp
|
*.swp
|
||||||
|
|
||||||
*.sh
|
/config.yml
|
||||||
|
|
||||||
config.yml
|
|
||||||
|
|
||||||
npm-debug.log
|
npm-debug.log
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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"]
|
||||||
|
|
|
@ -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",
|
||||||
|
|
98
README.md
98
README.md
|
@ -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
|
||||||
|
@ -44,23 +72,28 @@ Add the following lines to your /etc/hosts to simulate multiple subdomains
|
||||||
127.0.0.1 mx1.mail.test.local
|
127.0.0.1 mx1.mail.test.local
|
||||||
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
|
||||||
|
|
||||||
|
### Deployment
|
||||||
|
|
||||||
Then, type the following command to build and deploy the services:
|
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
|
||||||
|
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
version: '2'
|
||||||
|
|
||||||
|
networks:
|
||||||
|
example-network:
|
||||||
|
driver: bridge
|
|
@ -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
|
|
||||||
|
|
|
@ -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"
|
|
||||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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=
|
|
||||||
#
|
|
||||||
|
|
|
@ -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
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 {
|
|
@ -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>
|
|
@ -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": {
|
||||||
|
|
|
@ -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 $*
|
|
@ -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 $*
|
|
@ -0,0 +1,4 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
./scripts/dc-example.sh build
|
||||||
|
./scripts/dc-example.sh up -d
|
|
@ -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."
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -0,0 +1,3 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
./scripts/dc-example.sh down
|
|
@ -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")
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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))
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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");
|
||||||
|
|
||||||
|
|
|
@ -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>;
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
FROM node:7-alpine
|
||||||
|
|
||||||
|
WORKDIR /usr/src
|
||||||
|
|
||||||
|
CMD ["./node_modules/.bin/mocha", "--compilers", "ts:ts-node/register", "--recursive", "test/integration"]
|
|
@ -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);
|
||||||
|
}
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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);
|
|
||||||
}
|
|
|
@ -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 = {
|
|
@ -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");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -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 () {
|
||||||
|
|
|
@ -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
|
|
@ -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()
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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();
|
||||||
|
|
Loading…
Reference in New Issue