Django rest api Change oauth response error format - django

I have successfully implemented oauth2 for my django rest api project. I want to change the error response format of the login api.
Current error response is
{
"error_description": "Invalid credentials given.",
"error": "invalid_grant"
}
I want to change the error_description key to detail (detail is the key of all other django error responses). I need to make a standardize all the error responses.
Expected result is
{
"detail": "Invalid credentials given."
}
This is executing from class OAuth2Error(Exception) in /lib/python2.7/site-packages/oauthlib/oauth2/rfc6749/errors.py file.

I would suggest intercepting this type of response and adapt it as you wish.
There are several ways, but the easiest choice would be to define your own view which is called for the authorization url, which internally calls oauth2 view and modifies response for this case, e.g. something like
from:
from oauth2_provider.views import TokenView
...
url('auth/token/', TokenView.as_view()),
to:
from oauth2_provider.views import TokenView
def custom_token_view(request, *args, **kwargs):
response = TokenView.as_view()(request, *args, **kwargs)
if "invalid_grant " in response.content:
response = do_whatever_needed(response) # i.e. response.content
return response
...
url('auth/token/', custom_token_view),
More flexible/general solution alternative
If you use Django rest framework (DRF) I would suggest:
setting up custom DRF JSON renderer - define your own,
your custom renderer should inherit default renderer (rest_framework.renderers.JSONRenderer)
in renderer intercept all responses - by calling default render function and detect this particular one (error=invalid_grant) and customize it
If you don't use DRF:
create custom middleware
if django < 1.10 then check implementing process_response, if django >=1.10 then call method
again, intercept all responses and detect only this one, which you can modify as you need

Related

Can I return a different Django Response based on whether or not the view is invoked from swagger?

I am using Django-Rest-Framework for my API. And I am documenting that API w/ swagger using drf_yasg. One of my views returns a Django FileResponse.
When I access the view directly at "http://localhost:8000/api/my_documents/1" it successfully displays the (PDF) file. But when I access it via swagger it successfully returns a 200 but gives the following message:
Unrecognized response type; displaying content as text.
This is b/c of this issue in swagger itself. As suggested in that ticket, the problem goes away if I change the "Content-Disposition" response header from "inline" to "attachment". However, I don't want to always download the file.
My question is: Can I determine whether the request was made by swagger in the view and conditionally change the headers? Something like:
class MyDocumentView(GenericAPIVIew):
def get(self, request, pk):
my_document = MyDocument.objects.get(pk=pk)
response = FileResponse(my_document.file) # (file is a FileField)
# WHAT DO I PUT HERE ?!?
if request.is_from_swagger:
response.headers["Content-Disposition"] = response.headers["Content-Disposition"].replace("inline", "attachment")
return response
Thanks.
You can do this by checking the request.headers.get('referer'). There should be your swagger url in there.

csrf_exempt set but CSRF Failed: Referer checking failed - no Referer

I have a backend API, it's in django and deployed on Google Endpoint.
I have a post request that insert data to my DB.
I created a script to use this endpoint but I got this error:
{"detail":"CSRF Failed: Referer checking failed - no Referer."}
Regarding over posts I added the crsf_exempt decorator to my class but it did not change.
I try to add the decorator two ways:
class AddUser(APIView):
""" Create user and company from csv """
#method_decorator(csrf_exempt)
def post(self, request):
#method_decorator(csrf_exempt, name='dispatch')
class AddUser(APIView):
""" Create user and company from csv """
def post(self, request):
But both failed.
This is how I contact my endpoint:
resp = requests.request(
method, url,
headers={'Authorization': 'Bearer {}'.format(
open_id_connect_token)}, **kwargs)
Any ideas ?
Thanks
EDIT
So I tried to add authentication classes to my views but it appears to be a bad idea. This is being real trouble for me.
I tried to get the csrftoken doing like this:
client = requests.session()
# Retrieve the CSRF token first
client.get(url) # sets cookie
print(client.cookies)
if 'csrftoken' in client.cookies:
# Django 1.6 and up
csrftoken = client.cookies['csrftoken']
else:
# older versions
csrftoken = client.cookies
Thing is, I am using IAP to protect my API and I do not have any csrftoken cookie but I do have a something looking like this:
<RequestsCookieJar[<Cookie GCP_IAP_XSRF_NONCE_Q0sNuY-M83380ypJogZscg=1
for ...
How can I use this to make post request to my API ?
So this happened to me because I did not set any authentication_classes to my generic view.
When this option is not set Django automatically use the SessionBackend, which need the csrf token.
I fixed it by adding this to my view: authentication_classes = [ModelBackend, GoogleOAuth2]
#Kimor - Can you try doing this in your urls.py
from django.views.decorators.csrf import csrf_exempt
url('^test/$', csrf_exempt(views.TestView.as_view())),
The get and post methods defined on the APIView class just tell DRF how the actual view should behave, but the view method that the Django router expects is not actually instantiated until you call TestView.as_view().
source
Django REST Framework CSRF Failed: CSRF cookie not set
So after working on this project for a while this is what I learned regarding the CSRF using Django.
First of all, if you are using django templates, or in any cases where your back-end and front-end are running behind the same server the most common practice is to use session for authentication.
This is activated by default by DRF.
This means that in your DRF configuration if you do not explicitly set the DEFAULT_AUTHENTICATION_CLASSES option default authentication will be set to Session + BasicAuth.
In this configuration you'll need to manage the CSRF token as described in the documentation (https://docs.djangoproject.com/en/4.0/ref/csrf/).
If your back-end and front-end are separated as in my case, using CSRF is not the only solution or even the recommended one.
As in my case I use JWT behind IAP (Identity Aware Proxy, provided by google). I had to write my own authentication classes and then use it in my DRF conf:
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': [
'main.authentication_backend.custom_auth.IAPAuthentication'],
...
}
Here is explain how to write your own authentication class: https://www.django-rest-framework.org/api-guide/authentication/#custom-authentication.

What is the correctly way to pass an orientdb #rid as a parameter, to DELETE request in django rest framework?

I'm creating a delete method in a DRF API, by passing parameters, but I don't know how to pass correctly an orientdb #rid.
I have a relationship in orientdb called "worksat", in OrientDB Studio i can see the #rid with the structure name like #:, i.e: "#33:1" is the #rid of a worksat relationship record.
So I need to pass that string in my DRF URL api relationship:
http://127.0.0.1:8000/api/oworksat/
But passing like:
http://127.0.0.1:8000/api/oworksat/#33:1
I see GET request, with the message below (I expect to see DELETE):
Allow: GET, POST, HEAD, OPTIONS
If a pass a simple number:
http://127.0.0.1:8000/api/oworksat/1
Then I see DELETE request (obviously "1" doesn't exist):
HTTP 404 Not Found
Allow: GET, PUT, PATCH, DELETE, HEAD, OPTIONS
api.py:
class OWorksAtViewSet(viewsets.ModelViewSet):
queryset = graph.oworksat.query()
serializer_class = OWorksAtSerializer
permission_classes = [
permissions.AllowAny
]
def destroy(self, request, *args, **kwargs):
print ("destroy")
import pdb;pdb.set_trace()
urls.py:
from django.conf.urls import include, url
from rest_framework import routers
from .api import (OWorksAtViewSet)
from rest_framework_swagger.views import get_swagger_view
router = routers.DefaultRouter()
router.register('api/oworksat', OWorksAtViewSet, 'oworksat')
schema_view = get_swagger_view(title='Swagger Documentation')
urlpatterns = [
url(r'^swagger/$', schema_view)
]
urlpatterns += router.urls
The interesting thing is that by accesing from swagger api, in the DELETE method, if I a pass in the ID of the request "#33:1", it works, the api call to my destroy method and recieve in kwargs: kwargs = {'pk': '#33:1'}.
How can I reach that behavior from DRF api?
Edited:
This is my temporal solution to implement my destroy method, but obviously this only works in Swagger UI, by passing #rid in the request.
from rest_framework import status
from rest_framework.response import Response
from core.pyorient_client import *
class OFriendsViewSet(viewsets.ModelViewSet):
def destroy(self, request, *args, **kwargs):
client = orientdbConnection()
client.command("delete edge ofriends where #rid = '" + kwargs['pk'] + "'")
return Response(status=status.HTTP_204_NO_CONTENT)
I assume that when speaking about not being able to delete or see "Delete" button, you talk about Django Rest Framework browsable API.
When you access your API through DRF browsable API you get list of objects when you navigate to for example http://127.0.0.1:8000/api/oworksat/. This is the "list endpoint" and it doesn't support DELETE.
Delete button will be there when you access "detail endpoint" of single object, for example: http://127.0.0.1:8000/api/oworksat/123.
In your case however when you try to pass OrientDB #RID as object ID, browser thinks you want to get to list endpoint. This is because in urls everything after # is called fragment and this is not passed to server. So when you navigate to http://127.0.0.1:8000/api/oworksat/#1:23 browser actually requests page from http://127.0.0.1:8000/api/oworksat/ and thus gives you the list endpoint without delete button.
Why Swagger then works?
Swagger works probably because the request is not made the same way as browsers normally load pages. Swagger makes Ajax request to your API when you click the Delete button and thus the fragment part is not stripped from your url. Swagger also probably url encodes the value you type into UI and this would transform #1:22 to %231%3A22 and thus remove # which causes our problem. DRF then knows how to url decode the url automatically and ends up with correct looking ID.
Example of request Swagger probably does:
function deleteData(url, item) {
return fetch(url + '/' + item, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json'
}
})
.then(response => console.log(response.json()));
}
deleteData("http://127.0.0.1:8000/api/oworksat/", encodeURIComponent("#1:23"));
How to fix this?
Your API probably works correctly when #RID is url encoded and Ajax request is made. It just doesn't work with DRF browsable API.
However to make it nicer to work with for your users and also to make DRF Browsable API to work I would remove # from the ID's when they are serialized through your API. Users would then make requests to urls like http://127.0.0.1:8000/api/oworksat/1:23. Of course by doing it like this you would then need to prepend the client provided id with # before passing it to OrientDB query.

DRF - extend obtain auth token

I have Django Rest Framework with token auth. I have a following url url(r'^api/auth/', views.obtain_auth_token), which returns me token.
What I need to do is perform some db logic, when user performs authorization which is getting the token. I need to query db and do some stuff there.
It seems to me that I have somehow to override default behaviour and add some custom logic to obtain_auth_token.
How can I do that ?
ObtainAuthToken from Rest Framework gets or creates a token for an specific user and then sends it in the Response, all of these behaviour is done in the post method.
The documentation says:
If you need a customized version of the obtain_auth_token view, you can do so by overriding the ObtainAuthToken view class, and using that in your url conf instead.
So you can override the post method, or even create your own APIView to create the Token and add the behaviour you want. In order to do that, change your url:
url(r'^api/auth/', views.custom_obtain_token)
And in views.py:
class CustomObtainToken(APIView):
...
def post(self, request):
<your logic>
<get token n your own way or using DRF way>
return Response({'token': token})
custom_obtain_token = CustomObtainToken.as_view()

Django rest framework call a viewset programatically passing authentication headers from another view

I am trying to call an api made with Django Rest Framework drf. My case is that I want to call the api from another view get the response and display the data in a template. I have referred to this SO post, but could not make a successful call since my viewset requires authentication. Viewset works perfect if called using urls, the default way. Viewset is as below
class ViewSet(viewsets.ModelViewSet):
"""
API endpoint that allows Objects to be viewed or edited.
"""
permission_classes = (permissions.IsAuthenticated,)
queryset = Myclass.objects.all()
serializer_class = MyclassSerializer
....
....
return response(json)
On calling this api from another view the response that I get is a 401 page from drf api.
ipdb>view = Myview.as_view({'get':'list'}
ipdb>obj=view(request, *args, **kwargs)
ipdb> obj.data
{u'detail': u'Authentication credentials were not provided.'}
ipdb>obj.has_header('Authentication')
False
I tried passing Authentication headers also, but I dont know whether its the right way.
view = MyViewSet.as_view({'get':'list'},{'token':'token'})
This returned an error argument token is not accepted. And I tried
view = MyViewSet.as_view({'get':'list'},Authentication={'token':'token'})
But this Also returned me errors. How is it possible to call an api viewset from another view by passing auth params?
TIA