Angular 5 Django CSRF token - django

I am trying to do a simple login page with the Django Rest Framework and I keep getting a csrf token error. To get around this for now, I have appended the #csrf_exempt annotation on my login method which works but is unsecure.
This is my method:
#csrf_exempt
def login(request):
print(request.COOKIES)
username = request.POST.get('username')
password = request.POST.get('password')
print("username {} password {}".format(username, password))
user = authenticate(request, username=username, password=password)
group = None
if user is not None:
django_login(request, user)
request.session.set_expiry(0)
result = True
status = 200
else:
result = False
status = 401
data = {'result': result, 'username': username}
return HttpResponse(json.dumps(data), content_type="application/json", status=status)
My Rest Framework Settings:
REST_FRAMEWORK = {
'DATETIME_FORMAT': "%m/%d/%Y %H:%M:%S",
'DEFAULT_AUTHENTICATION_CLASSES': (
'rest_framework_jwt.authentication.JSONWebTokenAuthentication',
),
'DEFAULT_FILTER_BACKENDS': (
'rest_framework.filters.SearchFilter',
'django_filters.rest_framework.DjangoFilterBackend',
),
'EXCEPTION_HANDLER': 'common.custom_exception_handler.custom_exception_handler'
}
Without the csrf_exempt annotation, I get
Forbidden (CSRF token missing or incorrect.): /authentication/login
however, when I print the cookies I actually get a token in my cookie.
{'csrftoken': 'HZc8vPqoad...7eIvTzep', 'sessionid': 'n71c....g5c7'}
gets printed when I add the #csrf_exempt annotation back in.
In my angular code, I have also tried to attach the csrf token as a request header with 'X-CSRFToken' but I noticed two things
1) in my request, the X-CSRFToken i obtain from document.cookies is NOT the same as the token above. There are two different CSRF tokens - why?
If you notice, the X-CSRFToken header and the token in the cookie differ. And I receive the same CSRF token missing or incorrect.
2) Even if I remove the use of the JWT Authentication, It has no effect.
I have also tried to use the new XSRF Strategy replacement with the new Cookie strategy in my app.module like this:
{
provide: XSRFStrategy,
useValue: new CookieXSRFStrategy('csrftoken', 'X-CSRFToken')
}
But to no avail - I get the same issue described in point (1).
However, when i add the #csrf_exempt annotation back in and check the cookie in the request, The cookie in the image shows up!!
So my main question is: why is the DRF not able to read the token even though the csrf cookie is part of the request?

Related

Django reset password url in email only works if copy and paste it, not click on it

