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 <git-2022@chris-wells.net>
This commit is contained in:
EnsuingRequiem 2022-09-18 16:00:41 -05:00 committed by GitHub
parent 2690ab4876
commit 46159850c4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 81 additions and 4 deletions

View File

@ -1,4 +1,5 @@
import time from os import getenv
from time import time
import pytz import pytz
@ -6,6 +7,7 @@ from django.conf import settings
from django.utils import timezone, translation from django.utils import timezone, translation
from django.conf.locale.en import formats as formats_en_us 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.conf.locale.en_GB import formats as formats_en_gb
from django.contrib.auth.middleware import RemoteUserMiddleware
def update_en_us_date_formats(): def update_en_us_date_formats():
@ -134,12 +136,20 @@ class RollingSessionMiddleware:
session_refresh = request.session.get("session_refresh") session_refresh = request.session.get("session_refresh")
if session_refresh: if session_refresh:
try: try:
delta = int(time.time()) - session_refresh delta = int(time()) - session_refresh
except (ValueError, TypeError): except (ValueError, TypeError):
delta = settings.ROLLING_SESSION_REFRESH + 1 delta = settings.ROLLING_SESSION_REFRESH + 1
if delta > settings.ROLLING_SESSION_REFRESH: 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) request.session.set_expiry(settings.SESSION_COOKIE_AGE)
else: else:
request.session["session_refresh"] = int(time.time()) request.session["session_refresh"] = int(time())
return self.get_response(request) 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")

View File

@ -143,6 +143,14 @@ LOGIN_URL = "babybuddy:login"
LOGOUT_REDIRECT_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 # Timezone
# https://docs.djangoproject.com/en/4.0/topics/i18n/timezones/ # https://docs.djangoproject.com/en/4.0/topics/i18n/timezones/

View File

@ -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)

View File

@ -38,6 +38,34 @@ Each entry must contain both the scheme (http, https) and fully-qualified domain
- [`ALLOWED_HOSTS`](#allowed_hosts) - [`ALLOWED_HOSTS`](#allowed_hosts)
- [`SECURE_PROXY_SSL_HEADER`](#secure_proxy_ssl_header) - [`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` ## `SECRET_KEY`
*Default:* `None` *Default:* `None`