raise exception in django oauth toolkit - django

I'm using Django 2.x and django-oauth-toolkit to generate access token.
I have written a custom token view to run a few checks on the account and then generate access token to the user. If custom check fails, I want to raise an exception with 400 status code.
class CustomTokenView(TokenView):
def create_token_response(self, request):
login = request.POST.pop('username', None)
username = get_user_model().objects.filter(
email=login[0]
).last()
if not username.verified:
raise HttpResponse(content='User not verified', status=status.HTTP_400_BAD_REQUEST)
request.POST._mutable = mutable
return super(TokenView, self).create_token_response(request)
But this gives error as
TypeError: exceptions must derive from BaseException
I also tried with
from rest_framework.response import Response
return Response('User not verified', status=status.HTTP_400_BAD_REQUEST)
But none is working.

You cannot raise a response. Response is not an exception. Instead you can either return it or raise an actual exception from django-rest-framework (all available exceptions described here, select one that suits best your case. In my opinion it should be your custom one, created from APIException).

Related

Return values of custom authentication in Django REST Framework

I'm trying to write my basic custom authentication for Django REST Framework. I have the following auth backend:
class JoeAuth(authentication.BaseAuthentication):
def authenticate(self, request):
username = request.META.get('HTTP_X_FORWARDED_USER')
if not username:
return
try:
user = User.objects.get(krb_name=username, active=True).name
except User.DoesNotExist:
raise PermissionDenied('Unauthorized user')
return (user, None)
Accompanied with a view:
#api_view()
def hello(request):
return Response(data='hello')
And of course enabled in settings.py:
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': (
'helloworld.auth.JoeAuth',
)
}
Now, if a request comes in and does not specify the HTTP_X_FORWARDED_USER header, the authenticate() function returns None. According so DRF docs:
To implement a custom authentication scheme, subclass BaseAuthentication and override the .authenticate(self, request) method. The method should return a two-tuple of (user, auth) if authentication succeeds, or None otherwise.
In some circumstances instead of returning None, you may want to raise an AuthenticationFailed exception from the .authenticate() method.
A None means authentication failed and should ideally return 401 or 403. However, in practice this doesn't seem to be the case. A request without the HTTP_X_FORWARDED_USER is simply allowed and 200 is returned:
$ http http://127.0.0.1:8000/ HTTP_X_FORWARDED_USER:joe
HTTP/1.1 200 OK
"hello"
$ http http://127.0.0.1:8000/
HTTP/1.1 200 OK
"hello"
Am I misunderstanding the documentation in the sense that a None is considered a successful authentication attempt?
The problem is that you are confusing authentication and authorization(permissions in Django). What the authentication does is identifyy the user, however it does not in any way, restrict the user - that is the work of authorization(permission). The permission classes do the work of checking the rights a user has over a particular resource.
From what I can see, it seems you have a default global AllowAny permission set, which allows anyone access. You need to set the permission to restrict the endpoint to only authenticated users.
In your case, you need to add a permission_classes to the view or use global perm issions in DRF settings.
You can add permission classes to a function-based API view this way:
from rest_framework.permissions import IsAuthenticated
from rest_framework.decorators import permission_classes, api_view
#api_view()
#permission_classes([IsAuthenticated])
def hello(request):
return Response(data='hello')

Over-ride DRF custom exception response

I want to send error_codes while sending the response back to the client, in case of an error!!
So, I've a form where params a and b is required. If any of the param is not POSTed, then DRF serializer sends back a response saying This field is required. I want to add the error code to the response as well for the client to identify. Different errors different error codes.
So, I wrote my own custom exception handler. It goes like this.
response = exception_handler(exc, context)
if response is not None:
error = {
'error_code': error_code, # Need to identify the error code, based on the type of fail response.
'errors': response.data
}
return Response(error, status=http_code)
return response
The problem I'm facing is that I need to identify the type of exception received, so that I can send the error_code accordingly. How can I achieve this?
REST framework's views handle various exceptions, and deal with returning appropriate error responses.
The handled exceptions are:
APIException raised inside REST framework.
Django's Http404
Django's PermissionDenied exception.
You can identify the type of exception received by status
from rest_framework import exceptions, status
response = exception_handler(exc, context)
if response is not None:
if response.status == status.HTTP_404_NOT_FOUND:
# Django's Http404
elif response.status == status.HTTP_403_FORBIDDEN:
# PermissionDenied exception
else:
# APIException raised
Also most error responses will include a key detail in the body of the response.
You can check it and set custom error codes.
response = exception_handler(exc, context)
if response is not None:
# identify the type of exception received, detail can be text, list or dictionary of items.
if response.data.detail == 'some text':
error_code = 111
elif more_conditions:
...
Reference: http://www.django-rest-framework.org/api-guide/exceptions/

Custom Authorization class in Django Tastypie

I used the following Custom Authorization class
class CustomDjangoAuthorization(DjangoAuthorization):
def read_detail(self, object_list, bundle):
result = super(CustomDjangoAuthorization, self).read_detail(object_list, bundle)
# now we check here for specific permission
if bundle.request.user.profile.user_status:
raise Unauthorized("You are not allowed to access that resource.")
return result
It gives
401 Unauthorized
when the user_status = 1. But when I change the user_status to 0, it still shows
401 Unauthorized
error.
My unsends authorizationderstanding was that for each request, tastypie checks Authorization and gives a 200 response for Ok and 401 for Unauthorized. Am I missing something here?
Hey Sean, I tried moving custom code before super. I get a
AttributeError: ‘AnonymousUser’ object has no attribute ‘profile’
Everything is working in localhost, production is giving a problem.
This happens in both cases, when user_status = 1 & when user_status = 0
Using Django 1.8 and Tastypie 0.13.3.
#Sean Hayes is right, my user was logged out. I am using a custom url and didn't know that it didn't take my APIAuthentication by default.
I had to add self.is_authenticated(request) in my custom method for it to work.
Move your custom code before the call to super(), and add a check to see if the user is anonymous:
class CustomDjangoAuthorization(DjangoAuthorization):
def read_detail(self, object_list, bundle):
# check here for specific permission
if (not bundle.request.user.is_authenticated()) or bundle.request.user.profile.user_status:
raise Unauthorized("You are not allowed to access that resource.")
result = super(CustomDjangoAuthorization, self).read_detail(object_list, bundle)
return result
You were getting AttributeError: ‘AnonymousUser’ object has no attribute ‘profile’ because your user was logged out, so request.user was an AnonymousUser, therefore no profile.
Based on Your code and symptoms I guess You have bad authorization details. Check Your username and api_key. Probably You have typo or You use a local authorization details on production.

django-rest-auth: social login with google

