From 51291a8b6b6ad19f8dbd1e2d5379782f234ebf41 Mon Sep 17 00:00:00 2001 From: Erin of Yukis Date: Sun, 10 Aug 2025 10:51:54 +0200 Subject: [PATCH 1/3] Do not log OAuth2 secret --- radicale_modoboa_auth_oauth2/__init__.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/radicale_modoboa_auth_oauth2/__init__.py b/radicale_modoboa_auth_oauth2/__init__.py index 15a999f..4a7a4de 100644 --- a/radicale_modoboa_auth_oauth2/__init__.py +++ b/radicale_modoboa_auth_oauth2/__init__.py @@ -1,6 +1,7 @@ """Authentication plugin for Radicale.""" import requests +import urllib3.util # requests dependency from radicale.auth.dovecot import Auth as DovecotAuth from radicale.log import logger @@ -26,7 +27,15 @@ def __init__(self, configuration): self._endpoint = configuration.get("auth", "oauth2_introspection_endpoint") except KeyError: raise RuntimeError("oauth2_introspection_endpoint must be set") - logger.warning("Using oauth2 introspection endpoint: %s" % (self._endpoint)) + + # Log OAuth2 introspection URL without secret + clean_endpoint_url = self._endpoint + clean_url_dict = urllib3.util.parse_url(clean_endpoint_url)._asdict() + auth_parts = clean_url_dict["auth"].split(":", 1) + if len(auth_parts) == 2: + clean_url_dict["auth"] = f"{auth_parts[0]}:********" + clean_endpoint_url = urllib3.util.Url(**clean_url_dict).url + logger.warning(f"Using OAuth2 introspection endpoint: {clean_endpoint_url}") def _login(self, login, password): headers = {"Content-Type": "application/x-www-form-urlencoded"} From 6419e564dac016d185e96efee48b6f0b26ef0616 Mon Sep 17 00:00:00 2001 From: Erin of Yukis Date: Sun, 10 Aug 2025 10:37:59 +0200 Subject: [PATCH 2/3] Allow reading client secret from separate file --- README.rst | 8 ++++++ radicale_modoboa_auth_oauth2/__init__.py | 36 ++++++++++++++++++------ 2 files changed, 36 insertions(+), 8 deletions(-) diff --git a/README.rst b/README.rst index 58b4189..618d7d9 100644 --- a/README.rst +++ b/README.rst @@ -20,3 +20,11 @@ Here is a configuration example:: type = radicale_modoboa_auth_oauth2 oauth2_introspection_endpoint = + +Alternatively, if you wish to keep the OAuth2 client secret in a seperate file:: + + [auth] + type = radicale_modoboa_auth_oauth2 + + oauth2_introspection_endpoint = + oauth2_introspection_endpoint_secret = diff --git a/radicale_modoboa_auth_oauth2/__init__.py b/radicale_modoboa_auth_oauth2/__init__.py index 4a7a4de..e387aca 100644 --- a/radicale_modoboa_auth_oauth2/__init__.py +++ b/radicale_modoboa_auth_oauth2/__init__.py @@ -19,22 +19,42 @@ class Auth(DovecotAuth): [auth] type = radicale_modoboa_auth_oauth2 oauth2_introspection_endpoint = + oauth2_introspection_endpoint_secret = # OPTIONAL: File containing the client secret (if not part of the URL) """ def __init__(self, configuration): super().__init__(configuration) try: - self._endpoint = configuration.get("auth", "oauth2_introspection_endpoint") + endpoint_url = configuration.get("auth", "oauth2_introspection_endpoint") except KeyError: - raise RuntimeError("oauth2_introspection_endpoint must be set") + raise RuntimeError("oauth2_introspection_endpoint must be set") from None + + # Optionally read client secret from separate file if not present in URL + endpoint_url_dict = urllib3.util.parse_url(endpoint_url)._asdict() + auth_parts = endpoint_url_dict["auth"].split(":", 1) + if len(auth_parts) == 1: + try: + secret_path = configuration.get("auth", "oauth2_introspection_endpoint_secret") + with open(secret_path) as f: + secret = f.read().rstrip("\r\n") + except KeyError: + raise RuntimeError( + "oauth2_introspection_endpoint has no client secret and " + "oauth2_introspection_endpoint_secret is not set" + ) from None + except IOError as exc: + raise RuntimeError( + f"Path oauth2_introspection_endpoint_secret ({secret_path}) " + f"could not be read: {type(exc).__name__}: {exc}" + ) from exc + else: + auth_parts.append(secret) + endpoint_url_dict["auth"] = ":".join(auth_parts) + self._endpoint = urllib3.util.Url(**endpoint_url_dict).url # Log OAuth2 introspection URL without secret - clean_endpoint_url = self._endpoint - clean_url_dict = urllib3.util.parse_url(clean_endpoint_url)._asdict() - auth_parts = clean_url_dict["auth"].split(":", 1) - if len(auth_parts) == 2: - clean_url_dict["auth"] = f"{auth_parts[0]}:********" - clean_endpoint_url = urllib3.util.Url(**clean_url_dict).url + clean_endpoint_url_dict = {**endpoint_url_dict, "auth": f"{auth_parts[0]}:********"} + clean_endpoint_url = urllib3.util.Url(**clean_endpoint_url_dict).url logger.warning(f"Using OAuth2 introspection endpoint: {clean_endpoint_url}") def _login(self, login, password): From d9bd2000c65dbefb9c747caca9ad659f5dcb7aa2 Mon Sep 17 00:00:00 2001 From: Erin of Yukis Date: Tue, 12 Aug 2025 10:22:06 +0200 Subject: [PATCH 3/3] Allow connecting to Modoboa using a Unix Domain Socket --- README.rst | 3 +++ radicale_modoboa_auth_oauth2/__init__.py | 19 ++++++++++--------- requirements.txt | 1 + 3 files changed, 14 insertions(+), 9 deletions(-) diff --git a/README.rst b/README.rst index 618d7d9..372802f 100644 --- a/README.rst +++ b/README.rst @@ -28,3 +28,6 @@ Alternatively, if you wish to keep the OAuth2 client secret in a seperate file:: oauth2_introspection_endpoint = oauth2_introspection_endpoint_secret = + +Introspection URL may also contain the path to a Unix domain socket for local +deployments: ``http+unix://radicale@%2Frun%2F%2Fgunicorn.sock/`` diff --git a/radicale_modoboa_auth_oauth2/__init__.py b/radicale_modoboa_auth_oauth2/__init__.py index e387aca..c164a8a 100644 --- a/radicale_modoboa_auth_oauth2/__init__.py +++ b/radicale_modoboa_auth_oauth2/__init__.py @@ -1,6 +1,6 @@ """Authentication plugin for Radicale.""" -import requests +import requests_unixsocket import urllib3.util # requests dependency from radicale.auth.dovecot import Auth as DovecotAuth @@ -49,12 +49,12 @@ def __init__(self, configuration): ) from exc else: auth_parts.append(secret) - endpoint_url_dict["auth"] = ":".join(auth_parts) + del endpoint_url_dict["auth"] self._endpoint = urllib3.util.Url(**endpoint_url_dict).url + self._endpoint_auth = tuple(auth_parts) # Log OAuth2 introspection URL without secret - clean_endpoint_url_dict = {**endpoint_url_dict, "auth": f"{auth_parts[0]}:********"} - clean_endpoint_url = urllib3.util.Url(**clean_endpoint_url_dict).url + clean_endpoint_url = urllib3.util.Url(**endpoint_url_dict, auth=f"{auth_parts[0]}:********").url logger.warning(f"Using OAuth2 introspection endpoint: {clean_endpoint_url}") def _login(self, login, password): @@ -62,8 +62,9 @@ def _login(self, login, password): data = { "token": password } - response = requests.post(self._endpoint, data=data, headers=headers) - content = response.json() - if response.status_code == 200 and content.get("active") and content.get("username") == login: - return login - return super()._login(login, password) + with requests_unixsocket.Session() as session: + response = session.post(self._endpoint, data=data, headers=headers, auth=self._endpoint_auth) + content = response.json() + if response.status_code == 200 and content.get("active") and content.get("username") == login: + return login + return super()._login(login, password) diff --git a/requirements.txt b/requirements.txt index 2e42ba4..445bf44 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,3 @@ Radicale requests +requests-unixsocket