Custom response for invalid token authentication in Django rest framework - django

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)

Related

custom message in raise PermissionDenied not working in Django rest

I tried raise PermissionDenied("Anonymous user") inside a custom permission function but the message I wrote is not showing in the api response. Instead, it is showing the default forbidden message that says you dont have permission to perform this action
My snippet is here:
class CustomPermission(BasePermission):
"""
returns permission based on the request method and slug
"""
def has_permission(self, request,view):
slug = request.resolver_match.kwargs["slug"]
if slug is not None and request.method == 'POST':
if slug == "abc":
user = request.user
if user.is_staff:
return True
if user.is_anonymous:
print("iam here")
raise PermissionDenied("Anonymous user")
elif slug == "mnp":
return True
else:
return True
Here in the above code I reached to ("iam here") but anonymous user is not printing instead showing the default message.
You can change error message in message property:
from rest_framework import permissions
class CustomerAccessPermission(permissions.BasePermission):
message = 'Your message'
def has_permission(self, request, view):
... # return True or False
Docs in : https://www.django-rest-framework.org/api-guide/permissions/#custom-permissions
After that, the permission_denied(self, request, message=None, code=None) function in your view would be called which message is the attribute that you have declared in your permission class.
You can use that, or even pass another message:
from rest_framework.exceptions import PermissionDenied
class YourView(...):
permission_classes = [CustomerAccessPermission]
def permission_denied(self, request, message=None, code=None):
raise PermissionDenied(message)

Customized Django SimpleJWT Views

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.

Handling exceptions in permission classes

I am using Django REST Framework with simple JWT, and I don't like how permission classes generate a response without giving me the final say on what gets sent to the client. With other class-based views not restricting permissions (login, registration, etc.), I have control over how I handle exceptions, and I can choose how the response data is structured.
However, anytime I introduce permission classes, undesired behavior occurs. My desired structure is best represented in my LoginView (see try/except block):
NON_FIELD_ERRORS_KEY = settings.REST_FRAMEWORK['NON_FIELD_ERRORS_KEY']
class LoginView(GenericAPIView):
"""
View for taking in an existing user's credentials and authorizing them if valid or denying access if invalid.
"""
serializer_class = LoginSerializer
def post(self, request):
"""
POST method for taking a token from a query string, checking if it is valid, and logging in the user if valid, or returning an error response if invalid.
"""
serializer = self.serializer_class(data=request.data)
try:
serializer.is_valid(raise_exception=True)
except AuthenticationFailed as e:
return Response({NON_FIELD_ERRORS_KEY: [e.detail]}, status=e.status_code)
return Response(serializer.data, status=status.HTTP_200_OK)
However, what happens when I try to define a view using a permission class?
class LogoutView(GenericAPIView):
"""
View for taking in a user's access token and logging them out.
"""
serializer_class = LogoutSerializer
permission_classes = [IsAuthenticated]
def post(self, request):
"""
POST method for taking a token from a request body, checking if it is valid, and logging out the user if valid, or returning an error response if invalid.
"""
access = request.META.get('HTTP_AUTHORIZATION', '')
serializer = self.serializer_class(data={'access': access})
try:
serializer.is_valid(raise_exception=True)
except AuthenticationFailed as e:
return Response({NON_FIELD_ERRORS_KEY: [e.detail]}, status=e.status_code)
return Response(serializer.save(), status=status.HTTP_205_RESET_CONTENT)
When I go to test this, requests with an invalid Authorization header are handled outside the scope of post(), so execution never reaches the method at all. Instead, I am forced to deal with a response that is inconsistent with the rest of my project. Here's an example:
# Desired output
{
'errors': [
ErrorDetail(string='Given token not valid for any token type', code='token_not_valid')
]
}
# Actual output
{
'detail': ErrorDetail(string='Given token not valid for any token type', code='token_not_valid'),
'code': ErrorDetail(string='token_not_valid', code='token_not_valid'),
'messages': [
{
'token_class': ErrorDetail(string='AccessToken', code='token_not_valid'),
'token_type': ErrorDetail(string='access', code='token_not_valid'),
'message': ErrorDetail(string='Token is invalid or expired', code='token_not_valid')
}
]
}
Is there a simple way to change how these responses are formatted?
After stepping through the code, I found that the exceptions with which I was concerned were being raised in the last commented section of APIView.initial.
# rest_framework/views.py (lines 399-416)
def initial(self, request, *args, **kwargs):
"""
Runs anything that needs to occur prior to calling the method handler.
"""
self.format_kwarg = self.get_format_suffix(**kwargs)
# Perform content negotiation and store the accepted info on the request
neg = self.perform_content_negotiation(request)
request.accepted_renderer, request.accepted_media_type = neg
# Determine the API version, if versioning is in use.
version, scheme = self.determine_version(request, *args, **kwargs)
request.version, request.versioning_scheme = version, scheme
# Ensure that the incoming request is permitted
self.perform_authentication(request)
self.check_permissions(request)
self.check_throttles(request)
To generate a custom response, either create a custom implementation of APIView with an overloaded initial() method, or override the method in specific instances of APIView (or descendants such as GenericAPIView).
# my_app/views.py
def initial(self, request, *args, **kwargs):
"""
This method overrides the default APIView method so exceptions can be handled.
"""
try:
super().initial(request, *args, **kwargs)
except (AuthenticationFailed, InvalidToken) as exc:
raise AuthenticationFailed(
{NON_FIELD_ERRORS_KEY: [_('The provided token is invalid.')]},
'invalid')
except NotAuthenticated as exc:
raise NotAuthenticated(
{
NON_FIELD_ERRORS_KEY:
[_('Authentication credentials were not provided.')]
}, 'not_authenticated')

