Django Rest Framework Authentication Errors - python

I'm making an angular app that authenticates in a django site using a rest api. In the doc of drf they state that a denied user authentication will result in two error, HTTP 401 and HTTP 403, as follows:
When an unauthenticated request is denied permission there are two different error codes that may be appropriate.
HTTP 401 Unauthorized
HTTP 403 Permission Denied
Howenever, when I try to authenticate with some dummy data, that is wrong on purpose, I get error response HTTP 400 and with non_field_errors. What is the issue here?
First I do a simple authentication with the username and the password, this made to get the user token, that will be needed on other operations in the site.
I'm using a method to obtain an expering auth token, you can see it below:
class ObtainExperingAuthToken(ObtainAuthToken):
def post(self, request, *args, **kargs):
serializer = self.serializer_class(data=request.data)
if serializer.is_valid():
print serializer
user = UserProfile.objects.get(email=serializer.data['username'])
token, created = Token.objects.get_or_create(user=user)
utc_now = timezone.now()
if not created and token.created < utc_now - datetime.timedelta(hours=24):
token.delete()
print serializer.data
token = Token.objects.create(user=serializer.data['user'])
token.created = datetime.datetime.utcnow()
token.save()
groups = [group.name for group in user.groups.all()]
response_data = {
'email': user.email,
'token': token.key,
'groups': groups
}
return HttpResponse(json.dumps(response_data), content_type='application/json')
return HttpResponse(serializer.errors, status=400)
#Edit
The value for REST_FRAMEWORK
REST_FRAMEWORK = {
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.DjangoModelPermissionsOrAnonReadOnly',
],
'DEFAULT_AUTHENTICATION_CLASSES': [
'rest_framework.authentication.SessionAuthentication',
'accounts.authentication.ExpiringTokenAuthentication',
],
'DEFAULT_PARSER_CLASSES': [
'rest_framework.parsers.JSONParser',
'rest_framework.parsers.FormParser',
'rest_framework.parsers.MultiPartParser',
'rest_framework.parsers.FileUploadParser'
]
}

Firstly although it's not very clear from your explanation, but I'm guessing you are getting 400 error in the api responsible for providing user access token. Since obviously there would be no authentication on this api (except Client Authentication maybe), so you are genuinely getting bad request error since you are providing it with invalid data as you mentioned yourself.
Authentication error comes when you access an api that doesn't allow unauthenticated access, which shouldn't be the case for the api that actually provides the authentication token.
Secondly to get Authentication error you need to add Authentication Classes to either REST_FRAMEWORK['DEFAULT_AUTHENTICATION_CLASSES'] or to the AUTHENTICATION_CLASSES variable of the view class.

Related

Why do we need to send JWT from client for every API after login, as DRF maintains isAuthenticated Flag after login in settings.py?

I have just started to explore django. I want to know whether JWT should be verified for every view manually in the server side or is there any better way of doing it.
Login View:
class LoginUser(APIView):
permission_classes = ()
def post(self, request, *args, **kwargs):
username = request.data.get('username')
password = request.data.get('password')
user = authenticate(username=username, password=password)
if user:
payload = jwt_payload_handler(user)
return Response({
'response_code':'success',
'response_msg':'Login successfull',
'username':user.username,
'token': jwt.encode(payload, SECRET_KEY)
},status.HTTP_200_OK)
else:
return Response(
{'response_code':'error',
'response_msg':'Invalid credentials'},status.HTTP_400_BAD_REQUEST
)
Menu View (to fetch menu)
def getMenu(request):
menu_list = serializers.serialize("json", Menu.objects.all())
return HttpResponse(menu_list)
settings.py
REST_FRAMEWORK = {
'DEFAULT_PERMISSION_CLASSES': (
'rest_framework.permissions.IsAuthenticated',
),
'DEFAULT_AUTHENTICATION_CLASSES': (
'rest_framework_jwt.authentication.JSONWebTokenAuthentication',
),
}
My Login View returns JWT token in response. Login view works fine, but post login if I want to fetch Food Menu without sending JWT(got in response of login view), i am able to get the menu list, but ideally it should fail, saying Token is missing and should not return the response, but i am getting response.
I am curious to know, if DRF is only checking whether user is Authenticated or not and based on that if it returns response, then what is the point of JWT.
Your menu view looks like a normal django view. You have to use a DRF view
since you've only configured authentication and permission for DRF.

DRF responses me by 403 error when I try to request as a client [Client Credential grant]

In settings.py file I have written this settings:
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': (
'oauth2_provider.contrib.rest_framework.OAuth2Authentication',
),
'DEFAULT_PERMISSION_CLASSES': (
'rest_framework.permissions.IsAuthenticated',
),
}
When I call any API with a token from a password grant application, then It's working fine, but when I try to call the same APIs with a token from a client credential grant application, it doesn't work and it responses with 403 error:
{ "detail": "You do not have permission to perform this action." }.
Is it because of default permission? I don't know what permission do I have to use instead!?
Finally solved! The problem was the permission I was using. In fact, the IsAuthenticated permission checks request.user which is None when you are using client credentials grant. Since there's no permission for supporting clien credentials grant in DRF, you must use your own DRF custom permission. This is what I needed and used:
from rest_framework.permissions import BasePermission
class IsAuthenticatedOrClientCredentialPermission(BasePermission):
def has_permission(self, request, view):
if request.auth is None:
return False
grant_type = request.auth.application.get_authorization_grant_type_display()
if request.user is None:
if grant_type == 'Client credentials':
request.user = request.auth.application.user # <-- this is because I needed to get the user either the grant is 'password' or 'client credentials'
return True
else:
return False
else:
return True
But you may want to have a permission just for checking if the grant type is client credentials and give the permission, if so, this is what you need:
from rest_framework.permissions import BasePermission
class ClientCredentialPermission(BasePermission):
def has_permission(self, request, view):
if request.auth is None:
return False
grant_type = request.auth.application.get_authorization_grant_type_display()
if request.user is None and grant_type == 'Client credentials':
return True
else:
return False
Note: if you want to use the second custom permission, be aware that the request.user is None and you can get the owner of the client (that is sending request to you) via request.auth.application.user.
Using (custom) permissions:
You can use your custom permission by adding them to proper views. (Just like using any DRF permissions under rest_framework.permissions)
class-based views:
class ExampleView(APIView):
permission_classes = [ClientCredentialPermission] # <-- Add your permissions to this list
def get(self, request, format=None):
content = {
'status': 'request was permitted'
}
return Response(content)
function-based views:
#api_view(['GET'])
#permission_classes([ClientCredentialPermission]) # <-- Add your permissions to this list
def example_view(request, format=None):
content = {
'status': 'request was permitted'
}
return Response(content)
I had the same problem. The issue in my case was the #authentication_classes that was originally enabled for when I was using credentials directly (not token). I removed them (see them below commented out). Idea came after reading the first answer here.
This works for me as I only want token base access, and so I don't need the other authentication classes. This is how my view decoration looks like:
#api_view(['GET'])
##authentication_classes([SessionAuthentication, BasicAuthentication])
#permission_classes([IsAuthenticated])
def apilink(request, format=None):
.....
you need to enable tokenAuthentication and run migration to apply changes in auth table DB
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': [
'rest_framework.authentication.TokenAuthentication', # <-- And here
],
}
INSTALLED_APPS = [
...
'rest_framework.authtoken'
]
Here is the perfect blog for your usecase.
https://www.django-rest-framework.org/api-guide/authentication/#tokenauthentication
https://simpleisbetterthancomplex.com/tutorial/2018/11/22/how-to-implement-token-authentication-using-django-rest-framework.html#implementing-the-token-authentication

Unauthorized response to POST request in Django Rest Framework with JWT Token

I am building a REST API with Django Rest Framework. I currently have an issue where some of my endpoints return HTTP 401 Unauthorized, whereas the vast majority of my endpoints return correct responses. For authentication I am using JWT tokens with djangorestframework-simplejwt.
I've configured Django to use token auth with djangorestframework-simplejwt.
# rest framework config settings
REST_FRAMEWORK = {
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.IsAuthenticated',
# 'rest_framework.permissions.AllowAny',
],
'DEFAULT_AUTHENTICATION_CLASSES': [
'rest_framework.authentication.SessionAuthentication',
'rest_framework.authentication.TokenAuthentication',
'rest_framework_simplejwt.authentication.JWTAuthentication',
],
The vast majority of my endpoints return valid data when I pass a valid access token in the request. If I do not send a valid token, I receive a HTTP 403.
On the other hand, I have some custom API views which return a HTTP 401 regardless of whether I pass a valid token or not.
I have included the code to one of my problematic views below.
class CheckDifferentialView(generics.GenericAPIView):
permission_classes = [IsAuthenticated]
authentication_classes = [TokenAuthentication]
serializer_class = QuizDifferentialSerializer
def post(self, request, *args, **kwargs):
"""
A function to check a quiz question and to update a user's record of questions answered
"""
print(request.META)
if 'answer' not in request.data:
return JsonResponse({'Error': 'answer not found in request'}, status=status.HTTP_400_BAD_REQUEST)
answer = get_object_or_404(Differential, pk=request.data['answer'])
serializer = QuizDifferentialSerializer(answer)
if answer.is_correct:
pass
# record correct results
else:
pass
# record failed result
return Response(serializer.data, status=status.HTTP_200_OK)
And here is my script which I am using to test my API
import requests
import json
POST_LOGIN_URL = 'http://localhost:8000/api/v1/token/'
POST_URL= 'http://localhost:8000/api/v1/check_differential'
REQUEST_URL = 'http://localhost:8000/api/v1/users'
with requests.Session() as session:
post = session.post(POST_LOGIN_URL, json={"username": "j", "monkey": "aphextwin21"})
token = json.loads(post.text)['access']
headers = {'Authorization': 'Bearer ' + token}
r = session.post(POST_URL, headers=headers, json={"answer": "2"})
# r = session.get(REQUEST_URL, headers=headers)
print(token)
print(r.text, r.status_code)
The desired behaviour is that if I send a POST request with a valid token to this endpoint that is would authorise me and carry on with its day. If no Authorization header with a valid access token is given, then I expect it to deny the request.
Update
Enthusiastic Martin kindly point out that
authentication_classes = [TokenAuthentication]
Was overriding the defaults found in my settings file. I was not aware that as far as Django is concerned TokenAuthentication and JWTAuthentication are treated differently. Now I know.
After removing the authentication_classess = [TokenAuthentication] from my views, the views are working as they should.
The view's authentication class is explicitly set to TokenAuthentication only. It wont work with JWT token.
authentication_classes = [TokenAuthentication]
You either remove this to let the default classes handle it or change it to accept JWTAuthentication.

Django Rest Framework - Using Session and Token Auth