The django-rest-auth documentation discusses Facebook integration, which I am not interested in-- my concern is providing social login via Google. I have tried this for quite some time now and I'm wondering if anyone else has any documentation on how they did this...even just a rough sketch would be helpful. So far, I have not found any results for this search. I am almost there, but cannot get it to work with the Django rest framework (DRF) browsable API.
Here is what I have so far:
I started from the demo project supplied on the django-rest-auth github page and modified the social login template HTML page to only require the 'code' input, not both 'code' AND 'access_token'. When I supply a valid code (obtained by a separate request to google's auth endpoint), this works fine; the browsable API renders the usual web page with the 'key' (my application's API token for the user) in the response. Checking the django admin, everything worked- the user is logged in, email is authenticated, etc. Good so far.
The issue is that starting point of supplying the 'code'- and how I get that code from google in the first place. When I previously (successfully) used the allauth package, I could simply click on a link, which would "invisibly" perform the whole OAuth2 flow (i.e. request the code, use that code to get the access token, and use the access token to get user's google account info).
To recreate that seamless flow (i.e. NOT starting out with the code), I figured I could interrupt the OAuth2 flow and "intercept" the code returned from google, and then POST that code to the rest-auth social login API. To that end, I created a custom allauth.socialaccount.providers.oauth2.views.OAuth2CallbackView by overriding the dispatch method:
class CustomOAuth2CallbackView(OAuth2CallbackView):
def dispatch(self, request):
# gets the code correctly:
code = request.GET['code']
# rp is of type requests.methods.Response
rp = requests.post(<REST-AUTH API ENDPOINT>, data = {'code':code})
return rp
Usually, this method is called when google sends a GET request to the callback uri you initially supply to google's auth endpoint. With this override, I am able to successfully parse the code returned from google in that callback. The POST request works and has the user's key in the resp._content field. However, it ultimately fails to produce the intended view in the DRF browsable API.
Based on diving down in the callstack, I find that rest_framework.views.APIView.dispatch returns an object of type rest_framework.response.Response. However, when the requests.post method used above completes, it returns an instance of type requests.models.Response. As a result, it does not have the proper attributes and fails in the django middleware. For example, it has no acceptable_renderer attribute and no 'get' method (which is used in django.middleware.clickjacking.py). I could, conceivably, add these requirements to the requests.models.Response (rp) instance, but then this hack becomes even more of a kludge.
Thanks for any help you can provide!
https://github.com/st4lk/django-rest-social-auth
class SocialLoginSignup(SocialSessionAuthView):
"""
Mobile user social signup and login api view
args:
provider: name of the social network
access_token: auth token got from the social sites
"""
serializer_class = SocialSignUpSerializer
authentication_classes = (TokenAuthentication,)
def post(self, request, *args, **kwargs):
serializer = self.serializer_class(data=request.data)
serializer.is_valid(raise_exception=True)
provider_name = serializer.validated_data['provider']
decorate_request(request, provider_name) # assign the provider class object in request
authed_user = request.user if not request.user.is_anonymous() else None
token = serializer.validated_data['access_token']
if self.oauth_v1() and request.backend.OAUTH_TOKEN_PARAMETER_NAME not in serializer.validated_data:
request_token = parse_qs(request.backend.set_unauthorized_token())
return Response(request_token)
try:
# authentication function get call from assign class object in request
user = request.backend.do_auth(token, user=authed_user)
except social_exceptions.AuthException as e:
raise exceptions.ParseError({'error':str(e)})
except social_exceptions.AuthTokenError as e:
raise exceptions.ParseError({'error': str(e)})
except social_exceptions.AuthAlreadyAssociated as e:
raise exceptions.ParseError({'error': str(e)})
except social_exceptions.AuthFailed as e:
raise exceptions.ParseError({'error':str(e)})
except social_exceptions.AuthUnknownError as e:
raise exceptions.ParseError({'error': str(e)})
except social_exceptions.WrongBackend as e:
raise exceptions.ParseError({'error':str(e)})
except Exception as e:
raise exceptions.ParseError({'error': social_message.INVALID_AUTH_TOKEN})
token, created = Token.objects.get_or_create(user=user)
return Response({'auth_token':token.key})

Raise Validation Error In Pre_Save Using Django Rest Framework

I am having difficulty raising a validation error using the Django Rest Framework.
I have an owner field. The owner field needs to access the request object. The documentation suggests using the following approach:
def pre_save(self, obj):
obj.owner = self.request.user
My problem is how to raise a validation error if this code fails. I tried raising an error inside the pre_save method, but this doesn't work properly. It actually results in an HTML response coming back from Django, rather than a message from the Django Rest Framework.
Use the django rest framework exceptions. For example:
from rest_framework.exceptions import ParseError
...
parsed_data = self.parse(some_data)
if not parsed_data:
raise ParseError('Some error occurred')
Also note that you won't see a 404 in there, that's because it uses the django.http.Http404 exception. These are returned by the API in a nice way.
Note:
If you are doing a significant amount of validation you might want to look at placing your logic in the serializer.