Django Channels Token Authorization
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 thequery_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:
- Cookies
- Query parameters
- 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:
- Be sure to clap and follow the writer ️👏️️
- Follow us: X | LinkedIn | YouTube | Discord | Newsletter
- Visit our other platforms: Stackademic | CoFeed | Venture | Cubed
- More content at PlainEnglish.io