support csrf tokens for anonymous users, bump to v0.2
This commit is contained in:
Родитель
410ebb6ba7
Коммит
37057931ef
40
README.rst
40
README.rst
|
@ -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.
|
||||
|
|
2
setup.py
2
setup.py
|
@ -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',
|
||||
|
|
Загрузка…
Ссылка в новой задаче