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..ff762422 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) > 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) > 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) > 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) > 0 + + def test_docs(): "Testing that docs are initializing csrf headers correctly"