From 19f9d0a8c78a520958803ea4553dd574a4c82f4f Mon Sep 17 00:00:00 2001 From: Max Berger Date: Wed, 27 May 2026 19:36:05 +0200 Subject: [PATCH] web ui: New setting for browser-based login This is useful in cases where OIDC login is active which would then redirect the user to a central login page. See https://github.com/Kozea/Radicale/issues/2143 --- DOCUMENTATION.md | 9 ++++++ config | 3 ++ radicale/config.py | 6 +++- radicale/tests/test_web.py | 5 ++++ radicale/web/internal.py | 28 ++++++++++++++++--- radicale/web/internal_data/js/config.js | 3 ++ .../web/internal_data/js/scenes/LoginScene.js | 3 +- 7 files changed, 51 insertions(+), 6 deletions(-) create mode 100644 radicale/web/internal_data/js/config.js diff --git a/DOCUMENTATION.md b/DOCUMENTATION.md index dbba6e64..dd66d15f 100644 --- a/DOCUMENTATION.md +++ b/DOCUMENTATION.md @@ -1699,6 +1699,15 @@ Available backends are: Default: `internal` +##### prefer_browser_login _(>= 3.7.4)_ + +Try to use browser based login rather than the built in login in the web UI. This +can be helpful if the login system you are using uses redirects, such as in +an OIDC setup. For this to work properly, you will also have to make sure CORS +headers are configured correctly.s + +Default: `False` + #### [logging] ##### level diff --git a/config b/config index 59888a99..0efb2ab0 100644 --- a/config +++ b/config @@ -363,6 +363,9 @@ # Value: none | internal #type = internal +# Prefer browser login +#prefer_browser_login = False + [logging] diff --git a/radicale/config.py b/radicale/config.py index 319f2478..394dac97 100644 --- a/radicale/config.py +++ b/radicale/config.py @@ -691,7 +691,11 @@ This is an automated message. Please do not reply.""", "value": "internal", "help": "web interface backend", "type": str_or_callable, - "internal": web.INTERNAL_TYPES})])), + "internal": web.INTERNAL_TYPES}), + ("prefer_browser_login", { + "value": "False", + "help": "prefer browser login", + "type": bool})])), ("logging", OrderedDict([ ("level", { "value": "info", diff --git a/radicale/tests/test_web.py b/radicale/tests/test_web.py index 205b9b6a..ab4de538 100644 --- a/radicale/tests/test_web.py +++ b/radicale/tests/test_web.py @@ -31,6 +31,11 @@ class TestBaseWebRequests(BaseTest): _, answer = self.get("/.web/") assert answer self.post("/.web", check=405) + _, answer = self.get("/.web/js/config.js") + assert "export const PREFER_BROWSER_LOGIN = false;" in answer + self.configure({"web": {"prefer_browser_login": "True"}}) + _, answer = self.get("/.web/js/config.js") + assert "export const PREFER_BROWSER_LOGIN = true;" in answer def test_none(self) -> None: self.configure({"web": {"type": "none"}}) diff --git a/radicale/web/internal.py b/radicale/web/internal.py index 1fd78e2d..2735b8e1 100644 --- a/radicale/web/internal.py +++ b/radicale/web/internal.py @@ -34,7 +34,27 @@ FALLBACK_MIMETYPE = httputils.FALLBACK_MIMETYPE # deprecated class Web(web.BaseWeb): - def get(self, environ: types.WSGIEnviron, base_prefix: str, path: str, - user: str, request_info: dict) -> types.WSGIResponse: - return httputils.serve_resource("radicale.web", "internal_data", - base_prefix, path) + def get( + self, + environ: types.WSGIEnviron, + base_prefix: str, + path: str, + user: str, + request_info: dict, + ) -> types.WSGIResponse: + if path == "/.web/js/config.js": + prefer_browser_login = ( + "true" + if self.configuration.get("web", "prefer_browser_login") + else "false" + ) + content = ( + f"export const PREFER_BROWSER_LOGIN = {prefer_browser_login};\n".encode( + "utf-8" + ) + ) + headers = {"Content-Type": "application/javascript"} + return 200, headers, content, None + return httputils.serve_resource( + "radicale.web", "internal_data", base_prefix, path + ) diff --git a/radicale/web/internal_data/js/config.js b/radicale/web/internal_data/js/config.js new file mode 100644 index 00000000..076268a2 --- /dev/null +++ b/radicale/web/internal_data/js/config.js @@ -0,0 +1,3 @@ +// This file is dynamically overwritten during serving. The contents here are just to enable development an testing. + +export const PREFER_BROWSER_LOGIN = false; diff --git a/radicale/web/internal_data/js/scenes/LoginScene.js b/radicale/web/internal_data/js/scenes/LoginScene.js index a0aaddf6..03ba7678 100644 --- a/radicale/web/internal_data/js/scenes/LoginScene.js +++ b/radicale/web/internal_data/js/scenes/LoginScene.js @@ -20,6 +20,7 @@ */ import { get_principal } from "../api/api.js"; +import { PREFER_BROWSER_LOGIN } from "../config.js"; import { ROOT_PATH, SERVER } from "../constants.js"; import { Collection } from "../models/collection.js"; import { extract_title, extractUsernameFromPrincipalCollection } from "../utils/collection_utils.js"; @@ -167,7 +168,7 @@ export class LoginScene { fetch(SERVER + ROOT_PATH, { method: 'PROPFIND', headers: { 'Depth': '0' }, - credentials: 'omit' + credentials: PREFER_BROWSER_LOGIN ? 'include' : 'omit' }).then((response) => { if (response.ok) { // Authenticated! Now it's safe to call get_principal