Saturday, November 16, 2024
Google search engine
HomeLanguagesToken Authentication in Django Channels and Websockets

Token Authentication in Django Channels and Websockets

Prerequisites: Django, WebSockets, Django channels, Token authentication

The most popular Django topics right now are WebSockets and Django channels because they make it possible to communicate quickly and easily while working in real-time without constantly refreshing the page. When working with the Django templating engine alone and not the REST framework, the authentication method that Django channels employ is session authentication. The user who is currently logged in is also reflected in the Django channels auth system.

// This is not possible in websockets .

var socket = new WebSocket(“path_of_the_socket” , headers = { “Authorization’: “token token_string” })

Well, using web-based authentication is one way you could reach your goal. Returning a cookie to the website allows you to connect to the WebSocket server once more with the cookie in hand, authenticate, and then continue. However, cookie-based authentication can be extremely unreliable and have various problems. Therefore, to address this issue, we can convey the authentication token from the socket’s query parameters and then build a new authentication stack that will sit on top of the existing ones when routing WebSocket connections.

However, working with the rest framework with token authentication or JWT authentication is not simple for WebSockets and channels; as a result, it can be challenging to log in to the user via channels and sockets when using token authentication or the rest framework. There is no way you can send auth/JWT tokens to the headers of a WebSocket like that of a simple post or get a request in the Authorization header.

Note: This process is totally asynchronous because WebSockets and channels work with ASGI (Asynchronous server gateway interface). 

Setting up our Project

File Structure

This is the file structure after completing all the steps in this article.

 

Step 1: Create a virtual environment and activate it.

Step 2: Install Django and start your project.

Step 3: Create a project named TokenAuth

django-adminb startproject TokenAuth

Step 4: Start an app named token_auth_app by moving to the project folder.

cd TokenAuth
python manage.py startapp token_auth_app

Step 5: Add your app to the installed apps in setting.py.

'token_auth_app.apps.TokenAuthAppConfig'

Step 6: To Set up Django channels for the backend part. Install Django-channels

python -m pip install -U channels

Step 7: Add channels to the installed apps.

Python3




INSTALLED_APPS = [
    'token_auth_app.apps.TokenAuthAppConfig',
    'channels'
]


Step 8: Installing the REST framework is required to create token authentication for backend applications. For token authentication, the rest framework’s auth token package, which contains a Token model, is used.

pip install django-rest-framework

Step 9: Install cors.

pip install django-cors-headers

Step 10: Add both to the INSTALLED_APPS and also token auth package.

Python3




INSTALLED_APPS = [
    'token_auth_app.apps.TokenAuthAppConfig',
    ...
    'channels',
    'rest_framework',
    'rest_framework.authtoken',
    'corsheaders',
]


Step 11: Set ALLOWED_HOSTS to *, Also, add the cors middleware to the MIDDLEWARE list The cors setup is done so that the host allows all the origins and we can communicate with the backend application.

Python3




ALLOWED_HOSTS = ["*"]
 
CORS_ALLOW_ALL_ORIGINS = True
 
 
MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
 
    # -----------------------------------------
    'corsheaders.middleware.CorsMiddleware',
    # -----------------------------------------
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
]


Code Implementation

Setting up files one by one:

token_auth_app/consumers.py file: 

In the app folder create a consumers.py that will deal with all the incoming receive and send commands from the WebSocket

Python3




from channels.generic.websocket import AsyncJsonWebsocketConsumer
 
class TokenAuthConsumer(AsyncJsonWebsocketConsumer):
    async def connect(self):
        await self.accept()
        print(self.scope["user"].username)
        print(self.scope["user"].email)
 
    async def disconnect(self, close_code):
        ...
 
    async def receive_json(self, message):
        command = message.get("command")
        if command == "Say hello !":
            print(message["data_string"])
            await self.send_json({
                "command_response": "The command to \
                say hello was received ",
                "data_string_bacK": message.get
              ("data_string", None)
            })


