From 0922b3c21583edee3bcc417f485adedf64ac75fd Mon Sep 17 00:00:00 2001 From: ViViDboarder Date: Thu, 7 Feb 2019 12:56:29 -0800 Subject: [PATCH] Build x-original-url from forwarded headers This is to allow broader support for proxies. In particular, this allows support with Traefik. This patch also includes some examples of configuration with Traefik. --- example/traefik/config.minimal.yml | 61 +++++++++++++++++++ example/traefik/docker-compose.yml | 38 ++++++++++++ example/traefik/traefik.toml | 30 +++++++++ example/traefik/users_database.yml | 29 +++++++++ server/src/lib/routes/verify/Get.ts | 4 +- server/src/lib/routes/verify/GetBasicAuth.ts | 7 ++- .../src/lib/routes/verify/GetSessionCookie.ts | 9 +-- server/src/lib/utils/RequestUrlGetter.ts | 20 ++++++ shared/constants.ts | 6 +- 9 files changed, 192 insertions(+), 12 deletions(-) create mode 100644 example/traefik/config.minimal.yml create mode 100644 example/traefik/docker-compose.yml create mode 100644 example/traefik/traefik.toml create mode 100644 example/traefik/users_database.yml create mode 100644 server/src/lib/utils/RequestUrlGetter.ts diff --git a/example/traefik/config.minimal.yml b/example/traefik/config.minimal.yml new file mode 100644 index 000000000..8afef58b6 --- /dev/null +++ b/example/traefik/config.minimal.yml @@ -0,0 +1,61 @@ +############################################################### +# 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 new file mode 100644 index 000000000..2717938ea --- /dev/null +++ b/example/traefik/docker-compose.yml @@ -0,0 +1,38 @@ +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 new file mode 100644 index 000000000..6cd052291 --- /dev/null +++ b/example/traefik/traefik.toml @@ -0,0 +1,30 @@ +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/example/traefik/users_database.yml b/example/traefik/users_database.yml new file mode 100644 index 000000000..7832e85b3 --- /dev/null +++ b/example/traefik/users_database.yml @@ -0,0 +1,29 @@ +############################################################### +# Users Database # +############################################################### + +# This file can be used if you do not have an LDAP set up. + +# List of users +users: + john: + password: "{CRYPT}$6$rounds=500000$jgiCMRyGXzoqpxS3$w2pJeZnnH8bwW3zzvoMWtTRfQYsHbWbD/hquuQ5vUeIyl9gdwBIt6RWk2S6afBA0DPakbeWgD/4SZPiS0hYtU/" + email: john.doe@authelia.com + groups: + - admins + - dev + + harry: + password: "{CRYPT}$6$rounds=500000$jgiCMRyGXzoqpxS3$w2pJeZnnH8bwW3zzvoMWtTRfQYsHbWbD/hquuQ5vUeIyl9gdwBIt6RWk2S6afBA0DPakbeWgD/4SZPiS0hYtU/" + emails: harry.potter@authelia.com + groups: [] + + bob: + password: "{CRYPT}$6$rounds=500000$jgiCMRyGXzoqpxS3$w2pJeZnnH8bwW3zzvoMWtTRfQYsHbWbD/hquuQ5vUeIyl9gdwBIt6RWk2S6afBA0DPakbeWgD/4SZPiS0hYtU/" + email: bob.dylan@authelia.com + groups: + - dev + + james: + password: "{CRYPT}$6$rounds=500000$jgiCMRyGXzoqpxS3$w2pJeZnnH8bwW3zzvoMWtTRfQYsHbWbD/hquuQ5vUeIyl9gdwBIt6RWk2S6afBA0DPakbeWgD/4SZPiS0hYtU/" + email: james.dean@authelia.com \ No newline at end of file diff --git a/server/src/lib/routes/verify/Get.ts b/server/src/lib/routes/verify/Get.ts index b647334dd..7edee47c1 100644 --- a/server/src/lib/routes/verify/Get.ts +++ b/server/src/lib/routes/verify/Get.ts @@ -9,8 +9,8 @@ import { AuthenticationSessionHandler } from "../../AuthenticationSessionHandler"; import { AuthenticationSession } from "../../../../types/AuthenticationSession"; -import GetHeader from "../../utils/GetHeader"; import HasHeader from "../..//utils/HasHeader"; +import { RequestUrlGetter } from "../../utils/RequestUrlGetter"; async function verifyWithSelectedMethod(req: Express.Request, res: Express.Response, @@ -31,7 +31,7 @@ async function verifyWithSelectedMethod(req: Express.Request, res: Express.Respo * @param res The response to write Redirect header to. */ function setRedirectHeader(req: Express.Request, res: Express.Response) { - const originalUrl = GetHeader(req, Constants.HEADER_X_ORIGINAL_URL); + const originalUrl = RequestUrlGetter.getOriginalUrl(req); res.set(Constants.HEADER_REDIRECT, originalUrl); } diff --git a/server/src/lib/routes/verify/GetBasicAuth.ts b/server/src/lib/routes/verify/GetBasicAuth.ts index d94868e2d..6bb74b3d0 100644 --- a/server/src/lib/routes/verify/GetBasicAuth.ts +++ b/server/src/lib/routes/verify/GetBasicAuth.ts @@ -3,10 +3,11 @@ import { ServerVariables } from "../../ServerVariables"; import { URLDecomposer } from "../../utils/URLDecomposer"; import { Level } from "../../authentication/Level"; import GetHeader from "../../utils/GetHeader"; -import { HEADER_X_ORIGINAL_URL, HEADER_PROXY_AUTHORIZATION } from "../../../../../shared/constants"; +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"; export default async function(req: Express.Request, res: Express.Response, vars: ServerVariables) @@ -38,10 +39,10 @@ export default async function(req: Express.Request, res: Express.Response, const password = splittedToken[1]; const groupsAndEmails = await vars.usersDatabase.checkUserPassword(username, password); - const uri = GetHeader(req, HEADER_X_ORIGINAL_URL); + const uri = RequestUrlGetter.getOriginalUrl(req); const urlDecomposition = URLDecomposer.fromUrl(uri); CheckAuthorizations(vars.authorizer, urlDecomposition.domain, urlDecomposition.path, username, groupsAndEmails.groups, req.ip, Level.ONE_FACTOR); setUserAndGroupsHeaders(res, username, groupsAndEmails.groups); -} \ No newline at end of file +} diff --git a/server/src/lib/routes/verify/GetSessionCookie.ts b/server/src/lib/routes/verify/GetSessionCookie.ts index d7ebb0ef1..a276fc7a6 100644 --- a/server/src/lib/routes/verify/GetSessionCookie.ts +++ b/server/src/lib/routes/verify/GetSessionCookie.ts @@ -3,13 +3,10 @@ import { ServerVariables } from "../../ServerVariables"; import { AuthenticationSession } from "../../../../types/AuthenticationSession"; import { URLDecomposer } from "../../utils/URLDecomposer"; -import GetHeader from "../../utils/GetHeader"; -import { - HEADER_X_ORIGINAL_URL, -} from "../../../../../shared/constants"; import setUserAndGroupsHeaders from "./SetUserAndGroupsHeaders"; import CheckAuthorizations from "./CheckAuthorizations"; import CheckInactivity from "./CheckInactivity"; +import { RequestUrlGetter } from "../../utils/RequestUrlGetter"; export default async function (req: Express.Request, res: Express.Response, @@ -19,7 +16,7 @@ export default async function (req: Express.Request, res: Express.Response, throw new Error("No cookie detected."); } - const originalUrl = GetHeader(req, HEADER_X_ORIGINAL_URL); + const originalUrl = RequestUrlGetter.getOriginalUrl(req); if (!originalUrl) { throw new Error("Cannot detect the original URL from headers."); @@ -36,4 +33,4 @@ export default async function (req: Express.Request, res: Express.Response, CheckAuthorizations(vars.authorizer, d.domain, d.path, username, groups, req.ip, authSession.authentication_level); CheckInactivity(req, authSession, vars.config, vars.logger); setUserAndGroupsHeaders(res, username, groups); -} \ No newline at end of file +} diff --git a/server/src/lib/utils/RequestUrlGetter.ts b/server/src/lib/utils/RequestUrlGetter.ts new file mode 100644 index 000000000..b4a174f69 --- /dev/null +++ b/server/src/lib/utils/RequestUrlGetter.ts @@ -0,0 +1,20 @@ +import Constants = require("../../../../../shared/constants"); +import Express = require("express"); +import GetHeader from "../../utils/GetHeader"; +import HasHeader from "../..//utils/HasHeader"; + +export class RequestUrlGetter { + static getOriginalUrl(req: Express.Request): string { + + if HasHeader(req, Constants.HEADER_X_ORIGINAL_URL) { + return GetHeader(req, Constants.HEADER_X_ORIGINAL_URL); + } + + 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); + + return "${proto}://${host}:${port}${uri}"; + } +} diff --git a/shared/constants.ts b/shared/constants.ts index 72ce3511f..1cc6a3904 100644 --- a/shared/constants.ts +++ b/shared/constants.ts @@ -5,10 +5,14 @@ export const REDIRECT_QUERY_PARAM = "rd"; 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"; export const GET_VARIABLE_KEY = "variables"; export const HEADER_REMOTE_USER = "Remote-User"; -export const HEADER_REMOTE_GROUPS = "Remote-Groups"; \ No newline at end of file +export const HEADER_REMOTE_GROUPS = "Remote-Groups";