Django REST: How do i return SimpleJWT access and refresh tokens as HttpOnly cookies with custom claims?

I want to send the SimpleJWT access and refresh tokens through HttpOnly cookie. I have customized the claim. I have defined a post() method in the MyObtainTokenPairView(TokenObtainPairView) in which I am setting the cookie. This is my code:
from .models import CustomUser
class MyObtainTokenPairView(TokenObtainPairView):
permission_classes = (permissions.AllowAny,)
serializer_class = MyTokenObtainPairSerializer
def post(self, request, *args, **kwargs):
serializer = self.serializer_class()
response = Response()
tokens = serializer.get_token(CustomUser)
access = tokens.access
response.set_cookie('token', access, httponly=True)
return response
It's returning this error:
AttributeError: 'RefreshToken' object has no attribute 'access'
The serializer:
class MyTokenObtainPairSerializer(TokenObtainPairSerializer):
#classmethod
def get_token(cls, user):
print(type(user))
token = super().get_token(user)
token['email'] = user.email
return token
But it's just not working. I think I should not define a post() method here like this. I think if I can only return the value of the get_token() function in the serializer, I could set it as HttpOnly cookie. But, I don't know how to do that.
How do I set the access and refresh tokens in the HttpOnly cookie?
EDIT:
I made these changes following anowlinorbit's answer:
I changed my serializer to this:
class MyTokenObtainPairSerializer(TokenObtainPairSerializer):
def validate(self, attrs):
attrs = super().validate(attrs)
token = self.get_token(self.user)
token["email"] = self.user.email
return token
Since this token contains the refresh token by default therefore, I decided that returning only this token would provide both access and refresh token.
If I add anything like token["access"] = str(token.access_token) it would just add the access token string inside the refresh token string, which it already contains.
But again in the view, I could not find how to get the refresh token. I could not get it using serializer.validated_data.get('refresh', None) since now I am returning the token from serializer which contains everything.
I changed my view to this:
class MyObtainTokenPairView(TokenObtainPairView):
permission_classes = (permissions.AllowAny,)
serializer_class = MyTokenObtainPairSerializer
def post(self, request, *args, **kwargs):
response = super().post(request, *args, **kwargs)
response.set_cookie('token', token, httponly=True)
return response
Now it's saying:
NameError: name 'token' is not defined
What's wrong here? In the view I want to get the token returned from serializer, then get the acces token using token.access_token and set both refresh and access as cookies.
I would leave .get_token() alone and instead focus on .validate(). In your MyTokenObtainPairSerializer I would remove your changes to .get_token() and add the following
def validate(self, attrs):
data = super().validate(attrs)
refresh = self.get_token(self.user)
data["refresh"] = str(refresh) # comment out if you don't want this
data["access"] = str(refresh.access_token)
data["email"] = self.user.email
""" Add extra responses here should you wish
data["userid"] = self.user.id
data["my_favourite_bird"] = "Jack Snipe"
"""
return data
It is by using the .validate() method with which you can choose which data you wish to return from the serializer object's validated_data attribute. N.B. I have also included the refresh token in the data which the serializer returns. Having both a refresh and access token is important. If a user doesn't have the refresh token they will have to login again when the access token expires. The refresh token allows them to get a new access token without having to login again.
If for whatever reason you don't want the refresh token, remove it from your validate() serializer method and adjust the view accordingly.
In this post method, we validate the serializer and access its validated data.
def post(self, request, *args, **kwargs):
# you need to instantiate the serializer with the request data
serializer = self.serializer(data=request.data)
# you must call .is_valid() before accessing validated_data
serializer.is_valid(raise_exception=True)
# get access and refresh tokens to do what you like with
access = serializer.validated_data.get("access", None)
refresh = serializer.validated_data.get("refresh", None)
email = serializer.validated_data.get("email", None)
# build your response and set cookie
if access is not None:
response = Response({"access": access, "refresh": refresh, "email": email}, status=200)
response.set_cookie('token', access, httponly=True)
response.set_cookie('refresh', refresh, httponly=True)
response.set_cookie('email', email, httponly=True)
return response
return Response({"Error": "Something went wrong", status=400)
If you didn't want the refresh token, you would remove the line beginning refresh = and remove the line where you add the refresh cookie.
class MyObtainTokenPairView(TokenObtainPairView):
permission_classes = (permissions.AllowAny,)
serializer_class = MyTokenObtainPairSerializer
def post(self, request, *args, **kwargs):
response = super().post(request, *args, **kwargs)
token = response.data["access"] # NEW LINE
response.set_cookie('token', token, httponly=True)
return response
You can solve it by setting the token as the "access" from the response data.
Btw, did you find any better solution for this?

rest framework token authentication without header

I am trying to restrict list view using Django Rest Framework.
If I am sending request with wrong token:
Authorization: Token f1cfedb0895105ee088e8aab5bf0ae7ca9752e79
It returns me HTTP 401 error which is correct
But if i just remove Authorization from HTTP Headers it will query without authentication and return HTTP 200
Am i doing something wrong here:
class OrderViewSet(viewsets.ModelViewSet):
serializer_class = OrderSerializer
model = Order
#permission_classes((IsAuthenticated,))
#authentication_classes((TokenAuthentication,))
def list(self, request, *args, **kwargs):
serializer = OrderSerializer(Order.objects.opened(), many=True)
return Response(serializer.data)
Also create method should not be restricted to authenticated users.
I would suggest standardizing your Viewset:
class OrderViewSet(viewsets.ModelViewSet):
serializer_class = OrderSerializer
model = Order
def get_queryset(self):
"""QuerySet for this entire ModelViewSet will only return
orders which are opened.
"""
return Order.objects.opened()
#permission_classes((IsAuthenticated,))
#authentication_classes((TokenAuthentication,))
def list(self, request, *args, **kwargs):
return super(OrderViewSet, self).list(request, *args, **kwargs)
Upon further investigation, I took a look at the source code for TokenAuthentication, and it appears that if you don't send in an authentication token at all, the authenticate() method returns None if the get_authorization_header() method returns nothing. Thus, if you entirely remove the HTTP_AUTHORIZATION from the header, this is the expected behavior.
I believe the intention here is to not raise an exception so that authentication can move on to the next possible authentication class. If this is not what you want to do, you can override the authenticate() method in your own class inherited from TokenAuthentication. See the code below.
def get_authorization_header(request):
"""
Return request's 'Authorization:' header, as a bytestring.
Hide some test client ickyness where the header can be unicode.
"""
auth = request.META.get('HTTP_AUTHORIZATION', b'')
if isinstance(auth, type('')):
# Work around django test client oddness
auth = auth.encode(HTTP_HEADER_ENCODING)
return auth
class TokenAuthentication(BaseAuthentication):
"""
Simple token based authentication.
Clients should authenticate by passing the token key in the "Authorization"
HTTP header, prepended with the string "Token ". For example:
Authorization: Token 401f7ac837da42b97f613d789819ff93537bee6a
"""
model = Token
"""
A custom token model may be used, but must have the following properties.
* key -- The string identifying the token
* user -- The user to which the token belongs
"""
def authenticate(self, request):
auth = get_authorization_header(request).split()
if not auth or auth[0].lower() != b'token':
return None
if len(auth) == 1:
msg = 'Invalid token header. No credentials provided.'
raise exceptions.AuthenticationFailed(msg)
elif len(auth) > 2:
msg = 'Invalid token header. Token string should not contain spaces.'
raise exceptions.AuthenticationFailed(msg)
return self.authenticate_credentials(auth[1])
def authenticate_credentials(self, key):
try:
token = self.model.objects.get(key=key)
except self.model.DoesNotExist:
raise exceptions.AuthenticationFailed('Invalid token')
if not token.user.is_active:
raise exceptions.AuthenticationFailed('User inactive or deleted')
return (token.user, token)
def authenticate_header(self, request):
return 'Token'
Finally, to make your token fail with a 401 instead of a 200, you can do:
class YourCustomTokenAuthentication(TokenAuthentication):
def authenticate(self, request):
auth = get_authorization_header(request).split()
if not auth or auth[0].lower() != b'token':
msg = 'Invalid token header. No credentials provided.'
raise exceptions.AuthenticationFailed(msg)
if len(auth) == 1:
msg = 'Invalid token header. No credentials provided.'
raise exceptions.AuthenticationFailed(msg)
elif len(auth) > 2:
msg = 'Invalid token header. Token string should not contain spaces.'
raise exceptions.AuthenticationFailed(msg)
return self.authenticate_credentials(auth[1])