Django (DRF) & React - Forbidden (CSRF cookie not set) - django

There are tens of questions that are essentially identical to the one I'm asking. However, none of their answers seem to be working for me.
I have a React front-end where I am using axios to send requests to the back-end. Example
const request = await axios.post('${BASE_URL}/logout/')
Most of the Django Rest Framework endpoints are made with ViewSets. However, I have a few that are custom and mostly made for authentication.
path('createaccount/', views.create_account),
path('me/', views.current_user),
path('logout/', views.logout),
path('login/', views.login),
path('resetpassword', views.reset_password),
For the development of this project I've included #csrf_exempt above these views because I didn't want to deal with it at the time. Now I'm nearing deployment and it's time to figure it out.
Some answers say I need to get a CSRF Token from Django which is stored in cookies and I need to pass that in the header of each request. Some answers say all I need to do is configure axios like
axios.defaults.xsrfHeaderName = "X-CSRFTOKEN";
axios.defaults.xsrfCookieName = "XCSRF-TOKEN";
And it will "just work". I've tried adjusting my CSRF_COOKIE_NAME to various values to get this to work too.
Some answers even say to keep #csrf_exempt but that sounds like a very, very bad idea.
Do I actually need to generate/get a CSRF cookie? Do I include it with every request? Or is it just a configuration of axios?

To make CSRF protection work you will need CSRF cookie sent from Django to
React as a response to some request (like login or sth else). It will set cookie using
Set-Cookie on frontend side. So make sure that you have a view that does that on Django side. If not, create a view that as response generates that token.
How Django (4.04) CSRF validation work (simplified, based on middleware/csrf.py):
gets CSRF token from cookie (so frontend needs to resend it back on
another request) - it might also get it from session but in case of
React I would not use it
def _get_token(self, request):
....
try:
cookie_token = request.COOKIES[settings.CSRF_COOKIE_NAME]
except KeyError:
return None
Compares that cookie CSRF token with non-cookie token from request:
def _check_token(self, request):
# Access csrf_token via self._get_token() as rotate_token() may have
# been called by an authentication middleware during the
# process_request() phase.
try:
csrf_token = self._get_token(request)
except InvalidTokenFormat as exc:
raise RejectRequest(f"CSRF cookie {exc.reason}.")
if csrf_token is None:
# No CSRF cookie. For POST requests, we insist on a CSRF cookie,
# and in this way we can avoid all CSRF attacks, including login
# CSRF.
raise RejectRequest(REASON_NO_CSRF_COOKIE)
# Check non-cookie token for match.
request_csrf_token = ""
if request.method == "POST":
try:
request_csrf_token = request.POST.get("csrfmiddlewaretoken", "")
except UnreadablePostError:
# Handle a broken connection before we've completed reading the
# POST data. process_view shouldn't raise any exceptions, so
# we'll ignore and serve the user a 403 (assuming they're still
# listening, which they probably aren't because of the error).
pass
if request_csrf_token == "":
# Fall back to X-CSRFToken, to make things easier for AJAX, and
# possible for PUT/DELETE.
try:
request_csrf_token = request.META[settings.CSRF_HEADER_NAME]
except KeyError:
raise RejectRequest(REASON_CSRF_TOKEN_MISSING)
token_source = settings.CSRF_HEADER_NAME
else:
token_source = "POST"
try:
request_csrf_token = _sanitize_token(request_csrf_token)
except InvalidTokenFormat as exc:
reason = self._bad_token_message(exc.reason, token_source)
raise RejectRequest(reason)
if not _does_token_match(request_csrf_token, csrf_token):
reason = self._bad_token_message("incorrect", token_source)
raise RejectRequest(reason)
As you can see you either need to include csrfmiddlewaretoken in POST request or include it in header with key: settings.CSRF_HEADER_NAME and value read from cookies on front-end side.
So for example you set withCredentials: true (to include initial cookie), read that initial CSRF cookie in React and add to header in axios request at specific key.
When in question, I would just debug request setting up breakpoints in this code of Django in middleware/csrf.py and you can trace what is missing and why CSRF validation fails.

