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
|
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.
|
Displays the login form and handles the login action.
|
||||||
"""
|
"""
|
||||||
|
@ -86,7 +95,7 @@ class LoginView(FormView):
|
||||||
)
|
)
|
||||||
url_is_safe = is_safe_url(
|
url_is_safe = is_safe_url(
|
||||||
url=redirect_to,
|
url=redirect_to,
|
||||||
allowed_hosts={self.request.get_host()},
|
allowed_hosts=self.get_success_url_allowed_hosts(),
|
||||||
require_https=self.request.is_secure(),
|
require_https=self.request.is_secure(),
|
||||||
)
|
)
|
||||||
if not url_is_safe:
|
if not url_is_safe:
|
||||||
|
@ -123,7 +132,7 @@ def login(request, *args, **kwargs):
|
||||||
return LoginView.as_view(**kwargs)(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.
|
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_is_safe = is_safe_url(
|
||||||
url=next_page,
|
url=next_page,
|
||||||
allowed_hosts={self.request.get_host()},
|
allowed_hosts=self.get_success_url_allowed_hosts(),
|
||||||
require_https=self.request.is_secure(),
|
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:
|
if not url_is_safe:
|
||||||
next_page = self.request.path
|
next_page = self.request.path
|
||||||
return next_page
|
return next_page
|
||||||
|
|
|
@ -101,6 +101,11 @@ Minor features
|
||||||
* :func:`~django.contrib.auth.update_session_auth_hash` now rotates the session
|
* :func:`~django.contrib.auth.update_session_auth_hash` now rotates the session
|
||||||
key to allow a password change to invalidate stolen session cookies.
|
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`
|
: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
|
authenticated users accessing the login page will be redirected as if
|
||||||
they had just successfully logged in. Defaults to ``False``.
|
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:
|
Here's what ``LoginView`` does:
|
||||||
|
|
||||||
* If called via ``GET``, it displays a login form that POSTs to the
|
* 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
|
* ``extra_context``: A dictionary of context data that will be added to the
|
||||||
default context data passed to the template.
|
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:**
|
**Template context:**
|
||||||
|
|
||||||
* ``title``: The string "Logged out", localized.
|
* ``title``: The string "Logged out", localized.
|
||||||
|
|
|
@ -822,6 +822,38 @@ class LoginRedirectAuthenticatedUser(AuthViewsTestCase):
|
||||||
self.client.get(url)
|
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):
|
class LogoutTest(AuthViewsTestCase):
|
||||||
|
|
||||||
def confirm_logged_out(self):
|
def confirm_logged_out(self):
|
||||||
|
@ -893,6 +925,27 @@ class LogoutTest(AuthViewsTestCase):
|
||||||
self.assertURLEqual(response.url, '/password_reset/')
|
self.assertURLEqual(response.url, '/password_reset/')
|
||||||
self.confirm_logged_out()
|
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):
|
def test_security_check(self):
|
||||||
logout_url = reverse('logout')
|
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/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/$', views.LogoutView.as_view(next_page='/somewhere/')),
|
||||||
url(r'^logout/next_page/named/$', views.LogoutView.as_view(next_page='password_reset')),
|
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'^remote_user/$', remote_user_auth_view),
|
||||||
|
|
||||||
url(r'^password_reset_from_email/$',
|
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_default/$', views.LoginView.as_view()),
|
||||||
url(r'^login/redirect_authenticated_user/$',
|
url(r'^login/redirect_authenticated_user/$',
|
||||||
views.LoginView.as_view(redirect_authenticated_user=True)),
|
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
|
# This line is only required to render the password reset with is_admin=True
|
||||||
url(r'^admin/', admin.site.urls),
|
url(r'^admin/', admin.site.urls),
|
||||||
|
|
Loading…
Reference in New Issue