Django Channels, WebSocket Authentication Deployment

Let's create a simple application with Django Channels. In this article, we will create a simple chat application. Users can register, log in, and chat. We will be using JWT to create a token for authentication, we will hash the password before saving it to the database. We will deploy that app to Heroku as well. 


Prerequisite

We expect you to have a basic understanding of the Django framework. Some understanding of JavaScript and DOM manipulation. If you have some basic understanding, you won't have difficulty understanding this article. 


What tools are we using for the application?

  • Django
  • Django Channels
  • WebSocket
  • pyjwt (python JSON Web Token)
  • daphne

Let's get started.


Django Project Setup

Let's first verify you have Django installed. Run python -m django --version. You should get some version numbers back, if you do not get it, install Django.

python -m pip install Django


Let's start a project. django-admin startproject pubchat Here it will create a project named pubchat. (Public Chat). It will create a directory named pubchat, and you can find the manage.py there. Let's create an app for chat. python3 manage.py startapp chat.


As we have created the chat app, we need to add this on the INSTALLED_APPS, in settings.py


INSTALLED_APPS = [
...
...

'chat',
]


Create templates directory in chat; add index.html there. Add some random HTML message there for now.


Define view for the index in views.py and point to that index.html. The render will look on 'templates/' directory


from django.shortcuts import render

# Create your views here.
def index(request):
return render(request, 'index.html')


Now, let's define the urls.py in the chat app to forward the request to the index view.

from django.urls import path

from . import views

urlpatterns = [
path('', views.index, name='index'),
]


Since we are using some static files, (CSS and JS for some design and WebSocket). Let's add on settings to load static files as well.

STATICFILES_DIRS = [
BASE_DIR / "static",
]

STATIC_URL = '/static/'


Now, on the parent (project's) urls.py we will forward the request to "/" to the chat app. and add static settings as well.


from django.contrib import admin
from django.urls import path

from django.conf.urls import include
from django.conf.urls.static import static
from django.conf import settings

urlpatterns = [
path('admin/', admin.site.urls),
path('', include('chat.urls')),
] + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)


Now, run python manage.py runserver, the server should be started on http://localhost:8000/. On browsing to localhost:8000/, you should get the index.html you had created earlier. 

No worries, if your code not running as expected, read thoroughly through this article to understand the flow of the code, We will provide the GitHub repository with each section on a separate branch.


User Model and Hashing

As we are talking about Authentication, we should save the users in the Database. So let's create the user model. For simplicity, every user will have a name, username, and password only. Wait, ya obviously the ID as well.


from django.db import models

from django.contrib.auth.hashers import make_password, check_password

# Create your models here.
class User(models.Model):
user_name = models.CharField(max_length=30, unique=True)
name = models.CharField(max_length=30)
password = models.CharField(max_length=30)

def __str__(self):
return '{\n\tid: %s\
\n\tuser_name: %s\
\n\tname: %s\
\n\tpassword: %s\n}' % (self.id, self.user_name, self.name, self.password)


# for testing
def create_user():
print("create user called")
a_pass = make_password("sita")
user = User(name="Ram", user_name="ram2", password=a_pass)
user.save()
print(user)

# create_user()

def validate_password():
user = User.objects.get(user_name="ram2")
print(str(user))
pass_matched = check_password("sita", user.password)
pass_matched2 = check_password("sita123", user.password)
print(pass_matched)
print(pass_matched2)

# validate_password()



We will have a name and username in plain text and saved as it is in the database. But we won't save the user's password as plain text, we need to hash it before saving it on the database. But how can we validate it during login? We will hash the user-entered password and compare it with the hashed password in the database.

That is where the make_password, check_password method comes in; make_password will create a hash out of the plain text; check_password compares the user-entered password with the password in the database. It will return True if matched, false if not.


Django Channels Setup

Now, let's set up Django Channels in the project. 

Make sure you have channels installed. python -c 'import channels; print(channels.__version__)'. You will get the version number back. If you do not get such a response you can install Django Channels

    python -m pip install -U channels


Now, add 'channels' in the INSTALLED_APPS.


INSTALLED_APPS = [
...
...

'channels',

'chat',
]


Now, let's create a consumer for the WebSocket request. We will create two consumers. auth consumers, in which the public can join, and chat consumer, where authenticated users can only join it. We will use the auth consumer to register and log in.

Then, use the token from the login to authenticate the chat server.

I will share part of the code and explain it, please find the full source code in the git repository. 


For Register

We will get the name, username, and password from the user and create a user based on that data


a_pass = make_password(password)
user = User(name=name, user_name=user_name, password=a_pass)


Since the user_name is to be unique, it will throw an error if it's not. We will catch that and send an error message as well. On success, we will send a success message.


For Login

We will get the username and password, compare the password, if matched we will create a jwt token and return the jwt to the client.


As we are using the jwt, let's install a package called 'pyjwt'.

    pip install pyjwt

user = False

try:
user = User.objects.get(user_name=user_name)
except:
# nothig
print("user not found")
pass_matched = check_password(password, user.password)

if(not pass_matched):
# notify password did not matched.
return
# print(user)
token = jwt.encode({
'id': user.id,
"userName": user.user_name,
"name": user.name,
}, "secret", algorithm="HS256")

# pass the token as well as user information back.

# for test
tokenData = jwt.decode(token, "secret", algorithms=["HS256"])
user_id = tokenData['id']
# print(user_id)



Here, we will try the token as well, just for us to make sure, we can decode the code and can get the information from the token. Remember, the "secret" should be a secret token, that should not be shared. else anyone can create the token and put any arbitrary data there.