I've got this problem once, I was using token authentication. That's how I solved it. But not sure If it is the best idea. I only used csrf_exempt for this view and all others views are viewsets.
#csrf_exempt
def get_current_user(request, *args, **kwargs):
if request.method == 'GET':
user = request.user
serializer = UserDataSerializer(user)
return JsonResponse(serializer.data, safe=False)
My middleware in settings.py
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'corsheaders.middleware.CorsMiddleware',
# 'django.contrib.auth.middleware.SessionAuthenticationMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.middleware.locale.LocaleMiddleware',
'oauth2_provider.middleware.OAuth2TokenMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
'auditlog.middleware.AuditlogMiddleware',
]

Related

Django Rest Framework APIView not CSFR Exempt

Making a POST requests to register a new user through postman returns 403 Forbidden, CSRF verification failed. Request aborted... Going by DRF documentation and knox auth documentation i have everything set up correctly. It appears that Django's SessionAuthentication is being activated even though i do not have it in my DEFAULT_AUTHENTICATION_CLASSES. I have tried every potential solution i could find but nothing is working. The app is a Django rest api with React front end. Any help would be greatly appreciated.
Authentication and Permission settings
'DEFAULT_AUTHENTICATION_CLASSES': (
'knox.auth.TokenAuthentication',
),
'DEFAULT_PERMISSION_CLASSES': (
'rest_framework.permissions.AllowAny',
),
url calling the view as_view
re_path('auth/register', Register.as_view(), name='register'),
Class based Register view extending APIView which should handle csrf
class Register(APIView):
"""User Registration API View"""
def post(self, request, *args, **kwargs):
if request.method == 'POST':
serializer = RegistrationSerializer(data=request.data)
data = {}
if serializer.is_valid():
user = serializer.save()
data['response'] = 'Account registered successfully'
data['firstName'] = user.first_name
data['lastName'] = user.last_name
data['email'] = user.email
data['token'] = AuthToken.objects.get(user=user).key
return Response(data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
Stack trace error
Forbidden (CSRF cookie not set.): /api/account/auth/register
[20/Jun/2020 12:15:14] "POST /api/account/auth/register HTTP/1.1" 403 2864
Update
I have found the issue and have added it as an answer below
For anyone else who may find themselves in this situation I hope this can be of some help. It appears that because I am integrating React with Django and using Reacts router the Django urls must be placed before the React urls in the base urls.py file otherwise React's router takes the request and runs it through its router, cannot find any matching urls and throws an error thus it never gets run through the Django api endpoints. This explains why i was getting CSRF errors as Django's SessionAuthentication was being hit through React router. After all the testing the answer was as simple as swapping two lines.
Previous Throwing Errors
urlpatterns = [
# Admin portal
re_path('admin/', admin.site.urls),
# React UI Entry
re_path('', include('frontend.urls'), name='frontend'),
# Rest API Urls
re_path('api/account/', include('account.api.urls'), name='account_api'),
]
Refactored No Errors
urlpatterns = [
# Admin portal
re_path('admin/', admin.site.urls),
# Rest API Urls
re_path('api/account/', include('account.api.urls'), name='account_api'),
# React UI Entry
re_path('', include('frontend.urls'), name='frontend'),
]

Enable Django View CSFR protection for anonymous Python clients using API requests (rather than templates)

Some Views in my Django 11.1 application get requested by both GUI Templates (user must login for access), and by anonymous python client apps executed as a CLI program (no login).
I needed to enable CSFR protection for all Views, but this protection requires a session id to work (without as session, CSFR attack wouldn't work anyway).
I came up with the solutions shown bellow, but would like to know if there is a better way of doing it.
Solution for GUI templates: This is rather simple and well documented in Django docs. Just include {% csrf_token %} between POST tags.
Solution for Python clients (anonymous): This solution is not clearly documented in the docs (or perhaps it is, but I didn't find an easy step by step instruction). So this is what I did:
In globals.py:
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
Create a Django View that returns a CSFR token and a session ID for anonymous clients:
from django.middleware.csrf import get_token
...
csfr_token = get_token(request)
...
if not request.session.session_key:
request.session.create()
....
return JsonResponse({'token': csfr_token, 'sessionid' : request.session.session_key})
The anonymous client, first requests the token and session ID with a GET:
...
myrequest = urllib.request.Request(url_to_the_django_view, method='GET')
myresponse = urllib.request.urlopen(myrequest )
myresponse _read = eval(myresponse.read())
return myresponse _read['token'], myresponse _read['sessionid']
Lastly, send a post request with CSFR information in the POST form and in HTTP header:
...
req.add_header('X-CSRFToken', token)
req.add_header('Cookie', 'csrftoken=' + token + ';' + 'sessionid=' + sessionid)
...
urllib.request.urlopen(req)
...

CSRF is only checked when authenticated in DRF?

TLDR; It seems that my POSTs (to DRF endpoints) are only CSRF protected, if the client has an authenticated session. This is wrong, and leaves the application option to login CSRF attacks. How can I fix this?
I'm starting to build a django rest framework API for a ReactJS frontend, and we want everything, including the authentication, to be handled via API. We are using SessionAuthentication.
If I have an authenticated session, then CSRF works entirely as expected (when auth'd the client should have a CSRF cookie set, and this needs to be paired with the csrfmiddlewaretoken in the POST data).
However, when not authenticated, no POSTs seem to be subject to CSRF checks. Including the (basic) login APIView that has been created. This leaves the site vulnerable to login CSRF exploits.
Does anyone know how to enforce CSRF checks even on unathenticated sessions? and/or how DRF seems to bypass CSRF checks for login?
Below is my rough setup ...
settings.py:
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': [
'rest_framework.authentication.SessionAuthentication',
],
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.IsAuthenticated',
],
}
views.py:
class Login(APIView):
permission_classes = (permissions.AllowAny,)
#method_decorator(csrf_protect) # shouldn't be needed
def post(self, request, format=None):
user = authenticate(
request,
username=request.POST['username'],
password=request.POST['password']
)
# ... perform login logic ...
def get(self, request, format=None):
"""
Client must GET the login to obtain CSRF token
"""
# Force generation of CSRF token so that it's set in the client
get_token(request)
return Response(None)
urls.py:
urlpatterns = [
url(r'^login/$', views.Login.as_view(), name='login'),
]
expected behaviour:
login_url = reverse('login')
login_details = {
'username': self.user.email,
'password': self.password,
}
client = APIClient(enforce_csrf_checks=True)
# Try to just POST to a CSRF protected view with no CSRF
response = client.post(reverse('login'), login_details)
# response status should be 403 Missing or incorrect CSRF
# GET the login API first to obtain CSRF
client.get(reverse('login'))
login_details['csrfmiddlewaretoken'] = client.cookies.get('csrftoken').value
# Now POST to the login API with the CSRF cookie and CSRF token in the POST data
response = client.post(reverse('login'), login_details)
# response status should now be 200 (and a newly rotated CSRF token delivered)
actual behaviour:
client = APIClient(enforce_csrf_checks=True)
# Try to just to a CSRF protected view with no CSRF
response = client.post(reverse('login'), login_details)
# BROKEN: response status is 200, client is now logged in
# Post to the exact same view again, still with no CSRF
response = client.post(reverse('login'), login_details)
# response status is now 403
# BROKEN: This prooves that this view is protected against CSRF, but ONLY for authenticated sessions.
Django REST Framework is disabling CSRF token requirement when using SessionAuthentication and user is not authenticated. This is by design to not mess up other authentication method that don't require CSRF authentication (because they're not based on cookies) and you should ensure by yourself that CSRF is validated on login request and it is mentioned in last paragraph of SessionAuthentication documentation. It is advised to either use non-API login process or ensure that API-based login process is fully protected.
You can check how DRFs SessionAuthentication is enforcing CSRF validation when you are logged in and base your view on that.
You can create a child class of APIView that forces CSRF.
from rest_framework import views
class ForceCRSFAPIView(views.APIView):
#classmethod
def as_view(cls, **initkwargs):
# Force enables CSRF protection. This is needed for unauthenticated API endpoints
# because DjangoRestFramework relies on SessionAuthentication for CSRF validation
view = super().as_view(**initkwargs)
view.csrf_exempt = False
return view
Then all you need to do is change your login view to descend from this
class Login(ForceCRSFAPIView)
# ...

