JWT authentication

Our cookicutter-django-rest used default Token Based authentication, but in our days JWT is more secure and popular that's why we are gonna use it. There is a library called django-rest-framework-jwt. So try to use it. Steps you have to do:

  1. Read the documentation for that library
  2. Use and check it.
  3. Compare with my commit #2

POSTMAN helps us

PostMan is great tool which helps us tp build API.

Recommend you to create folder for every application. For now create Auth folder and save there POST request for login.


Postman also has own environment, it's very helpful to check between local, test server. As you see I've stored url=localhost:8000 in gitGile local enviroment.


Reponse structure

We are building REST API for web and mobile appications, so that's why our response structure should be simple and comfortable for all of them. Our Response JSON structure looks like this:

{
    "success": false,
    "data": {},
    "errors": {
        "detail": "Given data is not correct"
    }
}

{
    "success": true,
    "data": "given data",
    "errors": {}
}

Let's do it

The first thing you have to is research. Look at the documentaion and try to find something about exception.

If you found this one good job. Read it carefully.

Try do it. Your response structure should be like below. Then you can check it with my commit #3

{
    "data": {},
    "success": false,
    "errors": {
        "username": [
            "A user with that username already exists."
        ]
    }
}

Now let's try to create user with any username and password via postman. The response is like this:

{
    "id": "3281dd3b-da56-42ab-8b22-1b6c7a39b321",
    "username": "user4",
    "first_name": "",
    "last_name": "",
    "email": "",
    "auth_token": "5c81ecdc1c1b697c1f6902032f929d52a24ea50e"
}

This isn't what we want. Change it. But before let's analyze UserCreateViewSet _which is return response. We know that it is inherited from mixins.CreateModelMixin and calls it's create()_ function. Look at it:

class CreateModelMixin(object):
    """
    Create a model instance.
    """
    def create(self, request, *args, **kwargs):
        serializer = self.get_serializer(data=request.data)
        serializer.is_valid(raise_exception=True)
        self.perform_create(serializer)
        headers = self.get_success_headers(serializer.data)
        return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)

    def perform_create(self, serializer):
        serializer.save()

    def get_success_headers(self, data):
        try:
            return {'Location': str(data[api_settings.URL_FIELD_NAME])}
        except (TypeError, KeyError):
            return {}

The important thing is the last line of create function. it returns serializer.data which means that we have to create custom ModelSerializer class and override data property. All model related serializers will inheritate from that class. Good luck!

It's commit #4

{
    "errors": {},
    "success": true,
    "data": {
        "id": "955a718b-f5c0-4750-9984-235484c9f7dd",
        "username": "user5",
        "first_name": "",
        "last_name": "",
        "email": "",
        "auth_token": "8bed729e6a1eeebaca93036670e774441d29e775"
    }
}

But wait what about other serializers? I think we will also use other serializers (not only ModelSerializer) so let it be more dynamic (commit #5)

class DefaultModelSerializer(BaseSerializer, ModelSerializer):
    pass

Look at the code. It says that DefaultModelSerializer inheritated from (first) BaseSerializer then (second) ModelSerializer. It's very useful to know the workflow of inheritance in python. So read about it, hope you will find and learn about C3 linearization.


Fix authentication response

If you do POST request to localhost:8000/api-token-auth/ the response will be like this:

{
    "token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2V..."
}

It's wrong cause we said that our responses will have other structure. So let's fix it. Read the documentation again and try to find something useful.

Read about _JWT_RESPONSE_PAYLOAD_HANDLER _in additional-settings section. Then try to do fix authentication response by yourself. Then check with my version of commit #6.

There is another problem when you try to login with invalid data, since response will be like this:

{
    "non_field_errors": [
        "Unable to log in with provided credentials."
    ]
}

So to solve this problem look at the urls.py file find which view is handles /api-token-auth POST request. You will find ObtainJSONWebToken inherited from JSONWebTokenAPIView _and JSONWebTokenAPIView _has post function, which is called for our POST request. Try to find something can help you:

def post(self, request, *args, **kwargs):
    serializer = self.get_serializer(data=request.data)

    if serializer.is_valid():    # THIS IS WHAT WE ARE LOOKING FOR
        user = serializer.object.get('user') or request.user
        token = serializer.object.get('token')
        response_data = jwt_response_payload_handler(token, user, request)
        response = Response(response_data)
        if api_settings.JWT_AUTH_COOKIE:
            expiration = (datetime.utcnow() +
                          api_settings.JWT_EXPIRATION_DELTA)
            response.set_cookie(api_settings.JWT_AUTH_COOKIE,
                                token,
                                expires=expiration,
                                httponly=True)
        return response

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

We have found that it calls serializer.is_valid() function. So let's change it. Here is my commit #7.

But wait look at the response carefully

{
    "data": {},
    "success": false,
    "errors": {
        "non_field_errors": [
            "Unable to log in with provided credentials."
        ]
    }
}

Hope that you have remarked that for some invalid data error message put into _non_field_errors _field. It's not followwing our convention, so try to fix it. (hint read documentation for DRF).

Hope you have found this. So just adding one line of code fixes our problem: commit #8


results matching ""

    No results matching ""