diff --git a/client/src/containers/views/SecurityKeyRegistrationView/SecurityKeyRegistrationView.ts b/client/src/containers/views/SecurityKeyRegistrationView/SecurityKeyRegistrationView.ts index 2bbc59967..800424c08 100644 --- a/client/src/containers/views/SecurityKeyRegistrationView/SecurityKeyRegistrationView.ts +++ b/client/src/containers/views/SecurityKeyRegistrationView/SecurityKeyRegistrationView.ts @@ -2,7 +2,7 @@ import { connect } from 'react-redux'; import SecurityKeyRegistrationView from '../../../views/SecurityKeyRegistrationView/SecurityKeyRegistrationView'; import { RootState } from '../../../reducers'; import { Dispatch } from 'redux'; -import * as U2fApi from "u2f-api"; +import U2fApi from "u2f-api"; import { Props } from '../../../views/SecurityKeyRegistrationView/SecurityKeyRegistrationView'; import { registerSecurityKey, registerSecurityKeyFailure, registerSecurityKeySuccess } from '../../../reducers/Portal/SecurityKeyRegistration/actions'; import AutheliaService from '../../../services/AutheliaService'; @@ -12,13 +12,8 @@ const mapStateToProps = (state: RootState) => ({ error: state.securityKeyRegistration.error, }); -async function checkIdentity(token: string) { - return fetch(`/api/secondfactor/u2f/identity/finish?token=${token}`, { - method: 'POST', - }); -} - function fail(dispatch: Dispatch, err: Error) { + console.error(err); dispatch(registerSecurityKeyFailure(err.message)); } @@ -27,9 +22,9 @@ const mapDispatchToProps = (dispatch: Dispatch, ownProps: Props) => { onInit: async (token: string) => { try { dispatch(registerSecurityKey()); - await checkIdentity(token); - const requestRegister = await AutheliaService.requestSecurityKeyRegistration(); - const registerResponse = await U2fApi.register(requestRegister, [], 60); + await AutheliaService.completeSecurityKeyRegistrationIdentityValidation(token); + const registerRequest = await AutheliaService.requestSecurityKeyRegistration(); + const registerResponse = await U2fApi.register([registerRequest], [], 60); await AutheliaService.completeSecurityKeyRegistration(registerResponse); dispatch(registerSecurityKeySuccess()); setTimeout(() => { diff --git a/client/src/services/AutheliaService.ts b/client/src/services/AutheliaService.ts index 51f346972..eb154f7cf 100644 --- a/client/src/services/AutheliaService.ts +++ b/client/src/services/AutheliaService.ts @@ -1,5 +1,5 @@ import RemoteState from "../views/AuthenticationView/RemoteState"; -import u2fApi, { SignRequest } from "u2f-api"; +import U2fApi, { SignRequest } from "u2f-api"; import Method2FA from "../types/Method2FA"; import RedirectResponse from "./RedirectResponse"; import PreferedMethodResponse from "./PreferedMethodResponse"; @@ -74,15 +74,11 @@ class AutheliaService { } static async requestSigning() { - return this.fetchSafe('/api/u2f/sign_request') - .then(async (res) => { - const body = await res.json(); - return body as SignRequest; - }); + return this.fetchSafeJson('/api/u2f/sign_request'); } static async completeSecurityKeySigning( - response: u2fApi.SignResponse, redirectionUrl: string | null) { + response: U2fApi.SignResponse, redirectionUrl: string | null) { const headers: Record = { 'Accept': 'application/json', @@ -199,7 +195,7 @@ class AutheliaService { return await this.fetchSafeJson('/api/secondfactor/available'); } - static async completeSecurityKeyRegistration(response: u2fApi.RegisterResponse): Promise { + static async completeSecurityKeyRegistration(response: U2fApi.RegisterResponse): Promise { return await this.fetchSafe('/api/u2f/register', { method: 'POST', headers: { @@ -211,7 +207,18 @@ class AutheliaService { } static async requestSecurityKeyRegistration() { - return this.fetchSafeJson('/api/u2f/register_request') + return this.fetchSafeJson('/api/u2f/register_request') + } + + static async completeSecurityKeyRegistrationIdentityValidation(token: string) { + const res = await this.fetchSafeJson(`/api/secondfactor/u2f/identity/finish?token=${token}`, { + method: 'POST', + }); + + if ('error' in res) { + throw new Error(res['error']); + } + return res; } } diff --git a/docker-compose.yml b/docker-compose.yml index 6ec0652bc..c92928be2 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,5 +1,4 @@ version: '2' -# services: {} networks: authelianet: driver: bridge diff --git a/example/compose/docker-compose.base.yml b/example/compose/docker-compose.base.yml deleted file mode 100644 index fcc3cbb43..000000000 --- a/example/compose/docker-compose.base.yml +++ /dev/null @@ -1,4 +0,0 @@ -version: '2' -networks: - authelianet: - external: true diff --git a/example/compose/nginx/backend/docker-compose.yml b/example/compose/nginx/backend/docker-compose.yml index a34bc38e8..92f61dcb9 100644 --- a/example/compose/nginx/backend/docker-compose.yml +++ b/example/compose/nginx/backend/docker-compose.yml @@ -2,5 +2,9 @@ version: '2' services: nginx-backend: image: authelia-example-backend + labels: + - traefik.frontend.rule=Host:home.example.com,public.example.com,secure.example.com,admin.example.com,singlefactor.example.com + - traefik.frontend.auth.forward.address=http://192.168.240.1:9091/api/verify?rd=https://login.example.com:8080/%23/ + - traefik.frontend.auth.forward.tls.insecureSkipVerify=true networks: - authelianet diff --git a/example/compose/nginx/portal/nginx.conf.ejs b/example/compose/nginx/portal/nginx.conf.ejs index 0e2c2d217..41b368882 100644 --- a/example/compose/nginx/portal/nginx.conf.ejs +++ b/example/compose/nginx/portal/nginx.conf.ejs @@ -35,10 +35,15 @@ http { # Serve the backend API for the portal. location /api { - proxy_set_header Host $http_host; - proxy_set_header X-Original-URI $request_uri; proxy_set_header X-Real-IP $remote_addr; + + # Required by Authelia because "trust proxy" option is used. + # See https://expressjs.com/en/guide/behind-proxies.html proxy_set_header X-Forwarded-Proto $scheme; + + # Required by Authelia to build correct links for identity validation. + proxy_set_header X-Forwarded-Host $http_host; + # Needed for network ACLs to work. It appends the IP of the client to the list of IPs # and allows Authelia to use it to match the network-based ACLs. proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; @@ -65,11 +70,15 @@ http { # Serve the backend API for the portal. location /api { - proxy_set_header Host $http_host; - proxy_set_header X-Original-URI $request_uri; proxy_set_header X-Real-IP $remote_addr; + + # Required by Authelia because "trust proxy" option is used. + # See https://expressjs.com/en/guide/behind-proxies.html proxy_set_header X-Forwarded-Proto $scheme; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + + # Required by Authelia to build correct links for identity validation. + proxy_set_header X-Forwarded-Host $http_host; + # Needed for network ACLs to work. It appends the IP of the client to the list of IPs # and allows Authelia to use it to match the network-based ACLs. proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; @@ -136,16 +145,15 @@ http { # to the virtual endpoint introduced by nginx and declared in the next block. location / { auth_request /auth_verify; - - auth_request_set $redirect $upstream_http_redirect; - auth_request_set $user $upstream_http_remote_user; - proxy_set_header X-Forwarded-User $user; + auth_request_set $user $upstream_http_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 $groups $upstream_http_remote_groups; + proxy_set_header Remote-Groups $groups; - proxy_set_header Host $http_host; + # Route the request to the correct virtual host in the backend. + proxy_set_header Host $http_host; # Authelia relies on Proxy-Authorization header to authenticate in basic auth. # but for the sake of simplicity (because Authorization in supported in most @@ -153,20 +161,35 @@ http { # Proxy-Authorization before sending it to Authelia. proxy_set_header Proxy-Authorization $http_authorization; - error_page 401 =302 https://login.example.com:8080/#/?rd=$redirect; + # mitigate HTTPoxy Vulnerability + # https://www.nginx.com/blog/mitigating-the-httpoxy-vulnerability-with-nginx/ + proxy_set_header Proxy ""; - proxy_pass $upstream_endpoint; + # Set the `target_url` variable based on the request. It will be used to build the portal + # URL with the correct redirection parameter. + set $target_url $scheme://$http_host$request_uri; + error_page 401 =302 https://login.example.com:8080/#/?rd=$target_url; + + proxy_pass $upstream_endpoint; } # Virtual endpoint forwarding requests to Authelia server. 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; + + # Provide either X-Original-URL and X-Forwarded-Proto or + # X-Forwarded-Proto, X-Forwarded-Host and X-Forwarded-Uri or both. + # Those headers will be used by Authelia to deduce the target url of the user. + # + # X-Forwarded-Proto is mandatory since Authelia uses the "trust proxy" option. + # See https://expressjs.com/en/guide/behind-proxies.html + proxy_set_header X-Original-URL $scheme://$http_host$request_uri; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Host $http_host; + proxy_set_header X-Forwarded-Uri $request_uri; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; # Authelia can receive Proxy-Authorization to authenticate however most of the clients @@ -176,24 +199,23 @@ http { proxy_pass_request_body off; proxy_set_header Content-Length ""; - proxy_pass $upstream_verify; + proxy_pass $upstream_verify; } # Used by suites to test the forwarded users and groups headers produced by Authelia. location /headers { auth_request /auth_verify; - auth_request_set $redirect $upstream_http_redirect; - - auth_request_set $user $upstream_http_remote_user; - proxy_set_header Custom-Forwarded-User $user; + auth_request_set $user $upstream_http_remote_user; + proxy_set_header Custom-Forwarded-User $user; - auth_request_set $groups $upstream_http_remote_groups; - proxy_set_header Custom-Forwarded-Groups $groups; + auth_request_set $groups $upstream_http_remote_groups; + proxy_set_header Custom-Forwarded-Groups $groups; - error_page 401 =302 https://login.example.com:8080/#/?rd=$redirect; + set $target_url $scheme://$http_host$request_uri; + error_page 401 =302 https://login.example.com:8080/#/?rd=$target_url; - proxy_pass $upstream_headers; + proxy_pass $upstream_headers; } } diff --git a/example/compose/smtp/docker-compose.yml b/example/compose/smtp/docker-compose.yml index e29ef3531..f79140424 100644 --- a/example/compose/smtp/docker-compose.yml +++ b/example/compose/smtp/docker-compose.yml @@ -4,5 +4,8 @@ services: image: schickling/mailcatcher ports: - "1025:1025" + labels: + - traefik.frontend.rule=Host:mail.example.com + - traefik.port=1080 networks: - authelianet diff --git a/example/compose/traefik/.gitignore b/example/compose/traefik/.gitignore new file mode 100644 index 000000000..f2ab7784c --- /dev/null +++ b/example/compose/traefik/.gitignore @@ -0,0 +1 @@ +traefik.toml \ No newline at end of file diff --git a/example/compose/traefik/docker-compose.yml b/example/compose/traefik/docker-compose.yml new file mode 100644 index 000000000..1eac19a97 --- /dev/null +++ b/example/compose/traefik/docker-compose.yml @@ -0,0 +1,14 @@ +version: '2' +services: + traefik: + image: traefik:v1.7.9-alpine + volumes: + - /var/run/docker.sock:/var/run/docker.sock + - ./example/compose/traefik/traefik.toml:/etc/traefik/traefik.toml + labels: + - traefik.frontend.rule=Host:traefik.example.com + - traefik.port=8081 + networks: + authelianet: + # Set the IP to be able to query on port 443 + ipv4_address: 192.168.240.100 diff --git a/example/compose/traefik/render.js b/example/compose/traefik/render.js new file mode 100755 index 000000000..5a4441ae8 --- /dev/null +++ b/example/compose/traefik/render.js @@ -0,0 +1,40 @@ +#!/usr/bin/env node + +const ejs = require('ejs'); +const fs = require('fs'); +const program = require('commander'); + +let backend; + +program + .version('0.1.0') + .option('-p, --production', 'Render template for production.') + .arguments('[backend]') + .action((backendArg) => backend = backendArg) + .parse(process.argv) + +const options = { + production: false, +} + +if (!backend) { + backend = 'http://192.168.240.1:9091' +} + +if (program.production) { + options['production'] = true; +} + +options['authelia_backend'] = backend; + +const templatePath = __dirname + '/traefik.toml.ejs'; +const outputPath = __dirname + '/traefik.toml'; + +html = ejs.renderFile(templatePath, options, (err, conf) => { + try { + var fd = fs.openSync(outputPath, 'w'); + fs.writeFileSync(fd, conf); + } catch (e) { + fs.writeFileSync(outputPath, conf); + } +}); \ No newline at end of file diff --git a/example/compose/traefik/traefik.toml.ejs b/example/compose/traefik/traefik.toml.ejs new file mode 100644 index 000000000..e6fe7d83a --- /dev/null +++ b/example/compose/traefik/traefik.toml.ejs @@ -0,0 +1,75 @@ +defaultEntryPoints = ["http", "https"] +logLevel = "DEBUG" + +[traefikLog] + filePath = "/var/log/traefik.log" + +[entryPoints] + [entryPoints.http] + address = ":80" + [entryPoints.http.redirect] + entryPoint = "https" + [entryPoints.https] + address = ":8080" + [entryPoints.https.tls] + [entryPoints.api] + address = ":8081" + +[file] + +# TODO(c.michaud): remove this template by providing a proxy doing +# the routing depending on the mode (production or dev) +<% if (!production) { %> + +[frontends] + [frontends.authelia_api] + backend = "authelia_api_backend" + [frontends.authelia_api.routes.route0] + rule = "Host:login.example.com; PathPrefix:/api;" + + [frontends.authelia_front] + backend = "authelia_front_backend" + [frontends.authelia_front.routes.route0] + rule = "Host:login.example.com" + +[backends] + [backends.authelia_api_backend] + [backends.authelia_api_backend.servers.server] + url = "http://192.168.240.1:9091" + + [backends.authelia_front_backend] + [backends.authelia_front_backend.servers.server] + url = "http://192.168.240.1:3000" + +<% } else { %> + +[frontends] + [frontends.authelia] + backend = "authelia_backend" + [frontends.authelia.routes.route0] + rule = "Host:login.example.com" + +[backends] + [backends.authelia_backend] + [backends.authelia_backend.servers.server] + url = "http://192.168.240.1:9091" + +<% } %> + + +[api] +# This is exposed via a subdomain and a proxy +entryPoint = "api" +dashboard = true + +[docker] +# Docker server endpoint. Can be a tcp or a unix socket endpoint. +endpoint = "unix:///var/run/docker.sock" +# network = "traefik_default" + +# Default domain used. +# Can be overridden by setting the "traefik.domain" label on a container. +domain = "localhost" + +# Enable watch docker changes +watch = true diff --git a/example/traefik/config.minimal.yml b/example/traefik/config.minimal.yml deleted file mode 100644 index 8afef58b6..000000000 --- a/example/traefik/config.minimal.yml +++ /dev/null @@ -1,61 +0,0 @@ -############################################################### -# Authelia minimal configuration # -############################################################### - -logs_level: debug - -authentication_backend: - file: - path: /etc/authelia/users_database.yml - -session: - secret: unsecure_session_secret - domain: example.com - -# Configuration of the storage backend used to store data and secrets. i.e. totp data -storage: - local: - path: /etc/authelia/storage - -# TOTP Issuer Name -# -# This will be the issuer name displayed in Google Authenticator -# See: https://github.com/google/google-authenticator/wiki/Key-Uri-Format for more info on issuer names -totp: - issuer: example.com - -# Access Control -# -# Access control is a set of rules you can use to restrict user access to certain -# resources. -access_control: - # Default policy can either be `bypass`, `one_factor`, `two_factor` or `deny`. - default_policy: deny - - rules: - - domain: traefik.example.com - policy: two_factor - - domain: who.example.com - policy: two_factor - -# Configuration of the authentication regulation mechanism. -regulation: - # Set it to 0 to disable max_retries. - max_retries: 3 - - # The user is banned if the authenticaction failed `max_retries` times in a `find_time` seconds window. - find_time: 120 - - # The length of time before a banned user can login again. - ban_time: 300 - -# Default redirection URL -# -# Note: this parameter is optional. If not provided, user won't -# be redirected upon successful authentication. -#default_redirection_url: https://authelia.example.domain - -notifier: - # For testing purpose, notifications can be sent in a file - filesystem: - filename: /tmp/authelia/notification.txt diff --git a/example/traefik/docker-compose.yml b/example/traefik/docker-compose.yml deleted file mode 100644 index 2717938ea..000000000 --- a/example/traefik/docker-compose.yml +++ /dev/null @@ -1,38 +0,0 @@ -version: '2' -services: - traefik: - image: traefik - ports: - - "80:80" - - "443:443" - volumes: - - /var/run/docker.sock:/var/run/docker.sock - - ./traefik.toml:/etc/traefik/traefik.toml - - /etc/traefik - labels: - - traefik.frontend.rule=Host:traefik.example.com - - traefik.port=8080 - - traefik.frontend.auth.forward.address=https://auth.example.com/api/verify?rd=https://auth.example.com - - traefik.frontend.auth.forward.tls.insecureSkipVerify=true - - authelia: - # image: clems4ever/authelia:latest - build: - context: ../.. - dockerfile: Dockerfile.dev - restart: always - volumes: - - ./config.minimal.yml:/etc/authelia/config.yml:ro - - ./users_database.yml:/etc/authelia/users_database.yml:rw - - /tmp/authelia:/tmp/authelia - environment: - - NODE_TLS_REJECT_UNAUTHORIZED=1 - labels: - - traefik.frontend.rule=Host:auth.example.com - - whoami: - image: emilevauge/whoami - labels: - - traefik.frontend.rule=Host:who.example.com - - traefik.frontend.auth.forward.address=https://auth.example.com/api/verify?rd=https://auth.example.com - - traefik.frontend.auth.forward.tls.insecureSkipVerify=true diff --git a/example/traefik/traefik.toml b/example/traefik/traefik.toml deleted file mode 100644 index 6cd052291..000000000 --- a/example/traefik/traefik.toml +++ /dev/null @@ -1,30 +0,0 @@ -defaultEntryPoints = ["http", "https"] -# logLevel = "DEBUG" - -[entryPoints] - [entryPoints.http] - address = ":80" - [entryPoints.http.redirect] - entryPoint = "https" - [entryPoints.https] - address = ":443" - [entryPoints.https.tls] - [entryPoints.api] - address = ":8080" - -[api] -# This is exposed via a subdomain and a proxy -entryPoint = "api" -dashboard = true - -[docker] -# Docker server endpoint. Can be a tcp or a unix socket endpoint. -endpoint = "unix:///var/run/docker.sock" -# network = "traefik_default" - -# Default domain used. -# Can be overridden by setting the "traefik.domain" label on a container. -domain = "localhost" - -# Enable watch docker changes -watch = true diff --git a/scripts/authelia-scripts-bootstrap b/scripts/authelia-scripts-bootstrap index 3b9699108..c72246dde 100755 --- a/scripts/authelia-scripts-bootstrap +++ b/scripts/authelia-scripts-bootstrap @@ -51,6 +51,9 @@ async function checkHostsFile() { await checkAndFixEntry(actualEntries, 'mail.example.com', '192.168.240.100'); await checkAndFixEntry(actualEntries, 'duo.example.com', '192.168.240.100'); + // For Traefik suite. + await checkAndFixEntry(actualEntries, 'traefik.example.com', '192.168.240.100'); + // For testing network ACLs. await checkAndFixEntry(actualEntries, 'proxy-client1.example.com', '192.168.240.201'); await checkAndFixEntry(actualEntries, 'proxy-client2.example.com', '192.168.240.202'); diff --git a/server/src/lib/IdentityCheckMiddleware.ts b/server/src/lib/IdentityCheckMiddleware.ts index 0ecdd8303..4a7f52257 100644 --- a/server/src/lib/IdentityCheckMiddleware.ts +++ b/server/src/lib/IdentityCheckMiddleware.ts @@ -10,11 +10,13 @@ import { AuthenticationSessionHandler } from "./AuthenticationSessionHandler"; import { AuthenticationSession } from "../../types/AuthenticationSession"; import { ServerVariables } from "./ServerVariables"; import { IdentityValidable } from "./IdentityValidable"; +import * as Constants from "../../../shared/constants"; import Identity = require("../../types/Identity"); import { IdentityValidationDocument } from "./storage/IdentityValidationDocument"; import { OPERATION_FAILED } from "../../../shared/UserMessages"; +import GetHeader from "./utils/GetHeader"; function createAndSaveToken(userid: string, challenge: string, userDataStore: IUserDataStore): BluebirdPromise { @@ -111,8 +113,9 @@ export function post_start_validation(handler: IdentityValidable, vars.userDataStore); }) .then((token: string) => { - const host = req.get("Host"); - const link_url = util.format("https://%s/#%s?token=%s", host, + const scheme = GetHeader(req, Constants.HEADER_X_FORWARDED_PROTO); + const host = GetHeader(req, Constants.HEADER_X_FORWARDED_HOST); + const link_url = util.format("%s://%s/#%s?token=%s", scheme, host, handler.destinationPath(), token); vars.logger.info(req, "Notification sent to user \"%s\"", identity.userid); diff --git a/server/src/lib/routes/secondfactor/totp/identity/RegistrationHandler.ts b/server/src/lib/routes/secondfactor/totp/identity/RegistrationHandler.ts index 62d6581a4..de3c3252f 100644 --- a/server/src/lib/routes/secondfactor/totp/identity/RegistrationHandler.ts +++ b/server/src/lib/routes/secondfactor/totp/identity/RegistrationHandler.ts @@ -63,8 +63,7 @@ export default class RegistrationHandler implements IdentityValidable { } preValidationResponse(req: Express.Request, res: Express.Response) { - res.status(204); - res.send(); + res.json({message: "OK"}); } postValidationInit(req: Express.Request) { diff --git a/server/src/lib/routes/secondfactor/u2f/U2FCommon.ts b/server/src/lib/routes/secondfactor/u2f/U2FCommon.ts deleted file mode 100644 index 7f16c0ee8..000000000 --- a/server/src/lib/routes/secondfactor/u2f/U2FCommon.ts +++ /dev/null @@ -1,11 +0,0 @@ - -import util = require("util"); -import express = require("express"); - -function extract_app_id(req: express.Request): string { - return util.format("https://%s", req.headers.host); -} - -export = { - extract_app_id: extract_app_id -}; \ No newline at end of file diff --git a/server/src/lib/routes/secondfactor/u2f/identity/RegistrationHandler.ts b/server/src/lib/routes/secondfactor/u2f/identity/RegistrationHandler.ts index c790832b8..3d9557835 100644 --- a/server/src/lib/routes/secondfactor/u2f/identity/RegistrationHandler.ts +++ b/server/src/lib/routes/secondfactor/u2f/identity/RegistrationHandler.ts @@ -51,8 +51,7 @@ export default class RegistrationHandler implements IdentityValidable { } preValidationResponse(req: express.Request, res: express.Response) { - res.status(204); - res.send(); + res.json({message: "OK"}); } postValidationInit(req: express.Request) { @@ -60,8 +59,7 @@ export default class RegistrationHandler implements IdentityValidable { } postValidationResponse(req: express.Request, res: express.Response) { - res.status(204); - res.send(); + res.json({message: "OK"}); } mailSubject(): string { diff --git a/server/src/lib/routes/secondfactor/u2f/register/post.ts b/server/src/lib/routes/secondfactor/u2f/register/post.ts index c7ac656df..8cd89eb3c 100644 --- a/server/src/lib/routes/secondfactor/u2f/register/post.ts +++ b/server/src/lib/routes/secondfactor/u2f/register/post.ts @@ -1,5 +1,4 @@ import objectPath = require("object-path"); -import u2f_common = require("../U2FCommon"); import BluebirdPromise = require("bluebird"); import express = require("express"); import U2f = require("u2f"); @@ -10,12 +9,16 @@ import { ServerVariables } from "../../../../ServerVariables"; import { AuthenticationSessionHandler } from "../../../../AuthenticationSessionHandler"; import UserMessages = require("../../../../../../../shared/UserMessages"); import { AuthenticationSession } from "../../../../../../types/AuthenticationSession"; +import GetHeader from "../../../../utils/GetHeader"; +import * as Constants from "../../../../../../../shared/constants"; export default function (vars: ServerVariables) { function handler(req: express.Request, res: express.Response): BluebirdPromise { let authSession: AuthenticationSession; - const appid = u2f_common.extract_app_id(req); + const scheme = GetHeader(req, Constants.HEADER_X_FORWARDED_PROTO); + const host = GetHeader(req, Constants.HEADER_X_FORWARDED_HOST); + const appid = scheme + "://" + host; const registrationResponse: U2f.RegistrationData = req.body; return new BluebirdPromise(function (resolve, reject) { diff --git a/server/src/lib/routes/secondfactor/u2f/register_request/get.ts b/server/src/lib/routes/secondfactor/u2f/register_request/get.ts index ecac24f4a..0b89aad24 100644 --- a/server/src/lib/routes/secondfactor/u2f/register_request/get.ts +++ b/server/src/lib/routes/secondfactor/u2f/register_request/get.ts @@ -1,5 +1,4 @@ -import u2f_common = require("../U2FCommon"); import BluebirdPromise = require("bluebird"); import express = require("express"); import U2f = require("u2f"); @@ -8,11 +7,15 @@ import { AuthenticationSessionHandler } from "../../../../AuthenticationSessionH import { AuthenticationSession } from "../../../../../../types/AuthenticationSession"; import UserMessages = require("../../../../../../../shared/UserMessages"); import { ServerVariables } from "../../../../ServerVariables"; +import GetHeader from "../../../../utils/GetHeader"; +import * as Constants from "../../../../../../../shared/constants"; export default function (vars: ServerVariables) { function handler(req: express.Request, res: express.Response): BluebirdPromise { let authSession: AuthenticationSession; - const appid: string = u2f_common.extract_app_id(req); + const scheme = GetHeader(req, Constants.HEADER_X_FORWARDED_PROTO); + const host = GetHeader(req, Constants.HEADER_X_FORWARDED_HOST); + const appid = scheme + "://" + host; return new BluebirdPromise(function (resolve, reject) { authSession = AuthenticationSessionHandler.get(req, vars.logger); diff --git a/server/src/lib/routes/secondfactor/u2f/sign/post.ts b/server/src/lib/routes/secondfactor/u2f/sign/post.ts index 2b70bf30a..29e8f2f40 100644 --- a/server/src/lib/routes/secondfactor/u2f/sign/post.ts +++ b/server/src/lib/routes/secondfactor/u2f/sign/post.ts @@ -1,5 +1,4 @@ import objectPath = require("object-path"); -import u2f_common = require("../U2FCommon"); import BluebirdPromise = require("bluebird"); import express = require("express"); import { U2FRegistrationDocument } from "../../../../storage/U2FRegistrationDocument"; @@ -11,11 +10,15 @@ import { AuthenticationSessionHandler } from "../../../../AuthenticationSessionH import UserMessages = require("../../../../../../../shared/UserMessages"); import { AuthenticationSession } from "../../../../../../types/AuthenticationSession"; import { Level } from "../../../../authentication/Level"; +import GetHeader from "../../../../utils/GetHeader"; +import * as Constants from "../../../../../../../shared/constants"; export default function (vars: ServerVariables) { function handler(req: express.Request, res: express.Response): BluebirdPromise { let authSession: AuthenticationSession; - const appId = u2f_common.extract_app_id(req); + const scheme = GetHeader(req, Constants.HEADER_X_FORWARDED_PROTO); + const host = GetHeader(req, Constants.HEADER_X_FORWARDED_HOST); + const appid = scheme + "://" + host; return new BluebirdPromise(function (resolve, reject) { authSession = AuthenticationSessionHandler.get(req, vars.logger); @@ -28,7 +31,7 @@ export default function (vars: ServerVariables) { }) .then(function () { const userid = authSession.userid; - return vars.userDataStore.retrieveU2FRegistration(userid, appId); + return vars.userDataStore.retrieveU2FRegistration(userid, appid); }) .then(function (doc: U2FRegistrationDocument): BluebirdPromise { const signRequest = authSession.sign_request; diff --git a/server/src/lib/routes/secondfactor/u2f/sign_request/get.ts b/server/src/lib/routes/secondfactor/u2f/sign_request/get.ts index 9e93dde06..389ef8a85 100644 --- a/server/src/lib/routes/secondfactor/u2f/sign_request/get.ts +++ b/server/src/lib/routes/secondfactor/u2f/sign_request/get.ts @@ -1,5 +1,3 @@ - -import u2f_common = require("../../../secondfactor/u2f/U2FCommon"); import BluebirdPromise = require("bluebird"); import express = require("express"); import { U2FRegistrationDocument } from "../../../../storage/U2FRegistrationDocument"; @@ -9,28 +7,31 @@ import { AuthenticationSessionHandler } from "../../../../AuthenticationSessionH import UserMessages = require("../../../../../../../shared/UserMessages"); import { ServerVariables } from "../../../../ServerVariables"; import { AuthenticationSession } from "../../../../../../types/AuthenticationSession"; +import GetHeader from "../../../../utils/GetHeader"; +import * as Constants from "../../../../../../../shared/constants"; export default function (vars: ServerVariables) { function handler(req: express.Request, res: express.Response): BluebirdPromise { let authSession: AuthenticationSession; - const appId = u2f_common.extract_app_id(req); + const scheme = GetHeader(req, Constants.HEADER_X_FORWARDED_PROTO); + const host = GetHeader(req, Constants.HEADER_X_FORWARDED_HOST); + const appid = scheme + "://" + host; return new BluebirdPromise(function (resolve, reject) { authSession = AuthenticationSessionHandler.get(req, vars.logger); resolve(); }) .then(function () { - return vars.userDataStore.retrieveU2FRegistration(authSession.userid, appId); + return vars.userDataStore.retrieveU2FRegistration(authSession.userid, appid); }) .then(function (doc: U2FRegistrationDocument): BluebirdPromise { if (!doc) return BluebirdPromise.reject(new exceptions.AccessDeniedError("No U2F registration document found.")); - const appId: string = u2f_common.extract_app_id(req); - vars.logger.info(req, "Start authentication of app '%s'", appId); - vars.logger.debug(req, "AppId = %s, keyHandle = %s", appId, JSON.stringify(doc.registration.keyHandle)); + vars.logger.info(req, "Start authentication of app '%s'", appid); + vars.logger.debug(req, "AppId = %s, keyHandle = %s", appid, JSON.stringify(doc.registration.keyHandle)); - const request = vars.u2f.request(appId, doc.registration.keyHandle); + const request = vars.u2f.request(appid, doc.registration.keyHandle); authSession.sign_request = request; res.json(request); return BluebirdPromise.resolve(); diff --git a/server/src/lib/routes/verify/Get.spec.ts b/server/src/lib/routes/verify/Get.spec.ts index d5d5d5b4f..fff376b29 100644 --- a/server/src/lib/routes/verify/Get.spec.ts +++ b/server/src/lib/routes/verify/Get.spec.ts @@ -88,7 +88,7 @@ describe("routes/verify/get", function () { req.query.rd = 'https://login.example.com/'; const mock = ImportMock.mockOther(GetBasicAuth, "default", () => Promise.reject(new NotAuthenticatedError('No!'))); await Get(vars)(req, res as any); - Assert(res.redirect.calledWith('https://login.example.com/')); + Assert(res.redirect.calledWith('https://login.example.com/?rd=https://secret.example.com/')); mock.restore(); }); }); diff --git a/server/src/lib/routes/verify/Get.ts b/server/src/lib/routes/verify/Get.ts index 7edee47c1..b9ccf8a19 100644 --- a/server/src/lib/routes/verify/Get.ts +++ b/server/src/lib/routes/verify/Get.ts @@ -10,7 +10,7 @@ import { AuthenticationSessionHandler } import { AuthenticationSession } from "../../../../types/AuthenticationSession"; import HasHeader from "../..//utils/HasHeader"; -import { RequestUrlGetter } from "../../utils/RequestUrlGetter"; +import RequestUrlGetter from "../../utils/RequestUrlGetter"; async function verifyWithSelectedMethod(req: Express.Request, res: Express.Response, @@ -24,42 +24,41 @@ async function verifyWithSelectedMethod(req: Express.Request, res: Express.Respo } } -/** - * The Redirect header is used to set the target URL in the login portal. - * - * @param req The request to extract X-Original-Url from. - * @param res The response to write Redirect header to. - */ -function setRedirectHeader(req: Express.Request, res: Express.Response) { - const originalUrl = RequestUrlGetter.getOriginalUrl(req); - res.set(Constants.HEADER_REDIRECT, originalUrl); -} - function getRedirectParam(req: Express.Request) { return req.query[Constants.REDIRECT_QUERY_PARAM] != "undefined" ? req.query[Constants.REDIRECT_QUERY_PARAM] : undefined; } +async function unsafeGet(vars: ServerVariables, req: Express.Request, res: Express.Response) { + const authSession = AuthenticationSessionHandler.get(req, vars.logger); + try { + await verifyWithSelectedMethod(req, res, vars, authSession); + res.status(204); + res.send(); + } catch (err) { + // Kubernetes ingress controller and Traefik use the rd parameter of the verify + // endpoint to provide the URL of the login portal. The target URL of the user + // is computed from X-Fowarded-* headers or X-Original-Url. + let redirectUrl = getRedirectParam(req); + const originalUrl = RequestUrlGetter.getOriginalUrl(req); + if (redirectUrl && originalUrl) { + redirectUrl = redirectUrl + `?${Constants.REDIRECT_QUERY_PARAM}=` + originalUrl; + ErrorReplies.redirectTo(redirectUrl, req, res, vars.logger)(err); + return; + } + + // Reply with an error. + throw err; + } +} + export default function (vars: ServerVariables) { return async function (req: Express.Request, res: Express.Response) : Promise { - const authSession = AuthenticationSessionHandler.get(req, vars.logger); - setRedirectHeader(req, res); - try { - await verifyWithSelectedMethod(req, res, vars, authSession); - res.status(204); - res.send(); + await unsafeGet(vars, req, res); } catch (err) { - // This redirect parameter is used in Kubernetes to annotate the ingress with - // the url to the authentication portal. - const redirectUrl = getRedirectParam(req); - if (redirectUrl) { - ErrorReplies.redirectTo(redirectUrl, req, res, vars.logger)(err); - return; - } - if (err instanceof Exceptions.NotAuthorizedError) { ErrorReplies.replyWithError403(req, res, vars.logger)(err); } else { diff --git a/server/src/lib/routes/verify/GetBasicAuth.ts b/server/src/lib/routes/verify/GetBasicAuth.ts index 6bb74b3d0..d47eb73e2 100644 --- a/server/src/lib/routes/verify/GetBasicAuth.ts +++ b/server/src/lib/routes/verify/GetBasicAuth.ts @@ -6,8 +6,7 @@ import GetHeader from "../../utils/GetHeader"; import { HEADER_PROXY_AUTHORIZATION } from "../../../../../shared/constants"; import setUserAndGroupsHeaders from "./SetUserAndGroupsHeaders"; import CheckAuthorizations from "./CheckAuthorizations"; -import { Level as AuthorizationLevel } from "../../authorization/Level"; -import { RequestUrlGetter } from "../../utils/RequestUrlGetter"; +import RequestUrlGetter from "../../utils/RequestUrlGetter"; export default async function(req: Express.Request, res: Express.Response, vars: ServerVariables) diff --git a/server/src/lib/routes/verify/GetSessionCookie.ts b/server/src/lib/routes/verify/GetSessionCookie.ts index a276fc7a6..98a794e9a 100644 --- a/server/src/lib/routes/verify/GetSessionCookie.ts +++ b/server/src/lib/routes/verify/GetSessionCookie.ts @@ -2,11 +2,11 @@ import Express = require("express"); import { ServerVariables } from "../../ServerVariables"; import { AuthenticationSession } from "../../../../types/AuthenticationSession"; -import { URLDecomposer } from "../../utils/URLDecomposer"; import setUserAndGroupsHeaders from "./SetUserAndGroupsHeaders"; import CheckAuthorizations from "./CheckAuthorizations"; import CheckInactivity from "./CheckInactivity"; -import { RequestUrlGetter } from "../../utils/RequestUrlGetter"; +import RequestUrlGetter from "../../utils/RequestUrlGetter"; +import * as URLParse from "url-parse"; export default async function (req: Express.Request, res: Express.Response, @@ -22,15 +22,16 @@ export default async function (req: Express.Request, res: Express.Response, throw new Error("Cannot detect the original URL from headers."); } - const d = URLDecomposer.fromUrl(originalUrl); - + const url = new URLParse(originalUrl); const username = authSession.userid; const groups = authSession.groups; - vars.logger.debug(req, "domain=%s, path=%s, user=%s, groups=%s, ip=%s", d.domain, - d.path, (username) ? username : "unknown", (groups instanceof Array && groups.length > 0) ? groups.join(",") : "unknown", req.ip); + vars.logger.debug(req, "domain=%s, path=%s, user=%s, groups=%s, ip=%s", url.hostname, + url.pathname, (username) ? username : "unknown", + (groups instanceof Array && groups.length > 0) ? groups.join(",") : "unknown", req.ip); - CheckAuthorizations(vars.authorizer, d.domain, d.path, username, groups, req.ip, authSession.authentication_level); + CheckAuthorizations(vars.authorizer, url.hostname, url.pathname, username, groups, + req.ip, authSession.authentication_level); CheckInactivity(req, authSession, vars.config, vars.logger); setUserAndGroupsHeaders(res, username, groups); } diff --git a/server/src/lib/utils/GetHeader.ts b/server/src/lib/utils/GetHeader.ts index a1666bf4c..869136795 100644 --- a/server/src/lib/utils/GetHeader.ts +++ b/server/src/lib/utils/GetHeader.ts @@ -11,7 +11,7 @@ import { GET_VARIABLE_KEY } from "../../../../shared/constants"; */ export default function(req: Express.Request, header: string): string | undefined { const variables: ServerVariables = req.app.get(GET_VARIABLE_KEY); - if (!variables) return undefined; + if (!variables) throw new Error("There are no server variables set."); const value = ObjectPath.get(req, "headers." + header, undefined); variables.logger.debug(req, "Header %s is set to %s", header, value); diff --git a/server/src/lib/utils/RequestUrlGetter.spec.ts b/server/src/lib/utils/RequestUrlGetter.spec.ts new file mode 100644 index 000000000..81e30fda2 --- /dev/null +++ b/server/src/lib/utils/RequestUrlGetter.spec.ts @@ -0,0 +1,51 @@ +import RequestUrlGetter from "./RequestUrlGetter"; +import * as Assert from "assert"; +import * as Sinon from "sinon"; +import { RequestLoggerStub } from "../logging/RequestLoggerStub.spec"; + +describe('RequestUrlGetter', function() { + let req: any; + beforeEach(function() { + req = { + app: { + get: Sinon.stub().returns({ + logger: new RequestLoggerStub() + }) + }, + headers: {} + } + }) + + it("should return the content of X-Original-Uri header", function() { + req.headers["x-original-url"] = "https://mytarget.example.com"; + Assert.equal(RequestUrlGetter.getOriginalUrl(req), "https://mytarget.example.com"); + }) + + describe("Use X-Forwarded-Proto, X-Forwarded-Host and X-Forwarded-Uri headers", function() { + it("should get URL from Forwarded headers", function() { + req.headers["x-forwarded-proto"] = "https"; + req.headers["x-forwarded-host"] = "mytarget.example.com"; + req.headers["x-forwarded-uri"] = "/" + Assert.equal(RequestUrlGetter.getOriginalUrl(req), "https://mytarget.example.com/"); + }) + + it("should get URL from Forwarded headers without URI", function() { + req.headers["x-forwarded-proto"] = "https"; + req.headers["x-forwarded-host"] = "mytarget.example.com"; + Assert.equal(RequestUrlGetter.getOriginalUrl(req), "https://mytarget.example.com"); + }) + }); + + it("should throw when no header is provided", function() { + Assert.throws(() => { + RequestUrlGetter.getOriginalUrl(req) + }) + }) + + it("should throw when only some of X-Forwarded-* headers are provided", function() { + req.headers["x-forwarded-proto"] = "https"; + Assert.throws(() => { + RequestUrlGetter.getOriginalUrl(req) + }) + }) +}) \ No newline at end of file diff --git a/server/src/lib/utils/RequestUrlGetter.ts b/server/src/lib/utils/RequestUrlGetter.ts index 8660d801b..630f76b9a 100644 --- a/server/src/lib/utils/RequestUrlGetter.ts +++ b/server/src/lib/utils/RequestUrlGetter.ts @@ -3,23 +3,27 @@ import Express = require("express"); import GetHeader from "./GetHeader"; import HasHeader from "./HasHeader"; -export class RequestUrlGetter { +export default class RequestUrlGetter { static getOriginalUrl(req: Express.Request): string { if (HasHeader(req, Constants.HEADER_X_ORIGINAL_URL)) { return GetHeader(req, Constants.HEADER_X_ORIGINAL_URL); } + // X-Forwarded-Port is not mandatory since the port is included in X-Forwarded-Host + // at least in nginx and Traefik. const proto = GetHeader(req, Constants.HEADER_X_FORWARDED_PROTO); const host = GetHeader(req, Constants.HEADER_X_FORWARDED_HOST); - const port = GetHeader(req, Constants.HEADER_X_FORWARDED_PORT); const uri = GetHeader(req, Constants.HEADER_X_FORWARDED_URI); - if (!proto || !host || !port) { - throw new Error("Missing headers holding requested URL. Requires X-Original-Url or X-Forwarded-Proto, X-Forwarded-Host, and X-Forwarded-Port") + if (!proto || !host) { + throw new Error("Missing headers holding requested URL. Requires either X-Original-Url or X-Forwarded-Proto, X-Forwarded-Host and X-Forwarded-Uri."); } - return "${proto}://${host}:${port}${uri}"; + if (!uri) { + return `${proto}://${host}`; + } + return `${proto}://${host}${uri}`; } } diff --git a/shared/constants.ts b/shared/constants.ts index 1cc6a3904..2d449013b 100644 --- a/shared/constants.ts +++ b/shared/constants.ts @@ -7,7 +7,6 @@ export const HEADER_X_TARGET_URL = "x-target-url"; export const HEADER_X_ORIGINAL_URL = "x-original-url"; export const HEADER_X_FORWARDED_PROTO = "x-forwarded-proto"; export const HEADER_X_FORWARDED_HOST = "x-forwarded-host"; -export const HEADER_X_FORWARDED_PORT = "x-forwarded-port"; export const HEADER_X_FORWARDED_URI = "x-forwarded-uri"; export const HEADER_PROXY_AUTHORIZATION = "proxy-authorization"; export const HEADER_REDIRECT = "redirect"; diff --git a/test/helpers/utils/Requests.ts b/test/helpers/utils/Requests.ts index 5b5a1e613..51a334899 100644 --- a/test/helpers/utils/Requests.ts +++ b/test/helpers/utils/Requests.ts @@ -5,11 +5,12 @@ import { StatusCodeError } from 'request-promise/errors'; process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0"; -export async function GET_ExpectError(url: string, statusCode: number) { +export async function GET_ExpectError(url: string, headers: {[key: string]: string}, statusCode: number) { try { await Request.get(url, { json: true, rejectUnauthorized: false, + headers: headers, }); throw new Error('No response'); } catch (e) { @@ -22,12 +23,12 @@ export async function GET_ExpectError(url: string, statusCode: number) { } // Sent a GET request to the url and expect a 401 -export async function GET_Expect401(url: string) { - return await GET_ExpectError(url, 401); +export async function GET_Expect401(url: string, headers: {[key: string]: string} = {}) { + return await GET_ExpectError(url, headers, 401); } -export async function GET_Expect502(url: string) { - return await GET_ExpectError(url, 502); +export async function GET_Expect502(url: string, headers: {[key: string]: string} = {}) { + return await GET_ExpectError(url, headers, 502); } export async function POST_Expect401(url: string, body?: any) { @@ -47,8 +48,8 @@ export async function POST_Expect401(url: string, body?: any) { return; } -export async function GET_ExpectRedirect(url: string, redirectionUrl: string) { - const response = await Fetch(url, {redirect: 'manual'}); +export async function GET_ExpectRedirect(url: string, redirectionUrl: string, headers: {[key: string]: string} = {}) { + const response = await Fetch(url, {redirect: 'manual', headers: headers}); if (response.status == 302) { const body = await response.text(); diff --git a/test/suites/basic-bypass-no-redirect/test.ts b/test/suites/basic-bypass-no-redirect/test.ts index 09826914f..eeb618237 100644 --- a/test/suites/basic-bypass-no-redirect/test.ts +++ b/test/suites/basic-bypass-no-redirect/test.ts @@ -6,6 +6,8 @@ import CustomHeadersForwarded from "./scenarii/CustomHeadersForwarded"; process.env["NODE_TLS_REJECT_UNAUTHORIZED"] = 0 as any; +process.env["NODE_TLS_REJECT_UNAUTHORIZED"] = 0 as any; + AutheliaSuite(__dirname, function() { this.timeout(10000); diff --git a/test/suites/basic/scenarii/VerifyEndpoint.ts b/test/suites/basic/scenarii/VerifyEndpoint.ts index b846e505f..e845512e8 100644 --- a/test/suites/basic/scenarii/VerifyEndpoint.ts +++ b/test/suites/basic/scenarii/VerifyEndpoint.ts @@ -2,14 +2,32 @@ import { GET_Expect401, GET_ExpectRedirect } from "../../../helpers/utils/Reques export default function() { describe('Query without authenticated cookie', function() { - it('should get a 401 on GET to https://login.example.com:8080/api/verify', async function() { - await GET_Expect401('https://login.example.com:8080/api/verify'); + it('should get a 401 on GET to http://192.168.240.1:9091/api/verify', async function() { + await GET_Expect401('http://192.168.240.1:9091/api/verify', { + 'X-Forwarded-Proto': 'https', + }); }); - describe('Parameter `rd` required by Kubernetes ingress controller', async function() { + describe('Kubernetes nginx ingress controller', async function() { it('should redirect to https://login.example.com:8080', async function() { - await GET_ExpectRedirect('https://login.example.com:8080/api/verify?rd=https://login.example.com:8080', - 'https://login.example.com:8080'); + await GET_ExpectRedirect('http://192.168.240.1:9091/api/verify?rd=https://login.example.com:8080/%23/', + 'https://login.example.com:8080/#/?rd=https://secure.example.com:8080', + { + 'X-Original-Url': 'https://secure.example.com:8080', + 'X-Forwarded-Proto': 'https' + }); + }); + }); + + describe('Traefik proxy', async function() { + it('should redirect to https://login.example.com:8080', async function() { + await GET_ExpectRedirect('http://192.168.240.1:9091/api/verify?rd=https://login.example.com:8080/%23/', + 'https://login.example.com:8080/#/?rd=https://secure.example.com:8080/', + { + 'X-Forwarded-Proto': 'https', + 'X-Forwarded-Host': 'secure.example.com:8080', + 'X-Forwarded-Uri': '/', + }); }); }); }); diff --git a/test/suites/traefik/README.md b/test/suites/traefik/README.md new file mode 100644 index 000000000..5c432c127 --- /dev/null +++ b/test/suites/traefik/README.md @@ -0,0 +1,11 @@ +# Traefik suite + +This suite has been created to test Authelia against Traefik. + +## Components + +Authelia, Traefik, fake webmail for registering devices. + +## Tests + +Authentication tests. \ No newline at end of file diff --git a/test/suites/traefik/config.yml b/test/suites/traefik/config.yml new file mode 100644 index 000000000..340e173a5 --- /dev/null +++ b/test/suites/traefik/config.yml @@ -0,0 +1,43 @@ +############################################################### +# Authelia minimal configuration # +############################################################### + +port: 9091 + +logs_level: debug + +authentication_backend: + file: + path: ./test/suites/basic/users_database.test.yml + +session: + secret: unsecure_session_secret + domain: example.com + expiration: 3600000 # 1 hour + inactivity: 300000 # 5 minutes + +storage: + local: + path: /tmp/authelia/db + +access_control: + default_policy: bypass + rules: + - domain: 'public.example.com' + policy: bypass + - domain: 'admin.example.com' + policy: two_factor + - domain: 'secure.example.com' + policy: two_factor + - domain: 'singlefactor.example.com' + policy: one_factor + +notifier: + smtp: + username: test + password: password + secure: false + host: 127.0.0.1 + port: 1025 + sender: admin@example.com + diff --git a/test/suites/traefik/environment.ts b/test/suites/traefik/environment.ts new file mode 100644 index 000000000..471069058 --- /dev/null +++ b/test/suites/traefik/environment.ts @@ -0,0 +1,36 @@ +import { exec } from "../../helpers/utils/exec"; +import AutheliaServer from "../../helpers/context/AutheliaServer"; +import DockerEnvironment from "../../helpers/context/DockerEnvironment"; +import * as fs from "fs"; + +const autheliaServer = new AutheliaServer(__dirname + '/config.yml'); +const dockerEnv = new DockerEnvironment([ + 'docker-compose.yml', + 'example/compose/nginx/backend/docker-compose.yml', + 'example/compose/traefik/docker-compose.yml', + 'example/compose/smtp/docker-compose.yml', +]) + +async function setup() { + await exec('./example/compose/traefik/render.js ' + (fs.existsSync('.suite') ? '': '--production')); + await exec(`cp ${__dirname}/users_database.yml ${__dirname}/users_database.test.yml`); + await exec('mkdir -p /tmp/authelia/db'); + await dockerEnv.start(); + await autheliaServer.start(); +} + +async function teardown() { + await autheliaServer.stop(); + await dockerEnv.stop(); + await exec('rm -rf /tmp/authelia/db'); +} + +const setup_timeout = 30000; +const teardown_timeout = 30000; + +export { + setup, + setup_timeout, + teardown, + teardown_timeout +}; \ No newline at end of file diff --git a/test/suites/traefik/test.ts b/test/suites/traefik/test.ts new file mode 100644 index 000000000..5c26e690d --- /dev/null +++ b/test/suites/traefik/test.ts @@ -0,0 +1,17 @@ +import AutheliaSuite from "../../helpers/context/AutheliaSuite"; +import { exec } from '../../helpers/utils/exec'; +import TwoFactorAuthentication from "../../helpers/scenarii/TwoFactorAuthentication"; +import SingleFactorAuthentication from "../../helpers/scenarii/SingleFactorAuthentication"; +import * as fs from "fs"; + +AutheliaSuite(__dirname, function() { + this.timeout(10000); + + beforeEach(async function() { + await exec('./example/compose/traefik/render.js ' + (fs.existsSync('.suite') ? '': '--production')); + await exec(`cp ${__dirname}/users_database.yml ${__dirname}/users_database.test.yml`); + }); + + describe('Single-factor authentication', SingleFactorAuthentication()); + describe('Second factor authentication', TwoFactorAuthentication()); +}); \ No newline at end of file diff --git a/example/traefik/users_database.yml b/test/suites/traefik/users_database.yml similarity index 93% rename from example/traefik/users_database.yml rename to test/suites/traefik/users_database.yml index 7832e85b3..6fe7a384d 100644 --- a/example/traefik/users_database.yml +++ b/test/suites/traefik/users_database.yml @@ -15,7 +15,7 @@ users: harry: password: "{CRYPT}$6$rounds=500000$jgiCMRyGXzoqpxS3$w2pJeZnnH8bwW3zzvoMWtTRfQYsHbWbD/hquuQ5vUeIyl9gdwBIt6RWk2S6afBA0DPakbeWgD/4SZPiS0hYtU/" - emails: harry.potter@authelia.com + email: harry.potter@authelia.com groups: [] bob: