From ce9a60ec47882e44a73e95a67b2381923dec3cb0 Mon Sep 17 00:00:00 2001 From: c4ffein Date: Sun, 6 Oct 2024 21:20:02 +0200 Subject: [PATCH 1/2] Better CSRF documentation --- docs/docs/reference/csrf.md | 5 +++- tests/test_csrf.py | 52 +++++++++++++++++++++++++++++++++++-- 2 files changed, 54 insertions(+), 3 deletions(-) diff --git a/docs/docs/reference/csrf.md b/docs/docs/reference/csrf.md index cef2a136..75fe1c66 100644 --- a/docs/docs/reference/csrf.md +++ b/docs/docs/reference/csrf.md @@ -54,9 +54,12 @@ api = NinjaAPI(auth=django_auth) #### Django `ensure_csrf_cookie` decorator You can use the Django [ensure_csrf_cookie](https://docs.djangoproject.com/en/4.2/ref/csrf/#django.views.decorators.csrf.ensure_csrf_cookie) decorator on an unprotected route to make it include a `Set-Cookie` header for the CSRF token. Note that: + - The route decorator must be executed before (i.e. above) the [ensure_csrf_cookie](https://docs.djangoproject.com/en/4.2/ref/csrf/#django.views.decorators.csrf.ensure_csrf_cookie) decorator). - You must `csrf_exempt` that route. -- The `ensure_csrf_cookie` decorator works only on a Django `HttpResponse` and not also on a dict like most Django Ninja decorators. +- The `ensure_csrf_cookie` decorator works only on a Django `HttpResponse` (and subclasses like `JsonResponse`) and not on a dict like most Django Ninja decorators. +- If you [set a Cookie based authentication (which includes `django_auth`) globally to your API](../guides/authentication.md), you'll have to specifically disable auth on that route (with `auth=None` in the route decorator) as Cookie based authentication would raise an Exception when applied to an unprotected route (for security reasons). + ```python hl_lines="4" from django.http import HttpResponse from django.views.decorators.csrf import csrf_exempt, ensure_csrf_cookie diff --git a/tests/test_csrf.py b/tests/test_csrf.py index eeab4884..b7bc4118 100644 --- a/tests/test_csrf.py +++ b/tests/test_csrf.py @@ -1,10 +1,11 @@ import re from django.conf import settings -from django.views.decorators.csrf import csrf_exempt +from django.http import JsonResponse +from django.views.decorators.csrf import csrf_exempt, ensure_csrf_cookie from ninja import NinjaAPI -from ninja.security import APIKeyCookie, APIKeyHeader +from ninja.security import APIKeyCookie, APIKeyHeader, django_auth from ninja.testing import TestClient as BaseTestClient @@ -17,6 +18,9 @@ def _build_request(self, *args, **kwargs): csrf_OFF = NinjaAPI(urls_namespace="csrf_OFF") csrf_ON = NinjaAPI(urls_namespace="csrf_ON", csrf=True) +csrf_ON_with_django_auth = NinjaAPI( + urls_namespace="csrf_ON", csrf=True, auth=django_auth +) @csrf_OFF.post("/post") @@ -98,6 +102,50 @@ def test_view(request): assert response.status_code == 200, response.content +def test_csrf_cookies_can_be_obtained(): + @csrf_ON.get("/obtain_csrf_token_get") + @ensure_csrf_cookie + def obtain_csrf_token_get(request): + return JsonResponse(data={"success": True}) + + @csrf_ON.post("/obtain_csrf_token_post") + @ensure_csrf_cookie + @csrf_exempt + def obtain_csrf_token_post(request): + return JsonResponse(data={"success": True}) + + @csrf_ON_with_django_auth.get("/obtain_csrf_token_get", auth=None) + @ensure_csrf_cookie + def obtain_csrf_token_get_no_auth_route(request): + return JsonResponse(data={"success": True}) + + @csrf_ON_with_django_auth.post("/obtain_csrf_token_post", auth=None) + @ensure_csrf_cookie + @csrf_exempt + def obtain_csrf_token_post_no_auth_route(request): + return JsonResponse(data={"success": True}) + + client = TestClient(csrf_ON) + # can get csrf cookie through get + response = client.get("/obtain_csrf_token_get") + assert response.status_code == 200 + assert len(response.cookies["csrftoken"].value) == 32 + # can get csrf cookie through exempted post + response = client.post("/obtain_csrf_token_post") + assert response.status_code == 200 + assert len(response.cookies["csrftoken"].value) == 32 + # Now testing a route with disabled auth from a client with django_auth set globally also works + client = TestClient(csrf_ON_with_django_auth) + # can get csrf cookie through get on route with disabled auth + response = client.get("/obtain_csrf_token_get") + assert response.status_code == 200 + assert len(response.cookies["csrftoken"].value) == 32 + # can get csrf cookie through exempted post on route with disabled auth + response = client.post("/obtain_csrf_token_post") + assert response.status_code == 200 + assert len(response.cookies["csrftoken"].value) == 32 + + def test_docs(): "Testing that docs are initializing csrf headers correctly" From 22e04cd7850dfd3531fce3ba4127c72a962d9bfc Mon Sep 17 00:00:00 2001 From: c4ffein Date: Sun, 6 Oct 2024 21:24:17 +0200 Subject: [PATCH 2/2] Quickfix --- tests/test_csrf.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/test_csrf.py b/tests/test_csrf.py index b7bc4118..ff762422 100644 --- a/tests/test_csrf.py +++ b/tests/test_csrf.py @@ -129,21 +129,21 @@ def obtain_csrf_token_post_no_auth_route(request): # can get csrf cookie through get response = client.get("/obtain_csrf_token_get") assert response.status_code == 200 - assert len(response.cookies["csrftoken"].value) == 32 + assert len(response.cookies["csrftoken"].value) > 0 # can get csrf cookie through exempted post response = client.post("/obtain_csrf_token_post") assert response.status_code == 200 - assert len(response.cookies["csrftoken"].value) == 32 + assert len(response.cookies["csrftoken"].value) > 0 # Now testing a route with disabled auth from a client with django_auth set globally also works client = TestClient(csrf_ON_with_django_auth) # can get csrf cookie through get on route with disabled auth response = client.get("/obtain_csrf_token_get") assert response.status_code == 200 - assert len(response.cookies["csrftoken"].value) == 32 + assert len(response.cookies["csrftoken"].value) > 0 # can get csrf cookie through exempted post on route with disabled auth response = client.post("/obtain_csrf_token_post") assert response.status_code == 200 - assert len(response.cookies["csrftoken"].value) == 32 + assert len(response.cookies["csrftoken"].value) > 0 def test_docs():