Django DRF CSRF token missing - DRF deleted request.POST data?

I am using Django Rest's browsable API to POST using Session Authentication, and am getting CSRF token missing, even though it was supplied. I am seeking advice on configuring my ModelViewSet subclass so that this works.
Here's my viewset:
class TreeAPI(ModelViewSet):
authentication_classes = (SessionAuthentication,)
queryset = Tree.objects.get_roots()
parser_classes = (JSONParser, FormParser, MultiPartParser)
permission_classes = (IsAdminUser,)
throttle_classes = (TreeThrottle,)
serializer_class = TreeSerializer
I am able to use the DRF Browsable API to GET this endpoint, but when I use it to POST to this endpoint, I get a 403 with the message CSRF token missing or incorrect.
When I set a breakpoint in the constructor to rest_framework.request.Request, I can see that the request passed in contains the needed csrfmiddleware token:
In Django Rest's Request class, POST is actually a property:
#property
def POST(self):
if not _hasattr(self, '_data'):
self._load_data_and_files()
if is_form_media_type(self.content_type):
# self.data is an empty QueryDict!
return self.data
return QueryDict('', encoding=self._request._encoding)
request.POST no longer contains the csrfmiddlewaretoken key; it is stripped of all keys supplied with the form:
As a result, the parameter passed to rest_framework.authentication.SessionAuthentication.enforce_csrf(request) which is then passed to django.middleware.csrf.CsrfViewMiddleware.process_view does not find the csrfmiddlewaretoken token:
if request.method == "POST":
request_csrf_token = request.POST.get('csrfmiddlewaretoken', '')
What can I check? What are the possible sources of error here?
Considerations
not interested in disabling CSRF
not interested in using token authentication
am familiar with how to use CSRF tokens and the Django docs on them
am familiar with Django REST's docs on CSRF tokens
this is the built-in Django REST browsable API; haven't modified anything in UI
EDIT 1 - Middleware
MIDDLEWARE_CLASSES = (
'django.middleware.common.CommonMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
)
EDIT 2 - versions of software
- djangorestframework==3.3.3
- Django==1.9.8
EDIT 3 - possibly related issues at the git project
'request.data' empty when multipart form data POST in 3.3.x
3814
Request.data empty when multipart/form-data POSTed
3951
EDIT 4 - possibly related stack overflow posts
- Having a POST'able API and Django's CSRF Middleware
- How to make a POST simple JSON using Django REST Framework? CSRF token missing or incorrect
- How to make a Django-Rest-Framework API that takes POST data?
- Django Rest Framework, ajax POST works but PATCH throws CSRF Failed: CSRF token missing or incorrect
- http://www.django-rest-framework.org/api-guide/parsers/#formparser
This problem does not manifest in djangorestframework==3.5.4. See http://www.django-rest-framework.org/topics/release-notes/; I have a feeling this was fixed after 3.3.x.

passing CSRF credentials as url parameters?

How do you handle csrf credentials sent to django as url parameters?
I ask because that is, evidently, the only way to submit a file upload via a form in an iFrame.
Most online examples show to pass csrf credentials as headers,
xhr.setRequestHeader("X-CSRFToken", csrfToken );
but this is not an option for iFrame transport in ie/opera.
I can use csrf_exempt, but this leaves my site vulnerable.
You could create some middleware that takes csrf_token from the GET params and places it on the request before CsrfViewMiddleware attempts to validate
class CsrfGetParamMiddleware(object):
def process_request(self, request):
request.META['HTTP_X_CSRFTOKEN'] = request.GET.get('csrf_token')
return None
Place this middleware above the CsrfViewMiddleware
MIDDLEWARE_CLASSES = (
'CsrfGetParamMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
)
This save you from validating it yourself or subclassing CsrfViewMiddleware