From 5401b125abca53200eacb62c8a10e602359b76d4 Mon Sep 17 00:00:00 2001 From: Benedict Etzel Date: Mon, 10 Nov 2025 13:29:34 +0100 Subject: [PATCH] Fixed #36717 -- Redirect authenticated users on admin login view to next URL. Co-authored-by: Natalia <124304+nessita@users.noreply.github.com> --- AUTHORS | 1 + django/contrib/admin/sites.py | 18 ++++++++---------- django/contrib/auth/views.py | 24 ++++++++++++++++-------- docs/releases/6.1.txt | 3 ++- tests/admin_views/tests.py | 26 ++++++++++++++++++++++++++ 5 files changed, 53 insertions(+), 19 deletions(-) diff --git a/AUTHORS b/AUTHORS index 5acbe27233..5e7bca67f5 100644 --- a/AUTHORS +++ b/AUTHORS @@ -159,6 +159,7 @@ answer newbie questions, and generally made Django that much better: Ben Slavin Ben Sturmfels Bendegúz Csirmaz + Benedict Etzel Berker Peksag Bernd Schlapsi Bernhard Essl diff --git a/django/contrib/admin/sites.py b/django/contrib/admin/sites.py index 17af19fd1b..410bf20da0 100644 --- a/django/contrib/admin/sites.py +++ b/django/contrib/admin/sites.py @@ -416,29 +416,27 @@ class AdminSite: """ Display the login form for the given HttpRequest. """ - if request.method == "GET" and self.has_permission(request): - # Already logged-in, redirect to admin index - index_path = reverse("admin:index", current_app=self.name) - return HttpResponseRedirect(index_path) - # Since this module gets imported in the application's root package, # it cannot import models from other applications at the module level, # and django.contrib.admin.forms eventually imports User. from django.contrib.admin.forms import AdminAuthenticationForm from django.contrib.auth.views import LoginView + redirect_url = LoginView().get_redirect_url(request) or reverse( + "admin:index", current_app=self.name + ) + if request.method == "GET" and self.has_permission(request): + # Already logged-in, redirect accordingly. + return HttpResponseRedirect(redirect_url) + context = { **self.each_context(request), "title": _("Log in"), "subtitle": None, "app_path": request.get_full_path(), "username": request.user.get_username(), + REDIRECT_FIELD_NAME: redirect_url, } - if ( - REDIRECT_FIELD_NAME not in request.GET - and REDIRECT_FIELD_NAME not in request.POST - ): - context[REDIRECT_FIELD_NAME] = reverse("admin:index", current_app=self.name) context.update(extra_context or {}) defaults = { diff --git a/django/contrib/auth/views.py b/django/contrib/auth/views.py index 295f2219cf..dcffb4aca6 100644 --- a/django/contrib/auth/views.py +++ b/django/contrib/auth/views.py @@ -40,20 +40,28 @@ class RedirectURLMixin: def get_success_url(self): return self.get_redirect_url() or self.get_default_redirect_url() - def get_redirect_url(self): - """Return the user-originating redirect URL if it's safe.""" - redirect_to = self.request.POST.get( - self.redirect_field_name, self.request.GET.get(self.redirect_field_name) + def get_redirect_url(self, request=None): + """Return the user-originating redirect URL if it's safe. + + Optionally takes a request argument, allowing use outside class-based + views. + """ + if request is None: + request = self.request + redirect_to = request.POST.get( + self.redirect_field_name, request.GET.get(self.redirect_field_name) ) url_is_safe = url_has_allowed_host_and_scheme( url=redirect_to, - allowed_hosts=self.get_success_url_allowed_hosts(), - require_https=self.request.is_secure(), + allowed_hosts=self.get_success_url_allowed_hosts(request), + require_https=request.is_secure(), ) return redirect_to if url_is_safe else "" - def get_success_url_allowed_hosts(self): - return {self.request.get_host(), *self.success_url_allowed_hosts} + def get_success_url_allowed_hosts(self, request=None): + if request is None: + request = self.request + return {request.get_host(), *self.success_url_allowed_hosts} def get_default_redirect_url(self): """Return the default redirect URL.""" diff --git a/docs/releases/6.1.txt b/docs/releases/6.1.txt index 036fee53cf..e0edf6876a 100644 --- a/docs/releases/6.1.txt +++ b/docs/releases/6.1.txt @@ -92,7 +92,8 @@ Minor features :mod:`django.contrib.admin` ~~~~~~~~~~~~~~~~~~~~~~~~~~~ -* ... +* The admin site login view now redirects authenticated users to the next URL, + if available, instead of always redirecting to the admin index page. :mod:`django.contrib.admindocs` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/tests/admin_views/tests.py b/tests/admin_views/tests.py index 868b616d76..f7eaad659e 100644 --- a/tests/admin_views/tests.py +++ b/tests/admin_views/tests.py @@ -2413,6 +2413,32 @@ class AdminViewPermissionsTest(TestCase): self.assertEqual(response.status_code, 200) self.assertEqual(response.context[REDIRECT_FIELD_NAME], reverse("admin:index")) + def test_login_redirect_when_logged_in(self): + self.client.force_login(self.superuser) + response = self.client.get(reverse("admin:login")) + self.assertRedirects(response, reverse("admin:index")) + + def test_login_redirect_to_next_url_when_logged_in(self): + self.client.force_login(self.superuser) + next_url = reverse("admin:admin_views_article_add") + response = self.client.get( + reverse("admin:login"), + query_params={REDIRECT_FIELD_NAME: next_url}, + ) + self.assertRedirects(response, next_url) + + def test_login_redirect_unsafe_next_url_when_logged_in(self): + self.client.force_login(self.superuser) + response = self.client.get( + reverse("admin:login"), + query_params={ + REDIRECT_FIELD_NAME: "https://example.com/bad", + }, + ) + self.assertRedirects( + response, reverse("admin:index"), fetch_redirect_response=False + ) + def test_login_has_permission(self): # Regular User should not be able to login. response = self.client.get(reverse("has_permission_admin:index"))