Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Better csrf doc #1314

Merged
merged 2 commits into from
Oct 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion docs/docs/reference/csrf.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
52 changes: 50 additions & 2 deletions tests/test_csrf.py
Original file line number Diff line number Diff line change
@@ -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


Expand All @@ -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")
Expand Down Expand Up @@ -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"

Expand Down
Loading