djangodjango-rest-frameworkbasic-authentication

Django Rest Framework APIClient login is not working


I have test function to test whether user can login or not. I am using DRF's APIClient and logging in. When I do a get request to a protected API endpoint I get 401 response.

I tried to print:

print(client.login(username='instructor', password='asdf'))

And it is returning True, which means user is found and logged in.

But still when I do:

response = client.get(reverse('recommend-list'))

I am getting 401.

Here is my test function:

def test_user_authentication(self):
    client = APIClient()
    user_data = {
        'username': 'instructor',
        'email': 'myemail@example.com',
        'password': 'asdf'
    }
    client.post(reverse('user-list'), user_data, format='json')
    self.assertTrue(User.objects.filter(username='instructor').exists())
    print(client.login(username='instructor', password='asdf'))
    response = client.get(reverse('recommend-list'))
    self.assertEqual(response.status_code, 200)

Here is how I create a new user:

class UserViewSet(viewsets.ViewSet):
    def create(self, request):
        serializer = UserSerializer(data=request.data)
        User = get_user_model()

        if serializer.is_valid():
            validated_data = serializer.validated_data

            try:
                user = User.objects.create_user(
                    username=validated_data['username'],
                    email=validated_data['email'],
                    password=validated_data['password']
                )

                return Response({'id': user.id, 'username': user.username}, status=status.HTTP_201_CREATED)
            except IntegrityError:
                return Response({'error': 'Username is already taken.'}, status=status.HTTP_400_BAD_REQUEST)

        else:
            return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

Making get request to here:

class RecommendViewSet(viewsets.GenericViewSet):
    authentication_classes = [BasicAuthentication]
    permission_classes = [IsAuthenticated]

    def list(self, request):
        user_id = request.user.id
        movies = recommend_movies_for_user(user_id)
        return Response({"message": movies}, status=status.HTTP_200_OK)

The user serializer looks like this:

class UserSerializer(serializers.ModelSerializer):
    id = serializers.IntegerField(read_only=True)
    username = serializers.CharField(max_length=150)
    email = serializers.EmailField()
    password = serializers.CharField(write_only=True)
    follower_list = serializers.SerializerMethodField()
    following_list = serializers.SerializerMethodField()

    class Meta:
        model = User
        fields = ['id', 'username', 'email', 'password',
                  'follower_list', 'following_list']

    def retrieve(self, instance):
        return instance

    def create(self, validated_data):
        user = User.objects.create_user(**validated_data)
        return user

    def get_follower_list(self, obj):
        follower = obj.followers_set.all()
        return FollowSerializer(follower, many=True).data

    def get_following_list(self, obj):
        following = obj.following_set.all()
        return FollowSerializer(following, many=True).data

Solution

  • Alright, I thought it could have been a simple hashing problem, but I was wrong.

    The problem is that you are using BasicAuthentication to authenticate credentials, which tries to find an username and a password that must be present in the request headers. Thus, you need to encode credentials and properly set the headers:

    class TestUser(TestCase):
    
        def test_user_authentication(self):
            client = APIClient()
            user_data = {
                "username": "instructor",
                "email": "myemail@example.com",
                "password": "asdf",
            }
    
            client.post(reverse("user-list"), user_data, format="json")
            get_user_model().objects.get(username="instructor")
    
            # Encode credentials and set the header
            cstring = f"{user_data["username"]}:{user_data["password"]}".encode("utf-8")
            c=base64.b64encode(cstring)
            client.credentials(HTTP_AUTHORIZATION=f"Basic {c.decode()}")
    
            response = client.get(reverse("recommend-list"))
            self.assertEqual(response.status_code, 200)
    

    On the other hand, what .login does is attach an User to the session with the authenticate method. Therefore, if you use SessionAuthentication your original code would work just fine.