Skip to content

Commit

Permalink
Add config option to opt-out of default behavior to renew CSRF token …
Browse files Browse the repository at this point in the history
…on every request
  • Loading branch information
Lizards committed Jul 1, 2024
1 parent 1314b02 commit c5806e7
Show file tree
Hide file tree
Showing 4 changed files with 62 additions and 2 deletions.
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -267,13 +267,20 @@ Default settings:
```python
NEXTJS_SETTINGS = {
"nextjs_server_url": "http://127.0.0.1:3000",
"ensure_csrf_token": True,
}
```

### `nextjs_server_url`

The URL of Next.js server (started by `npm run dev` or `npm run start`)

### `ensure_csrf_token`

If user does not have a CSRF token, ensure that one is generated and included in the initial request to the NextJS
server, by calling Django's `django.middleware.csrf.get_token`. If `django.middleware.csrf.CsrfViewMiddleware` is
installed, he initial response will include a `Set-Cookie` header to persist the CSRF token value on the client.

## Development

- Install development dependencies in your virtualenv with `pip install -e '.[dev]'`
Expand Down
2 changes: 2 additions & 0 deletions django_nextjs/app_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,5 @@
NEXTJS_SETTINGS = getattr(settings, "NEXTJS_SETTINGS", {})

NEXTJS_SERVER_URL = NEXTJS_SETTINGS.get("nextjs_server_url", "http://127.0.0.1:3000")

ENSURE_CSRF_TOKEN = NEXTJS_SETTINGS.get("ensure_csrf_token", True)
6 changes: 4 additions & 2 deletions django_nextjs/render.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from django.template.loader import render_to_string
from multidict import MultiMapping

from .app_settings import NEXTJS_SERVER_URL
from .app_settings import ENSURE_CSRF_TOKEN, NEXTJS_SERVER_URL
from .utils import filter_mapping_obj

morsel = Morsel()
Expand Down Expand Up @@ -49,7 +49,9 @@ def _get_nextjs_request_cookies(request: HttpRequest):
https://docs.djangoproject.com/en/3.2/ref/csrf/#is-posting-an-arbitrary-csrf-token-pair-cookie-and-post-data-a-vulnerability
"""
unreserved_cookies = {k: v for k, v in request.COOKIES.items() if k and not morsel.isReservedKey(k)}
return {**unreserved_cookies, settings.CSRF_COOKIE_NAME: get_csrf_token(request)}
if all([ENSURE_CSRF_TOKEN, settings.CSRF_COOKIE_NAME not in unreserved_cookies]):
unreserved_cookies[settings.CSRF_COOKIE_NAME] = get_csrf_token(request)
return unreserved_cookies


def _get_nextjs_request_headers(request: HttpRequest, headers: Union[Dict, None] = None):
Expand Down
49 changes: 49 additions & 0 deletions tests/test_render.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,55 @@ async def test_nextjs_page(rf: RequestFactory):
assert kwargs["headers"]["extra"] == "headers"


@pytest.mark.asyncio
async def test_set_csrftoken(rf: RequestFactory):
def get_mock_request():
return rf.get("/random/path")

async def get_mock_response(request: RequestFactory):
with patch("aiohttp.ClientSession") as mock_session:
with patch("aiohttp.ClientSession.get") as mock_get:
mock_get.return_value.__aenter__.return_value.text = AsyncMock(return_value="<html></html>")
mock_get.return_value.__aenter__.return_value.status = 200
mock_session.return_value.__aenter__ = AsyncMock(return_value=MagicMock(get=mock_get))
return await nextjs_page(allow_redirects=True)(request), mock_session

# User does not have csrftoken and django-nextjs is not configured to guarantee one
with patch("django_nextjs.render.ENSURE_CSRF_TOKEN", False):
http_request = get_mock_request()
_, mock_session = await get_mock_response(http_request)
args, kwargs = mock_session.call_args
# This triggers CsrfViewMiddleware to call response.set_cookie with updated csrftoken value
assert "CSRF_COOKIE_NEEDS_UPDATE" not in http_request.META
assert "csrftoken" not in kwargs["cookies"]

# User does not have csrftoken and django-nextjs is configured to guarantee one
with patch("django_nextjs.render.ENSURE_CSRF_TOKEN", True):
http_request = get_mock_request()
_, mock_session = await get_mock_response(http_request)
args, kwargs = mock_session.call_args
assert "CSRF_COOKIE_NEEDS_UPDATE" in http_request.META
assert "csrftoken" in kwargs["cookies"]

# User has csrftoken and django-nextjs is not configured to guarantee one
with patch("django_nextjs.render.ENSURE_CSRF_TOKEN", False):
http_request = get_mock_request()
http_request.COOKIES["csrftoken"] = "whatever"
_, mock_session = await get_mock_response(http_request)
args, kwargs = mock_session.call_args
assert "CSRF_COOKIE_NEEDS_UPDATE" not in http_request.META
assert "csrftoken" in kwargs["cookies"]

# User has csrftoken and django-nextjs is configured to guarantee one
with patch("django_nextjs.render.ENSURE_CSRF_TOKEN", True):
http_request = get_mock_request()
http_request.COOKIES["csrftoken"] = "whatever"
_, mock_session = await get_mock_response(http_request)
args, kwargs = mock_session.call_args
assert "CSRF_COOKIE_NEEDS_UPDATE" not in http_request.META
assert "csrftoken" in kwargs["cookies"]


@pytest.mark.asyncio
async def test_render_nextjs_page_to_string(rf: RequestFactory):
request = rf.get(f"/random/path")
Expand Down

0 comments on commit c5806e7

Please sign in to comment.