diff --git a/django/contrib/auth/__init__.py b/django/contrib/auth/__init__.py index dbcc28cc59a..d4cf945e3b6 100644 --- a/django/contrib/auth/__init__.py +++ b/django/contrib/auth/__init__.py @@ -2,6 +2,7 @@ import re from django.core.exceptions import ImproperlyConfigured from django.utils.importlib import import_module +from django.middleware.csrf import rotate_token from django.contrib.auth.signals import user_logged_in, user_logged_out, user_login_failed SESSION_KEY = '_auth_user_id' @@ -92,6 +93,7 @@ def login(request, user): request.session[BACKEND_SESSION_KEY] = user.backend if hasattr(request, 'user'): request.user = user + rotate_token(request) user_logged_in.send(sender=user.__class__, request=request, user=user) diff --git a/django/contrib/auth/tests/views.py b/django/contrib/auth/tests/views.py index 89e23e3ffae..629587b684e 100644 --- a/django/contrib/auth/tests/views.py +++ b/django/contrib/auth/tests/views.py @@ -7,18 +7,21 @@ from django.contrib.auth.models import User from django.core import mail from django.core.exceptions import SuspiciousOperation from django.core.urlresolvers import reverse, NoReverseMatch -from django.http import QueryDict +from django.http import QueryDict, HttpRequest from django.utils.encoding import force_text from django.utils.html import escape from django.utils.http import urlquote from django.utils._os import upath from django.test import TestCase from django.test.utils import override_settings +from django.middleware.csrf import CsrfViewMiddleware +from django.contrib.sessions.middleware import SessionMiddleware from django.contrib.auth import SESSION_KEY, REDIRECT_FIELD_NAME from django.contrib.auth.forms import (AuthenticationForm, PasswordChangeForm, SetPasswordForm, PasswordResetForm) from django.contrib.auth.tests.utils import skipIfCustomUser +from django.contrib.auth.views import login as login_view @override_settings( @@ -361,6 +364,41 @@ class LoginTest(AuthViewsTestCase): self.assertTrue(good_url in response['Location'], "%s should be allowed" % good_url) + def test_login_csrf_rotate(self, password='password'): + """ + Makes sure that a login rotates the currently-used CSRF token. + """ + # Do a GET to establish a CSRF token + # TestClient isn't used here as we're testing middleware, essentially. + req = HttpRequest() + CsrfViewMiddleware().process_view(req, login_view, (), {}) + req.META["CSRF_COOKIE_USED"] = True + resp = login_view(req) + resp2 = CsrfViewMiddleware().process_response(req, resp) + csrf_cookie = resp2.cookies.get(settings.CSRF_COOKIE_NAME, None) + token1 = csrf_cookie.coded_value + + # Prepare the POST request + req = HttpRequest() + req.COOKIES[settings.CSRF_COOKIE_NAME] = token1 + req.method = "POST" + req.POST = {'username': 'testclient', 'password': password, 'csrfmiddlewaretoken': token1} + req.REQUEST = req.POST + + # Use POST request to log in + SessionMiddleware().process_request(req) + CsrfViewMiddleware().process_view(req, login_view, (), {}) + req.META["SERVER_NAME"] = "testserver" # Required to have redirect work in login view + req.META["SERVER_PORT"] = 80 + req.META["CSRF_COOKIE_USED"] = True + resp = login_view(req) + resp2 = CsrfViewMiddleware().process_response(req, resp) + csrf_cookie = resp2.cookies.get(settings.CSRF_COOKIE_NAME, None) + token2 = csrf_cookie.coded_value + + # Check the CSRF token switched + self.assertNotEqual(token1, token2) + @skipIfCustomUser class LoginURLSettings(AuthViewsTestCase): diff --git a/django/middleware/csrf.py b/django/middleware/csrf.py index 339f42a1105..c7c25bf34f2 100644 --- a/django/middleware/csrf.py +++ b/django/middleware/csrf.py @@ -53,6 +53,14 @@ def get_token(request): return request.META.get("CSRF_COOKIE", None) +def rotate_token(request): + """ + Changes the CSRF token in use for a request - should be done on login + for security purposes. + """ + request.META["CSRF_COOKIE"] = _get_new_csrf_key() + + def _sanitize_token(token): # Allow only alphanum if len(token) > CSRF_KEY_LENGTH: