From afbf85b2493462390d8e2959b6e0e99fd3a01d4f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sascha=20I=C3=9Fbr=C3=BCcker?= Date: Mon, 5 Jan 2026 12:37:49 +0100 Subject: [PATCH] Add option to disable login form (#1269) --- .env.sample | 2 + bookmarks/settings/base.py | 7 +++ bookmarks/styles/auth.css | 4 ++ bookmarks/styles/theme-light.css | 1 + bookmarks/templates/registration/login.html | 47 +++++++++---------- .../registration/password_change_done.html | 2 +- .../registration/password_change_form.html | 5 +- bookmarks/tests/test_login_view.py | 40 ++++++++++++++++ bookmarks/views/auth.py | 1 + docs/src/content/docs/options.md | 8 ++++ 10 files changed, 89 insertions(+), 28 deletions(-) create mode 100644 bookmarks/styles/auth.css diff --git a/.env.sample b/.env.sample index 00aaf59..db31e88 100644 --- a/.env.sample +++ b/.env.sample @@ -24,6 +24,8 @@ LD_AUTH_PROXY_USERNAME_HEADER= # The URL that linkding should redirect to after a logout, when using an auth proxy # See docs/Options.md for more details LD_AUTH_PROXY_LOGOUT_URL= +# Disables the login form, useful to enforce OIDC authentication +LD_DISABLE_LOGIN_FORM=False # List of trusted origins from which to accept POST requests # See docs/Options.md for more details LD_CSRF_TRUSTED_ORIGINS= diff --git a/bookmarks/settings/base.py b/bookmarks/settings/base.py index f73d34d..6759abf 100644 --- a/bookmarks/settings/base.py +++ b/bookmarks/settings/base.py @@ -180,6 +180,13 @@ HUEY = { }, } +# Disable login form if configured +LD_DISABLE_LOGIN_FORM = os.getenv("LD_DISABLE_LOGIN_FORM", False) in ( + True, + "True", + "true", + "1", +) # Enable OICD support if configured LD_ENABLE_OIDC = os.getenv("LD_ENABLE_OIDC", False) in (True, "True", "true", "1") diff --git a/bookmarks/styles/auth.css b/bookmarks/styles/auth.css new file mode 100644 index 0000000..18a6d79 --- /dev/null +++ b/bookmarks/styles/auth.css @@ -0,0 +1,4 @@ +.auth-page { + margin: 0 auto; + max-width: 350px; +} diff --git a/bookmarks/styles/theme-light.css b/bookmarks/styles/theme-light.css index 03dacfd..9676dcf 100644 --- a/bookmarks/styles/theme-light.css +++ b/bookmarks/styles/theme-light.css @@ -31,3 +31,4 @@ @import "settings.css"; @import "bundles.css"; @import "tags.css"; +@import "auth.css"; diff --git a/bookmarks/templates/registration/login.html b/bookmarks/templates/registration/login.html index d899835..25134ee 100644 --- a/bookmarks/templates/registration/login.html +++ b/bookmarks/templates/registration/login.html @@ -4,33 +4,32 @@ {% with page_title="Login - Linkding" %}{{ block.super }}{% endwith %} {% endblock %} {% block content %} -
+

Login

-
- {% csrf_token %} - {% if form.errors %} -

Your username and password didn't match. Please try again.

- {% endif %} -
- {% formlabel form.username 'Username' %} - {% formfield form.username class='form-input' %} -
-
- {% formlabel form.password 'Password' %} - {% formfield form.password class='form-input' %} -
-
-
- - - {% if enable_oidc %} - Login with OIDC + {% if not disable_login %} + + {% csrf_token %} + {% if form.errors %} +

Your username and password didn't match. Please try again.

{% endif %} -
-
+
+ {% formlabel form.username 'Username' %} + {% formfield form.username class='form-input' %} +
+
+ {% formlabel form.password 'Password' %} + {% formfield form.password class='form-input' %} +
+ + + + {% endif %} + {% if enable_oidc %} + Login with OIDC + {% endif %}
{% endblock %} diff --git a/bookmarks/templates/registration/password_change_done.html b/bookmarks/templates/registration/password_change_done.html index b566d23..1be92aa 100644 --- a/bookmarks/templates/registration/password_change_done.html +++ b/bookmarks/templates/registration/password_change_done.html @@ -3,7 +3,7 @@ {% with page_title="Password changed - Linkding" %}{{ block.super }}{% endwith %} {% endblock %} {% block content %} -
+

Password Changed

diff --git a/bookmarks/templates/registration/password_change_form.html b/bookmarks/templates/registration/password_change_form.html index 2b37d48..3ea88b0 100644 --- a/bookmarks/templates/registration/password_change_form.html +++ b/bookmarks/templates/registration/password_change_form.html @@ -4,7 +4,7 @@ {% with page_title="Change password - Linkding" %}{{ block.super }}{% endwith %} {% endblock %} {% block content %} -
+

Change Password

@@ -25,10 +25,9 @@ {% formfield form.new_password2 class='form-input' %} {{ form.new_password2.errors }} -
+ class="btn btn-primary width-100 mt-4">
{% endblock %} diff --git a/bookmarks/tests/test_login_view.py b/bookmarks/tests/test_login_view.py index 566c4c5..0546924 100644 --- a/bookmarks/tests/test_login_view.py +++ b/bookmarks/tests/test_login_view.py @@ -41,3 +41,43 @@ class LoginViewTestCase(TestCase, BookmarkFactoryMixin, HtmlTestMixin): # should have turbo disabled self.assertEqual("false", oidc_login_link.get("data-turbo")) + + def test_should_show_login_form_by_default(self): + response = self.client.get("/login/") + soup = self.make_soup(response.content.decode()) + + form = soup.find("form", {"action": "/login/"}) + username_input = soup.find("input", {"name": "username"}) + password_input = soup.find("input", {"name": "password"}) + submit_button = soup.find("input", {"type": "submit", "value": "Login"}) + + self.assertIsNotNone(form) + self.assertIsNotNone(username_input) + self.assertIsNotNone(password_input) + self.assertIsNotNone(submit_button) + + @override_settings(LD_DISABLE_LOGIN_FORM=True) + def test_should_hide_login_form_when_disabled(self): + response = self.client.get("/login/") + soup = self.make_soup(response.content.decode()) + + form = soup.find("form", {"action": "/login/"}) + username_input = soup.find("input", {"name": "username"}) + password_input = soup.find("input", {"name": "password"}) + submit_button = soup.find("input", {"type": "submit", "value": "Login"}) + + self.assertIsNone(form) + self.assertIsNone(username_input) + self.assertIsNone(password_input) + self.assertIsNone(submit_button) + + @override_settings(LD_DISABLE_LOGIN_FORM=True, LD_ENABLE_OIDC=True) + def test_should_only_show_oidc_login_when_login_disabled_and_oidc_enabled(self): + response = self.client.get("/login/") + soup = self.make_soup(response.content.decode()) + + form = soup.find("form", {"action": "/login/"}) + oidc_login_link = soup.find("a", string="Login with OIDC") + + self.assertIsNone(form) + self.assertIsNotNone(oidc_login_link) diff --git a/bookmarks/views/auth.py b/bookmarks/views/auth.py index 3a3b90c..24336b0 100644 --- a/bookmarks/views/auth.py +++ b/bookmarks/views/auth.py @@ -19,6 +19,7 @@ class LinkdingLoginView(auth_views.LoginView): context = super().get_context_data(**kwargs) context["enable_oidc"] = settings.LD_ENABLE_OIDC + context["disable_login"] = settings.LD_DISABLE_LOGIN_FORM return context def form_invalid(self, form): diff --git a/docs/src/content/docs/options.md b/docs/src/content/docs/options.md index 5bc09a7..6cdebde 100644 --- a/docs/src/content/docs/options.md +++ b/docs/src/content/docs/options.md @@ -179,6 +179,14 @@ identity_providers: +### `LD_DISABLE_LOGIN_FORM` + +Values: `True`, `False` | Default = `False` + +Disables the login form on the login page. +This is useful when you want to enforce authentication through OIDC only. +When enabled, users will not be able to log in using their username and password, and only the "Login with OIDC" button will be shown on the login page. + ### `LD_CSRF_TRUSTED_ORIGINS` Values: `String` | Default = None