Add tests for minimal configuration

pull/242/head
Clement Michaud 2018-08-09 22:24:02 +02:00
parent 21653bc7e3
commit 6d6162f26c
51 changed files with 3000 additions and 1879 deletions

View File

@ -22,6 +22,7 @@ addons:
- mx2.mail.example.com
- public.example.com
- authelia.example.com
- admin.example.com
before_install:
- npm install -g npm@'>=2.13.5'

View File

@ -12,8 +12,7 @@ RUN apk --update add --no-cache --virtual \
COPY dist/server /usr/src/server
COPY dist/shared /usr/src/shared
ENV PORT=80
EXPOSE 80
EXPOSE 8080
VOLUME /etc/authelia
VOLUME /var/lib/authelia

View File

@ -41,10 +41,14 @@ module.exports = function (grunt) {
cmd: "./node_modules/.bin/mocha",
args: ['--colors', '--require', 'ts-node/register', 'client/test/**/*.test.ts']
},
"test-int": {
"test-cucumber": {
cmd: "./scripts/run-cucumber.sh",
args: ["./test/features"]
},
"test-minimal-config": {
cmd: "./node_modules/.bin/mocha",
args: ['--colors', '--require', 'ts-node/register', 'test/minimal-config/**/*.ts']
},
"docker-build": {
cmd: "docker",
args: ['build', '-t', 'clems4ever/authelia', '.']
@ -183,7 +187,7 @@ module.exports = function (grunt) {
grunt.registerTask('test-server', ['env:env-test-server-unit', 'run:test-server-unit'])
grunt.registerTask('test-client', ['env:env-test-client-unit', 'run:test-client-unit'])
grunt.registerTask('test-unit', ['test-server', 'test-client']);
grunt.registerTask('test-int', ['run:test-int']);
grunt.registerTask('test-int', ['run:test-cucumber', 'run:test-minimal-config']);
grunt.registerTask('copy-resources', ['copy:resources', 'copy:views', 'copy:images', 'copy:thirdparties', 'concat:css']);
grunt.registerTask('generate-config-schema', ['run:generate-config-schema', 'copy:schema']);

View File

@ -15,10 +15,9 @@ ldap:
password: password
session:
# The secret to encrypt the session cookies.
# The secret to encrypt the session cookies with.
secret: unsecure_session_secret
# 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.
# Note: Authelia must also be served by that domain.
domain: example.com

View File

@ -3,7 +3,7 @@
###############################################################
# The port to listen on
port: 80
port: 8080
# Log level
#

View File

@ -0,0 +1,27 @@
version: '2'
services:
authelia:
build:
context: .
dockerfile: Dockerfile
restart: always
volumes:
- ./server:/usr/src/server
- ./dist/server/src/public_html:/usr/src/server/src/public_html
- ./client:/usr/src/client
- ./shared:/usr/src/shared
- ./node_modules:/usr/src/node_modules
- ./config.minimal.yml:/etc/authelia/config.yml:ro
- /tmp/authelia:/tmp/authelia
environment:
- NODE_TLS_REJECT_UNAUTHORIZED=0
depends_on:
- redis
networks:
- example-network
command:
- "./node_modules/.bin/ts-node"
- "-P"
- "server/tsconfig.json"
- "server/src/index.ts"
- "/etc/authelia/config.yml"

View File

@ -10,4 +10,4 @@ services:
depends_on:
- redis
networks:
- example-network
- example-network

View File

@ -0,0 +1,12 @@
version: '2'
services:
authelia:
build: .
restart: always
volumes:
- ./config.minimal.yml:/etc/authelia/config.yml:ro
- /tmp/authelia:/tmp/authelia
environment:
- NODE_TLS_REJECT_UNAUTHORIZED=0
networks:
- example-network

View File

@ -9,5 +9,6 @@ services:
- NODE_TLS_REJECT_UNAUTHORIZED=0
depends_on:
- redis
- mongo
networks:
- example-network
- example-network

View File

@ -1,8 +0,0 @@
version: '2'
services:
authelia:
volumes:
- ./dist/server:/usr/src/server
- ./dist/shared:/usr/src/shared
networks:
- example-network

View File

@ -1,8 +0,0 @@
version: '2'
services:
nginx-authelia:
image: nginx:alpine
volumes:
- ./example/compose/nginx/authelia/nginx.conf:/etc/nginx/nginx.conf
networks:
- example-network

View File

@ -1,22 +0,0 @@
worker_processes 1;
events {
worker_connections 1024;
}
http {
server {
listen 80;
resolver 127.0.0.11 ipv6=off;
set $upstream_endpoint http://authelia;
location / {
proxy_set_header Host $http_host;
proxy_pass $upstream_endpoint;
}
}
}

View File

@ -0,0 +1,12 @@
version: '2'
services:
nginx-portal:
image: nginx:alpine
volumes:
- ./example/compose/nginx/minimal/nginx.conf:/etc/nginx/nginx.conf
- ./example/compose/nginx/minimal/html:/usr/share/nginx/html
- ./example/compose/nginx/minimal/ssl:/etc/ssl
ports:
- "8080:443"
networks:
- example-network

View File

@ -0,0 +1,11 @@
<html>
<head>
<title>Secret</title>
<link rel="icon" href="/icon.png" type="image/png" />
</head>
<body>
<h1>Secret</h1>
This is a very important secret!<br/>
Go back to <a href="https://home.example.com:8080/">home page</a>.
</body>
</html>

View File

@ -0,0 +1,32 @@
<!DOCTYPE>
<html>
<head>
<title>Home page</title>
<link rel="icon" href="/icon.png" type="image/png" />
</head>
<body>
<h1>Access the secret</h1>
<span style="font-size: 1.2em; color: red">You need to log in to access the secret!</span><br/><br/> Try to access it using
the following links to test Authelia.<br/>
<ul>
<li>
admin.example.com <a href="https://admin.example.com:8080/secret.html"> / secret.html</a>
</li>
</ul>
You can also log off by visiting the following <a href="https://login.example.com:8080/logout?rd=https://home.example.com:8080/">link</a>.
<h1>List of users</h1>
Here is the list of credentials you can log in with.<br/>
<br/> Once first factor is passed, you will need to follow the links to register a secret for the second factor.<br/> Authelia
will send you a fictituous email stored in a <strong>local file</strong> called <strong>/tmp/authelia/notification.txt</strong>.<br/>
It will provide you with the link to complete the registration allowing you to authenticate with 2-factor.
<ul>
<li><strong>john / password</strong>: belongs to <em>admin</em> and <em>dev</em> groups.</li>
<li><strong>bob / password</strong>: belongs to <em>dev</em> group only.</li>
<li><strong>harry / password</strong>: does not belong to any group.</li>
</ul>
</body>
</html>

View File

@ -0,0 +1,99 @@
worker_processes 1;
events {
worker_connections 1024;
}
http {
server {
listen 443 ssl;
server_name login.example.com;
resolver 127.0.0.11 ipv6=off;
set $upstream_endpoint http://authelia:8080;
ssl on;
ssl_certificate /etc/ssl/server.crt;
ssl_certificate_key /etc/ssl/server.key;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header X-Frame-Options "SAMEORIGIN";
location / {
proxy_set_header Host $http_host;
proxy_set_header X-Original-URI $request_uri;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_intercept_errors on;
proxy_pass $upstream_endpoint;
if ($request_method !~ ^(POST)$){
error_page 401 = /error/401;
error_page 403 = /error/403;
error_page 404 = /error/404;
}
}
}
server {
listen 443 ssl;
server_name home.example.com;
resolver 127.0.0.11 ipv6=off;
set $upstream_endpoint http://nginx-backend;
root /usr/share/nginx/html/home;
ssl on;
ssl_certificate /etc/ssl/server.crt;
ssl_certificate_key /etc/ssl/server.key;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header X-Frame-Options "SAMEORIGIN";
}
server {
listen 443 ssl;
server_name admin.example.com;
root /usr/share/nginx/html/admin;
resolver 127.0.0.11 ipv6=off;
set $upstream_verify http://authelia:8080/api/verify;
ssl on;
ssl_certificate /etc/ssl/server.crt;
ssl_certificate_key /etc/ssl/server.key;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header X-Frame-Options "SAMEORIGIN";
location /auth_verify {
internal;
proxy_set_header Host $http_host;
proxy_set_header X-Original-URI $request_uri;
proxy_set_header X-Original-URL $scheme://$http_host$request_uri;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_pass_request_body off;
proxy_set_header Content-Length "";
proxy_pass $upstream_verify;
}
location / {
auth_request /auth_verify;
auth_request_set $redirect $upstream_http_redirect;
auth_request_set $user $upstream_http_remote_user;
auth_request_set $groups $upstream_http_remote_groups;
error_page 401 =302 https://login.example.com:8080?rd=$redirect;
error_page 403 = https://login.example.com:8080/error/403;
}
}
}

View File

@ -0,0 +1,13 @@
-----BEGIN CERTIFICATE-----
MIICATCCAWoCCQCvH2RvyOshNzANBgkqhkiG9w0BAQsFADBFMQswCQYDVQQGEwJB
VTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0
cyBQdHkgTHRkMB4XDTE3MDExNzIzMTc0M1oXDTE4MDExNzIzMTc0M1owRTELMAkG
A1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoMGEludGVybmV0
IFdpZGdpdHMgUHR5IEx0ZDCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEAzZaE
4XE1QyFNbrHBHRhSA53anAsJ5mBeG7Om6SdQcZAYahlDWEbtdoY4hy0gPNGcITcW
eE+WA+PvNRr7PczKEhneIyUUgV+nrz010fM5JnECPxLTe1oFzl4U8dyYiBpTziNz
hiUfq733PRYjcd9BQtcKcN4LdmQvjUHnnQ73TysCAwEAATANBgkqhkiG9w0BAQsF
AAOBgQAUFICtbuqXgL4HBRAg7yGbwokoH8Ar1QKZGe+F2WTR8vaDLOYUL7VsltLE
EJIGrcfs31nItHOBcLJuflrS8y0CQqes5puRw33LL2usSvO8z2q7JhCx+DSBi6yN
RbhcrGOllIdjsrbmd/zAMBVTUyxSisq3Nmk1cZayDvKg+GSAEA==
-----END CERTIFICATE-----

View File

@ -0,0 +1,11 @@
-----BEGIN CERTIFICATE REQUEST-----
MIIBhDCB7gIBADBFMQswCQYDVQQGEwJBVTETMBEGA1UECAwKU29tZS1TdGF0ZTEh
MB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIGfMA0GCSqGSIb3DQEB
AQUAA4GNADCBiQKBgQDNloThcTVDIU1uscEdGFIDndqcCwnmYF4bs6bpJ1BxkBhq
GUNYRu12hjiHLSA80ZwhNxZ4T5YD4+81Gvs9zMoSGd4jJRSBX6evPTXR8zkmcQI/
EtN7WgXOXhTx3JiIGlPOI3OGJR+rvfc9FiNx30FC1wpw3gt2ZC+NQeedDvdPKwID
AQABoAAwDQYJKoZIhvcNAQELBQADgYEAmCX60kspIw1Zfb79AQOarFW5Q2K2h5Vx
/cRbDyHlKtbmG77EtICccULyqf76B1gNRw5Zq3lSotSUcLzsWcdesXCFDC7k87Qf
mpQKPj6GdTYJvdWf8aDwt32tAqWuBIRoAbdx5WbFPPWVfDcm7zDJefBrhNUDH0Qd
vcnxjvPMmOM=
-----END CERTIFICATE REQUEST-----

View File

@ -0,0 +1,15 @@
-----BEGIN RSA PRIVATE KEY-----
MIICXQIBAAKBgQDNloThcTVDIU1uscEdGFIDndqcCwnmYF4bs6bpJ1BxkBhqGUNY
Ru12hjiHLSA80ZwhNxZ4T5YD4+81Gvs9zMoSGd4jJRSBX6evPTXR8zkmcQI/EtN7
WgXOXhTx3JiIGlPOI3OGJR+rvfc9FiNx30FC1wpw3gt2ZC+NQeedDvdPKwIDAQAB
AoGBAIwGcfkO30UawJ+daDeF4g5ejI/toM+NYWuiwBNbWJoQl+Bj1o+gt4obvxKq
tKNX7OxelepZ4oZB0CIuf2LHQfU6cVGdu//or7nfS2FLBYStopZyL6KorZbkqsj1
ikQN4GosJQqaYkexnwjItMFaHaRRX6YnIXp42Jl1glitO3+5AkEA+thn/vwFo24I
fC+7ORpmLi+BVAkTuhMm+C6TIV6s64B+A5oQ82OBCYK9YCOWmS6JHHFDrxJla+3M
2U9KXky63wJBANHQCFCirfuT6esSjbqpCeqtmZG5LWHtL12V9DF7yjHPjmHL9uRu
e9W+Uz33IJbqd82gtZ/ARfpYEjD0JEieQTUCQFo872xzDTQ1qSfDo/5u2MNUo5mv
ikEuEp7FYnhmrp4poyt4iRCFgy4Ask+bfdmtO/XXaRnZ7FJfQYoLVB2ITNECQQCN
gOiauZztl4yj5heAVJFDnWF9To61BOp1C7VtyjdL8NfuTUluNrV+KqapnAp2vhue
q0zTOTH47X0XVxFBiLohAkBuQzPey5I3Ui8inE4sDt/fqX8r/GMhBTxIb9KlV/H6
jKZNs/83n5/ohaX36er8svW9PB4pcqENZ+kBpvDtKVwS
-----END RSA PRIVATE KEY-----

View File

@ -10,7 +10,7 @@ http {
server_name login.example.com;
resolver 127.0.0.11 ipv6=off;
set $upstream_endpoint http://nginx-authelia;
set $upstream_endpoint http://authelia:8080;
ssl on;
ssl_certificate /etc/ssl/server.crt;
@ -61,7 +61,7 @@ http {
server_name public.example.com;
resolver 127.0.0.11 ipv6=off;
set $upstream_verify http://nginx-authelia/api/verify;
set $upstream_verify http://authelia:8080/api/verify;
set $upstream_endpoint http://nginx-backend;
set $upstream_headers http://httpbin:8000/headers;
@ -129,7 +129,7 @@ http {
server_name admin.example.com;
resolver 127.0.0.11 ipv6=off;
set $upstream_verify http://nginx-authelia/api/verify;
set $upstream_verify http://authelia:8080/api/verify;
set $upstream_endpoint http://nginx-backend;
ssl on;
@ -179,7 +179,7 @@ http {
server_name dev.example.com;
resolver 127.0.0.11 ipv6=off;
set $upstream_verify http://nginx-authelia/api/verify;
set $upstream_verify http://authelia:8080/api/verify;
set $upstream_endpoint http://nginx-backend;
ssl on;
@ -229,7 +229,7 @@ http {
server_name mx1.mail.example.com mx2.mail.example.com;
resolver 127.0.0.11 ipv6=off;
set $upstream_verify http://nginx-authelia/api/verify;
set $upstream_verify http://authelia:8080/api/verify;
set $upstream_endpoint http://nginx-backend;
ssl on;
@ -279,7 +279,7 @@ http {
server_name single_factor.example.com;
resolver 127.0.0.11 ipv6=off;
set $upstream_verify http://nginx-authelia/api/verify;
set $upstream_verify http://authelia:8080/api/verify;
set $upstream_endpoint http://nginx-backend;
set $upstream_headers http://httpbin:8000/headers;
@ -350,7 +350,7 @@ http {
server_name authelia.example.com;
resolver 127.0.0.11 ipv6=off;
set $upstream_endpoint http://authelia;
set $upstream_endpoint http://authelia:8080;
ssl on;
ssl_certificate /etc/ssl/server.crt;

4058
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,5 +0,0 @@
#!/bin/bash
npm i
grunt schema
grunt build

View File

@ -3,12 +3,10 @@
set -e
docker-compose \
-f docker-compose.yml \
-f docker-compose.dev.yml \
-f example/compose/docker-compose.base.yml \
-f example/compose/authelia/docker-compose.dev.yml \
-f example/compose/mongo/docker-compose.yml \
-f example/compose/redis/docker-compose.yml \
-f example/compose/nginx/authelia/docker-compose.yml \
-f example/compose/nginx/backend/docker-compose.yml \
-f example/compose/nginx/portal/docker-compose.yml \
-f example/compose/smtp/docker-compose.yml \

View File

@ -7,7 +7,6 @@ docker-compose \
-f example/compose/docker-compose.base.yml \
-f example/compose/mongo/docker-compose.yml \
-f example/compose/redis/docker-compose.yml \
-f example/compose/nginx/authelia/docker-compose.yml \
-f example/compose/nginx/backend/docker-compose.yml \
-f example/compose/nginx/portal/docker-compose.yml \
-f example/compose/smtp/docker-compose.yml \

View File

@ -3,4 +3,4 @@
DC_SCRIPT=./scripts/example-commit/dc-example.sh
$DC_SCRIPT build
$DC_SCRIPT up -d httpbin mongo redis openldap authelia smtp nginx-authelia nginx-portal nginx-backend
$DC_SCRIPT up -d httpbin mongo redis openldap authelia smtp nginx-portal nginx-backend

View File

@ -7,7 +7,6 @@ docker-compose \
-f example/compose/docker-compose.base.yml \
-f example/compose/mongo/docker-compose.yml \
-f example/compose/redis/docker-compose.yml \
-f example/compose/nginx/authelia/docker-compose.yml \
-f example/compose/nginx/backend/docker-compose.yml \
-f example/compose/nginx/portal/docker-compose.yml \
-f example/compose/smtp/docker-compose.yml \

View File

@ -3,4 +3,4 @@
DC_SCRIPT=./scripts/example-dockerhub/dc-example.sh
#$DC_SCRIPT build
$DC_SCRIPT up -d httpbin mongo redis openldap authelia smtp nginx-authelia nginx-portal nginx-backend
$DC_SCRIPT up -d httpbin mongo redis openldap authelia smtp nginx-portal nginx-backend

View File

@ -1,25 +1,12 @@
#!/bin/bash
DC_SCRIPT=./scripts/example-commit/dc-example.sh
EXPECTED_SERVICES_COUNT=9
EXPECTED_SERVICES_COUNT=8
build_services() {
$DC_SCRIPT build authelia
}
start_services() {
$DC_SCRIPT up -d httpbin mongo redis openldap authelia smtp nginx-authelia nginx-portal nginx-backend
sleep 3
}
shut_services() {
containers_exist=`docker ps -aq | wc -l`
if [ "$containers_exist" -ne "0" ]
then
docker rm -f $(docker ps -aq)
fi
}
expect_services_count() {
EXPECTED_COUNT=$1
service_count=`docker ps -a | grep "Up " | wc -l`
@ -35,19 +22,11 @@ expect_services_count() {
}
run_integration_tests() {
echo "Start services..."
start_services
expect_services_count $EXPECTED_SERVICES_COUNT
sleep 5
./node_modules/.bin/grunt run:test-int
shut_services
./node_modules/.bin/grunt test-int
}
run_other_tests() {
echo "Test dev environment deployment (commands in README)"
# rm -rf node_modules
# ./scripts/build-dev.sh
./scripts/example-commit/deploy-example.sh
expect_services_count $EXPECTED_SERVICES_COUNT
./scripts/example-commit/undeploy-example.sh
@ -60,18 +39,14 @@ run_other_tests_docker() {
./scripts/example-dockerhub/undeploy-example.sh
}
set -e
echo "Make sure services are not already running"
shut_services
# Build the container
build_services
# Pull all images
$DC_SCRIPT pull
# Prepare & test example from end user perspective
run_integration_tests

View File

@ -4,6 +4,8 @@ set -e
docker --version
docker-compose --version
echo "node `node -v`"
echo "npm `npm -v`"
# Generate configuration schema
grunt schema

View File

@ -22,23 +22,36 @@ export interface Configuration {
totp?: TotpConfiguration;
}
export function complete(configuration: Configuration): [Configuration, string[]] {
const newConfiguration: Configuration = JSON.parse(JSON.stringify(configuration));
export function complete(
configuration: Configuration):
[Configuration, string[]] {
const newConfiguration: Configuration = JSON.parse(
JSON.stringify(configuration));
const errors: string[] = [];
newConfiguration.access_control = AclConfigurationComplete(newConfiguration.access_control);
newConfiguration.ldap = LdapConfigurationComplete(newConfiguration.ldap);
newConfiguration.access_control = AclConfigurationComplete(
newConfiguration.access_control);
newConfiguration.ldap = LdapConfigurationComplete(
newConfiguration.ldap);
newConfiguration.authentication_methods = AuthenticationMethodsConfigurationComplete(newConfiguration.authentication_methods);
newConfiguration.authentication_methods =
AuthenticationMethodsConfigurationComplete(
newConfiguration.authentication_methods);
if (!newConfiguration.logs_level) {
newConfiguration.logs_level = "info";
}
// In single factor mode, notifier section is optional.
if (!MethodCalculator.isSingleFactorOnlyMode(newConfiguration.authentication_methods)) {
const [notifier, error] = NotifierConfigurationComplete(newConfiguration.notifier);
if (!MethodCalculator.isSingleFactorOnlyMode(
newConfiguration.authentication_methods) ||
newConfiguration.notifier) {
const [notifier, error] = NotifierConfigurationComplete(
newConfiguration.notifier);
newConfiguration.notifier = notifier;
if (error) errors.push(error);
}
@ -46,10 +59,14 @@ export function complete(configuration: Configuration): [Configuration, string[]
newConfiguration.port = 8080;
}
newConfiguration.regulation = RegulationConfigurationComplete(newConfiguration.regulation);
newConfiguration.session = SessionConfigurationComplete(newConfiguration.session);
newConfiguration.storage = StorageConfigurationComplete(newConfiguration.storage);
newConfiguration.totp = TotpConfigurationComplete(newConfiguration.totp);
newConfiguration.regulation = RegulationConfigurationComplete(
newConfiguration.regulation);
newConfiguration.session = SessionConfigurationComplete(
newConfiguration.session);
newConfiguration.storage = StorageConfigurationComplete(
newConfiguration.storage);
newConfiguration.totp = TotpConfigurationComplete(
newConfiguration.totp);
return [newConfiguration, errors];
}

View File

@ -2,10 +2,19 @@ import Assert = require("assert");
import { NotifierConfiguration, complete } from "./NotifierConfiguration";
describe("configuration/schema/NotifierConfiguration", function() {
it("should ensure at least one key is provided", function() {
it("should use a default notifier when none is provided", function() {
const configuration: NotifierConfiguration = {};
const [newConfiguration, error] = complete(configuration);
Assert.deepEqual(newConfiguration.filesystem, {filename: "/tmp/authelia/notification.txt"})
});
it("should ensure correct key is provided", function() {
const configuration = {
abc: 'badvalue'
};
const [newConfiguration, error] = complete(configuration as any);
Assert.equal(error, "Notifier must have one of the following keys: 'filesystem', 'email' or 'smtp'");
});

View File

@ -29,7 +29,7 @@ export function complete(configuration: NotifierConfiguration): [NotifierConfigu
const newConfiguration: NotifierConfiguration = (configuration) ? JSON.parse(JSON.stringify(configuration)) : {};
if (Object.keys(newConfiguration).length == 0)
newConfiguration.filesystem = { filename: '/tmp/authelia-notification.txt' };
newConfiguration.filesystem = { filename: "/tmp/authelia/notification.txt" };
const ERROR = "Notifier must have one of the following keys: 'filesystem', 'email' or 'smtp'";
@ -42,4 +42,4 @@ export function complete(configuration: NotifierConfiguration): [NotifierConfigu
return [newConfiguration, ERROR];
return [newConfiguration, undefined];
}
}

View File

@ -0,0 +1,41 @@
const { exec } = require('child_process');
import Bluebird = require("bluebird");
function docker_compose(includes: string[]) {
const compose_args = includes.map((dc: string) => `-f ${dc}`).join(' ');
return `docker-compose ${compose_args}`;
}
export function setup(includes: string[], setupTime: number = 2000): Bluebird<void> {
const command = docker_compose(includes) + ' up -d'
console.log('Starting up environment.');
console.log('Running: %s', command);
return new Bluebird<void>(function(resolve, reject) {
exec(command, function(err, stdout, stderr) {
if(err) {
reject(err);
return;
}
setTimeout(function() {
resolve();
}, setupTime);
});
});
}
export function cleanup(includes: string[]): Bluebird<void> {
const command = docker_compose(includes) + ' down';
console.log('Shutting down environment.');
console.log('Running: %s', command);
return new Bluebird<void>(function(resolve, reject) {
exec(command, function(err, stdout, stderr) {
if(err) {
reject(err);
return;
}
resolve();
});
});
}

View File

@ -10,6 +10,7 @@ Feature: User is correctly redirected
And I login with user "john" and password "badpassword"
And I wait for notification to disappear
And I clear field "username"
And I clear field "password"
And I login with user "john" and password "password"
And I use "REGISTERED" as TOTP token handle
And I click on "Sign in"

View File

@ -8,7 +8,6 @@ When("I query {string}", function (url: string) {
const that = this;
return Request(url, { followRedirect: false })
.then(function(response) {
console.log(response);
that.response = response;
})
.catch(function(err: Error) {
@ -26,7 +25,7 @@ Then("I get error code 401", function() {
if(that.response)
reject(new Error("No error thrown"));
else if(that.error.statusCode != 401)
reject(new Error("Error code != 401"));
reject(new Error(`Error code (${that.error.statusCode}) != 401`));
}
});
});

View File

@ -1,4 +1,4 @@
import {setDefaultTimeout, After, Before} from "cucumber";
import {setDefaultTimeout, After, Before, BeforeAll, AfterAll} from "cucumber";
import fs = require("fs");
import BluebirdPromise = require("bluebird");
import ChildProcess = require("child_process");
@ -10,11 +10,32 @@ import { TotpHandler } from "../../../server/src/lib/authentication/totp/TotpHan
import Speakeasy = require("speakeasy");
import Request = require("request-promise");
import { TOTPSecret } from "../../../server/types/TOTPSecret";
import Environment = require("../../environment");
setDefaultTimeout(20 * 1000);
setDefaultTimeout(30 * 1000);
const exec = BluebirdPromise.promisify<any, any>(ChildProcess.exec);
const includes = [
"docker-compose.yml",
"example/compose/docker-compose.base.yml",
"example/compose/mongo/docker-compose.yml",
"example/compose/redis/docker-compose.yml",
"example/compose/nginx/backend/docker-compose.yml",
"example/compose/nginx/portal/docker-compose.yml",
"example/compose/smtp/docker-compose.yml",
"example/compose/httpbin/docker-compose.yml",
"example/compose/ldap/docker-compose.yml"
]
BeforeAll(function() {
return Environment.setup(includes, 10000);
});
AfterAll(function() {
return Environment.cleanup(includes)
});
Before(function () {
this.jar = Request.jar();
})

View File

@ -0,0 +1,12 @@
import SeleniumWebdriver = require("selenium-webdriver");
import Bluebird = require("bluebird");
export default function(driver: any) {
return driver.findElement(
SeleniumWebdriver.By.tagName('h1')).getText()
.then(function(content: string) {
return (content.indexOf('Secret') > -1)
? Bluebird.resolve()
: Bluebird.reject(new Error("Secret is not accessible."));
})
}

View File

@ -0,0 +1,13 @@
import SeleniumWebdriver = require("selenium-webdriver");
export default function(driver: any, buttonText: string) {
return driver.wait(
SeleniumWebdriver.until.elementLocated(
SeleniumWebdriver.By.tagName("button")), 5000)
.then(function () {
return driver
.findElement(SeleniumWebdriver.By.tagName("button"))
.findElement(SeleniumWebdriver.By.xpath("//button[contains(.,'" + buttonText + "')]"))
.click();
});
};

View File

@ -0,0 +1,17 @@
import SeleniumWebdriver = require("selenium-webdriver");
export default function(driver: any, username: string, password: string) {
return driver.wait(SeleniumWebdriver.until.elementLocated(SeleniumWebdriver.By.id("username")), 5000)
.then(function () {
return driver.findElement(SeleniumWebdriver.By.id("username"))
.sendKeys(username);
})
.then(function () {
return driver.findElement(SeleniumWebdriver.By.id("password"))
.sendKeys(password);
})
.then(function () {
return driver.findElement(SeleniumWebdriver.By.tagName("button"))
.click();
});
};

View File

@ -0,0 +1,11 @@
import VisitPage from "./visit-page";
import FillLoginPageAndClick from './fill-login-page-and-click';
import RegisterTotp from './register-totp';
import WaitRedirected from './wait-redirected';
export default function(driver: any, user: string) {
return VisitPage(driver, "https://login.example.com:8080/")
.then(() => FillLoginPageAndClick(driver, user, "password"))
.then(() => WaitRedirected(driver, "https://login.example.com:8080/secondfactor"))
.then(() => RegisterTotp(driver));
}

View File

@ -0,0 +1,32 @@
import Bluebird = require("bluebird");
import SeleniumWebdriver = require("selenium-webdriver");
import Fs = require("fs");
function retrieveValidationLinkFromNotificationFile(): Bluebird<string> {
return Bluebird.promisify(Fs.readFile)("/tmp/authelia/notification.txt")
.then(function (data: any) {
const regexp = new RegExp(/Link: (.+)/);
const match = regexp.exec(data);
const link = match[1];
return Bluebird.resolve(link);
});
};
export default function(driver: any): Bluebird<string> {
return driver.wait(SeleniumWebdriver.until.elementLocated(SeleniumWebdriver.By.className("register-totp")), 5000)
.then(function () {
return driver.findElement(SeleniumWebdriver.By.className("register-totp")).click();
})
.then(function () {
return retrieveValidationLinkFromNotificationFile();
})
.then(function (link: string) {
return driver.get(link);
})
.then(function () {
return driver.wait(SeleniumWebdriver.until.elementLocated(SeleniumWebdriver.By.id("secret")), 5000);
})
.then(function () {
return driver.findElement(SeleniumWebdriver.By.id("secret")).getText();
});
};

View File

@ -0,0 +1,18 @@
import SeleniumWebdriver = require("selenium-webdriver");
import Assert = require("assert");
export default function(driver: any, type: string, message: string) {
const notificationEl = driver.findElement(SeleniumWebdriver.By.className("notification"));
return driver.wait(SeleniumWebdriver.until.elementIsVisible(notificationEl), 5000)
.then(function () {
return notificationEl.getText();
})
.then(function (txt: string) {
Assert.equal(message, txt);
return notificationEl.getAttribute("class");
})
.then(function (classes: string) {
Assert(classes.indexOf(type) > -1, "Class '" + type + "' not found in notification element.");
return driver.sleep(500);
});
}

View File

@ -0,0 +1,20 @@
import Speakeasy = require("speakeasy");
import SeleniumWebdriver = require("selenium-webdriver");
import ClickOnButton from "./click-on-button";
export default function(driver: any, secret: string) {
const token = Speakeasy.totp({
secret: secret,
encoding: "base32"
});
return driver.wait(
SeleniumWebdriver.until.elementLocated(
SeleniumWebdriver.By.id("token")), 5000)
.then(function () {
return driver.findElement(SeleniumWebdriver.By.id("token"))
.sendKeys(token);
})
.then(function () {
return ClickOnButton(driver, "Sign in");
});
}

View File

@ -0,0 +1,8 @@
import SeleniumWebdriver = require("selenium-webdriver");
export default function(driver: any, url: string, timeout: number = 5000) {
return driver.get(url)
.then(function () {
return driver.wait(SeleniumWebdriver.until.urlIs(url), timeout);
});
}

View File

@ -0,0 +1,5 @@
import SeleniumWebdriver = require("selenium-webdriver");
export default function(driver: any, url: string, timeout: number = 5000) {
return driver.wait(SeleniumWebdriver.until.urlIs(url), timeout);
}

View File

@ -0,0 +1,13 @@
import SeleniumWebdriver = require("selenium-webdriver");
export default function() {
before(function() {
this.driver = new SeleniumWebdriver.Builder()
.forBrowser("chrome")
.build();
})
after(function() {
this.driver.quit();
});
}

View File

@ -0,0 +1,20 @@
require("chromedriver");
import Environment = require('../environment');
const includes = [
"docker-compose.minimal.yml",
"example/compose/docker-compose.base.yml",
"example/compose/nginx/minimal/docker-compose.yml",
"example/compose/ldap/docker-compose.yml"
]
before(function() {
this.timeout(20000);
return Environment.setup(includes);
});
after(function() {
this.timeout(30000);
return Environment.cleanup(includes);
});

View File

@ -0,0 +1,34 @@
import Bluebird = require("bluebird");
import SeleniumWebdriver = require("selenium-webdriver");
import Fs = require("fs");
import Speakeasy = require("speakeasy");
import WithDriver from '../helpers/with-driver';
import FillLoginPageWithUserAndPasswordAndClick from '../helpers/fill-login-page-and-click';
import WaitRedirected from '../helpers/wait-redirected';
import VisitPage from '../helpers/visit-page';
import SeeNotification from '../helpers/see-notification';
/**
* When user provides bad password,
* Then he gets a notification message.
*/
describe("Provide bad password", function() {
WithDriver();
describe('failed login as john', function() {
before(function() {
this.timeout(10000);
const driver = this.driver;
return VisitPage(driver, "https://login.example.com:8080/")
.then(function() {
return FillLoginPageWithUserAndPasswordAndClick(driver, 'john', 'bad_password');
});
});
it('should get a notification message', function() {
this.timeout(10000);
return SeeNotification(this.driver, "error", "Authentication failed. Please check your credentials.");
});
});
});

View File

@ -0,0 +1,46 @@
require("chromedriver");
import Bluebird = require("bluebird");
import SeleniumWebdriver = require("selenium-webdriver");
import Fs = require("fs");
import Speakeasy = require("speakeasy");
import WithDriver from '../helpers/with-driver';
import FillLoginPageWithUserAndPasswordAndClick from '../helpers/fill-login-page-and-click';
import WaitRedirected from '../helpers/wait-redirected';
import VisitPage from '../helpers/visit-page';
import RegisterTotp from '../helpers/register-totp';
import ValidateTotp from '../helpers/validate-totp';
import AccessSecret from "../helpers/access-secret";
import LoginAndRegisterTotp from '../helpers/login-and-register-totp';
import seeNotification from "../helpers/see-notification";
/**
* Given john has registered a TOTP secret,
* When he fails the TOTP challenge,
* Then he gets a notification message.
*/
describe('Fail TOTP challenge', function() {
this.timeout(10000);
WithDriver();
describe('successfully login as john', function() {
before(function() {
const that = this;
return LoginAndRegisterTotp(this.driver, "john");
});
describe('fail second factor', function() {
before(function() {
const BAD_TOKEN = "125478";
const driver = this.driver;
return VisitPage(driver, "https://login.example.com:8080/?rd=https://admin.example.com:8080/secret.html")
.then(() => FillLoginPageWithUserAndPasswordAndClick(driver, 'john', 'password'))
.then(() => ValidateTotp(driver, BAD_TOKEN));
});
it("get a notification message", function() {
return seeNotification(this.driver, "error", "Authentication failed. Have you already registered your secret?");
});
});
});
});

View File

@ -0,0 +1,32 @@
import SeleniumWebdriver = require("selenium-webdriver");
import WithDriver from '../helpers/with-driver';
import LoginAndRegisterTotp from '../helpers/login-and-register-totp';
/**
* Given the user logs in as john,
* When he register a TOTP token,
* Then he reach a page containing the secret as string an qrcode
*/
describe('Registering TOTP', function() {
this.timeout(10000);
WithDriver();
describe('successfully login as john', function() {
before('register successfully', function() {
this.timeout(10000);
return LoginAndRegisterTotp(this.driver, "john");
})
it("should see generated qrcode", function() {
this.driver.findElement(
SeleniumWebdriver.By.id("qrcode"),
5000);
});
it("should see generated secret", function() {
this.driver.findElement(
SeleniumWebdriver.By.id("secret"),
5000);
});
});
});

View File

@ -0,0 +1,56 @@
require("chromedriver");
import Bluebird = require("bluebird");
import SeleniumWebdriver = require("selenium-webdriver");
import Fs = require("fs");
import Speakeasy = require("speakeasy");
import WithDriver from '../helpers/with-driver';
import FillLoginPageWithUserAndPasswordAndClick from '../helpers/fill-login-page-and-click';
import WaitRedirected from '../helpers/wait-redirected';
import VisitPage from '../helpers/visit-page';
import RegisterTotp from '../helpers/register-totp';
import ValidateTotp from '../helpers/validate-totp';
import AccessSecret from "../helpers/access-secret";
import LoginAndRegisterTotp from '../helpers/login-and-register-totp';
/**
* Given john has registered a TOTP secret,
* When he validates the TOTP second factor,
* Then he has access to secret page.
*/
describe('Validate TOTP factor', function() {
this.timeout(10000);
WithDriver();
describe('successfully login as john', function() {
before(function() {
const that = this;
return LoginAndRegisterTotp(this.driver, "john")
.then(function(secret: string) {
that.secret = secret;
})
});
describe('validate second factor', function() {
before(function() {
const secret = this.secret;
if(!secret) return Bluebird.reject(new Error("No secret!"));
const driver = this.driver;
return VisitPage(driver, "https://login.example.com:8080/?rd=https://admin.example.com:8080/secret.html")
.then(function() {
return FillLoginPageWithUserAndPasswordAndClick(driver, 'john', 'password');
})
.then(function () {
return ValidateTotp(driver, secret);
})
.then(function() {
return WaitRedirected(driver, "https://admin.example.com:8080/secret.html")
});
});
it("should access the secret", function() {
return AccessSecret(this.driver);
});
});
});
});