From 30389f45255ada48a5a80fee05297ca373bf2a37 Mon Sep 17 00:00:00 2001 From: Peter Bieringer Date: Sun, 2 Feb 2025 08:29:02 +0100 Subject: [PATCH 01/13] initial from https://gitlab.mim-libre.fr/alphabet/radicale_oauth/-/blob/dev/oauth2/radicale_auth_oauth2/__init__.py --- radicale/auth/oauth2.py | 44 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 radicale/auth/oauth2.py diff --git a/radicale/auth/oauth2.py b/radicale/auth/oauth2.py new file mode 100644 index 00000000..4efde374 --- /dev/null +++ b/radicale/auth/oauth2.py @@ -0,0 +1,44 @@ +""" +Authentication backend that checks credentials against an oauth2 server auth endpoint +""" + +from radicale import auth +from radicale.log import logger +import requests +from requests.utils import quote + + +class Auth(auth.BaseAuth): + def __init__(self, configuration): + super().__init__(configuration) + self._endpoint = configuration.get("auth", "oauth2_token_endpoint") + logger.warning("Using oauth2 token endpoint: %s" % (self._endpoint)) + + def login(self, login, password): + """Validate credentials. + Sends login credentials to oauth auth endpoint and checks that a token is returned + """ + try: + # authenticate to authentication endpoint and return login if ok, else "" + req_params = { + "username": login, + "password": password, + "grant_type": "password", + "client_id": "radicale", + } + req_headers = {"Content-Type": "application/x-www-form-urlencoded"} + response = requests.post( + self._endpoint, data=req_params, headers=req_headers + ) + if ( + response.status_code == requests.codes.ok + and "access_token" in response.json() + ): + return login + except OSError as e: + raise RuntimeError( + "Failed to authenticate against oauth server %r: %s" + % (self._endpoint, e) + ) from e + logger.warning("User %s failed to authenticate" % (str(login))) + return "" From 063883797ce703c4b862b74ea56f92b6435caa94 Mon Sep 17 00:00:00 2001 From: Peter Bieringer Date: Sun, 2 Feb 2025 08:32:42 +0100 Subject: [PATCH 02/13] add copyright --- radicale/auth/oauth2.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/radicale/auth/oauth2.py b/radicale/auth/oauth2.py index 4efde374..cfa5dbe5 100644 --- a/radicale/auth/oauth2.py +++ b/radicale/auth/oauth2.py @@ -1,3 +1,25 @@ +# This file is part of Radicale Server - Calendar Server +# +# Original from https://gitlab.mim-libre.fr/alphabet/radicale_oauth/ +# Copyright © 2021-2022 Bruno Boiget +# Copyright © 2022-2022 Daniel Dehennin +# +# Since migration into upstream +# Copyright © 2025-2025 Peter Bieringer +# +# This library is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Radicale. If not, see . + """ Authentication backend that checks credentials against an oauth2 server auth endpoint """ From 937acf38f7f26c578c8a6ae912d1d9d4fce6e26b Mon Sep 17 00:00:00 2001 From: Peter Bieringer Date: Sun, 2 Feb 2025 08:33:49 +0100 Subject: [PATCH 03/13] oauth2 config check improvement --- radicale/auth/oauth2.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/radicale/auth/oauth2.py b/radicale/auth/oauth2.py index cfa5dbe5..644bdb8a 100644 --- a/radicale/auth/oauth2.py +++ b/radicale/auth/oauth2.py @@ -29,12 +29,14 @@ from radicale.log import logger import requests from requests.utils import quote - class Auth(auth.BaseAuth): def __init__(self, configuration): super().__init__(configuration) self._endpoint = configuration.get("auth", "oauth2_token_endpoint") - logger.warning("Using oauth2 token endpoint: %s" % (self._endpoint)) + if not self._endpoint: + logger.error("auth.oauth2_token_endpoint URL missing") + raise RuntimeError("OAuth2 token endpoint URL is required") + logger.info("auth OAuth2 token endpoint: %s" % (self._endpoint)) def login(self, login, password): """Validate credentials. From e28b719233a58b04517abfda2e206cc520a8a15a Mon Sep 17 00:00:00 2001 From: Peter Bieringer Date: Sun, 2 Feb 2025 09:01:40 +0100 Subject: [PATCH 04/13] oauth2 example config --- config | 3 +++ 1 file changed, 3 insertions(+) diff --git a/config b/config index c775a3c1..ba53ef3e 100644 --- a/config +++ b/config @@ -125,6 +125,9 @@ # Value: tls | starttls | none #imap_security = tls +# OAuth2 token endpoint URL +#oauth2_token_endpoint = + # Htpasswd filename #htpasswd_filename = /etc/radicale/users From 87dc5538d201e3e2587d70fcf5a84e0953c7a4d4 Mon Sep 17 00:00:00 2001 From: Peter Bieringer Date: Sun, 2 Feb 2025 09:01:58 +0100 Subject: [PATCH 05/13] oauth2 module enabling --- radicale/auth/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/radicale/auth/__init__.py b/radicale/auth/__init__.py index 71854e2a..e92272f8 100644 --- a/radicale/auth/__init__.py +++ b/radicale/auth/__init__.py @@ -42,6 +42,7 @@ INTERNAL_TYPES: Sequence[str] = ("none", "remote_user", "http_x_remote_user", "htpasswd", "ldap", "imap", + "oauth2", "dovecot") CACHE_LOGIN_TYPES: Sequence[str] = ( @@ -49,6 +50,7 @@ CACHE_LOGIN_TYPES: Sequence[str] = ( "ldap", "htpasswd", "imap", + "oauth2", ) AUTH_SOCKET_FAMILY: Sequence[str] = ("AF_UNIX", "AF_INET", "AF_INET6") From 23a68b2fb1a923aa23be3296ca43ef4d00248388 Mon Sep 17 00:00:00 2001 From: Peter Bieringer Date: Sun, 2 Feb 2025 09:03:25 +0100 Subject: [PATCH 06/13] extend mypy options --- pyproject.toml | 2 +- setup.cfg.legacy | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 5784971a..16d539fc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -72,7 +72,7 @@ skip_install = true [tool.tox.env.mypy] deps = ["mypy==1.11.0"] -commands = [["mypy", "."]] +commands = [["mypy", "--install-types", "--non-interactive", "."]] skip_install = true diff --git a/setup.cfg.legacy b/setup.cfg.legacy index 94a39915..e27241b4 100644 --- a/setup.cfg.legacy +++ b/setup.cfg.legacy @@ -24,7 +24,7 @@ skip_install = True [testenv:mypy] deps = mypy==1.11.0 -commands = mypy . +commands = mypy --install-types --non-interactive . skip_install = True [tool:isort] From 04523e50874d72e8b489b2e56780f706bda07730 Mon Sep 17 00:00:00 2001 From: Peter Bieringer Date: Sun, 2 Feb 2025 09:03:42 +0100 Subject: [PATCH 07/13] oauth2 config option --- radicale/config.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/radicale/config.py b/radicale/config.py index 9b4e9af4..5f46022e 100644 --- a/radicale/config.py +++ b/radicale/config.py @@ -307,6 +307,10 @@ DEFAULT_CONFIG_SCHEMA: types.CONFIG_SCHEMA = OrderedDict([ "value": "tls", "help": "Secure the IMAP connection: *tls*|starttls|none", "type": imap_security}), + ("oauth2_token_endpoint", { + "value": "", + "help": "OAuth2 token endpoint URL", + "type": str}), ("strip_domain", { "value": "False", "help": "strip domain from username", From 7b6146405f014adf1c3073b6f84e44f0ce5a8a12 Mon Sep 17 00:00:00 2001 From: Peter Bieringer Date: Sun, 2 Feb 2025 09:04:06 +0100 Subject: [PATCH 08/13] make tox happy --- radicale/auth/oauth2.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/radicale/auth/oauth2.py b/radicale/auth/oauth2.py index 644bdb8a..c9ae4359 100644 --- a/radicale/auth/oauth2.py +++ b/radicale/auth/oauth2.py @@ -24,10 +24,11 @@ Authentication backend that checks credentials against an oauth2 server auth endpoint """ +import requests + from radicale import auth from radicale.log import logger -import requests -from requests.utils import quote + class Auth(auth.BaseAuth): def __init__(self, configuration): From d2be086cd1de73df1ebb7270f40b2e8bc0b0db05 Mon Sep 17 00:00:00 2001 From: Peter Bieringer Date: Sun, 2 Feb 2025 09:04:20 +0100 Subject: [PATCH 09/13] oauth2 adjustments to radicale changes in the past --- radicale/auth/oauth2.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/radicale/auth/oauth2.py b/radicale/auth/oauth2.py index c9ae4359..7ca5eb9d 100644 --- a/radicale/auth/oauth2.py +++ b/radicale/auth/oauth2.py @@ -39,9 +39,9 @@ class Auth(auth.BaseAuth): raise RuntimeError("OAuth2 token endpoint URL is required") logger.info("auth OAuth2 token endpoint: %s" % (self._endpoint)) - def login(self, login, password): + def _login(self, login, password): """Validate credentials. - Sends login credentials to oauth auth endpoint and checks that a token is returned + Sends login credentials to oauth token endpoint and checks that a token is returned """ try: # authenticate to authentication endpoint and return login if ok, else "" From e0d20edbcd8116790dd02b387b04177c6ca46e69 Mon Sep 17 00:00:00 2001 From: Peter Bieringer Date: Sun, 2 Feb 2025 09:04:42 +0100 Subject: [PATCH 10/13] oauth2 do not throw exception in case server is not reachable --- radicale/auth/oauth2.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/radicale/auth/oauth2.py b/radicale/auth/oauth2.py index 7ca5eb9d..838a786e 100644 --- a/radicale/auth/oauth2.py +++ b/radicale/auth/oauth2.py @@ -61,9 +61,6 @@ class Auth(auth.BaseAuth): ): return login except OSError as e: - raise RuntimeError( - "Failed to authenticate against oauth server %r: %s" - % (self._endpoint, e) - ) from e - logger.warning("User %s failed to authenticate" % (str(login))) + logger.critical("Failed to authenticate against OAuth2 server %s: %s" % (self._endpoint, e)) + logger.warning("User failed to authenticate using OAuth2: %r" % login) return "" From cfcfbbd231cc4ba48102fb7f436149841eae674c Mon Sep 17 00:00:00 2001 From: Peter Bieringer Date: Sun, 2 Feb 2025 09:08:57 +0100 Subject: [PATCH 11/13] oauth2 changelog --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 620073c1..3a1d896e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## 3.4.2.dev + +* Add: option [auth] type oauth2 by code migration from https://gitlab.mim-libre.fr/alphabet/radicale_oauth/-/blob/dev/oauth2/ + ## 3.4.1 * Add: option [auth] dovecot_connection_type / dovecot_host / dovecot_port * Add: option [auth] type imap by code migration from https://github.com/Unrud/RadicaleIMAP/ From f3a7641baa73a96feeab51bdd795716520b35083 Mon Sep 17 00:00:00 2001 From: Peter Bieringer Date: Sun, 2 Feb 2025 09:09:08 +0100 Subject: [PATCH 12/13] 3.4.2.dev --- pyproject.toml | 2 +- setup.py.legacy | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 16d539fc..eac75049 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,7 +3,7 @@ name = "Radicale" # When the version is updated, a new section in the CHANGELOG.md file must be # added too. readme = "README.md" -version = "3.4.1" +version = "3.4.2.dev" authors = [{name = "Guillaume Ayoub", email = "guillaume.ayoub@kozea.fr"}, {name = "Unrud", email = "unrud@outlook.com"}, {name = "Peter Bieringer", email = "pb@bieringer.de"}] license = {text = "GNU GPL v3"} description = "CalDAV and CardDAV Server" diff --git a/setup.py.legacy b/setup.py.legacy index 547f9dda..09d323a9 100644 --- a/setup.py.legacy +++ b/setup.py.legacy @@ -20,7 +20,7 @@ from setuptools import find_packages, setup # When the version is updated, a new section in the CHANGELOG.md file must be # added too. -VERSION = "3.4.1" +VERSION = "3.4.2.dev" with open("README.md", encoding="utf-8") as f: long_description = f.read() From 6f68a64855b2d2e266fa23d8fca11363410c94de Mon Sep 17 00:00:00 2001 From: Peter Bieringer Date: Sun, 2 Feb 2025 09:14:04 +0100 Subject: [PATCH 13/13] oauth2 doc --- DOCUMENTATION.md | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/DOCUMENTATION.md b/DOCUMENTATION.md index 84b3a0cf..c2e586ef 100644 --- a/DOCUMENTATION.md +++ b/DOCUMENTATION.md @@ -824,7 +824,10 @@ Available backends: : Use a Dovecot server to authenticate users. `imap` -: Use a IMAP server to authenticate users. +: Use an IMAP server to authenticate users. + +`oauth2` +: Use an OAuth2 server to authenticate users. Default: `none` @@ -1019,6 +1022,12 @@ Secure the IMAP connection: tls | starttls | none Default: `tls` +##### oauth2_token_endpoint + +OAuth2 token endpoint URL + +Default: + ##### lc_username Сonvert username to lowercase, must be true for case-insensitive auth