diff --git a/README.rst b/README.rst index 58b4189..372802f 100644 --- a/README.rst +++ b/README.rst @@ -20,3 +20,14 @@ 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 = + +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 15a999f..c164a8a 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 requests_unixsocket +import urllib3.util # requests dependency from radicale.auth.dovecot import Auth as DovecotAuth from radicale.log import logger @@ -18,23 +19,52 @@ 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") - logger.warning("Using oauth2 introspection endpoint: %s" % (self._endpoint)) + 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) + 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 = 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): headers = {"Content-Type": "application/x-www-form-urlencoded"} 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