Code explanation:

We have created a Consumer with the class name TokenAuthConcumser as it consumes events sent and received by the ASGI application which we will see create later. Here, we inherit AsyncJsonWebsocketConsumer, which is the Parent Consumer class. In the AsyncJsonWebsocketConsumer module, we have send_json and receive_json functions which accept in JSON and send JSON without dumping JSON.

  1. connect function: The first and foremost function that is executed in order to establish a connection is the one that takes the WS to connect request, thus we must connect it. The scope is the dictionary, which functions similarly to the request parameter in function-based views (def fun(request)) in that it contains all the information about the incoming connection. user is accessed in scope as a dict object scope[“user”], which will provide information on the user who is currently logged in. The user’s username and email are then retrieved and displayed.
  2. disconnect function: Takes the close_code parameter and then does nothing. 
  3. receive_json function: We must implement the receive method in AsyncWebsocketConsumer. then, in order to retrieve items, we must load the data (json.loads). However, you have direct access to the stuff here. This procedure uses the message sent from the front end. We will send this data as given below:

Javascript




{
    "command" : "Say hello !" ,
    "data_string" : "This is the data string !" ,
}


As this data is received, we interpret it and send responses accordingly in the front end. 

token_auth_app/middlewares.py file:

It will have the custom auth which we will create and the custom stack will be superposed above all the stacks.

Python3




from rest_framework.authtoken.models import Token
from urllib.parse import parse_qs
from channels.db import database_sync_to_async
from django.contrib.auth.models import AnonymousUser
 
 
@database_sync_to_async
def returnUser(token_string):
    try:
        user = Token.objects.get
            (key=token_string).user
    except:
        user = AnonymousUser()
    return user
 
 
class TokenAuthMiddleWare:
    def __init__(self, app):
        self.app = app
 
    async def __call__(self, scope, receive, send):
        query_string = scope["query_string"]
        query_params = query_string.decode()
        query_dict = parse_qs(query_params)
        token = query_dict["token"][0]
        user = await returnUser(token)
        scope["user"] = user
        return await self.app(scope, receive, send)


Code explanation:

The uses of different modules are described below:

  1. Token: This is used for token auth validation.
  2. parse_qs: It is used for parsing the query parameters from string to dict.
  3. database_sync_to_async: It is used for retrieving the data from the database as Django ORM is totally synchronous.
  4. AnonymousUser: This helps if we don’t find the user based on the token, i.e. returnUser function returns the user related to the token passed to the function else it returns the AnonymousUser. 

In the __init__ function we define the current instance app to the app which is passed into the stack. Next, the async __call__() function takes three parameters scope, receive and send. The scope is the dictionary which has all the data about the connection as stated above. send and receive are the commands for sending and receiving commands. Every ASGI application has three parameters environ, send and receive, here environ is the scope var. The query_string is the string passed from the front request, For example:

ws://localhost:8000/?token=fkdsahjkfshiadjkfhoasdfk"

The query string that is provided in the query string variable in scope is token=fkdsahjkfshiadjkfhoasdfk. It is sent in bytes as a string. This indicates that the query string’s type is bytes.
The byte string is converted to a python string using a query string.decode(). p Parse qs(query params), on the other hand, decodes the string and adds it to the Python dictionary. The dict is presented as a key-value pair. Therefore, we access the token key, then access the token’s first element, token=qury_dict[“token”][0], and finally, we visit the returnUser method to obtain the user. and we set it to the scope’s “user” property before returning the application.

The interesting part about this scope variable is that you can completely change it and make any changes you desire. This entails that you are free to change the variables in the scope parameter as well as add new ones.

for adding, just add like a normal Python dictionary, i.e. scope[“some_another_key”] = “value_for_the_key”,

for deleting, del scope[“some_key_that_exist”]

TokenAuth/routing.py file:

It will create the ASGI application which will be served and which will route all the asynchronous WebSocket URLs to the respective consumers.

Python3




