Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,14 @@ Here is a configuration example::
type = radicale_modoboa_auth_oauth2

oauth2_introspection_endpoint = <introspection url>

Alternatively, if you wish to keep the OAuth2 client secret in a seperate file::

[auth]
type = radicale_modoboa_auth_oauth2

oauth2_introspection_endpoint = <introspection url with no secret/password>
oauth2_introspection_endpoint_secret = <path to file containing secret>

Introspection URL may also contain the path to a Unix domain socket for local
deployments: ``http+unix://radicale@%2Frun%2F<service>%2Fgunicorn.sock/<path>``
48 changes: 39 additions & 9 deletions radicale_modoboa_auth_oauth2/__init__.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -18,23 +19,52 @@ class Auth(DovecotAuth):
[auth]
type = radicale_modoboa_auth_oauth2
oauth2_introspection_endpoint = <URL HERE>
oauth2_introspection_endpoint_secret = <FILEPATH> # 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)
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
Radicale
requests
requests-unixsocket