django-ninja
django-ninja copied to clipboard
[BUG] CSRF Fails with Django Auth
Describe the bug When using django_auth (SessionAuth), the CSRF check fails from the Swagger docs page. The login endpoint returns a 200 and seems to be successful. However, when calling the update profile endpoint, it returns a 403 and says the CSRF check failed. This is from a brand new project and the settings are all default.
api.py
from django.contrib.auth import get_user_model, authenticate, login as django_login
from ninja import Schema, Field, NinjaAPI
from django.shortcuts import HttpResponse
from ninja.security import django_auth
from django.views.decorators.csrf import csrf_exempt, ensure_csrf_cookie
api = NinjaAPI(csrf=True, auth=None)
UserModel = get_user_model()
class UserIn(Schema):
email: str = Field(..., pattern=r'^\S+@\S+\.\S+$')
password: str = Field(..., min_length=8)
@api.post('/login/', response={200: str, 401: str})
@csrf_exempt
@ensure_csrf_cookie
def login(request, user_in: UserIn):
user = authenticate(request, username=user_in.email, password=user_in.password)
if user is not None:
django_login(request, user)
return HttpResponse('Login successful', status=200)
else:
return 401, 'Invalid login'
class UserProfileIn(Schema):
first_name: str
last_name: str
class UserProfileOut(Schema):
email: str
first_name: str
last_name: str
@api.post('/profile/', auth=django_auth, response=UserProfileOut)
def update_profile(request, payload: UserProfileIn):
for attr, value in payload.dict().items():
setattr(request.user, attr, value)
request.user.save()
return request.user
Versions (please complete the following information):
- Python version: 3.10.11
- Django version: 4.2.10
- Django-Ninja version: 1.1.0
- Pydantic version: 2.6.3
+1 same issue here
when calling the update profile
endpoint, csrf_token should be included in request body
when calling the
update profile
endpoint, csrf_token should be included in request body
I'm a beginner. Could you please elaborate on how to include CSRF in request body ?
when calling the
update profile
endpoint, csrf_token should be included in request bodyI'm a beginner. Could you please elaborate on how to include CSRF in request body ?
https://stackoverflow.com/questions/5100539/django-csrf-check-failing-with-an-ajax-post-request this may help.
when calling the
update profile
endpoint, csrf_token should be included in request bodyI'm a beginner. Could you please elaborate on how to include CSRF in request body ?
https://stackoverflow.com/questions/5100539/django-csrf-check-failing-with-an-ajax-post-request this may help.
Okay.. But how do we pass csrf_token value to api consuming client (Eg: Mobile app) ?
when calling the
update profile
endpoint, csrf_token should be included in request bodyI'm a beginner. Could you please elaborate on how to include CSRF in request body ?
https://stackoverflow.com/questions/5100539/django-csrf-check-failing-with-an-ajax-post-request this may help.
Okay.. But how do we pass csrf_token value to api consuming client (Eg: Mobile app) ?
Correct me if I'm wrong but it should be in the HttpResponse (header?) automatically if you are using django authentication.
when calling the
update profile
endpoint, csrf_token should be included in request body
I've verified, via a custom Middleware, that both the CSRF header is being (X-CSRFToken) and the cookie are being passed to the server. When those aren't present, I get a different error.
To add on, I've figured out a workaround for this issue. It seems like the value contained in the X-CSRFToken header is incorrect when the request is coming from the Swagger docs. I wrote a middleware to take the unencrypted cookie value and replace the X-CSRFToken header with that value instead. That allows the request to go through and pass the CSRF check.
class CsrfInterceptMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
request.META['HTTP_X_CSRFTOKEN'] = request.COOKIES.get('csrftoken', request.META['HTTP_X_CSRFTOKEN'])
response = self.get_response(request)
return response
did anyone get this to work? I tried your solution @benjamin-lawson but no success for me. I get a KeyError
:
backend_web-1 | Traceback (most recent call last):
backend_web-1 | File "/usr/local/lib/python3.12/site-packages/django/core/handlers/exception.py", line 55, in inner
backend_web-1 | response = get_response(request)
backend_web-1 | ^^^^^^^^^^^^^^^^^^^^^
backend_web-1 | File "/usr/src/app/core/middleware.py", line 12, in __call__
backend_web-1 | "csrftoken", request.META["HTTP_X_CSRFTOKEN"]
backend_web-1 | ~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^
backend_web-1 | KeyError: 'HTTP_X_CSRFTOKEN'
Thoughts?
This is example code that worksï¼› django-ninja use https://github.com/django/django/blob/main/django/middleware/csrf.py#L349 check csrf
import requests
"""
python manage.py migrate
python manage.py shell -c "from django.contrib.auth import get_user_model; get_user_model().objects.create_superuser('[email protected]', 'adminadmin', 'adminadmin')"
"""
def main():
base_url = 'http://127.0.0.1:8000'
session = requests.Session()
resp = session.post(base_url + '/login/', json={'email': '[email protected]', 'password': 'adminadmin'})
print(resp.status_code)
print(resp.text)
print(resp.cookies)
csrf_token = resp.cookies['csrftoken']
print(csrf_token)
resp = session.post(base_url + '/profile/', json={'first_name': 'John', 'last_name': 'Doe'}, headers={"X-CSRFTOKEN": csrf_token})
print(resp.status_code)
print(resp.text)
if __name__ == '__main__':
main()
Yeah for me the CSRF works fine when making requests from my frontend. I'm specifically talking about the Swagger docs, that isn't working with the following setup:
api = NinjaAPI(....stuff, csrf=True)
api.add_router(
"/v1",
base_router,
auth=django_auth,
)
# Some route
@router.post(
"/",
summary="Ping pong!",
auth=None,
)
@ensure_csrf_cookie
def ping(request):
return {"ping": "pong!"}
That will fail when making a request from Swagger. I want to protect my route with CSRF but not don't require a user to be logged in. @yzongyue
ensure_csrf_cookie
may not required or just put it before @router.get
, my test code:
@ensure_csrf_cookie
@api.get(
"/pingpong",
summary="Ping pong!",
auth=None,
)
def ping(request):
return {"ping": "pong!"}
@api.post(
"/postpingpong",
summary="Ping pong!",
auth=None,
# auth=django_auth
)
def postping(request, code: int):
return {"ping": "pong!"}
and ensure django.middleware.csrf.CsrfViewMiddleware
exist in settings.MIDDLEWARE @rafrasenberg
Thanks @yzongyue
The .get
works indeed. However .post
gets me CSRF verification failed. Request aborted.
from Swagger.
This is how I solved this problem.
https://django-ninja.dev/reference/csrf/
How to protect against CSRF with Django Ninja
Use an authentication method not automatically embedded in the request CSRF attacks rely on authentication methods that are automatically included in requests started from another site, like cookies or Basic access authentication. Using an authentication method that does not automatically gets embedded, such as the Authorization: Bearer
header for exemple, mitigates this attack.
from django.contrib.auth import authenticate, login
from django.views.decorators.csrf import csrf_exempt
from typing import List
from ninja import NinjaAPI, Schema, Form, Redoc
from ninja.security import HttpBearer
from .schemas import *
class AuthBearer(HttpBearer):
def authenticate(self, request, token):
if token == "1kh@g%5#01_8d89*2bheymz#vxt+3c#z9-xbs_a7swg%*1":
return token
return None
class MultipleAuth(HttpBearer):
def authenticate(self, request, token):
if token == "1kh@g%5#01_8d89*2bheymz#vxt+3c#z9-xbs_a7swg%*1" and request.user.is_authenticated:
return request.user
return None
api = NinjaAPI( csrf = False, auth = MultipleAuth() )
@api.post("/login", auth = AuthBearer(), response = { 200: LoginSchema, 401: MessageSchema })
@csrf_exempt
def user_login( request, username: Form[str], password: Form[str] ):
user = authenticate( request, username = username, password = password )
if user is None:
return 401, {"message": "Password is incorrect. If you have forgot, please reset your password."}
elif not user.is_active:
return 401, {"message": "Your account status is inactive."}
login(request, user)
return user
@api.post("/create-post")
def create_post(request, payload: SinglePostSchema):
group_post = GroupPost.objects.create(**payload.dict())
return { "id": group_post.id }
This is how I solved this problem.
https://django-ninja.dev/reference/csrf/
How to protect against CSRF with Django Ninja
Use an authentication method not automatically embedded in the request CSRF attacks rely on authentication methods that are automatically included in requests started from another site, like cookies or Basic access authentication. Using an authentication method that does not automatically gets embedded, such as the
Authorization: Bearer
header for exemple, mitigates this attack.from django.contrib.auth import authenticate, login from django.views.decorators.csrf import csrf_exempt from typing import List from ninja import NinjaAPI, Schema, Form, Redoc from ninja.security import HttpBearer from .schemas import * class AuthBearer(HttpBearer): def authenticate(self, request, token): if token == "1kh@g%5#01_8d89*2bheymz#vxt+3c#z9-xbs_a7swg%*1": return token return None class MultipleAuth(HttpBearer): def authenticate(self, request, token): if token == "1kh@g%5#01_8d89*2bheymz#vxt+3c#z9-xbs_a7swg%*1" and request.user.is_authenticated: return request.user return None
api = NinjaAPI( csrf = False, auth = MultipleAuth() )
@api.post("/login", auth = AuthBearer(), response = { 200: LoginSchema, 401: MessageSchema }) @csrf_exempt def user_login( request, username: Form[str], password: Form[str] ): user = authenticate( request, username = username, password = password ) if user is None: return 401, {"message": "Password is incorrect. If you have forgot, please reset your password."} elif not user.is_active: return 401, {"message": "Your account status is inactive."} login(request, user) return user
@api.post("/create-post") def create_post(request, payload: SinglePostSchema): group_post = GroupPost.objects.create(**payload.dict()) return { "id": group_post.id }
This is how we solved the issue as well, switch to JWT or another type of auth that doesn't rely on CSRF checks. But that isn't really a solution to the problem, but a work around.
This issue is because APIKeyCookie
which SessionAuth
inherits from for django_auth
defaults to csrf = True
even if the NinjaAPI has CSRF off.
I fixed it with a custom django_auth
class CustomSessionAuth(SessionAuth):
def __init__(self, csrf=True):
super().__init__(csrf)
self.csrf = False
django_auth = CustomSessionAuth()
I guess when using APIKeyCookie
, CSRF On is best for security? However, for session auth that is not always the case because session auth has several mechanisms. Maybe there should be a setting to tweak CSRF on or off for SessionAuth?