Fixed #26956 -- Added success_url_allowed_hosts to LoginView and LogoutView.
Allows specifying additional hosts to redirect after login and log out.
This commit is contained in:
parent
f227b8d15d
commit
66e1ebbffc
|
@ -53,7 +53,16 @@ def deprecate_current_app(func):
|
|||
return inner
|
||||
|
||||
|
||||
class LoginView(FormView):
|
||||
class SuccessURLAllowedHostsMixin(object):
|
||||
success_url_allowed_hosts = set()
|
||||
|
||||
def get_success_url_allowed_hosts(self):
|
||||
allowed_hosts = {self.request.get_host()}
|
||||
allowed_hosts.update(self.success_url_allowed_hosts)
|
||||
return allowed_hosts
|
||||
|
||||
|
||||
class LoginView(SuccessURLAllowedHostsMixin, FormView):
|
||||
"""
|
||||
Displays the login form and handles the login action.
|
||||
"""
|
||||
|
@ -86,7 +95,7 @@ class LoginView(FormView):
|
|||
)
|
||||
url_is_safe = is_safe_url(
|
||||
url=redirect_to,
|
||||
allowed_hosts={self.request.get_host()},
|
||||
allowed_hosts=self.get_success_url_allowed_hosts(),
|
||||
require_https=self.request.is_secure(),
|
||||
)
|
||||
if not url_is_safe:
|
||||
|
@ -123,7 +132,7 @@ def login(request, *args, **kwargs):
|
|||
return LoginView.as_view(**kwargs)(request, *args, **kwargs)
|
||||
|
||||
|
||||
class LogoutView(TemplateView):
|
||||
class LogoutView(SuccessURLAllowedHostsMixin, TemplateView):
|
||||
"""
|
||||
Logs out the user and displays 'You are logged out' message.
|
||||
"""
|
||||
|
@ -157,10 +166,11 @@ class LogoutView(TemplateView):
|
|||
)
|
||||
url_is_safe = is_safe_url(
|
||||
url=next_page,
|
||||
allowed_hosts={self.request.get_host()},
|
||||
allowed_hosts=self.get_success_url_allowed_hosts(),
|
||||
require_https=self.request.is_secure(),
|
||||
)
|
||||
# Security check -- don't allow redirection to a different host.
|
||||
# Security check -- Ensure the user-originating redirection URL is
|
||||
# safe.
|
||||
if not url_is_safe:
|
||||
next_page = self.request.path
|
||||
return next_page
|
||||
|
|
|
@ -101,6 +101,11 @@ Minor features
|
|||
* :func:`~django.contrib.auth.update_session_auth_hash` now rotates the session
|
||||
key to allow a password change to invalidate stolen session cookies.
|
||||
|
||||
* The new ``success_url_allowed_hosts`` attribute for
|
||||
:class:`~django.contrib.auth.views.LoginView` and
|
||||
:class:`~django.contrib.auth.views.LogoutView` allows specifying a set of
|
||||
hosts that are safe for redirecting after login and logout.
|
||||
|
||||
:mod:`django.contrib.contenttypes`
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
|
|
|
@ -999,6 +999,10 @@ implementation details see :ref:`using-the-views`.
|
|||
authenticated users accessing the login page will be redirected as if
|
||||
they had just successfully logged in. Defaults to ``False``.
|
||||
|
||||
* ``success_url_allowed_hosts``: A :class:`set` of hosts, in addition to
|
||||
:meth:`request.get_host() <django.http.HttpRequest.get_host>`, that are
|
||||
safe for redirecting after login. Defaults to an empty :class:`set`.
|
||||
|
||||
Here's what ``LoginView`` does:
|
||||
|
||||
* If called via ``GET``, it displays a login form that POSTs to the
|
||||
|
@ -1138,6 +1142,10 @@ implementation details see :ref:`using-the-views`.
|
|||
* ``extra_context``: A dictionary of context data that will be added to the
|
||||
default context data passed to the template.
|
||||
|
||||
* ``success_url_allowed_hosts``: A :class:`set` of hosts, in addition to
|
||||
:meth:`request.get_host() <django.http.HttpRequest.get_host>`, that are
|
||||
safe for redirecting after logout. Defaults to an empty :class:`set`.
|
||||
|
||||
**Template context:**
|
||||
|
||||
* ``title``: The string "Logged out", localized.
|
||||
|
|
|
@ -822,6 +822,38 @@ class LoginRedirectAuthenticatedUser(AuthViewsTestCase):
|
|||
self.client.get(url)
|
||||
|
||||
|
||||
class LoginSuccessURLAllowedHostsTest(AuthViewsTestCase):
|
||||
def test_success_url_allowed_hosts_same_host(self):
|
||||
response = self.client.post('/login/allowed_hosts/', {
|
||||
'username': 'testclient',
|
||||
'password': 'password',
|
||||
'next': 'https://testserver/home',
|
||||
})
|
||||
self.assertIn(SESSION_KEY, self.client.session)
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertURLEqual(response.url, 'https://testserver/home')
|
||||
|
||||
def test_success_url_allowed_hosts_safe_host(self):
|
||||
response = self.client.post('/login/allowed_hosts/', {
|
||||
'username': 'testclient',
|
||||
'password': 'password',
|
||||
'next': 'https://otherserver/home',
|
||||
})
|
||||
self.assertIn(SESSION_KEY, self.client.session)
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertURLEqual(response.url, 'https://otherserver/home')
|
||||
|
||||
def test_success_url_allowed_hosts_unsafe_host(self):
|
||||
response = self.client.post('/login/allowed_hosts/', {
|
||||
'username': 'testclient',
|
||||
'password': 'password',
|
||||
'next': 'https://evil/home',
|
||||
})
|
||||
self.assertIn(SESSION_KEY, self.client.session)
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertURLEqual(response.url, '/accounts/profile/')
|
||||
|
||||
|
||||
class LogoutTest(AuthViewsTestCase):
|
||||
|
||||
def confirm_logged_out(self):
|
||||
|
@ -893,6 +925,27 @@ class LogoutTest(AuthViewsTestCase):
|
|||
self.assertURLEqual(response.url, '/password_reset/')
|
||||
self.confirm_logged_out()
|
||||
|
||||
def test_success_url_allowed_hosts_same_host(self):
|
||||
self.login()
|
||||
response = self.client.get('/logout/allowed_hosts/?next=https://testserver/')
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertURLEqual(response.url, 'https://testserver/')
|
||||
self.confirm_logged_out()
|
||||
|
||||
def test_success_url_allowed_hosts_safe_host(self):
|
||||
self.login()
|
||||
response = self.client.get('/logout/allowed_hosts/?next=https://otherserver/')
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertURLEqual(response.url, 'https://otherserver/')
|
||||
self.confirm_logged_out()
|
||||
|
||||
def test_success_url_allowed_hosts_unsafe_host(self):
|
||||
self.login()
|
||||
response = self.client.get('/logout/allowed_hosts/?next=https://evil/')
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertURLEqual(response.url, '/logout/allowed_hosts/')
|
||||
self.confirm_logged_out()
|
||||
|
||||
def test_security_check(self):
|
||||
logout_url = reverse('logout')
|
||||
|
||||
|
|
|
@ -67,6 +67,7 @@ urlpatterns = auth_urlpatterns + [
|
|||
url(r'^logout/custom_query/$', views.LogoutView.as_view(redirect_field_name='follow')),
|
||||
url(r'^logout/next_page/$', views.LogoutView.as_view(next_page='/somewhere/')),
|
||||
url(r'^logout/next_page/named/$', views.LogoutView.as_view(next_page='password_reset')),
|
||||
url(r'^logout/allowed_hosts/$', views.LogoutView.as_view(success_url_allowed_hosts={'otherserver'})),
|
||||
url(r'^remote_user/$', remote_user_auth_view),
|
||||
|
||||
url(r'^password_reset_from_email/$',
|
||||
|
@ -106,6 +107,8 @@ urlpatterns = auth_urlpatterns + [
|
|||
url(r'^login/redirect_authenticated_user_default/$', views.LoginView.as_view()),
|
||||
url(r'^login/redirect_authenticated_user/$',
|
||||
views.LoginView.as_view(redirect_authenticated_user=True)),
|
||||
url(r'^login/allowed_hosts/$',
|
||||
views.LoginView.as_view(success_url_allowed_hosts={'otherserver'})),
|
||||
|
||||
# This line is only required to render the password reset with is_admin=True
|
||||
url(r'^admin/', admin.site.urls),
|
||||
|
|
Loading…
Reference in New Issue