How to log out in rest_framework?
This is my user serializer. I'm using rest and very very newbie. Sign up and Login working, but have no clue how to impelement logout.
class UserSerializer(serializers.ModelSerializer):
email = serializers.EmailField(write_only=True)
class Meta:
model = User
fields = ('id', 'username', 'password', 'email')
write_only_fields = ('username', 'email', 'password',)
read_only_fields = ('id', )
Sign up part
class UserCreateAPIView(generics.CreateAPIView):
queryset = User.objects.all()
serializer_class = UserSerializer
permission_classes = [permissions.AllowAny]
Loggin code; i am using JWT authentication.
path(r'login/', obtain_jwt_token, name='ObtainJWTToken'),
When i use this code:
#api_view(['POST'])
def logout(request):
request.auth.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
i get error: 'bytes' object has no attribute 'delete'
When i use Djoser code:
permission_classes = settings.PERMISSIONS.token_destroy
def post(self, request):
utils.logout_user(request)
return Response(status=status.HTTP_204_NO_CONTENT)
getting this error: type object 'Token' has no attribute 'objects'
I assume you use TokenAuthentication.
In this case, give this method a shot:
class LogoutAPIView(APIView):
def get(self, request):
request.user.auth_token.delete()
return Response(status=status.HTTP_200_OK)
However, please note that this deletes the token and consequently forces a logout.
In case you use SessionAuthentication, the body of your get() method inside the LogoutAPIView class could be just logout(request).
eg.
def get(self, request):
logout(request)
return Response(status=status.HTTP_200_OK)
In the case of JWT authentication, as it is stateless and each JWT token is valid for some time, that means even if you remove the token from the DB, the user will still be able to use your endpoints for a short amount of time, until it's expired.
Therefore, depending on how deep you want to go, you can eg. implement a "tokens_unable_to_login" cache key and check if the requests include a JWT token that should not log in. Or you let it expire - you can toy around with the expiration time of the token and see what's ideal for your use-case. That's up to your business requirements.
Let me know how did this go for you.
Related
I have created a basic app using Django's built in authentication system. I successfully created a User object in the shell using
>>python manage.py createsuperuser.
I then created a basic view, 'UserLogin' along with corresponding serializers/urls, to log an existing user in using the django.contrib.auth authenticate(), and login() functions. Upon testing with the credentials of my created user, the login function seemed to have worked successfully.
To test this, I created another view function, 'CurrentUser' which returns the username of the currently logged in user. However, this view returns the user as empty.
Why would the 'CurrentUser' view be returning no user as logged in? I have attached my code (minus imports) below.
views.py:
class UserLogin(APIView):
def post(self, request, format = None):
serializer = UserLoginSerializer(data=request.data)
if serializer.is_valid():
user = authenticate(username=serializer.validated_data["username"], password=serializer.validated_data["password"])
if user is not None:
login(request, user)
return Response(UserSerializer(user).data, status=status.HTTP_201_CREATED)
return Response("Invalid username/password", status=status.HTTP_401_UNAUTHORIZED)
return Response(serializer.errors, status=status.HTTP_401_UNAUTHORIZED)
class CurrentUser(APIView):
def get(self, request, format = None):
return Response(self.request.user.username)
serializers.py:
class UserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ['id', 'username']
class UserLoginSerializer(serializers.Serializer):
username = serializers.CharField(max_length=300, required=True)
password = serializers.CharField(required=True, write_only=True)
urls.py:
urlpatterns = [
path('login/', views.UserLogin.as_view()),
path('current/', views.CurrentUser.as_view())
]
Any guidance would be much appreciated.
Thanks
You have to set the default auth class as session authenticate class in DRF settings. Read more about it here [1].
Session auth uses session id to identify the user. So you have to send the cookie based session id in the request. Read about session auth here [2].
for example:
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': [
'rest_framework.authentication.SessionAuthentication', # <-- set this class
]
}
Use this code:
def post(self, request, format = None):
serializer = UserLoginSerializer(data=request.data)
if serializer.is_valid():
user = authenticate(username=serializer.validated_data["username"], password=serializer.validated_data["password"])
if user:
return Response(UserSerializer(user).data, status=status.HTTP_201_CREATED)
return Response("Invalid username/password", status=status.HTTP_401_UNAUTHORIZED)
return Response(serializer.errors, status=status.HTTP_401_UNAUTHORIZED)
But my recommendation is to use Token auth [3].
To use token auth 2 things will change:
The default auth class in DRF settings
When sending a request to any DRF API view you to send the Auth header as Token <token-value>
Your post method and API views code will remain same.
[1] https://www.django-rest-framework.org/api-guide/authentication/#setting-the-authentication-scheme
[2] https://www.django-rest-framework.org/api-guide/authentication/#sessionauthentication
[3] https://www.django-rest-framework.org/api-guide/authentication/#tokenauthentication
I am using DRF for login. You can check this serializers and login view.
serializers.py:
class LoginSerializer(serializers.Serializer):
username = serializers.CharField()
password = serializers.CharField()
def validate(self, data):
user = authenticate(**data)
if user and user.is_active:
return user
raise serializers.ValidationError("Incorrect Credentials")
login view:
class LoginAPIView(generics.GenericAPIView):
queryset = User.objects.all()
permission_classes = [AllowAny]
serializer_class = LoginSerializer
def post(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
user = serializer.validated_data
return Response({
"user": UserSerializer(user, context=self.get_serializer_context()).data,
"token": AuthToken.objects.create(user)[1]
})
settings.py rest framework classes :
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': [
# 'rest_framework.authentication.BasicAuthentication',
# 'rest_framework.authentication.SessionAuthentication',
'knox.auth.TokenAuthentication',
],
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.DjangoModelPermissionsOrAnonReadOnly'
],
'DEFAULT_FILTER_BACKENDS': ['django_filters.rest_framework.DjangoFilterBackend']
}
You were trying to test the authentication in the wrong way. This is API, which is stateless. That is Django doesn't have any idea about the previous request that you have made to the UserLogin.
So, you need to attach the token and session-id (or something that can be used to identify the user) with the "second request".
Since you are using DRF, you don't have to write your own authentication mechanisms. You can use DRF's BasicAuthentication (or any other authentication scheme) to authenticate.
So, change your view as
from rest_framework.authentication import BasicAuthentication
from rest_framework.permissions import IsAuthenticated
class CurrentUser(APIView):
authentication_classes = (BasicAuthentication,)
permission_classes = (IsAuthenticated,)
def get(self, request, format=None):
return Response({
"user": {
"id": request.user.id,
"username": request.user.username
}})
Execution using requests
In [5]: import requests
In [6]: from requests import auth
In [7]: response = requests.get(url=url, auth=auth.HTTPBasicAuth("admin", "passme123##$"))
In [8]: print(response.json())
{'user': {'id': 1, 'username': 'admin'}}
Since you are using DRF, you can make use of any of these DRF authentications schemes in your code-base
Since you are using Django rest framework, i m assuming you are developing rest apis.
Rest Apis by design are meant to be stateless meaning no two requests will be dependent on each other.
What this essentially means is you have to authenticate every request and set the user in request after authentication.
There are many ways of doing it like many have suggested to use basicAuth, or token based authentication. All of which can we easily implemented by reading documentation
Auth Doc
The out of the box django views uses cookies to identify logged in user. Which in this case is not helpful.
A cause for this could be a missing SessionMiddleware:
Your /login view works
It persists stuff in the session but the middleware isn't installed
The next request to /current isn't bound to the authenticated session
Can you double check that settings.MIDDLEWARES contain django.contrib.sessions.middleware.SessionMiddleware?
You have to attach an authentication token and session-id for your purpose.
I wish this document would helpful for you.
I recommend you to use Token Auth2.
https://www.django-rest-framework.org/api-guide/authentication/#tokenauthentication
I am writing custom permission and inheriting in the generic views but it is not working it returns
TypeError: Cannot cast AnonymousUser to int. Are you trying to use it in place of User?
instead of User required to perform this action
my custom permission as follow
class IsOwnedByUser(permissions.BasePermission):
def has_object_permission(self, request, view, obj):
message = {'detail': 'User required to perform this action'}
return obj.user == request.user
views.py
class OrderAddressView(generics.ListCreateAPIView):
queryset = OrderAddress.objects.all()
serializer_class = OrderAddressSerializer
permission_classes = (IsOwnedByUser,)
def perform_create(self, serializer):
serializer.save(user=self.request.user)
def get_queryset(self):
return OrderAddress.objects.filter(user=self.request.user)
BTW, it works fine with default rest framework permission class when I use like this
permission_class = (permissions.IsAuthenticated,)
but with my custom permission it is not working( Can anyone tell why it is that? Thanks in advance!
Actually, as Pavan Kumar told you, you should use both (permissions.IsAuthenticated, IsOwnedByUser)
But if you want to use only one permission, you can create your own:
class IsAuthenticatedAndOwner(permissions.BasePermission):
message = 'User required to perform this action'
def has_permission(self, request, view):
return request.user and request.user.is_authenticated
def has_object_permission(self, request, view, obj):
return obj.user == request.user
You can also add a default in your settings.py file. Try with the following (adjust the permissions as you want though as in docs):
REST_FRAMEWORK = {
# Use Django's standard `django.contrib.auth` permissions,
# or allow read-only access for unauthenticated users.
'DEFAULT_AUTHENTICATION_CLASSES': (
'rest_framework.authentication.TokenAuthentication',
'rest_framework.authentication.SessionAuthentication',
),
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.IsAuthenticated'
]
}
I'm creating a simple web app and I cannot find any way to forbid other users from changing your password. This is minimal code:
# serializers.py
from rest_framework import serializers
class UserSerializer(serializers.ModelSerializer):
def create(self, validated_data):
# have to use custom create method because default method calls `User.objects.create()` which doesn't take care of password hashing and other important stuff
return User.objects.create_user(**validated_data)
class Meta:
model = User
fields = ('id', 'username', 'email', 'password')
read_only_fields = ('id', 'username')
# password is set to write_only because I don't want to send it to anybode (even though it's just a hash)
extra_kwargs = {'password': {'write_only': True}}
# views.py
from .serializers import UserSerializer
from rest_framework import generics
from django.contrib.auth.models import User
class UserDetails(generics.RetrieveUpdateAPIView):
queryset = User.objects.all()
serializer_class = UserSerializer
I could take care of this myself by using APIView.
# views.py
from .serializers import UserSerializer
from rest_framework.views import APIView
from django.contrib.auth.models import User
class UserDetails(APIView):
def put(self, request, format=None):
serialized = UserSerializer(data=request.DATA)
if not serialized.is_valid():
return # probably HTTP 400 Error code
if request.user.id != serialized.data['id']:
# this if is what I'm trying to achieve
return # probably HTTP 403 Error code
user = User.objects.update(
id=serialized.data['id'],
email=serialized.data['email']
)
if 'password' in request.DATA:
user.set_password(request.DATA['password'])
return # probably HTTP 200 Error code
Unfortunately, that would have caused that scheme generated by rest_framework.schemas.get_schema_view would be incomplete. And I use for communication CoreAPI (which cannot communicate with something that is not described in the scheme) so I cannot do that. I haven't found anything in the official documentation.
This seems to be a too basic problem that has a super easy solution that I missed. Thanks for any ideas or places where to look.
PS: I'm using django2.1 with python3.6
Edit: osobacho's solutions is clean and works like charm. Anyway I also need to allow modifications (let's say of TODO-list) only to creator of that todo list. Thought that solution for password problem would be applicable but it's not.
You can get the user in the request. That way each user will only be able to change their own password.
class UserDetails(generics.RetrieveUpdateAPIView):
queryset = User.objects.all()
serializer_class = UserSerializer
def get_object(self):
return self.request.user
For your second question take a look at django_restframework http://www.django-rest-framework.org/api-guide/permissions/ permissions here is an example:
class IsOwnerOrReadOnly(permissions.BasePermission):
"""
Object-level permission to only allow owners of an object to edit it.
Assumes the model instance has an `owner` attribute.
"""
def has_object_permission(self, request, view, obj):
# Read permissions are allowed to any request,
# so we'll always allow GET, HEAD or OPTIONS requests.
if request.method in permissions.SAFE_METHODS:
return True
# Instance must have an attribute named `owner`.
return obj.owner == request.user
then you need to add to your view:
permission_classes = (IsOwnerOrReadOnly,)
hope it helps
As stated in DRF documentation http://www.django-rest-framework.org/api-guide/parsers/#multipartparser, in order to parse multipart/form-data, the MultiPart and form parser must be used. I have a supiscion this is a problem in the Django Rest Framework because I saw a solution on their github issue saying it work using APIView.
from django.contrib.auth.models import User
from rest_framework import viewsets
from api.serializers import UserSerializer,
from rest_framework.parsers import MultiPartParser, FormParser
from rest_framework.response import Response
from rest_framework import status
class UserViewSet(viewsets.ModelViewSet):
"""
API endpoint that allows users to be viewed or edited.
"""
queryset = User.objects.all()
parser_classes = (MultiPartParser, FormParser)
serializer_class = UserSerializer
Picture of me sending request to Postman with result:
Edit: Adding UserSerializer class
class UserSerializer(serializers.HyperlinkedModelSerializer):
snapcapsules = SnapCapsuleSerializer(
many=True,
read_only=True,
allow_null=True,
)
class Meta:
model = User
fields = ('snapcapsules', 'url', 'username', 'email', 'password', )
write_only_fields = ('password',)
def create(self, validated_data):
user = User.objects.create(
username=validated_data['username'],
email=validated_data['email'],
)
user.set_password(validated_data['password'])
user.save()
return user
def update(self, instance, validated_data):
capsules_data = validated_data.pop('snapcapsules')
for capsule in capsules_data:
SnapCapsule.objects.create(user=instance, **capsule)
return instance
This is might not the issue with the ContentType. The error response says that,the UserSerializer excpect a payloadordata which include username and password field
So, Please try to add those fields to the request body and try again
UPDATE
The problem with the Orginal Post was, he added extra headers in POSTMAN (see the pic below) .
Postman will add appropriate Headers aromatically for you. So, you don't have to explicitly mention it
I removed one line and then it worked!
class UserViewSet(viewsets.ModelViewSet):
"""
API endpoint that allows users to be viewed or edited.
"""
queryset = User.objects.all()
serializer_class = UserSerializer
Also, the serializer can be rewrite as follows:
class UserSerializer(serializers.HyperlinkedModelSerializer):
...
def create(self, validated_data):
return User.objects.create_user(
username=validated_data['username'],
email=validated_data['email'],
password=validated_data['password']
)
...
I have a basic django rest service, which
registers a person and
updates his password.
I want to add jwt authentication on top of it. If I follow the tutorial I would need to add a new url named "api-token-auth" in project's urls.py. But, I don't want to add this new url and want my register call to send a token in response.
Here's my code:
serializers.py
class UserSerializer(serializers.HyperlinkedModelSerializer):
def create(self, validated_data):
user = User(
username=validated_data['username']
)
user.set_password(validated_data['password'])
user.save()
return user
def update(self, instance, validated_data):
instance.set_password(validated_data['password'])
instance.save()
return instance
class Meta:
model = User
fields = ('url', 'username', 'password')
lookup_field = 'username'
write_only_fields = ('password',)
views.py
class UserViewSet(viewsets.ModelViewSet):
"""
API endpoint that allows users to be viewed or edited.
"""
queryset = User.objects.exclude(is_superuser=1)
serializer_class = UserSerializer
lookup_field = 'username'
What should be done to achieve this ? should I call api-auth-token inside my serializer's create method ?
How does django-rest-framework-jwt handles multiple authentication tokens and correctly identifies which token belongs to which user ? Especially when it doesn't store tokens in a db.
How can I use this authentication mechanism to limit my user to view/update/delete only his user ?
How can I use this authentication mechanism to do anything in general. For instance if a user wants to write his name to /tmp/abcd.txt. How can I make sure that only authenticated users would be able to do so ?
Are there any potential loopholes in this approach. Should I use the same code if my app is going to store lots of classified data ?
Question 1: To generate tokens manually on registration you can define and make use of a method like this:
import jwt
from rest_framework_jwt.utils import jwt_payload_handler
def create_token(user):
payload = jwt_payload_handler(user)
token = jwt.encode(payload, settings.SECRET_KEY)
return token.decode('unicode_escape')
you can add this method to the view and generate the token once the user has been registered and return it in the response.
Question 2: JWT tokens do not need to be stored in the database. You can read more about how JWT works at http://jwt.io/.
Question 3 and 4: To use tokens to limit access to a specific view, especially an APIView or one of its subclasses or a view provided by Django Rest framework, you need to specify the permission classes. for example:
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework.views import APIView
class ExampleView(APIView):
permission_classes = (IsAuthenticated,)
def get(self, request, format=None):
content = {
'status': 'request was permitted'
}
return Response(content)
Question 5: One potential loophole while working with Django Rest Framework is the default permissions that you setup from the settings of your application; if for example you AllowAny in the settings it'll make all the views publicly accessible unless you specifically override the permission classes in each view.
The Accepted answer has some code that generates token but it doesn't show how to integrate it in serializer/view. Also not sure that manual jwt.encode is a good modern way of doing this if we already have jwt_encode_handler
to do this. You can create SerializerMethodField and create tokens there:
token = serializers.SerializerMethodField()
def get_token(self, obj):
jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER
jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER
payload = jwt_payload_handler(obj)
token = jwt_encode_handler(payload)
return token
Then add token field to Meta.fields.
Working Example