diff --git a/config b/config
index 0d17dae..bd35574 100644
--- a/config
+++ b/config
@@ -66,7 +66,7 @@
[auth]
# Authentication method
-# Value: None | htpasswd
+# Value: None | htpasswd | remote_user | http_x_remote_user
#type = None
# Htpasswd filename
diff --git a/radicale/__init__.py b/radicale/__init__.py
index 9b85c6b..5137434 100644
--- a/radicale/__init__.py
+++ b/radicale/__init__.py
@@ -371,15 +371,19 @@ class Application:
function = getattr(self, "do_%s" % environ["REQUEST_METHOD"].upper())
# Ask authentication backend to check rights
- authorization = environ.get("HTTP_AUTHORIZATION", None)
- if authorization and authorization.startswith("Basic"):
+ external_login = self.Auth.get_external_login(environ)
+ authorization = environ.get("HTTP_AUTHORIZATION", "")
+ if external_login:
+ login, password = external_login
+ elif authorization.startswith("Basic"):
authorization = authorization[len("Basic"):].strip()
login, password = self.decode(base64.b64decode(
authorization.encode("ascii")), environ).split(":", 1)
- user = self.Auth.map_login_to_user(login)
else:
- user = self.Auth.map_login_to_user(environ.get("REMOTE_USER", ""))
+ # DEPRECATED: use remote_user backend instead
+ login = environ.get("REMOTE_USER", "")
password = ""
+ user = self.Auth.map_login_to_user(login)
# If "/.well-known" is not available, clients query "/"
if path == "/.well-known" or path.startswith("/.well-known/"):
@@ -437,7 +441,7 @@ class Application:
status, headers, answer = NOT_ALLOWED
if (status, headers, answer) == NOT_ALLOWED and not (
- user and is_authenticated):
+ user and is_authenticated) and not external_login:
# Unknown or unauthorized user
self.logger.debug("Asking client for authentication")
status = client.UNAUTHORIZED
diff --git a/radicale/auth.py b/radicale/auth.py
index b202c5d..06f15f1 100644
--- a/radicale/auth.py
+++ b/radicale/auth.py
@@ -67,6 +67,10 @@ def load(configuration, logger):
logger.debug("Authentication type is %s", auth_type)
if auth_type == "None":
class_ = NoneAuth
+ elif auth_type == "remote_user":
+ class_ = RemoteUserAuth
+ elif auth_type == "http_x_remote_user":
+ class_ = HttpXRemoteUserAuth
elif auth_type == "htpasswd":
class_ = Auth
else:
@@ -79,6 +83,14 @@ class BaseAuth:
self.configuration = configuration
self.logger = logger
+ def get_external_login(self, environ):
+ """Optionally provide the login and password externally.
+
+ Returns a tuple (login, password) or ().
+
+ """
+ return ()
+
def is_authenticated(self, user, password):
"""Validate credentials.
@@ -201,3 +213,13 @@ class Auth(BaseAuth):
if login_ok & password_ok:
return True
return False
+
+
+class RemoteUserAuth(NoneAuth):
+ def get_external_login(self, environ):
+ return environ.get("REMOTE_USER", ""), ""
+
+
+class HttpXRemoteUserAuth(NoneAuth):
+ def get_external_login(self, environ):
+ return environ.get("HTTP_X_REMOTE_USER", ""), ""
diff --git a/radicale/tests/test_auth.py b/radicale/tests/test_auth.py
index 806dcd1..3ac3f08 100644
--- a/radicale/tests/test_auth.py
+++ b/radicale/tests/test_auth.py
@@ -109,6 +109,34 @@ class TestBaseAuthRequests(BaseTest):
"bcrypt",
"tmp:$2y$05$oD7hbiQFQlvCM7zoalo/T.MssV3VNTRI3w5KDnj8NTUKJNWfVpvRq")
+ def test_remote_user(self):
+ self.configuration.set("auth", "type", "remote_user")
+ self.application = Application(self.configuration, self.logger)
+ status, _, answer = self.request(
+ "PROPFIND", "/",
+ """
+
+
+
+
+ """, REMOTE_USER="test")
+ assert status == 207
+ assert ">/test/<" in answer
+
+ def test_http_x_remote_user(self):
+ self.configuration.set("auth", "type", "http_x_remote_user")
+ self.application = Application(self.configuration, self.logger)
+ status, _, answer = self.request(
+ "PROPFIND", "/",
+ """
+
+
+
+
+ """, HTTP_X_REMOTE_USER="test")
+ assert status == 207
+ assert ">/test/<" in answer
+
def test_custom(self):
"""Custom authentication."""
self.configuration.set("auth", "type", "tests.custom.auth")