diff --git a/django/contrib/auth/__init__.py b/django/contrib/auth/__init__.py index e032038775..029193d582 100644 --- a/django/contrib/auth/__init__.py +++ b/django/contrib/auth/__init__.py @@ -3,6 +3,7 @@ import re from django.conf import settings from django.core.exceptions import ImproperlyConfigured, PermissionDenied from django.utils.module_loading import import_by_path +from django.middleware.csrf import rotate_token from .signals import user_logged_in, user_logged_out, user_login_failed @@ -84,6 +85,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/test_views.py b/django/contrib/auth/tests/test_views.py index a6bea47877..fe1d7fb52f 100644 --- a/django/contrib/auth/tests/test_views.py +++ b/django/contrib/auth/tests/test_views.py @@ -12,18 +12,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( @@ -460,6 +463,41 @@ class LoginTest(AuthViewsTestCase): # the custom authentication form used by this login asserts # that a request is passed to the form successfully. + 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 98974f011a..1b5732fbbf 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: