Customized Django SimpleJWT Views - django

I am using a django-simplejwt and I have a custom token view for obtaining token. My problem is, how can i override the 401 Unauthorized response from the view?
https://django-rest-framework-simplejwt.readthedocs.io/en/latest/customizing_token_claims.html
from rest_framework_simplejwt.serializers import TokenObtainPairSerializer
from rest_framework_simplejwt.views import TokenObtainPairView
class ObtainTokenSerializer(TokenObtainPairSerializer):
#classmethod
def get_token(cls, user):
token = super().get_token(user)
# Add custom claims
token['name'] = user.first_name
# ...
return token
class ObtainToken(TokenObtainPairView):
serializer_class = ObtainTokenSerializer

The answer might resides in the Exception raised.
As you dive in the simplejwt package you'd see something as following :
class InvalidToken(AuthenticationFailed):
status_code = status.HTTP_401_UNAUTHORIZED
default_detail = _('Token is invalid or expired')
default_code = 'token_not_valid'
This is where comes from the 401 you see.
Therefore your class ObtainToken should override the post method from the mother class TokenViewBase with you specific Exception or Response.
For example :
from rest_framework import status
...
class ObtainToken(TokenObtainPairView):
serializer_class = ObtainTokenSerializer
...
def post(self, request):
serializer = self.serializer_class(
data=request.data,
context={'request': request}
)
try:
serializer.is_valid()
return Response(
serializer.validated_data,
status=status.HTTP_200_OK
)
except Exception:
return Response(status=status.HTTP_404)
Although above is just an example, you should implement something similar as the post method from TokenViewBase but with your customized status and data.

Related

Django AssertionError at /play/api/5/play/ The `request` argument must be instance of `django.http.HttpRequest`, not `myApp.views.TrackPlayAPIToggle`

I am trying to implement an api version of a play button on a django website.
This is how far I got:
models.py
class Note(models.Model):
plays = models.ManyToManyField(settings.AUTH_USER_MODEL,blank=True,related_name='track_plays')
def get_play_url(self):
return "/play/{}/play".format(self.pk)
def get_api_like_url(self):
return "/play/{}/play-api-toggle".format(self.pk)
views.py
class TrackPlayToggle(RedirectView):
def get_redirect_url(self,*args,**kwargs):
id = self.kwargs.get("id")
obj = get_object_or_404(Note,id=id)
url_ = obj.get_absolute_url()
user = self.request.user
if user.is_authenticated():
if user in obj.plays.all():
obj.plays.add(user)
else:
obj.plays.add(user)
return url_
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import authentication,permissions
from rest_framework.decorators import api_view
class TrackPlayAPIToggle(RedirectView):
authentication_classes = (authentication.SessionAuthentication,)
permission_classes = (permissions.IsAuthenticated,)
#api_view(['GET', 'POST', ])
def get(self,request,format=None):
obj = get_object_or_404(Note,id=id)
url_ = obj.get_absolute_url()
user = self.request.user
updated = False
played = False
if user.is_authenticated():
if user in obj.plays.all():
played = True
obj.plays.add(user)
else:
played = True
obj.plays.add(user)
played = False
updated = True
data = {
"updated":updated,
"played":played
}
return Response(data)
urls.py
url(r'^(?P<id>\d+)/play/', TrackPlayToggle.as_view(), name='play-toggle'),
url(r'^api/(?P<id>\d+)/play/', TrackPlayAPIToggle.as_view(), name='play-api-toggle'),
Ive added the API Decorator, because without it, I get a TypeError:
get() got an unexpected keyword argument 'id'
and when I try to add id=None I get an AssertionError:
.accepted_renderer not set on Response
Is this because I used id instead of slug?
Thank you for any suggestions
I don't understand why you thought adding the #api_view decorator would solve your TypeError. That decorator is for function-based views; it has no use in class-based views, where you define which methods are supported by simply defining the relevant methods. Remove the decorator.
The way to solve the original problem is to add the id parameter to the method; and the way to solve the problem with the renderer is to inherit from the correct parent class, which should clearly not be RedirectView.
class TrackPlayAPIToggle(GenericAPIView):
authentication_classes = (authentication.SessionAuthentication,)
permission_classes = (permissions.IsAuthenticated,)
def get(self, request, id, format=None):
...

Django rest_framework custom error message

I have a API endpoint where it will do input validation using rest_framework's serializer.is_valid() where it will return custom error message and response.
serializer = FormSerializer(data=data)
if not serializer.is_valid(raise_exception=False):
return Response({"Failure": "Error"}, status=status.HTTP_400_BAD_REQUEST)
Is it possible to populate validation errors without using the generic response provided by raise_exception=True? I am trying to avoid using the generic response as it will display all the validation errors if there are more than one error.
The response will be something like
return Response(
{
"Failure": "Error",
"Error_list": {"field1": "This field is required"}
},
status=status.HTTP_400_BAD_REQUEST
)
Create a Custom Exception class as,
from rest_framework.exceptions import PermissionDenied
from rest_framework import status
class MyCustomExcpetion(PermissionDenied):
status_code = status.HTTP_400_BAD_REQUEST
default_detail = "Custom Exception Message"
default_code = 'invalid'
def __init__(self, detail, status_code=None):
self.detail = detail
if status_code is not None:
self.status_code = status_code
Why I'm inherrited from PermissionDenied exception class ??
see this SO post -- Why DRF ValidationError always returns 400
Then in your serializer, raise exceptions as,
class SampleSerializer(serializers.ModelSerializer):
class Meta:
fields = '__all__'
model = SampleModel
def validate_age(self, age): # field level validation
if age > 10:
raise MyCustomExcpetion(detail={"Failure": "error"}, status_code=status.HTTP_400_BAD_REQUEST)
return age
def validate(self, attrs): # object level validation
if some_condition:
raise MyCustomExcpetion(detail={"your": "exception", "some_other": "key"}, status_code=status.HTTP_410_GONE)
return attrs
age and name are two fields of SampleModel class
Response will be like this
By using this method,
1. You can customize the JSON Response
2. You can return any status codes
3. You don't need to pass True in serializer.is_valid() method (This is not reccomended)
A simple way is to use one of the exception messages, eg NotFound. See docs
# views.py
from rest_framework.exceptions import NotFound
class myview(viewsets.ModelViewSet):
def perform_create(self, serializer):
raise NotFound("My text here")
That will return a 404 and change the response to your text
HTTP 404 Not Found
Allow: GET, POST, HEAD, OPTIONS
Content-Type: application/json
Vary: Accept
{
"detail": "my text here"
}
You can write custom error handler:
from rest_framework.views import exception_handler
def custom_exception_handler(exc, context):
response = exception_handler(exc, context)
if response is not None:
response.data['Failure'] = 'Error'
return response

Django class based views, passing parameters

I've the following class based view in django rest,
class UserRoom(views.APIView):
def add_user_to_persistent_room(self, request):
try:
user = User.objects.get(id=int(request.data['user_id']))
club = Club.objects.get(id=int(request.data['club_id']))
location = Location.objects.get(id=int(request.data['location_id']))
name = location.city + '-' + club.name
room, created = PersistentRoom.objects.get_or_create(name=name,
defaults={'club': club, 'location': location})
room.users.add(user)
room.save()
return Response(PersistentRoomSerializer(room).data, status=status.HTTP_201_CREATED)
except User.DoesNotExist:
return Response("{Error: Either User or Club does not exist}", status=status.HTTP_404_NOT_FOUND)
def find_all_rooms_for_user(self, request, **kwargs):
try:
user = User.objects.get(id=int(kwargs.get('user_id')))
persistent_rooms = user.persistentroom_set.all()
floating_rooms = user.floatingroom_set.all()
rooms = [PersistentRoomSerializer(persistent_room).data for persistent_room in persistent_rooms]
for floating_room in floating_rooms:
rooms.append(FloatingRoomSerializer(floating_room).data)
return Response(rooms, status=status.HTTP_200_OK)
except User.DoesNotExist:
return Response("{Error: User does not exist}", status=status.HTTP_404_NOT_FOUND)
This is my urls.py
urlpatterns = [
url(r'^rooms/persistent/(?P<user_id>[\w.-]+)/(?P<club_id>[\w.-]+)/(?P<location_id>[\w.-]+)/$',
UserRoom.add_user_to_persistent_room(),
name='add_user_to_persistent_room'),
url(r'^rooms/all/(?P<user_id>[\w.-]+)/$', UserRoom.find_all_rooms_for_user(), name='find_all_rooms')
]
When I run this I get the following error,
TypeError: add_user_to_persistent_room() missing 2 required positional arguments: 'self' and 'request'
I understand the reason for this error clearly, my question is how do I pass the request object in the urls.py?
I think you used Class Based Views the wrong way. Your method must be named get or post. For example:
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import authentication, permissions
from django.contrib.auth.models import User
class ListUsers(APIView):
"""
View to list all users in the system.
* Requires token authentication.
* Only admin users are able to access this view.
"""
authentication_classes = (authentication.TokenAuthentication,)
permission_classes = (permissions.IsAdminUser,)
def get(self, request, format=None):
"""
Return a list of all users.
"""
usernames = [user.username for user in User.objects.all()]
return Response(usernames)
More info here: http://www.django-rest-framework.org/api-guide/views/#class-based-views

