Tracking User Login Activity in Django Rest Framework: JWT Authentication

Using Django login signals with JSON Web Token Authentication

The first choice when it comes to creating REST API’s for your web app in Python/Django is Django Rest Framework(DRF), a powerful toolkit that integrates well with Django. DRF comes with various Authentication mechanisms like Basic Authentication, Token Authentication, Session Authentication etc. and can be easily customized to support your own Authentication mechanism. The commonly used and recommended method for authentication in client-server configurations is token-based authentication. This authentication scheme validates the credentials provided by client and issues a signed token. This token is sent with every request to the server which verifies it and responds accordingly. For a better understanding of token-based authentication check this.

source

JSON Web Token(JWT) Authentication is a fairly new standard for token-based authentication. Unlike the token-based Authentication, which generates token and saves it to the database, JWT Authentication doesn’t need to store tokens in the database. Read more about JWT Authentication here.

In one of my recent projects, I used django-rest-framework-jwt to implement JWT Authentication mechanism. I found it easy to use as it comes with a lot of configurations which can be tweaked according to your needs. Things were going fine until I got a use case to track user login activity. This use case required tracking the various data related to user login event like whether login failed or succeeded, what is login IP, login email, info related to user-agent and login timestamp. The best way to handle this in Django is to go by the Signals.

Understanding Django Signals

  • Django signals implement Observer Design Pattern wherein it emits signals when certain events occur and all the receivers listening to that signal get notified.
  • Django provides various signals like pre_save, post_save, request_started, request_finished etc. On login event user_logged_in, user_login_failed and user_logged_out are the signals which are emitted. user_logged_in and user_login_failed are the two signals which serve our purpose.
  • As the name suggests, the user_logged_in signal is emitted when a user is authenticated and logged in successfully. If authentication attempt fails user_login_failed signal is emitted.
  • The user_logged_in signal is emitted by login() method of django.contrib.auth and user_login_failed is emitted by authenticate() method of django.contrib.auth

However, since we are doing JSON Web Token Authentication using django-rest-framework-jwt which doesn’t use the login() method from django.contrib.auth, it doesn’t emit the user_logged_in signal. It calls the authenticate() method to validate the credentials and then issues the JSON Web Token. Therefore, in order to make it work, I created a custom view and serializer which issues the token after authentication and emits the user_logged_in signal. Rest of the article contains the step-by-step implementation details of customizing django-rest-framework-jwt to track user login activity using signals.

Step One: Create a model to track login activity

We need to have our Django project setup with Django Rest Framework and django-rest-framework-jwt installed and configured. Now, we will create our UserLoginActivity model in our models.py file.

UserLoginActivity Model (model.py)

Step Two: Create a custom serializer

The JSONWebTokenSerializer of rest_framework_jwt.serializers is the serializer responsible for authenticating the user and returning the token. We will create a custom serializer which will inherit the JSONWebTokenSerializer and will override the validate() method to emit the user_logged_in signal on successful authentication. Let’s name this serializer as JWTSerializer:

JWTSerializer (serializer.py)

Step Three: Create a custom view

Create a custom view ObtainJWTView which will inherit from rest_framework_jwt.views.ObtainJSONWebToken and will set the serializer _class to the JWTSerializer which we have created.

ObtainJWTView (views.py)

Step Four: Create the route for login

We have our view and serializer ready to be used. Now, we have to create an URL to obtain jwt token in our url.py file.

urls.py

Let’s test the work we have done till now. Create a user using manage.py shell:

user@host> manage.py shell
>>> from django.contrib.auth.models import User
>>> user=User.objects.create_user('John', password='password123')
>>> user.is_superuser=False
>>> user.is_staff=False
>>> user.save()

Start the development server and hit the login endpoint http://127.0.0.1:8000/login/ . Enter the login credentials and send the post request. If everything works fine you will get a JSON Web Token in response:

{
"token":"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoiMzgwNjlkZTQtYjU4OC00MzYwLWFkYTQtMGI5NmI5NTk2NzJiIiwidXNlcm5hbWUiOiJyYXZpMkBscmQuY29tIiwiZXhwIjoxNTE0NzIwNDc2LCJlbWFpbCI6InJhdmkyQGxyZC5jb20ifQ.l7_-bVFGsJwEDqrcZq7oKAhSnOk9HhKmXa8cM-X0buI"
}

Step Five: Create receiver for user_logged_in signal

As of now, we have our login endpoint working with the user_login_signal and user_login_failed signals being emitted properly. We will now write the code to handle these signals and keep our signal handling code in a separate signals.py file inside our app.

Receiver function to handle user_logged_in signal (signals.py)

Here, the log_user_logged_in_success() function is registered as a receiver to the user_logged_in signal using the receiver() decorator. When a user is successfully authenticated our receiver function log_user_logged_in_success() will be called with the following arguments:

  • sender: specifies a particular sender of the signal.
  • user: User model object
  • request: the request object
  • kwargs: extra keyword arguments

get_client_ip() is our helper function which will return the client IP from the request object.

helper function to get client IP (signals.py)

Finally, create a model object of our UserLoginActivity model and save it to the database.

Step Six: Create receiver for user_login_failed signal

The user_login_failed signal receiver can be written in a similar manner as we have done for the user_logged_in signal receiver. The only difference is that now we would not get the user object as an argument, instead, we will get a dictionary containing the credentials used for login.

Receiver function to handle user_login_failed signal (signals.py)

Final Step:

As a final step, we need to import signals in the ready() method of our AppConfig class in app.py file in order to make them discoverable and specify the app config file in the <app>/__init__.py file as follows:

default_app_config = 'activitylog.apps.ActivitylogConfig'

AppConfig (app.py)

Voila! Our User Login Activity task has been completed. Start the development server and hit the login endpoint http://127.0.0.1:8000/login/ . Try out this endpoint with some valid and invalid credentials. You can check UserLoginActivity model entries by registering it in django admin panel or by browsing the database.


Notes:

  • After writing our custom view and serializer to emit the signal properly we can also use django-axes for tracking the access log. This will reduce the amount of work in our model.py and signals.py file.
  • Complete code for this blog is available on my Github repo: user-login-activity

References:

If this helped you, don’t forget to clap it up and share it with others to find!