From 93ed76a779e77b9211a4a64b45c6fd71c8e323d3 Mon Sep 17 00:00:00 2001 From: Toksi Date: Mon, 17 Nov 2025 13:04:07 +0500 Subject: [PATCH] =?UTF-8?q?=D0=9F=D0=B5=D1=80=D0=B5=D0=B2=D0=BE=D0=B4=20We?= =?UTF-8?q?bSocket-=D0=B0=D0=B2=D1=82=D0=BE=D1=80=D0=B8=D0=B7=D0=B0=D1=86?= =?UTF-8?q?=D0=B8=D0=B8=20=D0=BD=D0=B0=20token=20=D1=87=D0=B5=D1=80=D0=B5?= =?UTF-8?q?=D0=B7=20subprotocol,=20=D0=B5=D0=B4=D0=B8=D0=BD=D1=8B=D0=B9=20?= =?UTF-8?q?middleware/routing?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- chats/consumers/chat.py | 10 ++++++++- core/auth/__init__.py | 3 +++ {chats => core/auth}/middleware.py | 36 ++++++++++++++++-------------- procollab/asgi.py | 6 ++--- procollab/websocket_routing.py | 4 ++++ 5 files changed, 38 insertions(+), 21 deletions(-) create mode 100644 core/auth/__init__.py rename {chats => core/auth}/middleware.py (82%) create mode 100644 procollab/websocket_routing.py diff --git a/chats/consumers/chat.py b/chats/consumers/chat.py index 13dd954d..4861e435 100644 --- a/chats/consumers/chat.py +++ b/chats/consumers/chat.py @@ -77,7 +77,15 @@ async def connect(self): await self.channel_layer.group_add( EventGroupType.GENERAL_EVENTS, self.channel_name ) - await self.accept() + # Confirm selected subprotocol so browser clients finish handshake. + subprotocol = None + if ( + self.scope.get("subprotocols") + and len(self.scope["subprotocols"]) >= 1 + ): + subprotocol = self.scope["subprotocols"][0] + + await self.accept(subprotocol=subprotocol) async def disconnect(self, close_code): """User disconnected from websocket""" diff --git a/core/auth/__init__.py b/core/auth/__init__.py new file mode 100644 index 00000000..2469dbdf --- /dev/null +++ b/core/auth/__init__.py @@ -0,0 +1,3 @@ +""" +Authentication utilities for ASGI/WebSocket middleware. +""" diff --git a/chats/middleware.py b/core/auth/middleware.py similarity index 82% rename from chats/middleware.py rename to core/auth/middleware.py index 593c182e..247baeea 100644 --- a/chats/middleware.py +++ b/core/auth/middleware.py @@ -1,5 +1,3 @@ -from urllib.parse import parse_qs - import jwt from channels.db import database_sync_to_async from django.conf import settings @@ -97,33 +95,37 @@ def get_user(scope): class TokenAuthMiddleware: """ - Custom middleware that takes a token from the query string and authenticates via Django Rest Framework authtoken. + Custom middleware that takes a token from WebSocket subprotocols and authenticates via JWT. """ + SUBPROTOCOL_KEYWORD = "Bearer" + def __init__(self, app): # Store the ASGI application we were passed self.app = app async def __call__(self, scope, receive, send): - # Look up user from query string - - # TODO: (you should also do things like - # checking if it is a valid user ID, or if scope["user" ] is already - # populated). - - query_string = scope["query_string"].decode() - query_dict = parse_qs(query_string) - try: - token = query_dict["token"][0] - if token is None: - raise ValueError("Token is missing from headers") + # Extract token from Sec-WebSocket-Protocol header. + token = self._extract_token_from_subprotocol(scope.get("subprotocols", [])) + if token: scope["token"] = token scope["user"] = await get_user(scope) - except (ValueError, KeyError, IndexError): - # Token is missing from query string + else: from django.contrib.auth.models import AnonymousUser scope["user"] = AnonymousUser() return await self.app(scope, receive, send) + + def _extract_token_from_subprotocol(self, subprotocols: list[str]) -> str | None: + """ + Expect subprotocols in the form ["Bearer", ""]. + """ + if not subprotocols: + return None + + if len(subprotocols) >= 2 and subprotocols[0] == self.SUBPROTOCOL_KEYWORD: + return subprotocols[1] + + return None diff --git a/procollab/asgi.py b/procollab/asgi.py index 13f70e69..3551aebe 100644 --- a/procollab/asgi.py +++ b/procollab/asgi.py @@ -8,12 +8,12 @@ # Ensure Django app registry is loaded before importing project routes. django_asgi_app = get_asgi_application() -import chats.routing # noqa: E402 -from chats.middleware import TokenAuthMiddleware # noqa: E402 +from core.auth.middleware import TokenAuthMiddleware # noqa: E402 +from procollab.websocket_routing import websocket_urlpatterns # noqa: E402 application = ProtocolTypeRouter( { "http": django_asgi_app, - "websocket": TokenAuthMiddleware(URLRouter(chats.routing.websocket_urlpatterns)), + "websocket": TokenAuthMiddleware(URLRouter(websocket_urlpatterns)), } ) diff --git a/procollab/websocket_routing.py b/procollab/websocket_routing.py new file mode 100644 index 00000000..5da70ebe --- /dev/null +++ b/procollab/websocket_routing.py @@ -0,0 +1,4 @@ +from chats.routing import websocket_urlpatterns as chat_websocket_urlpatterns + +websocket_urlpatterns = [] +websocket_urlpatterns += chat_websocket_urlpatterns