from channels.routing import ProtocolTypeRouter, URLRouter
from channels.security.websocket import AllowedHostsOriginValidator
from django.urls import path
from token_auth_app.consumers import TokenAuthConsumer
from token_auth_app.middlewares import TokenAuthMiddleWare
 
application = ProtocolTypeRouter(
    {
        "websocket": TokenAuthMiddleWare(
            AllowedHostsOriginValidator(
                URLRouter(
                [path("", TokenAuthConsumer.as_asgi())]
                )
            )
        )
    }
)


Code explanation:

The uses of different modules are described below:

  1. ProtocolTypeRouter: This routes the server of the whole application based on the type of protocol. If the protocol is HTTP it serves the Django WSGI app and if WS then the ASGI app. 
  2. URLRouter: This routes the WS connection routes. 
  3. AllowedHostsOriginValidator: This security stack is used to validate the connection and data acceptance only from the hosts which are allowed in the settings.py file of the Django application.
  4. path: Routing the path.
  5. TokenAuthConsumer: To serve the consumer instance for a particular connection. 
  6. TokenAuthMiddleware: For setting user instance using token passed to the connection. 

Then, set the stacks for serving the ASGI app in the application variable. Keep in mind that we make use of TokenAuthMiddleWare, and that’s where we can set the user using the token. In the token auth app middlewares.py, we recently developed that class.
You may have noticed that we do not use AuthMiddlewareStack. This is because it is helpful when serving the ASGI app from the same application or templating engine. It stores the data in the sessions.

ASGI Application and Channel Layers in settings.py

Set your ASGI_APPLICATION will serve the required consumers and route. Whereas in CHANNEL_LAYERS, we are using InMemoryChannel this is for a development environment.

Python3




WSGI_APPLICATION = 'TokenAuth.wsgi.application'
ASGI_APPLICATION = 'TokenAuth.routing.application'
 
 
CHANNEL_LAYERS = {
    "default": {
        "BACKEND":
      "channels.layers.InMemoryChannelLayer",
    }
}


token_auth_app/signals.py

This file creates a token object for each and every user that will be created and this signal.py will be imported by the apps.py file in the ready function.

Python3




from django.dispatch import receiver
from django.contrib.auth.models import User
from django.db.models.signals import post_save
from rest_framework.authtoken.models import Token
 
 
@receiver(post_save, sender=User)
def create_token(sender, instance, created, **kwargs):
    if created:
        token = Token.objects.create(user=instance)
        token.save()


token_auth_app/apps.py

Now after setting all the application’s backend, and database, this will add the token model, sessions, and rest framework into work. 

Python3




from django.apps import AppConfig
 
 
class TokenAuthAppConfig(AppConfig):
    default_auto_field =
        'django.db.models.BigAutoField'
    name = 'token_auth_app'
 
    def ready(self):
        import token_auth_app.signals


token_auth_app/views.py

Here we will create a login page for the frontend part and from there we will make a post request with username and password then the backend view will validate whether the user is there or not, if the user is there then the token will be sent from the backend in the response and we will create a socket connection from there. For this, we will need a views.py which will have will handle login and send the token. 

The uses of different modules are described below:

  1. Response: This is the response object which is sent from the API view
  2. api_view: It converts the normal Django view to the API view.
  3. authenticate: This will return the user, based on the username and password.

Python3




from rest_framework.response import Response
from rest_framework.decorators import api_view
from rest_framework.authtoken.models import Token
from django.contrib.auth import authenticate
 
 
@api_view(["POST"])
def loginView(request, *args, **kwargs):
    username = request.POST.get("username")
    password = request.POST.get("password")
    try:
        user = authenticate(username=username,
                            password=password)
    except:
        user = None
    if not user:
        return Response({
            "user_not_found": "There is no user \
            with this username and password !"
        })
    token = Token.objects.get(user=user)
    return Response({
        "token": token.key,
    })


