diff --git a/.travis.yml b/.travis.yml
index b5052c4fe..2ec3f9f54 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,23 +1,37 @@
+dist: trusty
language: node_js
+sudo: required
node_js:
-- node
+ - "7"
services:
-- docker
-- ntp
+ - docker
+ - ntp
addons:
+ chrome: stable
apt:
+ sources:
+ - google-chrome
packages:
- - libgif-dev
+ - libgif-dev
+ - google-chrome-stable
hosts:
- auth.test.local
- home.test.local
+ - public.test.local
- secret.test.local
- secret1.test.local
- secret2.test.local
- mx1.mail.test.local
- mx2.mail.test.local
-before_install: npm install -g npm@'>=2.13.5'
+before_install:
+ - npm install -g npm@'>=2.13.5'
+
+before_script:
+ - export DISPLAY=:99.0
+ - sh -e /etc/init.d/xvfb start
+ - sleep 3
+
script:
- ./scripts/travis.sh
diff --git a/Gruntfile.js b/Gruntfile.js
index ba2c93e8f..c5b590bb7 100644
--- a/Gruntfile.js
+++ b/Gruntfile.js
@@ -12,17 +12,13 @@ module.exports = function (grunt) {
cmd: "./node_modules/.bin/tslint",
args: ['-c', 'tslint.json', '-p', 'tsconfig.json']
},
- "test": {
+ "unit-tests": {
cmd: "./node_modules/.bin/mocha",
args: ['--compilers', 'ts:ts-node/register', '--recursive', 'test/unit']
},
- "test-int": {
- cmd: "./node_modules/.bin/mocha",
- args: ['--compilers', 'ts:ts-node/register', '--recursive', 'test/integration']
- },
- "test-system": {
- cmd: "./node_modules/.bin/mocha",
- args: ['--compilers', 'ts:ts-node/register', '--recursive', 'test/system']
+ "integration-tests": {
+ cmd: "./node_modules/.bin/cucumber-js",
+ args: ["--compiler", "ts:ts-node/register", "./test/features"]
},
"docker-build": {
cmd: "docker",
@@ -165,5 +161,8 @@ module.exports = function (grunt) {
grunt.registerTask('docker-build', ['run:docker-build']);
grunt.registerTask('docker-restart', ['run:docker-restart']);
- grunt.registerTask('test', ['run:test']);
+ grunt.registerTask('unit-tests', ['run:unit-tests']);
+ grunt.registerTask('integration-tests', ['run:unit-tests']);
+
+ grunt.registerTask('test', ['unit-tests']);
};
diff --git a/README.md b/README.md
index 669960862..aa8d2d07f 100644
--- a/README.md
+++ b/README.md
@@ -91,6 +91,7 @@ Make sure you don't have anything listening on port 8080.
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 public.test.local
127.0.0.1 secret.test.local
127.0.0.1 secret1.test.local
127.0.0.1 secret2.test.local
diff --git a/config.template.yml b/config.template.yml
index e21ca9967..638b59202 100644
--- a/config.template.yml
+++ b/config.template.yml
@@ -48,7 +48,7 @@ ldap:
# beginning of the pattern.
access_control:
default:
- - home.test.local
+ - public.test.local
groups:
admin:
- '*.test.local'
diff --git a/example/ldap/base.ldif b/example/ldap/base.ldif
index f1fbdb887..06e962c04 100644
--- a/example/ldap/base.ldif
+++ b/example/ldap/base.ldif
@@ -45,3 +45,10 @@ mail: bob.dylan@example.com
sn: Bob Dylan
userpassword: {SHA}W6ph5Mm5Pz8GgiULbPgzG37mj9g=
+dn: cn=james,ou=users,dc=example,dc=com
+cn: james
+objectclass: inetOrgPerson
+objectclass: top
+mail: james.dean@example.com
+sn: James Dean
+userpassword: {SHA}W6ph5Mm5Pz8GgiULbPgzG37mj9g=
diff --git a/example/nginx/html/index.html b/example/nginx/html/index.html
index 8f76ab5b3..f009d515e 100644
--- a/example/nginx/html/index.html
+++ b/example/nginx/html/index.html
@@ -9,6 +9,9 @@
You need to log in to access the secret!
Try to access it via one of the following links.
+ -
+ public.test.local
+
-
secret.test.local
@@ -18,9 +21,6 @@
-
secret2.test.local
- -
- home.test.local
-
-
mx1.mail.test.local
@@ -45,7 +45,7 @@
- Default policy
- - home.test.local
+ - public.test.local
- Groups policy
diff --git a/example/nginx/nginx.conf b/example/nginx/nginx.conf
index 53e4e3b8f..c9688f8f3 100644
--- a/example/nginx/nginx.conf
+++ b/example/nginx/nginx.conf
@@ -53,7 +53,8 @@ http {
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;
+ home.test.local mx1.mail.test.local mx2.mail.test.local
+ public.test.local;
ssl on;
ssl_certificate /etc/ssl/server.crt;
diff --git a/package.json b/package.json
index f593dc54d..33c880322 100644
--- a/package.json
+++ b/package.json
@@ -7,7 +7,7 @@
"authelia": "dist/src/server/index.js"
},
"scripts": {
- "test": "./node_modules/.bin/grunt test",
+ "test": "./node_modules/.bin/grunt unit-tests",
"cover": "NODE_ENV=test nyc npm t",
"serve": "node dist/server/index.js"
},
@@ -48,6 +48,7 @@
"@types/body-parser": "^1.16.3",
"@types/connect-redis": "0.0.6",
"@types/cors": "^2.8.1",
+ "@types/cucumber": "^2.0.1",
"@types/ejs": "^2.3.33",
"@types/express": "^4.0.35",
"@types/express-session": "0.0.32",
@@ -64,6 +65,7 @@
"@types/query-string": "^4.3.1",
"@types/randomstring": "^1.1.5",
"@types/request": "0.0.46",
+ "@types/selenium-webdriver": "^3.0.4",
"@types/sinon": "^2.2.1",
"@types/speakeasy": "^2.0.1",
"@types/tmp": "0.0.33",
@@ -71,6 +73,8 @@
"@types/yamljs": "^0.2.30",
"apidoc": "^0.17.6",
"browserify": "^14.3.0",
+ "chromedriver": "^2.31.0",
+ "cucumber": "^2.3.1",
"grunt": "^1.0.1",
"grunt-browserify": "^5.0.0",
"grunt-contrib-concat": "^1.0.1",
@@ -82,7 +86,7 @@
"jquery": "^3.2.1",
"js-logger": "^1.3.0",
"jsdom": "^11.0.0",
- "mocha": "^3.2.0",
+ "mocha": "^3.4.2",
"mockdate": "^2.0.1",
"notifyjs-browser": "^0.4.2",
"nyc": "^10.3.2",
@@ -90,11 +94,12 @@
"proxyquire": "^1.8.0",
"query-string": "^4.3.4",
"request": "^2.81.0",
+ "selenium-webdriver": "^3.5.0",
"should": "^11.1.1",
"sinon": "^2.3.8",
"sinon-promise": "^0.1.3",
"tmp": "0.0.31",
- "ts-node": "^3.0.4",
+ "ts-node": "^3.3.0",
"tslint": "^5.2.0",
"typescript": "^2.3.2",
"u2f-api": "0.0.9",
diff --git a/scripts/dc-dev.sh b/scripts/dc-dev.sh
index 91985ae89..b9cd0630b 100755
--- a/scripts/dc-dev.sh
+++ b/scripts/dc-dev.sh
@@ -3,11 +3,10 @@
set -e
docker-compose \
- -f docker-compose.base.yml \
+ -f docker-compose.base.yml \
-f docker-compose.yml \
-f docker-compose.dev.yml \
-f example/mongo/docker-compose.yml \
-f example/redis/docker-compose.yml \
-f example/nginx/docker-compose.yml \
- -f example/ldap/docker-compose.yml \
- -f test/integration/docker-compose.yml $*
+ -f example/ldap/docker-compose.yml $*
diff --git a/scripts/example/dc-example.sh b/scripts/example/dc-example.sh
index b5669a76b..640976d62 100755
--- a/scripts/example/dc-example.sh
+++ b/scripts/example/dc-example.sh
@@ -8,5 +8,4 @@ docker-compose \
-f example/mongo/docker-compose.yml \
-f example/redis/docker-compose.yml \
-f example/nginx/docker-compose.yml \
- -f example/ldap/docker-compose.yml \
- -f test/integration/docker-compose.yml $*
+ -f example/ldap/docker-compose.yml $*
diff --git a/scripts/integration-tests.sh b/scripts/integration-tests.sh
index 03b662d9f..10d4973dd 100755
--- a/scripts/integration-tests.sh
+++ b/scripts/integration-tests.sh
@@ -1,10 +1,10 @@
#!/bin/bash
DC_SCRIPT=./scripts/example/dc-example.sh
-EXPECTED_SERVICES_COUNT=6
+EXPECTED_SERVICES_COUNT=5
start_services() {
- $DC_SCRIPT up -d mongo redis openldap authelia nginx nginx-tests
+ $DC_SCRIPT up -d mongo redis openldap authelia nginx
sleep 3
}
@@ -27,39 +27,12 @@ expect_services_count() {
}
run_integration_tests() {
- echo "Prepare nginx-test configuration"
- cat example/nginx/nginx.conf | sed 's/listen 443 ssl/listen 8080 ssl/g' | dd of="test/integration/nginx.conf"
-
- echo "Build services images..."
- $DC_SCRIPT build
-
- echo "Start services..."
- start_services
- docker ps -a
-
- echo "Display services logs..."
- $DC_SCRIPT logs redis
- $DC_SCRIPT logs openldap
- $DC_SCRIPT logs nginx
- $DC_SCRIPT logs nginx-tests
- $DC_SCRIPT logs authelia
-
- echo "Check number of services"
- expect_services_count $EXPECTED_SERVICES_COUNT
-
- echo "Run integration tests..."
- $DC_SCRIPT run --rm integration-tests
-
- echo "Shutdown services..."
- shut_services
-}
-
-run_system_tests() {
echo "Start services..."
start_services
expect_services_count $EXPECTED_SERVICES_COUNT
- ./node_modules/.bin/mocha --compilers ts:ts-node/register --recursive test/system
+ sleep 5
+ ./node_modules/.bin/grunt run:integration-tests
shut_services
}
@@ -80,11 +53,8 @@ set -e
echo "Make sure services are not already running"
shut_services
-# Prepare & run integration tests
-run_integration_tests
-
# Prepare & test example from end user perspective
-run_system_tests
+run_integration_tests
# Other tests like executing the deployment script
run_other_tests
diff --git a/src/client/firstfactor/FirstFactorValidator.ts b/src/client/firstfactor/FirstFactorValidator.ts
index 369cd535c..07a27f7d6 100644
--- a/src/client/firstfactor/FirstFactorValidator.ts
+++ b/src/client/firstfactor/FirstFactorValidator.ts
@@ -13,7 +13,7 @@ export function validate(username: string, password: string, $: JQueryStatic): B
})
.fail(function (xhr: JQueryXHR, textStatus: string) {
if (xhr.status == 401)
- reject(new Error("Authetication failed. Please check your credentials"));
+ reject(new Error("Authetication failed. Please check your credentials."));
reject(new Error(textStatus));
});
});
diff --git a/src/client/reset-password/reset-password-request.ts b/src/client/reset-password/reset-password-request.ts
index e390fbc5e..606309773 100644
--- a/src/client/reset-password/reset-password-request.ts
+++ b/src/client/reset-password/reset-password-request.ts
@@ -30,7 +30,7 @@ export default function(window: Window, $: JQueryStatic) {
requestPasswordReset(username)
.then(function () {
- $.notify("An email has been sent. Click on the link to change your password", "success");
+ $.notify("An email has been sent. Click on the link to change your password.", "success");
setTimeout(function () {
window.location.replace(Endpoints.FIRST_FACTOR_GET);
}, 1000);
diff --git a/src/server/lib/AuthenticationRegulator.ts b/src/server/lib/AuthenticationRegulator.ts
index 6741586f0..b3319b4e9 100644
--- a/src/server/lib/AuthenticationRegulator.ts
+++ b/src/server/lib/AuthenticationRegulator.ts
@@ -2,7 +2,7 @@
import * as BluebirdPromise from "bluebird";
import exceptions = require("./Exceptions");
import { UserDataStore } from "./storage/UserDataStore";
-import {AuthenticationTraceDocument} from "./storage/AuthenticationTraceDocument";
+import { AuthenticationTraceDocument } from "./storage/AuthenticationTraceDocument";
const MAX_AUTHENTICATION_COUNT_IN_TIME_RANGE = 3;
@@ -22,19 +22,19 @@ export class AuthenticationRegulator {
regulate(userId: string): BluebirdPromise {
return this.userDataStore.retrieveLatestAuthenticationTraces(userId, false, 3)
- .then((docs: AuthenticationTraceDocument[]) => {
- if (docs.length < MAX_AUTHENTICATION_COUNT_IN_TIME_RANGE) {
- // less than the max authorized number of authentication in time range, thus authorizing access
+ .then((docs: AuthenticationTraceDocument[]) => {
+ if (docs.length < MAX_AUTHENTICATION_COUNT_IN_TIME_RANGE) {
+ // less than the max authorized number of authentication in time range, thus authorizing access
+ return BluebirdPromise.resolve();
+ }
+
+ const oldestDocument = docs[MAX_AUTHENTICATION_COUNT_IN_TIME_RANGE - 1];
+ const noLockMinDate = new Date(new Date().getTime() - this.lockTimeInSeconds * 1000);
+ if (oldestDocument.date > noLockMinDate) {
+ throw new exceptions.AuthenticationRegulationError("Max number of authentication. Please retry in few minutes.");
+ }
+
return BluebirdPromise.resolve();
- }
-
- const oldestDocument = docs[MAX_AUTHENTICATION_COUNT_IN_TIME_RANGE - 1];
- const noLockMinDate = new Date(new Date().getTime() - this.lockTimeInSeconds * 1000);
- if (oldestDocument.date > noLockMinDate) {
- throw new exceptions.AuthenticationRegulationError("Max number of authentication. Please retry in few minutes.");
- }
-
- return BluebirdPromise.resolve();
- });
+ });
}
}
diff --git a/src/server/lib/ldap/Client.ts b/src/server/lib/ldap/Client.ts
index b59b33ee1..d2a6fc6b0 100644
--- a/src/server/lib/ldap/Client.ts
+++ b/src/server/lib/ldap/Client.ts
@@ -41,10 +41,10 @@ export class Client {
reconnect: true
});
- const clientLogger = (ldapClient as any).log;
+ /*const clientLogger = (ldapClient as any).log;
if (clientLogger) {
clientLogger.level("trace");
- }
+ }*/
this.client = BluebirdPromise.promisifyAll(ldapClient) as ldapjs.ClientAsync;
}
diff --git a/src/server/lib/notifiers/FileSystemNotifier.ts b/src/server/lib/notifiers/FileSystemNotifier.ts
index a7b171451..042877ac6 100644
--- a/src/server/lib/notifiers/FileSystemNotifier.ts
+++ b/src/server/lib/notifiers/FileSystemNotifier.ts
@@ -1,7 +1,7 @@
import * as BluebirdPromise from "bluebird";
import * as util from "util";
-import * as fs from "fs";
+import * as Fs from "fs";
import { INotifier } from "./INotifier";
import { Identity } from "../../../types/Identity";
@@ -17,7 +17,7 @@ export class FileSystemNotifier implements INotifier {
notify(identity: Identity, subject: string, link: string): BluebirdPromise {
const content = util.format("Date: %s\nUser: %s\nSubject: %s\nLink: %s", new Date().toString(), identity.userid,
subject, link);
- const writeFilePromised = BluebirdPromise.promisify(fs.writeFile);
+ const writeFilePromised: any = BluebirdPromise.promisify(Fs.writeFile);
return writeFilePromised(this.filename, content);
}
}
diff --git a/src/server/views/errors/401.pug b/src/server/views/errors/401.pug
index dad56a9b6..3cb97413f 100644
--- a/src/server/views/errors/401.pug
+++ b/src/server/views/errors/401.pug
@@ -8,4 +8,4 @@ block form-header
block content
-
You are not authorized.
+ You are either not authorized or not logged in.
diff --git a/src/server/views/errors/403.pug b/src/server/views/errors/403.pug
index 934e75086..b2dc8be65 100644
--- a/src/server/views/errors/403.pug
+++ b/src/server/views/errors/403.pug
@@ -8,4 +8,4 @@ block form-header
block content
- You are not authorized.
+ You are either not authorized or not logged in.
diff --git a/src/server/views/firstfactor.pug b/src/server/views/firstfactor.pug
index fbed5fdb2..ee8ed48a7 100644
--- a/src/server/views/firstfactor.pug
+++ b/src/server/views/firstfactor.pug
@@ -12,7 +12,7 @@ block content
- a(href=reset_password_request_endpoint, class="pull-right link") Forgot password?
+ a(href=reset_password_request_endpoint, class="pull-right link forgot-password") Forgot password?
diff --git a/src/server/views/layout/layout.pug b/src/server/views/layout/layout.pug
index c267782aa..2c0246a0d 100644
--- a/src/server/views/layout/layout.pug
+++ b/src/server/views/layout/layout.pug
@@ -20,7 +20,7 @@ html
diff --git a/src/server/views/secondfactor.pug b/src/server/views/secondfactor.pug
index 1f824c2cf..9ed8a7b53 100644
--- a/src/server/views/secondfactor.pug
+++ b/src/server/views/secondfactor.pug
@@ -9,14 +9,14 @@ block content
-
- a(href=totp_identity_start_endpoint, class="pull-right link") Need to register?
+
+ a(href=totp_identity_start_endpoint, class="pull-right link register-totp") Need to register?
diff --git a/test/features/access-control.feature b/test/features/access-control.feature
new file mode 100644
index 000000000..d853a62ba
--- /dev/null
+++ b/test/features/access-control.feature
@@ -0,0 +1,38 @@
+Feature: User has access restricted access to domains
+
+ Scenario: User john has admin access
+ When I register TOTP and login with user "john" and password "password"
+ Then I have access to:
+ | url |
+ | https://public.test.local:8080/secret.html |
+ | https://secret.test.local:8080/secret.html |
+ | https://secret1.test.local:8080/secret.html |
+ | https://secret2.test.local:8080/secret.html |
+ | https://mx1.mail.test.local:8080/secret.html |
+ | https://mx2.mail.test.local:8080/secret.html |
+
+ Scenario: User bob has restricted access
+ When I register TOTP and login with user "bob" and password "password"
+ Then I have access to:
+ | url |
+ | https://public.test.local:8080/secret.html |
+ | https://secret.test.local:8080/secret.html |
+ | https://secret2.test.local:8080/secret.html |
+ | https://mx1.mail.test.local:8080/secret.html |
+ | https://mx2.mail.test.local:8080/secret.html |
+ And I have no access to:
+ | url |
+ | https://secret1.test.local:8080/secret.html |
+
+ Scenario: User harry has restricted access
+ When I register TOTP and login with user "harry" and password "password"
+ Then I have access to:
+ | url |
+ | https://public.test.local:8080/secret.html |
+ | https://secret1.test.local:8080/secret.html |
+ And I have no access to:
+ | url |
+ | https://secret.test.local:8080/secret.html |
+ | https://secret2.test.local:8080/secret.html |
+ | https://mx1.mail.test.local:8080/secret.html |
+ | https://mx2.mail.test.local:8080/secret.html |
\ No newline at end of file
diff --git a/test/features/authentication.feature b/test/features/authentication.feature
new file mode 100644
index 000000000..b17c11c74
--- /dev/null
+++ b/test/features/authentication.feature
@@ -0,0 +1,53 @@
+Feature: User validate first factor
+
+ Scenario: User succeeds first factor
+ Given I visit "https://auth.test.local:8080/"
+ When I set field "username" to "bob"
+ And I set field "password" to "password"
+ And I click on "Sign in"
+ Then I'm redirected to "https://auth.test.local:8080/secondfactor"
+
+ Scenario: User fails first factor
+ Given I visit "https://auth.test.local:8080/"
+ When I set field "username" to "john"
+ And I set field "password" to "bad-password"
+ And I click on "Sign in"
+ Then I get a notification with message "Error during authentication: Authetication failed. Please check your credentials."
+
+ Scenario: User succeeds TOTP second factor
+ Given I visit "https://auth.test.local:8080/"
+ And I login with user "john" and password "password"
+ And I register a TOTP secret called "Sec0"
+ When I visit "https://secret.test.local:8080/secret.html" and get redirected "https://auth.test.local:8080/"
+ And I login with user "john" and password "password"
+ And I use "Sec0" as TOTP token handle
+ And I click on "TOTP"
+ Then I'm redirected to "https://secret.test.local:8080/secret.html"
+
+ Scenario: User fails TOTP second factor
+ When I visit "https://secret.test.local:8080/secret.html" and get redirected "https://auth.test.local:8080/"
+ And I login with user "john" and password "password"
+ And I use "BADTOKEN" as TOTP token
+ And I click on "TOTP"
+ Then I get a notification with message "Error while validating TOTP token. Cause: error"
+
+ Scenario: User logs out
+ Given I visit "https://auth.test.local:8080/"
+ And I login with user "john" and password "password"
+ And I register a TOTP secret called "Sec0"
+ And I visit "https://auth.test.local:8080/"
+ And I login with user "john" and password "password"
+ And I use "Sec0" as TOTP token handle
+ When I visit "https://auth.test.local:8080/logout?redirect=https://www.google.fr"
+ And I visit "https://secret.test.local:8080/secret.html"
+ Then I'm redirected to "https://auth.test.local:8080/"
+
+ Scenario: Logout redirects user
+ Given I visit "https://auth.test.local:8080/"
+ And I login with user "john" and password "password"
+ And I register a TOTP secret called "Sec0"
+ And I visit "https://auth.test.local:8080/"
+ And I login with user "john" and password "password"
+ And I use "Sec0" as TOTP token handle
+ When I visit "https://auth.test.local:8080/logout?redirect=https://www.google.fr"
+ Then I'm redirected to "https://www.google.fr"
\ No newline at end of file
diff --git a/test/features/redirection.feature b/test/features/redirection.feature
new file mode 100644
index 000000000..7b8f24acb
--- /dev/null
+++ b/test/features/redirection.feature
@@ -0,0 +1,7 @@
+Feature: User is redirected to authelia when he is not authenticated
+
+ Scenario: User is redirected to authelia
+ Given I'm on https://home.test.local:8080
+ When I click on the link to secret.test.local
+ Then I'm redirected to "https://auth.test.local:8080/"
+
diff --git a/test/features/reset-password.feature b/test/features/reset-password.feature
new file mode 100644
index 000000000..b383840cf
--- /dev/null
+++ b/test/features/reset-password.feature
@@ -0,0 +1,33 @@
+Feature: User is able to reset his password
+
+ Scenario: User is redirected to password reset page
+ Given I'm on https://auth.test.local:8080
+ When I click on the link "Forgot password?"
+ Then I'm redirected to "https://auth.test.local:8080/password-reset/request"
+
+ Scenario: User get an email with a link to reset password
+ Given I'm on https://auth.test.local:8080/password-reset/request
+ When I set field "username" to "james"
+ And I click on "Reset Password"
+ Then I get a notification with message "An email has been sent. Click on the link to change your password."
+
+ Scenario: User resets his password
+ Given I'm on https://auth.test.local:8080/password-reset/request
+ And I set field "username" to "james"
+ And I click on "Reset Password"
+ When I click on the link of the email
+ And I set field "password1" to "newpassword"
+ And I set field "password2" to "newpassword"
+ And I click on "Reset Password"
+ Then I'm redirected to "https://auth.test.local:8080/"
+
+
+ Scenario: User does not confirm new password
+ Given I'm on https://auth.test.local:8080/password-reset/request
+ And I set field "username" to "james"
+ And I click on "Reset Password"
+ When I click on the link of the email
+ And I set field "password1" to "newpassword"
+ And I set field "password2" to "newpassword2"
+ And I click on "Reset Password"
+ Then I get a notification with message "The passwords are different"
\ No newline at end of file
diff --git a/test/features/resilience.feature b/test/features/resilience.feature
new file mode 100644
index 000000000..a580db4e1
--- /dev/null
+++ b/test/features/resilience.feature
@@ -0,0 +1,13 @@
+Feature: Authelia keeps user sessions despite the application restart
+
+ Scenario: Session is still valid after Authelia restarts
+ When I register TOTP and login with user "john" and password "password"
+ And the application restarts
+ Then I have access to:
+ | url |
+ | https://public.test.local:8080/secret.html |
+ | https://secret.test.local:8080/secret.html |
+ | https://secret1.test.local:8080/secret.html |
+ | https://secret2.test.local:8080/secret.html |
+ | https://mx1.mail.test.local:8080/secret.html |
+ | https://mx2.mail.test.local:8080/secret.html |
\ No newline at end of file
diff --git a/test/features/restrictions.feature b/test/features/restrictions.feature
new file mode 100644
index 000000000..f87cd3ae8
--- /dev/null
+++ b/test/features/restrictions.feature
@@ -0,0 +1,17 @@
+Feature: Non authenticated users have no access to certain pages
+
+ Scenario Outline: User has no access to protected pages
+ When I visit ""
+ Then I get an error
+
+ Examples:
+ | url | error code |
+ | https://auth.test.local:8080/secondfactor | 401 |
+ | https://auth.test.local:8080/verify | 401 |
+ | https://auth.test.local:8080/secondfactor/u2f/identity/start | 401 |
+ | https://auth.test.local:8080/secondfactor/u2f/identity/finish | 403 |
+ | https://auth.test.local:8080/secondfactor/totp/identity/start | 401 |
+ | https://auth.test.local:8080/secondfactor/totp/identity/finish | 403 |
+ | https://auth.test.local:8080/password-reset/identity/start | 403 |
+ | https://auth.test.local:8080/password-reset/identity/finish | 403 |
+
\ No newline at end of file
diff --git a/test/features/step_definitions/after.ts b/test/features/step_definitions/after.ts
new file mode 100644
index 000000000..c5132ddb2
--- /dev/null
+++ b/test/features/step_definitions/after.ts
@@ -0,0 +1,7 @@
+import Cucumber = require("cucumber");
+
+Cucumber.defineSupportCode(function({After}) {
+ After(function() {
+ return this.driver.quit();
+ });
+});
\ No newline at end of file
diff --git a/test/features/step_definitions/authentication.ts b/test/features/step_definitions/authentication.ts
new file mode 100644
index 000000000..ae86f3f40
--- /dev/null
+++ b/test/features/step_definitions/authentication.ts
@@ -0,0 +1,92 @@
+import Cucumber = require("cucumber");
+import seleniumWebdriver = require("selenium-webdriver");
+import Assert = require("assert");
+import Fs = require("fs");
+import Speakeasy = require("speakeasy");
+import CustomWorld = require("../support/world");
+
+Cucumber.defineSupportCode(function ({ Given, When, Then }) {
+ When(/^I visit "(https:\/\/[a-z0-9:.\/=?-]+)"$/, function (link: string) {
+ return this.visit(link);
+ });
+
+ When("I set field {stringInDoubleQuotes} to {stringInDoubleQuotes}", function (fieldName: string, content: string) {
+ return this.setFieldTo(fieldName, content);
+ });
+
+ When("I click on {stringInDoubleQuotes}", function (text: string) {
+ return this.clickOnButton(text);
+ });
+
+ Given("I login with user {stringInDoubleQuotes} and password {stringInDoubleQuotes}", function (username: string, password: string) {
+ return this.loginWithUserPassword(username, password);
+ });
+
+ Given("I register a TOTP secret called {stringInDoubleQuotes}", function (handle: string) {
+ return this.registerTotpSecret(handle);
+ });
+
+ Given("I use {stringInDoubleQuotes} as TOTP token", function (token: string) {
+ return this.useTotpToken(token);
+ });
+
+ Given("I use {stringInDoubleQuotes} as TOTP token handle", function (handle) {
+ return this.useTotpTokenHandle(handle);
+ });
+
+ Then("I get a notification with message {stringInDoubleQuotes}", function (notificationMessage: string) {
+ const that = this;
+ that.driver.sleep(500);
+ return this.driver
+ .findElement(seleniumWebdriver.By.className("notifyjs-corner"))
+ .findElement(seleniumWebdriver.By.tagName("span"))
+ .findElement(seleniumWebdriver.By.xpath("//span[contains(.,'" + notificationMessage + "')]"));
+ });
+
+ When("I visit {stringInDoubleQuotes} and get redirected {stringInDoubleQuotes}", function (url: string, redirectUrl: string) {
+ const that = this;
+ return this.driver.get(url)
+ .then(function () {
+ return that.driver.wait(seleniumWebdriver.until.urlIs(redirectUrl), 2000);
+ });
+ });
+
+ Given("I register TOTP and login with user {stringInDoubleQuotes} and password {stringInDoubleQuotes}", function (username: string, password: string) {
+ return this.registerTotpAndSignin(username, password);
+ });
+
+ function hasAccessToSecret(link: string, driver: any) {
+ return driver.get(link)
+ .then(function () {
+ return driver.findElement(seleniumWebdriver.By.tagName("body")).getText()
+ .then(function (body: string) {
+ Assert(body.indexOf("This is a very important secret!") > -1);
+ });
+ });
+ }
+
+ function hasNoAccessToSecret(link: string, driver: any) {
+ return driver.get(link)
+ .then(function () {
+ return driver.wait(seleniumWebdriver.until.urlIs("https://auth.test.local:8080/"));
+ });
+ }
+
+ Then("I have access to:", function (dataTable: Cucumber.TableDefinition) {
+ const promises = [];
+ for (let i = 0; i < dataTable.rows().length; i++) {
+ const url = (dataTable.hashes() as any)[i].url;
+ promises.push(hasAccessToSecret(url, this.driver));
+ }
+ return Promise.all(promises);
+ });
+
+ Then("I have no access to:", function (dataTable: Cucumber.TableDefinition) {
+ const promises = [];
+ for (let i = 0; i < dataTable.rows().length; i++) {
+ const url = (dataTable.hashes() as any)[i].url;
+ promises.push(hasNoAccessToSecret(url, this.driver));
+ }
+ return Promise.all(promises);
+ });
+});
\ No newline at end of file
diff --git a/test/features/step_definitions/redirection.ts b/test/features/step_definitions/redirection.ts
new file mode 100644
index 000000000..240fa3184
--- /dev/null
+++ b/test/features/step_definitions/redirection.ts
@@ -0,0 +1,17 @@
+import Cucumber = require("cucumber");
+import seleniumWebdriver = require("selenium-webdriver");
+import Assert = require("assert");
+
+Cucumber.defineSupportCode(function ({ Given, When, Then }) {
+ Given("I'm on https://{string}", function (link: string) {
+ return this.driver.get("https://" + link);
+ });
+
+ When("I click on the link to {string}", function (link: string) {
+ return this.driver.findElement(seleniumWebdriver.By.linkText(link)).click();
+ });
+
+ Then("I'm redirected to {stringInDoubleQuotes}", function (link: string) {
+ return this.driver.wait(seleniumWebdriver.until.urlContains(link), 5000);
+ });
+});
\ No newline at end of file
diff --git a/test/features/step_definitions/reset-password.ts b/test/features/step_definitions/reset-password.ts
new file mode 100644
index 000000000..e1b1c5e0f
--- /dev/null
+++ b/test/features/step_definitions/reset-password.ts
@@ -0,0 +1,20 @@
+import Cucumber = require("cucumber");
+import seleniumWebdriver = require("selenium-webdriver");
+import Assert = require("assert");
+import Fs = require("fs");
+
+Cucumber.defineSupportCode(function ({ Given, When, Then }) {
+ When("I click on the link {stringInDoubleQuotes}", function (text: string) {
+ return this.driver.findElement(seleniumWebdriver.By.linkText(text)).click();
+ });
+
+ When("I click on the link of the email", function () {
+ const notif = Fs.readFileSync("./notifications/notification.txt").toString();
+ const regexp = new RegExp(/Link: (.+)/);
+ const match = regexp.exec(notif);
+ const link = match[1];
+ const that = this;
+
+ return this.driver.get(link);
+ });
+});
\ No newline at end of file
diff --git a/test/features/step_definitions/resilience.ts b/test/features/step_definitions/resilience.ts
new file mode 100644
index 000000000..1eb5f15a4
--- /dev/null
+++ b/test/features/step_definitions/resilience.ts
@@ -0,0 +1,12 @@
+import Cucumber = require("cucumber");
+import seleniumWebdriver = require("selenium-webdriver");
+import Assert = require("assert");
+import ChildProcess = require("child_process");
+import BluebirdPromise = require("bluebird");
+
+Cucumber.defineSupportCode(function ({ Given, When, Then }) {
+ When(/^the application restarts$/, {timeout: 15 * 1000}, function () {
+ const exec = BluebirdPromise.promisify(ChildProcess.exec);
+ return exec("./scripts/example/dc-example.sh restart authelia && sleep 2");
+ });
+});
\ No newline at end of file
diff --git a/test/features/step_definitions/restrictions.ts b/test/features/step_definitions/restrictions.ts
new file mode 100644
index 000000000..cf3ab289a
--- /dev/null
+++ b/test/features/step_definitions/restrictions.ts
@@ -0,0 +1,11 @@
+import Cucumber = require("cucumber");
+import seleniumWebdriver = require("selenium-webdriver");
+import Assert = require("assert");
+
+Cucumber.defineSupportCode(function ({ Given, When, Then }) {
+ Then("I get an error {number}", function (code: number) {
+ return this.driver
+ .findElement(seleniumWebdriver.By.tagName("h1"))
+ .findElement(seleniumWebdriver.By.xpath("//h1[contains(.,'Error " + code + "')]"));
+ });
+});
\ No newline at end of file
diff --git a/test/features/support/world.ts b/test/features/support/world.ts
new file mode 100644
index 000000000..367b4df07
--- /dev/null
+++ b/test/features/support/world.ts
@@ -0,0 +1,110 @@
+require("chromedriver");
+import seleniumWebdriver = require("selenium-webdriver");
+import Cucumber = require("cucumber");
+import Fs = require("fs");
+import Speakeasy = require("speakeasy");
+
+function CustomWorld() {
+ const that = this;
+ this.driver = new seleniumWebdriver.Builder()
+ .forBrowser("chrome")
+ .build();
+
+ this.totpSecrets = {};
+
+ this.visit = function (link: string) {
+ return this.driver.get(link);
+ };
+
+ this.setFieldTo = function (fieldName: string, content: string) {
+ return this.driver.findElement(seleniumWebdriver.By.id(fieldName))
+ .sendKeys(content);
+ };
+
+ this.clickOnButton = function (buttonText: string) {
+ return this.driver
+ .findElement(seleniumWebdriver.By.tagName("button"))
+ .findElement(seleniumWebdriver.By.xpath("//button[contains(.,'" + buttonText + "')]"))
+ .click();
+ };
+
+ this.loginWithUserPassword = function (username: string, password: string) {
+ return this.driver
+ .findElement(seleniumWebdriver.By.id("username"))
+ .sendKeys(username)
+ .then(function () {
+ return that.driver.findElement(seleniumWebdriver.By.id("password"))
+ .sendKeys(password);
+ })
+ .then(function () {
+ return that.driver.findElement(seleniumWebdriver.By.tagName("button"))
+ .click();
+ })
+ .then(function () {
+ return that.driver.wait(seleniumWebdriver.until.elementLocated(seleniumWebdriver.By.className("register-totp")), 4000);
+ });
+ };
+
+ this.registerTotpSecret = function (totpSecretHandle: string) {
+ return this.driver.findElement(seleniumWebdriver.By.className("register-totp")).click()
+ .then(function () {
+ const notif = Fs.readFileSync("./notifications/notification.txt").toString();
+ const regexp = new RegExp(/Link: (.+)/);
+ const match = regexp.exec(notif);
+ const link = match[1];
+ console.log("Link: " + link);
+ return that.driver.get(link);
+ })
+ .then(function () {
+ return that.driver.wait(seleniumWebdriver.until.elementLocated(seleniumWebdriver.By.id("secret")), 1000);
+ })
+ .then(function () {
+ return that.driver.findElement(seleniumWebdriver.By.id("secret")).getText();
+ })
+ .then(function (secret: string) {
+ that.totpSecrets[totpSecretHandle] = secret;
+ });
+ };
+
+ this.useTotpTokenHandle = function (totpSecretHandle: string) {
+ const token = Speakeasy.totp({
+ secret: this.totpSecrets[totpSecretHandle],
+ encoding: "base32"
+ });
+ return this.useTotpToken(token);
+ };
+
+ this.useTotpToken = function (totpSecret: string) {
+ return this.driver.findElement(seleniumWebdriver.By.id("token"))
+ .sendKeys(totpSecret);
+ };
+
+ this.registerTotpAndSignin = function (username: string, password: string) {
+ const totpHandle = "HANDLE";
+ const authUrl = "https://auth.test.local:8080/";
+ const that = this;
+ return this.visit(authUrl)
+ .then(function () {
+ return that.loginWithUserPassword(username, password);
+ })
+ .then(function () {
+ return that.registerTotpSecret(totpHandle);
+ })
+ .then(function () {
+ return that.visit(authUrl);
+ })
+ .then(function () {
+ return that.loginWithUserPassword(username, password);
+ })
+ .then(function () {
+ return that.useTotpTokenHandle(totpHandle);
+ })
+ .then(function () {
+ return that.clickOnButton("TOTP");
+ });
+ };
+}
+
+Cucumber.defineSupportCode(function ({ setWorldConstructor }) {
+ setWorldConstructor(CustomWorld);
+});
\ No newline at end of file
diff --git a/test/integration/Dockerfile b/test/integration/Dockerfile
deleted file mode 100644
index 16e8a2093..000000000
--- a/test/integration/Dockerfile
+++ /dev/null
@@ -1,4 +0,0 @@
-FROM node:7-alpine
-
-WORKDIR /usr/src
-
diff --git a/test/integration/docker-compose.yml b/test/integration/docker-compose.yml
deleted file mode 100644
index 090a132bc..000000000
--- a/test/integration/docker-compose.yml
+++ /dev/null
@@ -1,30 +0,0 @@
-version: '2'
-services:
- integration-tests:
- build: ./test/integration
- command: ./node_modules/.bin/mocha --compilers ts:ts-node/register --recursive test/integration
- volumes:
- - ./:/usr/src
- networks:
- - example-network
-
- nginx-tests:
- image: nginx:alpine
- volumes:
- - ./example/nginx/html:/usr/share/nginx/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
diff --git a/test/integration/main.ts b/test/integration/main.ts
deleted file mode 100644
index 0f6db7d5e..000000000
--- a/test/integration/main.ts
+++ /dev/null
@@ -1,112 +0,0 @@
-
-process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
-
-import Request = require("request");
-import Assert = require("assert");
-import BluebirdPromise = require("bluebird");
-import Util = require("util");
-import Redis = require("redis");
-import Endpoints = require("../../src/server/endpoints");
-
-const RequestAsync = BluebirdPromise.promisifyAll(Request) as typeof Request;
-
-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);
-const FIRST_FACTOR_URL = Util.format("%s/api/firstfactor", BASE_AUTH_URL);
-const LOGOUT_URL = Util.format("%s/logout", BASE_AUTH_URL);
-
-
-const redisOptions = {
- host: "redis",
- port: 6379
-};
-
-
-describe("integration tests", function () {
- let redisClient: Redis.RedisClient;
-
- before(function () {
- redisClient = Redis.createClient(redisOptions);
- });
-
- function str_contains(str: string, pattern: string) {
- return str.indexOf(pattern) != -1;
- }
-
- function test_homepage_is_correct(body: string) {
- Assert(str_contains(body, BASE_AUTH_URL + Endpoints.LOGOUT_GET + "?redirect=" + HOME_URL + "/"));
- Assert(str_contains(body, HOME_URL + "/secret.html"));
- Assert(str_contains(body, SECRET_URL + "/secret.html"));
- Assert(str_contains(body, SECRET1_URL + "/secret.html"));
- Assert(str_contains(body, SECRET2_URL + "/secret.html"));
- Assert(str_contains(body, MX1_URL + "/secret.html"));
- Assert(str_contains(body, MX2_URL + "/secret.html"));
- Assert(str_contains(body, "Access the secret"));
- }
-
- it("should access the home page", function () {
- return RequestAsync.getAsync(HOME_URL)
- .then(function (response: Request.RequestResponse) {
- Assert.equal(200, response.statusCode);
- test_homepage_is_correct(response.body);
- });
- });
-
- it("should access the authentication page", function () {
- return RequestAsync.getAsync(BASE_AUTH_URL)
- .then(function (response: Request.RequestResponse) {
- Assert.equal(200, response.statusCode);
- Assert(response.body.indexOf("Sign in") > -1);
- });
- });
-
- it("should fail first factor when wrong credentials are provided", function () {
- return RequestAsync.postAsync(FIRST_FACTOR_URL, {
- json: true,
- body: {
- username: "john",
- password: "wrong password"
- }
- })
- .then(function (response: Request.RequestResponse) {
- Assert.equal(401, response.statusCode);
- });
- });
-
- it("should redirect when correct credentials are provided during first factor", function () {
- return RequestAsync.postAsync(FIRST_FACTOR_URL, {
- json: true,
- body: {
- username: "john",
- password: "password"
- }
- })
- .then(function (response: Request.RequestResponse) {
- Assert.equal(302, response.statusCode);
- });
- });
-
- it("should have registered four sessions in redis", function (done) {
- redisClient.dbsize(function (err: Error, count: number) {
- Assert.equal(3, count);
- done();
- });
- });
-
- it("should redirect to home page when logout is called", function () {
- return RequestAsync.getAsync(Util.format("%s?redirect=%s", LOGOUT_URL, HOME_URL))
- .then(function (response: Request.RequestResponse) {
- Assert.equal(200, response.statusCode);
- Assert(response.body.indexOf("Access the secret") > -1);
- });
- });
-});
\ No newline at end of file
diff --git a/test/system/main.ts b/test/system/main.ts
deleted file mode 100644
index 7e7591b97..000000000
--- a/test/system/main.ts
+++ /dev/null
@@ -1,92 +0,0 @@
-
-process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
-
-import Request = require("request");
-import Assert = require("assert");
-import BluebirdPromise = require("bluebird");
-import Util = require("util");
-import Endpoints = require("../../src/server/endpoints");
-
-const RequestAsync = BluebirdPromise.promisifyAll(Request) as typeof Request;
-
-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);
-const FIRST_FACTOR_URL = Util.format("%s/api/firstfactor", BASE_AUTH_URL);
-const LOGOUT_URL = Util.format("%s/logout", BASE_AUTH_URL);
-
-
-describe("test example environment", function () {
- function str_contains(str: string, pattern: string) {
- return str.indexOf(pattern) != -1;
- }
-
- function test_homepage_is_correct(body: string) {
- Assert(str_contains(body, BASE_AUTH_URL + Endpoints.LOGOUT_GET + "?redirect=" + HOME_URL + "/"));
- Assert(str_contains(body, HOME_URL + "/secret.html"));
- Assert(str_contains(body, SECRET_URL + "/secret.html"));
- Assert(str_contains(body, SECRET1_URL + "/secret.html"));
- Assert(str_contains(body, SECRET2_URL + "/secret.html"));
- Assert(str_contains(body, MX1_URL + "/secret.html"));
- Assert(str_contains(body, MX2_URL + "/secret.html"));
- Assert(str_contains(body, "Access the secret"));
- }
-
- it("should access the home page", function () {
- return RequestAsync.getAsync(HOME_URL)
- .then(function (response: Request.RequestResponse) {
- Assert.equal(200, response.statusCode);
- test_homepage_is_correct(response.body);
- });
- });
-
- it("should access the authentication page", function () {
- return RequestAsync.getAsync(BASE_AUTH_URL)
- .then(function (response: Request.RequestResponse) {
- Assert.equal(200, response.statusCode);
- Assert(response.body.indexOf("Sign in") > -1);
- });
- });
-
- it("should fail first factor when wrong credentials are provided", function () {
- return RequestAsync.postAsync(FIRST_FACTOR_URL, {
- json: true,
- body: {
- username: "john",
- password: "wrong password"
- }
- })
- .then(function (response: Request.RequestResponse) {
- Assert.equal(401, response.statusCode);
- });
- });
-
- it("should redirect when correct credentials are provided during first factor", function () {
- return RequestAsync.postAsync(FIRST_FACTOR_URL, {
- json: true,
- body: {
- username: "john",
- password: "password"
- }
- })
- .then(function (response: Request.RequestResponse) {
- Assert.equal(302, response.statusCode);
- });
- });
-
- it("should redirect to home page when logout is called", function () {
- return RequestAsync.getAsync(Util.format("%s?redirect=%s", LOGOUT_URL, HOME_URL))
- .then(function (response: Request.RequestResponse) {
- Assert.equal(200, response.statusCode);
- Assert(response.body.indexOf("Access the secret") > -1);
- });
- });
-});
\ No newline at end of file
diff --git a/test/unit/client/firstfactor/FirstFactorValidator.test.ts b/test/unit/client/firstfactor/FirstFactorValidator.test.ts
index 7ac115d00..73a686dc2 100644
--- a/test/unit/client/firstfactor/FirstFactorValidator.test.ts
+++ b/test/unit/client/firstfactor/FirstFactorValidator.test.ts
@@ -42,7 +42,7 @@ describe("test FirstFactorValidator", function () {
});
it("should fail with error 401", () => {
- return should_fail_first_factor_validation(401, "Authetication failed. Please check your credentials");
+ return should_fail_first_factor_validation(401, "Authetication failed. Please check your credentials.");
});
});
});
\ No newline at end of file