From 46159850c404cfefe2c583fec45af622758893cd Mon Sep 17 00:00:00 2001 From: EnsuingRequiem Date: Sun, 18 Sep 2022 16:00:41 -0500 Subject: [PATCH] Add forward auth by way of remote user * Add forward auth related settings * Document forward auth settings * Rearrange code to match preference * Adjust forward auth configuration * Add tests for reverse proxy auth Closes #517 Co-authored-by: Christopher C. Wells --- babybuddy/middleware.py | 18 +++++++++--- babybuddy/settings/base.py | 8 ++++++ babybuddy/tests/tests_reverse_proxy_auth.py | 31 +++++++++++++++++++++ docs/configuration/security.md | 28 +++++++++++++++++++ 4 files changed, 81 insertions(+), 4 deletions(-) create mode 100644 babybuddy/tests/tests_reverse_proxy_auth.py diff --git a/babybuddy/middleware.py b/babybuddy/middleware.py index 362f8c0f..9af7cb70 100644 --- a/babybuddy/middleware.py +++ b/babybuddy/middleware.py @@ -1,4 +1,5 @@ -import time +from os import getenv +from time import time import pytz @@ -6,6 +7,7 @@ from django.conf import settings from django.utils import timezone, translation from django.conf.locale.en import formats as formats_en_us from django.conf.locale.en_GB import formats as formats_en_gb +from django.contrib.auth.middleware import RemoteUserMiddleware def update_en_us_date_formats(): @@ -134,12 +136,20 @@ class RollingSessionMiddleware: session_refresh = request.session.get("session_refresh") if session_refresh: try: - delta = int(time.time()) - session_refresh + delta = int(time()) - session_refresh except (ValueError, TypeError): delta = settings.ROLLING_SESSION_REFRESH + 1 if delta > settings.ROLLING_SESSION_REFRESH: - request.session["session_refresh"] = int(time.time()) + request.session["session_refresh"] = int(time()) request.session.set_expiry(settings.SESSION_COOKIE_AGE) else: - request.session["session_refresh"] = int(time.time()) + request.session["session_refresh"] = int(time()) return self.get_response(request) + + +class CustomRemoteUser(RemoteUserMiddleware): + """ + Middleware used for remote authentication when `REVERSE_PROXY_AUTH` is True. + """ + + header = getenv("PROXY_HEADER", "HTTP_REMOTE_USER") diff --git a/babybuddy/settings/base.py b/babybuddy/settings/base.py index c51fb1ce..d15609ae 100644 --- a/babybuddy/settings/base.py +++ b/babybuddy/settings/base.py @@ -143,6 +143,14 @@ LOGIN_URL = "babybuddy:login" LOGOUT_REDIRECT_URL = "babybuddy:login" +REVERSE_PROXY_AUTH = bool(strtobool(os.environ.get("REVERSE_PROXY_AUTH") or "False")) + +# Use remote user middleware when reverse proxy auth is enabled. +if REVERSE_PROXY_AUTH: + # Must appear AFTER AuthenticationMiddleware. + MIDDLEWARE.append("babybuddy.middleware.CustomRemoteUser") + AUTHENTICATION_BACKENDS.append("django.contrib.auth.backends.RemoteUserBackend") + # Timezone # https://docs.djangoproject.com/en/4.0/topics/i18n/timezones/ diff --git a/babybuddy/tests/tests_reverse_proxy_auth.py b/babybuddy/tests/tests_reverse_proxy_auth.py new file mode 100644 index 00000000..22b6bac2 --- /dev/null +++ b/babybuddy/tests/tests_reverse_proxy_auth.py @@ -0,0 +1,31 @@ +# -*- coding: utf-8 -*- +from django.core.management import call_command +from django.test import Client as HttpClient, TestCase, modify_settings + + +class ReverseProxyAuthTestCase(TestCase): + """ + Notes: + - A class method cannot be used to establish the HTTP client because of the + settings overrides required for these tests. + - Overriding the `REVERSE_PROXY_AUTH` environment variable directly is not + possible because environments variables are only evaluated once for the run. + """ + + def test_remote_user_authentication_disabled(self): + call_command("migrate", verbosity=0) + c = HttpClient() + response = c.get("/welcome/", HTTP_REMOTE_USER="admin", follow=True) + self.assertRedirects(response, "/login/?next=/welcome/") + + @modify_settings( + MIDDLEWARE={"append": "babybuddy.middleware.CustomRemoteUser"}, + AUTHENTICATION_BACKENDS={ + "append": "django.contrib.auth.backends.RemoteUserBackend" + }, + ) + def test_remote_user_authentication_enabled(self): + call_command("migrate", verbosity=0) + c = HttpClient() + response = c.get("/welcome/", HTTP_REMOTE_USER="admin") + self.assertEqual(response.status_code, 200) diff --git a/docs/configuration/security.md b/docs/configuration/security.md index 30d8fca5..4dcc7819 100644 --- a/docs/configuration/security.md +++ b/docs/configuration/security.md @@ -38,6 +38,34 @@ Each entry must contain both the scheme (http, https) and fully-qualified domain - [`ALLOWED_HOSTS`](#allowed_hosts) - [`SECURE_PROXY_SSL_HEADER`](#secure_proxy_ssl_header) +## `PROXY_HEADER` + +*Default:* `HTTP_REMOTE_USER` + +Sets the header to read the authenticated username from when +`REVERSE_PROXY_AUTH` has been enabled. + +**Example value** + + HTTP_X_AUTH_USER + +**See also** + +- [Django's documentation on the `REMOTE_USER` authentication method](https://docs.djangoproject.com/en/4.1/howto/auth-remote-user/) +- [`REVERSE_PROXY_AUTH`](#reverse_proxy_auth) + +## `REVERSE_PROXY_AUTH` + +*Default:* `False` + +Enable use of `PROXY_HEADER` to pass the username of an authenticated user. +This setting should *only* be used with a properly configured reverse proxy to +ensure the headers are not forwarded from sources other than your proxy. + +**See also** + +- [`PROXY_HEADER`](#proxy_header) + ## `SECRET_KEY` *Default:* `None`