TokenAuth/urls.py

 We need to set the URL for this page in Project urls.py add this URL to the loginview with URL route = “api/login/”. This file will handle the backend part for the login and will return a response on the basis of the username and the password.

Python3




from django.contrib import admin
from django.urls import path
from token_auth_app.views import loginView
 
urlpatterns = [
    path('admin/', admin.site.urls),
    path("api/login/", loginView),
]


It will look like this:

 

testing_html.html file 

After submitting a post request with a username and password, the file will be used to store the token that was received upon successful authentication. The TokenAuthMiddleWare in middlewares.py will assist us with this. Then, using Django channels, we will connect to a WebSocket to the backend and authenticate using the token.

HTML




<!DOCTYPE html>
<html>
    <head>
        <title>Token Auth</title>
    </head>
    <body>
        <input type ="text" name = "username" id = "form_username" /><br>
        <input type = "text" name = "password" id = "form_password" /><br>
        <button onclick = "loginViaBackend()"> Login </button>
     
 
        <script>
            function createSocket(token) {
                ws_path = "ws://localhost:8000/?token="+token ;
                socket = new WebSocket(ws_path) ;
 
                socket.onopen = function(e) {
                    console.log("Connected") ;
                    socket.send(
                        JSON.stringify(
                            {
                                "command" : "Say hello !" ,
                                "data_string" : "This is the data string !" ,
                            }
                        )
                    ) ;
                }
                 
                socket.onclose = function(e) {
                    console.log(e) ;
                }
                 
                socket.onmessage = function(data) {
                    data = JSON.parse(data.data) ;
                    console.log(data) ;
                    console.log(data["command_response"])
                }
            }
 
            function loginViaBackend() {
                console.log("The function was run !") ;
                var username = document.getElementById("form_username").value ;
                var password = document.getElementById("form_password").value ;
                 
                console.log(username) ;
                console.log(password) ;
 
                payload = {
                    "username" : username ,
                    "password" : password ,
                }
                 
                $.ajax(
                    {
                        "type": "POST" ,
                        "dataType" : "json" ,
                        "url" : "http://localhost:8000/api/login/" ,
                        "timeout" : 5000 ,
                        "data" : payload ,
                        "success" : function(data) {
                            console.log(data) ;
                            token = data["token"] ;
                            createSocket(token) ;
                        },
                        "error" : function(data) {
                            console.log(data) ;
                        }
                    }
                ) ;
            }
        </script>
    </body>
</html>


Code explanation: 

Once you click the login button the function loginViaBackend function is run and it makes a post request to the api/login URL which we have just created in the backend. (we are using ajax for making the post request), In the loginViaBackend function, we access the values of the username and the password and then send it via the post request. After the successful request we get the token from the backend as the successful request of a username and the password returns the token. 

Create a superuser 

python manage.py createsuperuser

I have created a superuser named, testuserone with password testing123, and suraj with password 123.

 

Migrate database

With the token, we run the function name createSocket(token), this takes and creates a WebSocket request to the backend server. Before running this part make sure to makemigrations and migrate the database. 

python manage.py makemigrations
python manage.py migrate

Run the server in the localhost with port 8000 and create a socket connection with a token parameter in the query params of the request. 

python manage.py runserver

path for WS connection, 

ws_path = "ws://localhost:8000/?token="+token ; 

The token is passed from the success parameter of the ajax post request. Then we create WS connection, once you get connected you will receive the data which was sent by the consumer. 

Output: 

First, we entered the correct login and password, which caused the token id to appear; second, we entered the incorrect password, which prevented the token from appearing.

 

To open the HTML file just copy the path of the file and paste it into your browser, or go to the folder and double-click on the file to launch the HTML file.

 

Dominic Rubhabha-Wardslaus
Dominic Rubhabha-Wardslaushttp://wardslaus.com
infosec,malicious & dos attacks generator, boot rom exploit philanthropist , wild hacker , game developer,
RELATED ARTICLES

Most Popular

Recent Comments