support csrf tokens for anonymous users, bump to v0.2

This commit is contained in:
Jeff Balogh 2011-04-20 13:29:37 -07:00
Родитель 410ebb6ba7
Коммит 37057931ef
4 изменённых файлов: 171 добавлений и 7 удалений

Просмотреть файл

@ -37,7 +37,41 @@ Replace ``django.middleware.csrf.CsrfViewMiddleware`` with
...
)
Everything else should be identical to the built-in CSRF protection.
Then we have to monkeypatch Django to fix the ``@csrf_protect`` decorator::
import session_csrf
session_csrf.monkeypatch()
Make sure that's in something like ``manage.py`` so the patch gets applied
before your views are imported.
Differences from Django
-----------------------
``django-session-csrf`` does not assign CSRF tokens to anonymous users because
we don't want to support a session for every anonymous user. Instead, views
that need anonymous forms can be decorated with ``@anonymous_csrf``::
from session_csrf import anonymous_csrf
@anonymous_csrf
def login(request):
...
``anonymous_csrf`` uses the cache to give anonymous users a lightweight
session. It sends a cookie to uniquely identify the user and stores the CSRF
token in the cache. It can be controlled through these settings:
``ANON_COOKIE``
the name used for the anonymous user's cookie
Default: ``anoncsrf``
``ANON_TIMEOUT``
the cache timeout (in seconds) to use for the anonymous CSRF tokens
Default: ``60 * 60 * 2 # 2 hours``
Why do I want this?
@ -54,6 +88,4 @@ Why don't I want this?
1. Storing tokens in sessions means you have to hit your session store more
often.
2. You want CSRF protection for anonymous users. ``django-session-csrf`` does
not create CSRF tokens for anonymous users since we're worried about the
scalability of that.
2. It's a little bit more work to CSRF-protect forms for anonymous users.

Просмотреть файл