jwt.encode() will take 3 params, payload (the data to sign), the secret and the algorithm and give a long string token.

jwt.decode() will take 3 params, the token, the secret (same) and the algorithm and return back the payload on that token.


create routing.py


from django.urls import re_path

from . import auth_consumers
from . import chat_consumers

websocket_urlpatterns = [
re_path(r'ws/auth/', auth_consumers.AuthConsumer.as_asgi()),
]


Update asgi.py and include the HTTP and WebSocket protocol.


application = ProtocolTypeRouter({
"http": get_asgi_application(),
"websocket": AuthMiddlewareStack(
URLRouter(
chat.routing.websocket_urlpatterns
)
),
})


We will update the settings.py as well to include the ASGI_APPLICATION and CHANNEL_LAYERS


CHANNEL_LAYERS={
"default": {
"BACKEND": "channels.layers.InMemoryChannelLayer"
}
}

ASGI_APPLICATION = 'pubchat.asgi.application'


Here, in the Channel Layer, we will add the store to save the users and other information, to facilitate the flow of data. Here, we will use InMemoryChannelLayer, which is the memory (the RAM). the production alternative is Redis.

InMemory channel will work but it is not scalable. and the data will be lost on the server restart.


Implement JWT Authentication with Django Channels

Now, our users are able to register and log in. In login, they will get the token. Thus, we will use that token to authenticate the users.


If you remember the 'asgi.py' file we have already used the 'AuthMiddlewareStack', which supports standard Django authentication, where the user details are stored in the session.


As we have our own users and the token to validate, we need to define our own middleware.


import jwt

class UserAuthMiddleware:
def __init__(self, app):
# Store the ASGI application we were passed
self.app = app

async def __call__(self, scope, receive, send):

token= False
query = scope["query_string"]
if(query):
query = query.decode()
if(query):
query = query.split('=')
if(query[0] == 'token'):
token = query[1]
if(token):
# jwt token let's decode it
tokenData = jwt.decode(token, "secret", algorithms=["HS256"])
# save it on user scop
scope['user'] = tokenData

return await self.app(scope, receive, send)


Here, we will get the query string, if the query exists, we will decode it. Then we will split it on '='. Now, if the first key is a token, we will get the token from it.


Now, we will decode the token, get the information and add that information to the user key in the scope object or we call dictionary in python. :) .


Now, in the chat consumer's connected method we will check for the user in the scope. if the user did not exist, we will accept the request, send the message saying you are not authenticated. then close the connection right away.


We accept the request of the authenticated user.


user = self.scope.get('user', False)

if(not user):
print("Un authorized user")
self.accept()
self.send(text_data=json.dumps({
'type': 'message',
'data': {
'message': "Not Authenticated.."
}
}))
self.send(text_data=json.dumps({
'type': 'noAuth',
}))
self.close()
return


# print(user)
self.user = user
async_to_sync(self.channel_layer.group_add)(
"pubchat",
self.channel_name
)
self.accept()


Then, we will add that user to pubchat group as well, so we can forward any other even to that group.


Then, we will update the asgi.py to use our own middleware


import os

from channels.auth import AuthMiddlewareStack
from channels.routing import ProtocolTypeRouter, URLRouter
from django.core.asgi import get_asgi_application
import chat.routing

from chat.middleware.userAuthMiddlware import UserAuthMiddleware

os.environ.setdefault("DJANGO_SETTINGS_MODULE", "pubchat.settings")

# application = get_asgi_application()
application = ProtocolTypeRouter({
"http": get_asgi_application(),
"websocket": UserAuthMiddleware(
URLRouter(
chat.routing.websocket_urlpatterns
)
),
})


We will get the message from client and forward it to all other users as well. For that we make use of that group.

def receive(self, text_data):
text_data_json = json.loads(text_data)
# print(text_data_json)

eventType = text_data_json['type']

if eventType == 'message':
message = text_data_json['data']['message']
my_date = datetime.utcnow()

async_to_sync(self.channel_layer.group_send)(
"pubchat",
{
'type': 'new_message',
'data': {
'sender': self.user,
'message': message,
'date': my_date.astimezone().isoformat()
}
}
)
def new_message(self, event):

# print(event)
self.send(text_data=json.dumps({
'type': 'newMessage',
'data': event['data']
}))


The new_message will be called for each user on the group "pubchat", and will forward that message to their client-side.


Deployment

We will make the deployment as well. We will deploy this project to Heroku. 

For that, we will create 'Procfile' with contents


web: daphne pubchat.asgi:application --port $PORT --bind 0.0.0.0 -v2


We will deploy it on Heroku by connecting our GitHub account to Heroku.

We need to create requirements.txt as well, so Heroku installs the necessary packages. 


Using Postgres of Heroku

Heroku provides a Postgres Database, and SQLite won't work. We should use the env "DATABASE_URL" from Settings > Config Vars. You can parse the URL and define the DATABASE yourself, or you can use packages like 'django-heroku' or 'dj_database_url'. 


Github Repository

Here, are the Github branches for each section. You can check the branches and see the subsequent development of the application.


1_Initial_Setup
2_UserModel_Password_Hash
3_Channels_Setup_Register_Login_JWT
4_Implement_JWT_Authentication_Add_Chat_Update_Design
5_Fixes_Heroku_Deploy
* main


If you find any bug or need improvement, pull requests are welcome.


Conclusion

In this article, we start our basic Django application. We learn about the user model and how to save passwords. and how to compare the hashed password. Then we set up the Simple Django application, and learn about JWT. Then, we implement the authentication using the token from JWT. And finally, we deploy it in Heroku. 


Posted By: Sagar Devkota

0 Comments