Django Channels Token Authorization

Alex Zhydyk
Python in Plain English
4 min readMay 1, 2024

--

Authentication and authorization are fundamental features in the majority of applications. Its implementation has to pay attention because of many requirements like security, performance, integration and so on.

When building a general app using the REST architecture, implementing authentication and authorization over HTTP connections is straightforward.

The two most popular options are:

  • Saving the user session in a Http-Only cookie (stateful).
  • Setting the authorization token in the request header (stateless).

But if your app also uses a WebSocket connection, then some trouble can appear.

The WebSocket protocol is build on top of HTTP. The WebSocket connection starts with an HTTP handshake, where the client sends an HTTP request to the server requesting to upgrade the connection to a WebSocket connection. If the server supports WebSocket, it responds with an HTTP 101 status code (Switching Protocols), indicating that the connection has been successfully upgraded to a WebSocket connection. After the handshake, the communication switches to the WebSocket protocol, and both the client and server can exchange messages over the same TCP connection in a full-duplex manner.

Send the token in cookies

Based on this information, we might assume that if the websocket initiates the connection over HTTP, we can use the same authorization options as for the rest of the HTTP requests in our app.

This is true if your app uses session-based authorization. When the WebSocket connection request is sent, the browser also sends cookies associated with the requested domain. You can use these cookies to authorize a user before upgrading the connection.

This option is available from the box in Django channels package.

from django.urls import re_path

from channels.routing import ProtocolTypeRouter, URLRouter
from channels.auth import AuthMiddlewareStack
from channels.security.websocket import AllowedHostsOriginValidator

from myapp import consumers

application = ProtocolTypeRouter({

"websocket": AllowedHostsOriginValidator(
AuthMiddlewareStack(
URLRouter([
re_path(r"^front(end)/$", consumers.AsyncChatConsumer.as_asgi()),
])
)
),

})

What about the second authorization option?

Unfortunately it won’t work because of browsers WebSocket API doesn’t have an option to set needed authorization header in the handshake request.

How to overcome this?
There are a few possible solutions:

Pass the token as a parameter

  • Utilize a query string to pass the token as a parameter and then validate it on the server. To implement this in Django Channels, create a custom middleware and use it instead of the original AuthMiddlewareStack .
    For some cases it will work, but using the query_string is not secure enough. The request can be logged on the server, potentially exposing the token.
from channels.security.websocket import WebsocketDenier
from rest_framework_simplejwt.exceptions import InvalidToken, TokenError


class TokenAuthMiddleware:
"""
Custom middleware that takes a token from the query string and authenticates it.
"""

def __init__(self, app):
self.app = app

async def __call__(self, scope, receive, send):
token = ""
query_string = parse_qs(scope["query_string"].decode("utf8"))
if "token" in query_string:
token = query_string["token"][0]
try:
scope["user"] = await get_authenticated_user(token)
except (InvalidToken, TokenError, AuthenticationFailed, DecodeError) as e:
denier = WebsocketDenier()
return await denier(scope, receive, send)
return await self.app(scope, receive, send)
  • As we need to pass authorization token one time when the connection established we can create an ephemeral token that is not related with the token for HTTP requests authorization.
    For this, we need to create a HTTP route that will return generated token. When the WebSocket request comes we should validate previously saved token on validity, authorize the user and delete the token from database.
    You may to write this functionality by yourself or use some package like django-channels-jwt.

Send the token in Sec-WebSocket-Protocol header

According to browsers WebSocket API the client can set one header Sec-WebSocket-Protocol.

Of course, the purpose of this header is not to transfer authorization tokens, but utilizing it in other ways is not forbidden either.

Lets implement this option.

import copy

from channels.security.websocket import WebsocketDenier
from rest_framework_simplejwt.exceptions import InvalidToken, TokenError


class TokenAuthMiddleware:
"""
Custom middleware that takes a token from the subprotocols and authenticates it.
"""

def __init__(self, app):
self.app = app

async def __call__(self, scope, receive, send):
token = self._get_token(scope)
scope_cp = copy.deepcopy(scope)
try:
scope_cp["user"] = await get_authenticated_user(token)
except (DecodeError, TokenError, InvalidToken, ValueError):
denier = WebsocketDenier()
return await denier(scope, receive, send)
return await self.app(scope_cp, receive, send)

def _get_token(self, scope) -> str:
token = ""
if scope.get("subprotocols"):
token = scope["subprotocols"][1]
return token

But if we run the server and make a request

const socket = new WebSocket(
"ws://localhost:8000/ws/stream/",
["Authorization", "auth_token"]
);

// Event listeners for WebSocket lifecycle events
socket.onopen = () => {
console.log("WebSocket connection established.");
};

socket.onmessage = (event) => {
console.log("Message received:", event.data);
};

socket.onerror = (error) => {
console.error("WebSocket error:", error);
};

socket.onclose = (event) => {
console.log("WebSocket connection closed:", event.code, event.reason);
};

We will see an error Error: Server sent no subprotocol because originallySec-WebSocket-Protocol header has a list of subprotocols which the client can use. The server replies on this call must return one subprotocol from the client list to establish a connection. Therefore we need to return Authorization as a “subprotocol” to our client.

from channels.generic.websocket import AsyncWebsocketConsumer

class MyConsumer(AsyncWebsocketConsumer):

async def connect(self):
# Called on connection.
# To accept the connection and specify a chosen subprotocol.
# A list of subprotocols specified by the connecting client
# will be available in self.scope['subprotocols']
await self.accept("Authorization")


async def receive(self, text_data=None, bytes_data=None):
# Called with either text_data or bytes_data for each frame
# You can call:
await self.send(text_data="Hello world!")


async def disconnect(self, close_code):
# Called when the socket closes

Conclusion

There are several ways to pass the authorization token in WebSocket connection:

  1. Cookies
  2. Query parameters
  3. Sec-WebSocket-Protocol header

Which option is acceptable must be considered relative to your project.

In Plain English 🚀

Thank you for being a part of the In Plain English community! Before you go:

--

--