How do I get an Django ViewSet to return a 403 error on anonymous post

I'm trying to get my app to return a 403 error when an anonymous user attempts to POST to it. Right now it returns a 201 code, but doesn't save the post to the database.
The problem is that my unit test fails because it's checking for a 403 code.
Here is my views
from post.models import Post
from post.serializers import PostSerializer
from post.permissions import IsOwnerOrReadOnly, IsOwnerOrAdmin
from rest_framework import viewsets, status
from rest_framework.response import Response
class PostViewSet(viewsets.ModelViewSet):
"""
This viewset automatically provides `list`, `create`, `retrieve`,
`update` and `destroy` actions.
"""
queryset = Post.objects.all()
serializer_class = PostSerializer
# The default will be that anyone can read a post, but only owners can change it
permission_classes = (IsOwnerOrReadOnly,)
def get_permissions(self):
# Both owners and admins can destroy a post, so if we're destroying we change permissions
if self.action in ('destroy',):
self.permission_classes = [IsOwnerOrAdmin, ]
return super(self.__class__, self).get_permissions()
def perform_create(self, serializer):
if self.request.user.is_authenticated:
serializer.save(author=self.request.user)
else:
return Response('Cannot post anonymously', status=status.HTTP_403_FORBIDDEN)
You can see I'm checking if the user is authenticated and if not, return a Response with a 403 code, but for some reason a 201 code is being returned.
How do I get it to return a 403 code?
You are trying to send a response from perform_create, but this can't be done. You see, DRF (Django REST Framework) doesn't call the perform_create method in a straight-forward manner. What happens is that DRF first calls CreateModelMixin's create method. Which then calls the perform_create method and then returns the response.
In, short, the response is returned by the create method, not perform_create method. And the create method, by default, returns the 201 status code (or sometimes 400).
So, you will need to override the create method instead. First, take a look at the source code for this method. Now, override:
from rest_framework.exceptions import PermissionDenied
def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
if request.user.is_authenticated:
self.perform_create(serializer)
else:
raise PermissionDenied('Cannot post anonymously')
headers = self.get_success_headers(serializer.data)
return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
Instead of using return, use raise.
raise PermissionDenied

Custom response for invalid token authentication in Django rest framework

For the following piece of code, I would like to return a boolean corresponding to whether the user was authenticated or not.
class UserAuthenticatedView(APIView):
authentication_classes = (TokenAuthentication,)
permission_classes = (AllowAny,)
def get(self, request, format=None):
is_authenticated = request.user.is_authenticated()
resp = {'is_authenticated': is_authenticated}
return Response(resp, content_type="application/json", status=status.HTTP_200_OK)
However, for invalid token, the control is not not even going inside get method due to which I'm not able to customize the response. In such a case I'm getting the response: {'detail': 'invalid token'},
Any idea on how to customize the response for invalid token ?
You can create a CustomTokenAuthentication class and override the authenticate_credentials() method to return the custom response in case of invalid token.
class CustomTokenAuthentication(TokenAuthentication):
def authenticate_credentials(self, key):
try:
token = self.model.objects.select_related('user').get(key=key)
except self.model.DoesNotExist:
# modify the original exception response
raise exceptions.AuthenticationFailed('Custom error message')
if not token.user.is_active:
# can also modify this exception message
raise exceptions.AuthenticationFailed('User inactive or deleted')
return (token.user, token)
After doing this, define this custom token authentication class in your DRF settings or on a per-view/viewset basis.
Another option is to create a custom exception handler. In that, you can check if the exception raised was of type AuthenticationFailed and the exception message is 'invalid token'. There you can modify the exception message (also check this official DRF example).
This worked for me:
Custom Authentication class:
class MyAuthentication(authentication.TokenAuthentication):
def authenticate_credentials(self, key):
try:
token = self.model.objects.select_related('user').get(key=key)
except self.model.DoesNotExist:
return (None, '')
if not token.user.is_active:
raise exceptions.AuthenticationFailed(_('User inactive or deleted.'))
return (token.user, token)
view class:
class UserAuthenticatedView(APIView):
authentication_classes = (MyAuthentication,)
permission_classes = (AllowAny,)
def get(self, request, format=None):
is_authenticated = False
if request.user and request.user.is_authenticated():
is_authenticated = True
resp = {'is_authenticated': is_authenticated}
return Response(resp, content_type="application/json", status=status.HTTP_200_OK)