When I request a password reset with the built in django machinery, it will generate a URL in the email like this:
https://example/accounts/reset/MTA/atspc3-7c45df8a600243fde3dfb60c44873f15/
In gmail if I go show URL, the text is indentical since it is being generated as a plain/text email. If I click the link in gmail, or outlook then it goes to (maybe after a redirect, I can't tell since its so fast):
https://example.com/accounts/reset/MTA/set-password/
And it says
Password reset unsuccessful
The password reset link was invalid, possibly because it has already been used. Please request a new password reset.
However if I copy and paste the URL into the browser, then it works. However no-one copies and paste the URL, they all click the link and say it does not work.
I don't get why clicking on the link does not work since it is correct?
I managed to add logging to Django's code, I added logging to:
contrib.auth.views -> PasswordResetConfirmView.dispatch (line 247)
As you can see from the following two logs, in both scenarios they are being called with exactly the same locals (I print locals()), however this view seems to set the token in the session, then redirect. however after the redirect, if one cicked the link in the email, the session cookie is gone, and hence it fails. I still don't understand why
COPY and paste URL (Working)
------------------------------------------------------------
# First call to dispatch:
token: atspc3-7c45df8a600243fde3dfb60c44873f15
Entered dispatch {'self': <django.contrib.auth.views.PasswordResetConfirmView object at 0x7f20e63c2220>, 'args': (<WSGIRequest: GET '/accounts/reset/MTA/atspc3-7c45df8a600243fde3dfb60c44873f15/'>,), 'kwargs': {'uidb64': 'MTA', 'token': 'atspc3-7c45df8a600243fde3dfb60c44873f15'}, 'token': 'atspc3-7c45df8a600243fde3dfb60c44873f15', '__class__': <class 'django.contrib.auth.views.PasswordResetConfirmView'>}
user is not None
token is NOT reset
token atspc3-7c45df8a600243fde3dfb60c44873f15
Verfiy sessions token atszyc-e7c578496e3438f9dee367fcbebfabb1
redirect url /accounts/reset/MTA/set-password/
Djnago logging start
------------------------------------------------------------
# Second call to dispatch (after redirect):
token: set-password
Entered dispatch {'self': <django.contrib.auth.views.PasswordResetConfirmView object at 0x7f20e63c2220>, 'args': (<WSGIRequest: GET '/accounts/reset/MTA/set-password/'>,), 'kwargs': {'uidb64': 'MTA', 'token': 'set-password'}, 'token': 'set-password', '__class__': <class 'django.contrib.auth.views.PasswordResetConfirmView'>}
user is not None
token is self reset, session token: atspc3-7c45df8a600243fde3dfb60c44873f15
CLICK link in email (Not working)
------------------------------------------------------------
# First call to dispatch:
token: atspc3-7c45df8a600243fde3dfb60c44873f15
Entered dispatch {'self': <django.contrib.auth.views.PasswordResetConfirmView object at 0x7f20d9a77190>, 'args': (<WSGIRequest: GET '/accounts/reset/MTA/atspc3-7c45df8a600243fde3dfb60c44873f15/'>,), 'kwargs': {'uidb64': 'MTA', 'token': 'atspc3-7c45df8a600243fde3dfb60c44873f15'}, 'token': 'atspc3-7c45df8a600243fde3dfb60c44873f15', '__class__': <class 'django.contrib.auth.views.PasswordResetConfirmView'>}
user is not None
token is NOT reset
token atspc3-7c45df8a600243fde3dfb60c44873f15
Verfiy sessions token atszyc-e7c578496e3438f9dee367fcbebfabb1
redirect url /accounts/reset/MTA/set-password/
------------------------------------------------------------
# Second call to dispatch (after redirect):
token: set-password
Entered dispatch {'self': <django.contrib.auth.views.PasswordResetConfirmView object at 0x7f20d9adde20>, 'args': (<WSGIRequest: GET '/accounts/reset/MTA/set-password/'>,), 'kwargs': {'uidb64': 'MTA', 'token': 'set-password'}, 'token': 'set-password', '__class__': <class 'django.contrib.auth.views.PasswordResetConfirmView'>}
user is not None
token is self reset, session token: None
Here are the log statements I added to contrib.auth.views -> PasswordResetConfirmView.dispatch (line 247) :
#method_decorator(never_cache)
def dispatch(self, *args, **kwargs):
log_info('------------------------------------------------------------')
token = kwargs['token']
log_info('token:', token)
log_info('Entered dispatch', str(locals()))
assert 'uidb64' in kwargs and 'token' in kwargs
self.validlink = False
self.user = self.get_user(kwargs['uidb64'])
if self.user is not None:
token = kwargs['token']
log_info('user is not None')
if token == self.reset_url_token:
session_token = self.request.session.get(INTERNAL_RESET_SESSION_TOKEN)
log_info('token is self reset, session token:', session_token)
if self.token_generator.check_token(self.user, session_token):
# If the token is valid, display the password reset form.
self.validlink = True
return super().dispatch(*args, **kwargs)
else:
log_info('token is NOT reset')
if self.token_generator.check_token(self.user, token):
# Store the token in the session and redirect to the
# password reset form at a URL without the token. That
# avoids the possibility of leaking the token in the
# HTTP Referer header.
self.request.session[INTERNAL_RESET_SESSION_TOKEN] = token
log_info('token', token)
log_info('Verfiy sessions token', self.request.session[INTERNAL_RESET_SESSION_TOKEN])
redirect_url = self.request.path.replace(token, self.reset_url_token)
log_info('redirect url', redirect_url)
return HttpResponseRedirect(redirect_url)
else:
log_info('failed token generator check')
Note, in both cases the request has the following attributes (I thought it might be a secure cookie issue):
request.scheme: https
request.is_secure: True
request.full path /accounts/reset/MTA/set-password/
or
request.full path /accounts/reset/MTA/atspc3-7c45df8a600243fde3dfb60c44873f15/
In the rare case someone else has this issue, I had in my settings file:
SESSION_COOKIE_SAMESITE = 'Strict'
One must change the cookie to persist from an outside domain (the email client) as follows:
SESSION_COOKIE_SAMESITE = 'Lax' # default value
Refer to https://docs.djangoproject.com/en/stable/ref/settings/#session-cookie-samesite

How to get and save token using Django Web service

I'm not sure I'm right on track. Please give me a hint or direction.
I set up my Web service using Django and also made mobile app with React Native using Django REST framwork. Django uses the basic session authentication, but Django REST API uses token authentication to process the request from mobile app.
I want to implement small ReactJS app into my existing Django web. At this stage, I think my small react app will need auth token to communicate with REST api for itself.
So, my idea is that when user logs in web login page, user's API token needs to be received from API and save into cookie or localStorage while normal log in process is processing in Django Web service. Because I don't want to let users log in again to run react app on my web page to get auth token.
Am I right on track? if so, how can I make it works? Please refer to my code in Django login view.py Do i need to some code in order to get API auth token and save it into client side?
def Login(request):
if not request.user.is_authenticated:
if request.method == "POST":
email = request.POST['email']
password = request.POST['password']
user = authenticate(email = email, password = password)
if user is not None:
login(request, user)
messages.add_message(request, messages.SUCCESS, request.user.nickname + ' Welcome!')
return redirect('Search')
else:
messages.add_message(request, messages.WARNING, 'Please check Email / Password again')
return redirect('login')
else:
form = LoginForm()
return render(request, 'login.html', {'form': form })
else:
return redirect('main')
You have done some useless in your login function. you can use jwt. it has some good function for supporting login. In its login function, when send username and password with post, it return token to client.
http://getblimp.github.io/django-rest-framework-jwt/
You just need set urlpattern
from rest_framework_jwt.views import obtain_jwt_token
#...
urlpatterns = [
'',
# ...
url(r'^api-token-auth/', obtain_jwt_token),
]
It return token
$ curl -X POST -d "username=admin&password=password123" http://localhost:8000/api-token-auth/
In other request, if you need authentication, use following request
$ curl -H "Authorization: JWT <your_token>" http://localhost:8000/protected-url/
They both carrying out similar tasks with few differences.
Token
DRF's builtin Token Authentication
One Token for all sessions
No time stamp on the token
DRF JWT Token Authentication
One Token per session
Expiry timestamp on each token
Database access
DRF's builtin Token Authentication
Database access to fetch the user associated with the token
Verify user's status
Authenticate the user
DRF JWT Token Authentication
Decode token (get payload)
Verify token timestamp (expiry)
Database access to fetch user associated with the id in the payload
Verify user's status
Authenticate the user
Pros
DRF's builtin Token Authentication
Allows forced-logout by replacing the token in the database (ex: password change)
DRF JWT Token Authentication
Token with an expiration time
No database hit unless the token is valid
Cons
DRF's builtin Token Authentication
Database hit on all requests
Single token for all sessions
DRF JWT Token Authentication
Unable to recall the token without tracking it in the database
Once the token is issued, anyone with the token can make requests
Specs are open to interpretations, no consensus on how to do refresh
Reference: Django : DRF Token based Authentication VS JSON Web Token
I'd like to leave my answer after I solved in my way through my long research and study. My solution is quite simple.1. set DRF session authentication enable. Adding some code in setting.py
REST_FRAMEWORK = {
# ...
'DEFAULT_AUTHENTICATION_CLASSES': (
'rest_framework.authentication.TokenAuthentication',
'rest_framework.authentication.SessionAuthentication',
),
}
2. add 'credentials: "include"' into fetch code to use already logged in session cookie for authentication.
await fetch(API_URL, {
credentials: "include"
})
this solution solved my case.

Unable to create an integration test for Django's reset password flow

I'm trying to implement an integration test for the password reset flow, but I'm stuck at the "password_reset_confirm" view. I already tested the flow manually, and it works fine. Unfortunately, the Django unit test client seems unable to follow correctly the redirects required in this view.
urls config
from django.contrib.auth import views as auth_views
url(r"^accounts/password_change/$",
auth_views.PasswordChangeView.as_view(),
name="password_change"),
url(r"^accounts/password_change/done/$",
auth_views.PasswordChangeDoneView.as_view(),
name="password_change_done"),
url(r"^accounts/password_reset/$",
auth_views.PasswordResetView.as_view(email_template_name="app/email/accounts/password_reset_email.html",
success_url=reverse_lazy("app:password_reset_done"),
subject_template_name="app/email/accounts/password_reset_subject.html"),
name="password_reset"),
url(r"^accounts/password_reset/done/$",
auth_views.PasswordResetDoneView.as_view(),
name="password_reset_done"),
url(r"^accounts/reset/(?P<uidb64>[0-9A-Za-z_\-]+)/(?P<token>[0-9A-Za-z]{1,13}-[0-9A-Za-z]{1,20})/$",
auth_views.PasswordResetConfirmView.as_view(
success_url=reverse_lazy("app:password_reset_complete"),
form_class=CustomSetPasswordForm),
name="password_reset_confirm"),
url(r"^accounts/reset/complete/$",
auth_views.PasswordResetCompleteView.as_view(),
name="password_reset_complete"),
Test code
import re
from django.urls import reverse, NoReverseMatch
from django.test import TestCase, Client
from django.core import mail
from django.test.utils import override_settings
from django.contrib.auth import authenticate
VALID_USER_NAME = "username"
USER_OLD_PSW = "oldpassword"
USER_NEW_PSW = "newpassword"
PASSWORD_RESET_URL = reverse("app:password_reset")
def PASSWORD_RESET_CONFIRM_URL(uidb64, token):
try:
return reverse("app:password_reset_confirm", args=(uidb64, token))
except NoReverseMatch:
return f"/accounts/reset/invaliduidb64/invalid-token/"
def utils_extract_reset_tokens(full_url):
return re.findall(r"/([\w\-]+)",
re.search(r"^http\://.+$", full_url, flags=re.MULTILINE)[0])[3:5]
#override_settings(EMAIL_BACKEND="anymail.backends.test.EmailBackend")
class PasswordResetTestCase(TestCase):
#classmethod
def setUpClass(cls):
super().setUpClass()
cls.myclient = Client()
def test_password_reset_ok(self):
# ask for password reset
response = self.myclient.post(PASSWORD_RESET_URL,
{"email": VALID_USER_NAME},
follow=True)
# extract reset token from email
self.assertEqual(len(mail.outbox), 1)
msg = mail.outbox[0]
uidb64, token = utils_extract_reset_tokens(msg.body)
# change the password
response = self.myclient.post(PASSWORD_RESET_CONFIRM_URL(uidb64, token),
{"new_password1": USER_NEW_PSW,
"new_password2": USER_NEW_PSW},
follow=True)
self.assertIsNone(authenticate(username=VALID_USER_NAME,password=USER_OLD_PSW))
Now, the assert fails: the user is authenticated with the old password. From the log I'm able to detect that the change password is not executed.
A few extra useful information:
post returns a successful HTTP 200;
the response.redirect_chain is [('/accounts/reset/token_removed/set-password/', 302)] and I think this is wrong, as it should have another loop (in the manual case I see another call to the dispatch method);
I'm executing the test with the Django unit test tools.
Any idea on how to properly test this scenario? I need this to make sure emails and logging are properly executed (and never removed).
Many thanks!
EDIT: solution
As well explained by the accepted solution, here the working code for the test case:
def test_password_reset_ok(self):
# ask for password reset
response = self.myclient.post(PASSWORD_RESET_URL,
{"email": VALID_USER_NAME},
follow=True)
# extract reset token from email
self.assertEqual(len(mail.outbox), 1)
msg = mail.outbox[0]
uidb64, token = utils_extract_reset_tokens(msg.body)
# change the password
self.myclient.get(PASSWORD_RESET_CONFIRM_URL(uidb64, token), follow=True)
response = self.myclient.post(PASSWORD_RESET_CONFIRM_URL(uidb64, "set-password"),
{"new_password1": USER_NEW_PSW,
"new_password2": USER_NEW_PSW},
follow=True)
self.assertIsNone(authenticate(username=VALID_USER_NAME,password=USER_OLD_PSW))
This is very interesting; so it looks like Django has implemented a security feature in the password reset page to prevent the token from being leaked in the HTTP Referrer header. Read more about Referrer Header Leaks here.
TL;DR
Django is basically taking the sensitive token from the URL and placing it in Session and performing a internal redirect (same domain) to prevent you from clicking away to a different site and leaking the token via the Referer header.
Here's how:
When you hit /accounts/reset/uidb64/token/ (you should be doing a GET here, however you are doing a POST in your test case) the first time, Django pulls the token from the URL and sets it in session and redirects you to /accounts/reset/uidb64/set-password/.
This now loads the /accounts/reset/uidb64/set-password/ page, where you can set the passwords and perform a POST
When you POST from this page, the same View handles your POST request since the token URL param can handle both the token and the string set-password.
This time though, the view sees that you have accessed it with set-password and not a token, so it expects to pull your actual token from session, and then change the password.
Here's the flow as a chart:
GET /reset/uidb64/token/ --> Set token in session --> 302 Redirect to /reset/uidb64/set-token/ --> POST Password --> Get token from Session --> Token Valid? --> Reset password
Here's the code!
INTERNAL_RESET_URL_TOKEN = 'set-password'
INTERNAL_RESET_SESSION_TOKEN = '_password_reset_token'
#method_decorator(sensitive_post_parameters())
#method_decorator(never_cache)
def dispatch(self, *args, **kwargs):
assert 'uidb64' in kwargs and 'token' in kwargs
self.validlink = False
self.user = self.get_user(kwargs['uidb64'])
if self.user is not None:
token = kwargs['token']
if token == INTERNAL_RESET_URL_TOKEN:
session_token = self.request.session.get(INTERNAL_RESET_SESSION_TOKEN)
if self.token_generator.check_token(self.user, session_token):
# If the token is valid, display the password reset form.
self.validlink = True
return super().dispatch(*args, **kwargs)
else:
if self.token_generator.check_token(self.user, token):
# Store the token in the session and redirect to the
# password reset form at a URL without the token. That
# avoids the possibility of leaking the token in the
# HTTP Referer header.
self.request.session[INTERNAL_RESET_SESSION_TOKEN] = token
redirect_url = self.request.path.replace(token, INTERNAL_RESET_URL_TOKEN)
return HttpResponseRedirect(redirect_url)
# Display the "Password reset unsuccessful" page.
return self.render_to_response(self.get_context_data())
Notice the comment in the code where this magic happens:
Store the token in the session and redirect to the
password reset form at a URL without the token. That
avoids the possibility of leaking the token in the
HTTP Referer header.
I think this makes it clear how you can fix your unit test; do a GET on the PASSWORD_RESET_URL which will give you the redirect URL, you can then POST to this redirect_url and perform password resets!

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-rest-framework returning 403 response on POST, PUT, DELETE despite AllowAny permissions

I'm using a django-oneall to allow social login session authentication on my site. While it isn't one of the suggested auth providers for django-rest-framework, rest_framework.authentication.SessionAuthentication uses django's default session authentication. so I thought it should be fairly simple to integrate.
On the permissions side, ultimately I'll use IsAdmin, but for development purposes, I just had it set to IsAuthenticated. When that returning 403s, I relaxed the permissions to AllowAny, but still no dice. Here's my rest framework config:
settings.py
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': (
'rest_framework.authentication.SessionAuthentication',
),
'DEFAULT_PERMISSION_CLASSES': (
'rest_framework.permissions.AllowAny',
# 'rest_framework.permissions.IsAuthenticated',
# 'rest_framework.permissions.IsAdminUser',
),
'PAGE_SIZE': 100,
'DEFAULT_FILTER_BACKENDS': (
'rest_framework.filters.DjangoFilterBackend',
),
}
EDIT:
I got this working based on the answer below. It turns out that rest_framework expects both the csrftoken cookie and a a X-CSRFToken Header of the same value, I setup my front-end code to send that header for all ajax requests and everything worked fine.
Django REST Framework returns status code 403 under a couple of relevant circumstances:
When you don't have the required permission level (e.g. making an API request as an unauthenticated user when DEFAULT_PERMISSION_CLASSES is ('rest_framework.permissions.IsAuthenticated',).
When you doing an unsafe request type (POST, PUT, PATCH or DELETE - a request that should have side effects), you are using rest_framework.authentication.SessionAuthentication and you've not included your CSRFToken in the requeset.
When you are doing an unsafe request type and the CSRFToken you've included is no longer valid.
I'm going to make a few demo requests against a test API to give an example of each to help you diagnose which issue you are having and show how to resolve it. I'll be using the requests library.
The test API
I set up a very simple DRF API with a single model, Life, that contains a single field (answer, with a default value of 42). Everything from here on out is pretty straight forward; I set up a ModelSerializer - LifeSerializer, a ModelViewSet - LifeViewSet, and a DefaultRouter on the /life URL route. I've configured DRF to require user's be authenticated to use the API and to use SessionAuthentication.
Hitting the API
import json
import requests
response = requests.get('http://localhost:8000/life/1/')
# prints (403, '{"detail":"Authentication credentials were not provided."}')
print response.status_code, response.content
my_session_id = 'mph3eugf0gh5hyzc8glvrt79r2sd6xu6'
cookies = {}
cookies['sessionid'] = my_session_id
response = requests.get('http://localhost:8000/life/1/',
cookies=cookies)
# prints (200, '{"id":1,"answer":42}')
print response.status_code, response.content
data = json.dumps({'answer': 24})
headers = {'content-type': 'application/json'}
response = requests.put('http://localhost:8000/life/1/',
data=data, headers=headers,
cookies=cookies)
# prints (403, '{"detail":"CSRF Failed: CSRF cookie not set."}')
print response.status_code, response.content
# Let's grab a valid csrftoken
html_response = requests.get('http://localhost:8000/life/1/',
headers={'accept': 'text/html'},
cookies=cookies)
cookies['csrftoken'] = html_response.cookies['csrftoken']
response = requests.put('http://localhost:8000/life/1/',
data=data, headers=headers,
cookies=cookies)
# prints (403, '{"detail":"CSRF Failed: CSRF token missing or incorrect."}')
print response.status_code, response.content
headers['X-CSRFToken'] = cookies['csrftoken']
response = requests.put('http://localhost:8000/life/1/',
data=data, headers=headers,
cookies=cookies)
# prints (200, '{"id":1,"answer":24}')
print response.status_code, response.content
Just for anyone that might find the same problem.
If you are using viewsets without routers like:
user_list = UserViewSet.as_view({'get': 'list'})
user_detail = UserViewSet.as_view({'get': 'retrieve'})
Django Rest framework will return 403 unless you define permission_classes at a class level:
class UserViewSet(viewsets.ModelViewSet):
"""
A viewset for viewing and editing user instances.
"""
permission_classes= YourPermisionClass
Hope it helps!
For completeness sake, there is one more circumstance under which DRF returns code 403: if you forget to add as_view() to the view declaration in your urls.py file. Just happened to me, and I spent hours until I found where the issue was, so maybe this addition can save some time for someone.
For those that aren't able to even access their csrftoken from Javascript:
In my case I wasn't able to get the csrftoken from my Javascript code to be able to set it in my ajax POST. It always printed null. I finally discovered that the django CSRF_COOKIE_HTTPONLY environment variable was set to True.
From the Django Documentation
CSRF_COOKIE_HTTPONLY: If this is set to True, client-side JavaScript will not be able to access the CSRF cookie."
Changing CSRF_COOKIE_HTTPONLY to False allowed me to finally get the csrftoken.
https://docs.djangoproject.com/en/4.1/ref/settings/#std-setting-CSRF_COOKIE_HTTPONLY
One more situation that someone may find is that you get a 403 error on an AllowAny route when you pass an token as null in the "Authorization" header in your request. For example, you may want to allow anyone to use the route but also want to know if the person that used the route is an authenticated user.
E.g.
if (token) {
headers = {
"Content-Type": "application/json",
"Authorization": "Token " + token
}
} else {
headers = {
"Content-Type": "application/json"
}
}