diff --git a/.gitignore b/.gitignore index 43c1931f..e391408e 100644 --- a/.gitignore +++ b/.gitignore @@ -13,9 +13,6 @@ src/.baseDir.ts *.swp -/config.yml - -npm-debug.log # Directory used by example notifications/ @@ -29,3 +26,7 @@ dist/ .nyc_output/ *.tgz + +# Specific files +/config.yml +/test/integration/nginx.conf diff --git a/Gruntfile.js b/Gruntfile.js index 0c977751..f3f43c1e 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -154,8 +154,9 @@ module.exports = function (grunt) { grunt.registerTask('build-resources', ['copy:resources', 'copy:views', 'copy:images', 'copy:thirdparties', 'concat:css']); - grunt.registerTask('build-dev', ['run:tslint', 'run:build', 'browserify:dist', 'build-resources', 'run:make-dev-views']); - grunt.registerTask('build-dist', ['build-dev', 'run:minify', 'cssmin']); + grunt.registerTask('build-common', ['run:tslint', 'run:build', 'browserify:dist', 'build-resources']); + grunt.registerTask('build-dev', ['build-common', 'run:make-dev-views']); + grunt.registerTask('build-dist', ['build-common', 'run:minify', 'cssmin']); grunt.registerTask('docker-build', ['run:docker-build']); grunt.registerTask('docker-restart', ['run:docker-restart']); diff --git a/config.template.yml b/config.template.yml index c44ac688..e9383816 100644 --- a/config.template.yml +++ b/config.template.yml @@ -12,7 +12,7 @@ logs_level: info # Example: for user john, the DN will be cn=john,ou=users,dc=example,dc=com ldap: # The url of the ldap server - url: ldap://openldap-restriction + url: ldap://openldap # The base dn for every entries base_dn: dc=example,dc=com diff --git a/docker-compose.base.yml b/docker-compose.base.yml index 3432395a..7c610d62 100644 --- a/docker-compose.base.yml +++ b/docker-compose.base.yml @@ -1,5 +1,4 @@ version: '2' - networks: example-network: driver: bridge diff --git a/example/nginx/docker-compose.yml b/example/nginx/docker-compose.yml index f4127377..8857ee3b 100644 --- a/example/nginx/docker-compose.yml +++ b/example/nginx/docker-compose.yml @@ -12,13 +12,13 @@ services: 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 + - 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/example/nginx/nginx.conf b/example/nginx/nginx.conf index bb0749f3..53e4e3b8 100644 --- a/example/nginx/nginx.conf +++ b/example/nginx/nginx.conf @@ -40,9 +40,11 @@ http { proxy_intercept_errors on; - error_page 401 = /error/401; - error_page 403 = /error/403; - error_page 404 = /error/404; + if ($request_method !~ ^(POST)$){ + error_page 401 = /error/401; + error_page 403 = /error/403; + error_page 404 = /error/404; + } } } diff --git a/scripts/check-services.sh b/scripts/check-services.sh index a1085e56..49a76069 100755 --- a/scripts/check-services.sh +++ b/scripts/check-services.sh @@ -2,7 +2,7 @@ service_count=`docker ps -a | grep "Up " | wc -l` -if [ "${service_count}" -eq "4" ] +if [ "${service_count}" -eq "5" ] then echo "Service are up and running." exit 0 diff --git a/scripts/dc-example.sh b/scripts/dc-example.sh index 4b04a22e..82c77f06 100755 --- a/scripts/dc-example.sh +++ b/scripts/dc-example.sh @@ -2,4 +2,4 @@ set -e -docker-compose -f docker-compose.base.yml -f docker-compose.yml -f example/redis/docker-compose.yml -f example/nginx/docker-compose.yml -f example/ldap/docker-compose.yml $* +docker-compose -f docker-compose.base.yml -f 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 $* diff --git a/scripts/integration-tests.sh b/scripts/integration-tests.sh new file mode 100755 index 00000000..bb644a10 --- /dev/null +++ b/scripts/integration-tests.sh @@ -0,0 +1,35 @@ +#!/bin/bash + +set -e + +echo "Make sure services are not already running" +./scripts/dc-example.sh down + +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..." +./scripts/dc-example.sh build + +echo "Start services..." +./scripts/dc-example.sh up -d redis openldap +sleep 2 +./scripts/dc-example.sh up -d authelia nginx nginx-tests +sleep 3 +docker ps -a + +echo "Display services logs..." +./scripts/dc-example.sh logs redis +./scripts/dc-example.sh logs openldap +./scripts/dc-example.sh logs nginx +./scripts/dc-example.sh logs nginx-tests +./scripts/dc-example.sh logs authelia + +echo "Check number of services" +./scripts/check-services.sh + +echo "Run integration tests..." +./scripts/dc-example.sh run --rm integration-tests + +echo "Shutdown services..." +./scripts/dc-example.sh down diff --git a/scripts/run-int-test.sh b/scripts/run-int-test.sh deleted file mode 100755 index cf3df97a..00000000 --- a/scripts/run-int-test.sh +++ /dev/null @@ -1,24 +0,0 @@ -#!/bin/bash - -set -e - -echo "Build services images..." -./scripts/dc-test.sh build - -echo "Start services..." -./scripts/dc-test.sh up -d redis openldap -sleep 2 -./scripts/dc-test.sh up -d authelia nginx -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 diff --git a/scripts/run-staging.sh b/scripts/run-staging.sh deleted file mode 100755 index 7b03e236..00000000 --- a/scripts/run-staging.sh +++ /dev/null @@ -1,15 +0,0 @@ -#!/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 diff --git a/scripts/travis.sh b/scripts/travis.sh index ef0d7e15..7fcb42f3 100755 --- a/scripts/travis.sh +++ b/scripts/travis.sh @@ -11,11 +11,8 @@ 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 +# Run integration/example tests +./scripts/integration-tests.sh # Test npm deployment before actual deployment ./scripts/npm-deployment-test.sh diff --git a/test/integration/Server.test.ts b/test/integration/Server.test.ts deleted file mode 100644 index dd1aa6d6..00000000 --- a/test/integration/Server.test.ts +++ /dev/null @@ -1,164 +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 EXEC_PATH = "./dist/src/server/index.js"; -const CONFIG_PATH = "./test/integration/config.yml"; -const j = Request.jar(); -const 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 { - return getPromised(HOME_URL + "/"); -} - -function getLoginPage(): BluebirdPromise { - return getPromised(BASE_AUTH_URL + Endpoints.FIRST_FACTOR_GET); -} diff --git a/test/integration/config.yml b/test/integration/config.yml deleted file mode 100644 index 7854350e..00000000 --- a/test/integration/config.yml +++ /dev/null @@ -1,97 +0,0 @@ - -# 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 - redis: - host: redis - port: 6379 - - -# 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 - diff --git a/test/integration/docker-compose.yml b/test/integration/docker-compose.yml index afaab5b1..9303e2e9 100644 --- a/test/integration/docker-compose.yml +++ b/test/integration/docker-compose.yml @@ -1,15 +1,6 @@ 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: + integration-tests: build: ./test/integration command: ./node_modules/.bin/mocha --compilers ts:ts-node/register --recursive test/integration volumes: @@ -17,8 +8,7 @@ services: networks: - example-network - - nginx: + nginx-tests: image: nginx:alpine volumes: - ./example/nginx/index.html:/usr/share/nginx/html/index.html @@ -39,4 +29,3 @@ services: - mx1.mail.test.local - mx2.mail.test.local - auth.test.local - diff --git a/test/integration/main.ts b/test/integration/main.ts new file mode 100644 index 00000000..6e7c10f7 --- /dev/null +++ b/test/integration/main.ts @@ -0,0 +1,112 @@ + +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("test example environment", 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/integration/nginx.conf b/test/integration/nginx.conf deleted file mode 100644 index 39f7baa0..00000000 --- a/test/integration/nginx.conf +++ /dev/null @@ -1,86 +0,0 @@ -# nginx-sso - example nginx config -# -# (c) 2015 by Johannes Gilger -# -# 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; - } - } -} - diff --git a/test/integration/redis.test.ts b/test/integration/redis.test.ts deleted file mode 100644 index 185d9da4..00000000 --- a/test/integration/redis.test.ts +++ /dev/null @@ -1,23 +0,0 @@ - -import Redis = require("redis"); -import Assert = require("assert"); - -const redisOptions = { - host: "redis", - port: 6379 -}; - -describe("test redis is correctly used", function () { - let redisClient: Redis.RedisClient; - - before(function () { - redisClient = Redis.createClient(redisOptions); - }); - - it("should have registered at least one session", function (done) { - redisClient.dbsize(function (err: Error, count: number) { - Assert.equal(1, count); - done(); - }); - }); -}); \ No newline at end of file