I am trying to get this working but don't know if this is possible. It should be doing it like this.
I developed a web app using Django + Rest-Framework + jQuery, and I want to have an external application to consume the same REST API, using JWT Tokens for authentication.
My current config is like this.
settings.py
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': [
'rest_framework.authentication.SessionAuthentication',
'rest_framework_simplejwt.authentication.JWTAuthentication',
],
'DEFAULT_RENDERER_CLASS': [
'rest_framework.renderers.JSONRenderer',
]
}
SIMPLE_JWT = {
'AUTH_HEADER_TYPES': ('Bearer',),
}
views.py
class ListFileView(APIView):
permission_classes = (IsAuthenticated,)
def get(self, request, *args, **kwargs):
user = request.user
if user:
obj = Foo.objects.filter(owner=user)
serializer = FooSerializer(obj, many=True)
data = serializer.data
return Response(data, status=status.HTTP_200_OK)
return Response({'detail': 'You have no access to files.'}, status=status.HTTP_400_BAD_REQUEST)
The tricky part is that when I use:
permission_classes = (IsAuthenticated,)
I can perform ajax calls from an external application (using a valid JWT token), but jQuery calls from within the app (with an authenticated user) fail with:
{"detail":"Authentication credentials were not provided."}
And on the other hand, if I use autentication_classes instead of permission_classes:
authentication_classes = (SessionAuthentication, BasicAuthentication)
I can perform ajax calls from within the web app using jQuery, but external calls fail with the same 403 error.
I tried using both like this:
class ListFileView(APIView):
authentication_classes = (SessionAuthentication, BasicAuthentication)
permission_classes = (IsAuthenticated,)
def get(self, request, *args, **kwargs):
...
but the external calls are also rejected.
Is it possible to have these two types of Auth working together in the same class view, or should I separate into two endpoints?
EDIT
Example calls from within the app with jQuery:
<script type="text/javascript">
function loadTblDocs() {
$("#tbl-docs > tbody").empty();
$.ajaxSetup({
headers: { "X-CSRFToken": '{{csrf_token}}' }
});
$.ajax({
type: 'GET',
contentType: "application/json; charset=utf-8",
url: "/api/list/",
dataType: "json",
success: function (data) {
console.log(data);
}
});
};
</script>
And externally via VBA code:
Set web = CreateObject("WinHttp.WinHttpRequest.5.1")
web.Open "GET", "/api/list/", False
web.SetRequestHeader "Authorization", "Bearer <JWT_TOKEN_HERE>"
web.Send
I couldn't work out exactly what is going on in your case, because the behavior in the 3 cases you shared does not seem to be consistent, but going to explain simply how authentication and authorization is determined in DRF, this might help you figure the issue out.
You should be able to use two authentication classes with no problems. With DRF, authentication works like this:
At each request, DRF goes over the provided authentication classes, in the order they are defined. For each class, there are 3 cases:
If it can authenticate the request with the current class, DRF sets request.user. From this point on, this request is authenticated.
If no authentication credentials are present, DRF skips that class
If authentication credentials are present but invalid, such as an invalid JWT token in Authorization header, DRF raises an exception and returns a 403 response.
DRF views normally use the DEFAULT_AUTHENTICATION_CLASSES variable defined in the settings file, but if you provide them in a view, settings are overridden.
Authorization comes into play when you add permission_classes to your views. permission_classes control access to your views, so when you add IsAuthenticated to a view's permission classes, DRF rejects the request with a 403 response if you try to access that view without an authenticated user.
So in your initial case, your internal AJAX request failing in this case suggests that there is no authenticated user data in your request session. I do not know what could be causing this. Perhaps you do not update request session in your login view (Django's login method should do this automatically).
In your second case, you remove permission_classes from the view, so the view will serve the request regardless if there is an authenticated user in the request or not. So it is expected that your internal AJAX request succeeds here, but I do not know why your external request fails in this case.
In your third case, from the point of your internal AJAX request, the scenario seems to be the same as the first case, so I do not know why your internal AJAX request succeeds in this case but not in the first case. The external request failing here is expected because you added the IsAuthenticated permission class to the view, but did not include JWTAuthentication in the authentication_classes, so your request can not be authenticated with your JWT token here.

Using python social auth to register users by access token

I found this snippet of code that helps me authenticate a user and then create a rest_framework token for them. The client I am using is a native android app and I will get the access token from the client side and post it to django in the ObtainAuth class.
Here is the code for the server side.
#psa('social:complete')
def register_by_access_token(request, backend):
backend = request.strategy.backend
# Split by spaces and get the array
auth = get_authorization_header(request).split()
if not auth or auth[0].lower() != b'token':
msg = 'No token header provided.'
return msg
if len(auth) == 1:
msg = 'Invalid token header. No credentials provided.'
return msg
access_token = auth[1]
user = backend.do_auth(access_token)
return user
class ObtainAuthToken(APIView):
model = Token
serializer_class = AuthTokenSerializer
parser_classes = (parsers.FormParser, parsers.MultiPartParser, parsers.JSONParser,)
renderer_classes = (renderers.JSONRenderer,)
def post(self,request):
serializer = self.serializer_class(data= request.DATA)
if backend == 'auth':
if serializer.is_valid:
token, created = Token.objects.get_or_create(user=serializer.object['user'])
if token:
return Response({'token': token.key})
else:
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
else:
user = register_by_access_token(request, backend)
if user and user.is_active:
token, created = Token.objects.get_or_create(user=user)
return Response({'id': user.id, 'email': user.email, 'firstname': user.first_name, 'userRole': 'user', 'token': token.key})
The register_by_access_token method will get the facebook access token and then create a user with the rest_framework.It takes a request and the backend to be used e.g 'facebook'.
If a user logs in with my backend then the backend is 'auth' and it uses the normal process of retrieving the email and password and then giving me a token to use.As detailed here
My question is how do I post the authentication backend be it 'facebook' or 'auth' so that I can receive the token?
What I've tried.
I have tried sending the backend type ('facebook' or 'auth') with the access token but I get an error that the method takes 3 arguments and I've only provided 2.
I've tried making the url take a backend like this:
url(r'^login/(?P<backend>[^/]+)/$',views.ObtainAuthToken.as_view())
then sending the access token to a url like this mysite.com:8000/login/facebook.
None of these work and I don't have much expereience with psa or django to know how to pass this parameter.
How do I send which backend to use so that it can be accepted by the method? If anyone has ever had this use case please help me out.
according to my understanding social login requires a access token , so when you are login with facebook when you call 'mysite.com:8000/login/facebook' it is expecting a access token,
for my project i defined my urls as 'url(r'^login/(?P[^/]+)/$',register_by_access_token,name='register_by_access_token')',with the token i am sending it completes the login, for facebook i send backend as 'facebook' for google i send backend as 'google-oauth2' but both the case i am sending a token given my respective backend, when you are not using a third party backend you wont get the token and the login expects that.
so my suggestion is if you are going with auth use normal login post, not the same URL.

Categories