@ -1,7 +1,16 @@
"""CSRF protection without cookies."""
import functools
import django.core.context_processors
from django.conf import settings
from django.core.cache import cache
from django.middleware import csrf as django_csrf
from django.utils import crypto
from django.utils.cache import patch_vary_headers
ANON_COOKIE = getattr(settings, 'ANON_COOKIE', 'anoncsrf')
ANON_TIMEOUT = getattr(settings, 'ANON_TIMEOUT', 60 * 60 * 2) # 2 hours.
# This overrides django.core.context_processors.csrf to dump our csrf_token
@ -27,12 +36,17 @@ class CsrfMiddleware(object):
The token is available at request.csrf_token.
"""
if hasattr(request, 'csrf_token'):
return
if request.user.is_authenticated():
if 'csrf_token' not in request.session:
token = django_csrf._get_new_csrf_key()
request.csrf_token = request.session['csrf_token'] = token
else:
request.csrf_token = request.session['csrf_token']
elif ANON_COOKIE in request.COOKIES:
key = request.COOKIES[ANON_COOKIE]
request.csrf_token = cache.get(key, '')
else:
request.csrf_token = ''
@ -71,3 +85,33 @@ class CsrfMiddleware(object):
return self._reject(request, reason)
else:
return self._accept(request)
def anonymous_csrf(f):
"""Decorator that assigns a CSRF token to an anonymous user."""
@functools.wraps(f)
def wrapper(request, *args, **kw):
if not request.user.is_authenticated():
if ANON_COOKIE in request.COOKIES:
key = request.COOKIES[ANON_COOKIE]
token = cache.get(key)
else:
key = django_csrf._get_new_csrf_key()
token = django_csrf._get_new_csrf_key()
cache.set(key, token, ANON_TIMEOUT)
request.csrf_token = token
response = f(request, *args, **kw)
if not request.user.is_authenticated():
# Set or reset the cache and cookie timeouts.
response.set_cookie(ANON_COOKIE, key, max_age=ANON_TIMEOUT,
httponly=True)
patch_vary_headers(response, ['Cookie'])
return response
return wrapper
# Replace Django's middleware with our own.
def monkeypatch():
from django.views.decorators import csrf as csrf_dec
django_csrf.CsrfViewMiddleware = CsrfMiddleware
csrf_dec.csrf_protect = csrf_dec.decorator_from_middleware(CsrfMiddleware)

Просмотреть файл

@ -4,6 +4,8 @@ import django.test
from django import http
from django.conf.urls.defaults import patterns
from django.contrib.auth.models import User
from django.contrib.sessions.models import Session
from django.core.cache import cache
from django.core.handlers.wsgi import WSGIRequest
from django.db import close_connection
from django.shortcuts import render
@ -11,10 +13,13 @@ from django.template import context
import mock
from session_csrf import CsrfMiddleware
from session_csrf import CsrfMiddleware, anonymous_csrf
urlpatterns = patterns('', ('^$', lambda r: http.HttpResponse()))
urlpatterns = patterns('',
('^$', lambda r: http.HttpResponse()),
('^anon$', anonymous_csrf(lambda r: http.HttpResponse())),
)
class TestCsrfToken(django.test.TestCase):
@ -69,6 +74,22 @@ class TestCsrfMiddleware(django.test.TestCase):
def process_view(self, request, view=None):
return self.mw.process_view(request, view, None, None)
def test_anon_token_from_cookie(self):
rf = django.test.RequestFactory()
rf.cookies['anoncsrf'] = self.token
cache.set(self.token, 'woo')
request = rf.get('/')
request.session = {}
self.mw.process_request(request)
self.assertEqual(request.csrf_token, 'woo')
def test_set_csrftoken_once(self):
# Make sure process_request only sets request.csrf_token once.
request = self.rf.get('/')
request.csrf_token = 'woo'
self.mw.process_request(request)
self.assertEqual(request.csrf_token, 'woo')
def test_reject_view(self):
# Check that the reject view returns a 403.
response = self.process_view(self.rf.post('/'))
@ -122,6 +143,73 @@ class TestCsrfMiddleware(django.test.TestCase):
self.assertEqual(ctx['csrf_token'], self.token)
class TestAnonymousCsrf(django.test.TestCase):
def setUp(self):
self.token = 'a' * 32
self.rf = django.test.RequestFactory()
User.objects.create_user('jbalogh', 'j@moz.com', 'password')
self.client.handler = ClientHandler(enforce_csrf_checks=True)
def login(self):
assert self.client.login(username='jbalogh', password='password')
def test_authenticated_request(self):
# Nothing special happens, nothing breaks.
# Find the CSRF token in the session.
self.login()
response = self.client.get('/anon')
sessionid = response.cookies['sessionid'].value
session = Session.objects.get(session_key=sessionid)
token = session.get_decoded()['csrf_token']
response = self.client.post('/anon', HTTP_X_CSRFTOKEN=token)
self.assertEqual(response.status_code, 200)
def test_unauthenticated_request(self):
# We get a 403 since we're not sending a token.
response = self.client.post('/anon')
self.assertEqual(response.status_code, 403)
def test_no_anon_cookie(self):
# We don't get an anon cookie on non-@anonymous_csrf views.
response = self.client.get('/')
self.assertEqual(response.cookies, {})
def test_new_anon_token_on_request(self):
# A new anon user gets a key+token on the request and response.
response = self.client.get('/anon')
# Get the key from the cookie and find the token in the cache.
key = response.cookies['anoncsrf'].value
self.assertEqual(response._request.csrf_token, cache.get(key))
def test_existing_anon_cookie_on_request(self):
# We reuse an existing anon cookie key+token.
response = self.client.get('/anon')
key = response.cookies['anoncsrf'].value
# Now check that subsequent requests use that cookie.
response = self.client.get('/anon')
self.assertEqual(response.cookies['anoncsrf'].value, key)
self.assertEqual(response._request.csrf_token, cache.get(key))
def test_new_anon_token_on_response(self):
# The anon cookie is sent and we vary on Cookie.
response = self.client.get('/anon')
self.assertIn('anoncsrf', response.cookies)
self.assertEqual(response['Vary'], 'Cookie')
def test_existing_anon_token_on_response(self):
# The anon cookie is sent and we vary on Cookie, reusing the old value.
response = self.client.get('/anon')
key = response.cookies['anoncsrf'].value
response = self.client.get('/anon')
self.assertEqual(response.cookies['anoncsrf'].value, key)
self.assertIn('anoncsrf', response.cookies)
self.assertEqual(response['Vary'], 'Cookie')
class ClientHandler(django.test.client.ClientHandler):
"""
Handler that stores the real request object on the response.

Просмотреть файл

@ -6,7 +6,7 @@ ROOT = os.path.abspath(os.path.dirname(__file__))
setup(
name='django-session-csrf',
version='0.1',
version='0.2',
description='CSRF protection for Django without cookies.',
long_description=open(os.path.join(ROOT, 'README.rst')).read(),
author='Jeff Balogh',