Add option to disable login form (#1269)

This commit is contained in:
Sascha Ißbrücker
2026-01-05 12:37:49 +01:00
committed by GitHub
parent 9ab91e018b
commit afbf85b249
10 changed files with 89 additions and 28 deletions

View File

@@ -24,6 +24,8 @@ LD_AUTH_PROXY_USERNAME_HEADER=
# The URL that linkding should redirect to after a logout, when using an auth proxy # The URL that linkding should redirect to after a logout, when using an auth proxy
# See docs/Options.md for more details # See docs/Options.md for more details
LD_AUTH_PROXY_LOGOUT_URL= 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 # List of trusted origins from which to accept POST requests
# See docs/Options.md for more details # See docs/Options.md for more details
LD_CSRF_TRUSTED_ORIGINS= LD_CSRF_TRUSTED_ORIGINS=

View File

@@ -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 # Enable OICD support if configured
LD_ENABLE_OIDC = os.getenv("LD_ENABLE_OIDC", False) in (True, "True", "true", "1") LD_ENABLE_OIDC = os.getenv("LD_ENABLE_OIDC", False) in (True, "True", "true", "1")

View File

@@ -0,0 +1,4 @@
.auth-page {
margin: 0 auto;
max-width: 350px;
}

View File

@@ -31,3 +31,4 @@
@import "settings.css"; @import "settings.css";
@import "bundles.css"; @import "bundles.css";
@import "tags.css"; @import "tags.css";
@import "auth.css";

View File

@@ -4,10 +4,11 @@
{% with page_title="Login - Linkding" %}{{ block.super }}{% endwith %} {% with page_title="Login - Linkding" %}{{ block.super }}{% endwith %}
{% endblock %} {% endblock %}
{% block content %} {% block content %}
<main class="mx-auto width-50 width-md-100" aria-labelledby="main-heading"> <main class="auth-page" aria-labelledby="main-heading">
<div class="section-header"> <div class="section-header">
<h1 id="main-heading">Login</h1> <h1 id="main-heading">Login</h1>
</div> </div>
{% if not disable_login %}
<form method="post" action="{% url 'login' %}"> <form method="post" action="{% url 'login' %}">
{% csrf_token %} {% csrf_token %}
{% if form.errors %} {% if form.errors %}
@@ -21,16 +22,14 @@
{% formlabel form.password 'Password' %} {% formlabel form.password 'Password' %}
{% formfield form.password class='form-input' %} {% formfield form.password class='form-input' %}
</div> </div>
<br /> <input type="submit" value="Login" class="btn btn-primary width-100 mt-4" />
<div class="d-flex justify-between">
<input type="submit" value="Login" class="btn btn-primary btn-wide" />
<input type="hidden" name="next" value="{{ next }}" /> <input type="hidden" name="next" value="{{ next }}" />
</form>
{% endif %}
{% if enable_oidc %} {% if enable_oidc %}
<a class="btn btn-link" <a class="btn width-100 mt-4"
href="{% url 'oidc_authentication_init' %}" href="{% url 'oidc_authentication_init' %}"
data-turbo="false">Login with OIDC</a> data-turbo="false">Login with OIDC</a>
{% endif %} {% endif %}
</div>
</form>
</main> </main>
{% endblock %} {% endblock %}

View File

@@ -3,7 +3,7 @@
{% with page_title="Password changed - Linkding" %}{{ block.super }}{% endwith %} {% with page_title="Password changed - Linkding" %}{{ block.super }}{% endwith %}
{% endblock %} {% endblock %}
{% block content %} {% block content %}
<main class="mx-auto width-50 width-md-100" aria-labelledby="main-heading"> <main class="auth-page" aria-labelledby="main-heading">
<div class="section-header"> <div class="section-header">
<h1 id="main-heading">Password Changed</h1> <h1 id="main-heading">Password Changed</h1>
</div> </div>

View File

@@ -4,7 +4,7 @@
{% with page_title="Change password - Linkding" %}{{ block.super }}{% endwith %} {% with page_title="Change password - Linkding" %}{{ block.super }}{% endwith %}
{% endblock %} {% endblock %}
{% block content %} {% block content %}
<main class="mx-auto width-50 width-md-100" aria-labelledby="main-heading"> <main class="auth-page" aria-labelledby="main-heading">
<div class="section-header"> <div class="section-header">
<h1 id="main-heading">Change Password</h1> <h1 id="main-heading">Change Password</h1>
</div> </div>
@@ -25,10 +25,9 @@
{% formfield form.new_password2 class='form-input' %} {% formfield form.new_password2 class='form-input' %}
{{ form.new_password2.errors }} {{ form.new_password2.errors }}
</div> </div>
<br />
<input type="submit" <input type="submit"
value="Change Password" value="Change Password"
class="btn btn-primary btn-wide"> class="btn btn-primary width-100 mt-4">
</form> </form>
</main> </main>
{% endblock %} {% endblock %}

View File

@@ -41,3 +41,43 @@ class LoginViewTestCase(TestCase, BookmarkFactoryMixin, HtmlTestMixin):
# should have turbo disabled # should have turbo disabled
self.assertEqual("false", oidc_login_link.get("data-turbo")) 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)

View File

@@ -19,6 +19,7 @@ class LinkdingLoginView(auth_views.LoginView):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
context["enable_oidc"] = settings.LD_ENABLE_OIDC context["enable_oidc"] = settings.LD_ENABLE_OIDC
context["disable_login"] = settings.LD_DISABLE_LOGIN_FORM
return context return context
def form_invalid(self, form): def form_invalid(self, form):

View File

@@ -179,6 +179,14 @@ identity_providers:
</details> </details>
### `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` ### `LD_CSRF_TRUSTED_ORIGINS`
Values: `String` | Default = None Values: `String` | Default = None