Why is my WebSocket connection being rejected with "Unauthenticated user" in Django Channels even with a valid JWT token?

I am working on a real-time chat application using Django Channels and WebSockets. I have implemented a custom user authentication system using JWT tokens and attached the token-based authentication to the WebSocket connection using Django Channels middleware. However, my WebSocket connection is always being rejected with the message: "Unauthenticated user attempted to connect.", despite sending a valid JWT token in the Authorization header.

Here is the relevant code and setup for my project:

Project Setup:

  • Django Version: 4.1.4
  • Django Channels Version: 4.0.0
  • ASGI Server: Daphne
  • Custom User Model: BasicUserProfile (which stores the JWT token in the auth_token field)

Code:

1. CustomAuthMiddleware - Middleware for JWT Authentication:

import jwt
from datetime import datetime
from channels.middleware.base import BaseMiddleware
from authentication.models import BasicUserProfile
from django.contrib.auth.models import AnonymousUser
from django.conf import settings
from channels.db import database_sync_to_async

class CustomAuthMiddleware(BaseMiddleware):
    async def populate_scope(self, scope):
        user = scope.get('user', None)

        if user is None:
            token = self.get_token_from_headers(scope)
            if token:
                user = await self.get_user_by_token(token)
            else:
                user = AnonymousUser()

        scope['user'] = user

    def get_token_from_headers(self, scope):
        headers = dict(scope.get('headers', []))
        token = headers.get(b'authorization', None)
        if token:
            token_str = token.decode()
            if token_str.startswith("Bearer "):
                return token_str[len("Bearer "):]
        return None

    @database_sync_to_async
    def get_user_by_token(self, token):
        try:
            decoded_token = jwt.decode(token, settings.SECRET_KEY, algorithms=["HS256"])
            if decoded_token.get('exp') and decoded_token['exp'] < datetime.utcnow().timestamp():
                return AnonymousUser()

            user_profile = BasicUserProfile.objects.get(auth_token=token)
            return user_profile.user
        except (jwt.ExpiredSignatureError, jwt.DecodeError, BasicUserProfile.DoesNotExist):
            return AnonymousUser()

2. ChatConsumer - WebSocket Consumer:

import json
import logging
from channels.generic.websocket import AsyncWebsocketConsumer
from authentication.models import BasicUserProfile
from .models import Chat
from .serializers import ChatSerializer
from django.contrib.auth.models import AnonymousUser

logger = logging.getLogger(__name__)

class ChatConsumer(AsyncWebsocketConsumer):
    async def connect(self):
        current_user = self.scope.get('user', None)

        if current_user is None or not current_user.is_authenticated:
            logger.warning("Unauthenticated user attempted to connect.")
            await self.close(code=4000)
            return

        receiver_username = self.scope['url_route']['kwargs']['username']

        try:
            current_profile = await self.get_user_profile(current_user.id)
            receiver_profile = await self.get_receiver_profile(receiver_username)
        except BasicUserProfile.DoesNotExist:
            logger.error(f"Profile not found for user {current_user.id} or receiver {receiver_username}")
            await self.close(code=4001)
            return

        self.room_name = f'{min(current_profile.id, receiver_profile.id)}_{max(current_profile.id, receiver_profile.id)}'
        self.room_group_name = f'chat_{self.room_name}'

        self.sender_profile = current_profile
        self.receiver_profile = receiver_profile

        await self.channel_layer.group_add(self.room_group_name, self.channel_name)
        await self.accept()

    async def disconnect(self, close_code):
        if hasattr(self, 'room_group_name'):
            await self.channel_layer.group_discard(self.room_group_name, self.channel_name)
            logger.debug(f"User {self.scope['user'].username} disconnected from {self.room_group_name}")

    async def receive(self, text_data):
        data = json.loads(text_data)
        message = data.get('message', '')
        sender_username = data.get('senderUsername', '')

        sender_profile = await self.get_user_profile_by_username(sender_username)
        if not sender_profile:
            logger.error(f"Sender profile for {sender_username} not found.")
            return

        chat_message = await self.save_message(sender_profile, message)
        chat_history = await self.get_chat_history()

        await self.channel_layer.group_send(
            self.room_group_name,
            {
                'type': 'chat_message',
                'message': message,
                'senderUsername': sender_username,
                'chat_history': chat_history,
            }
        )

    async def chat_message(self, event):
        message = event['message']
        sender_username = event['senderUsername']
        chat_history = event['chat_history']

        await self.send(text_data=json.dumps({
            'message': message,
            'senderUsername': sender_username,
            'chat_history': chat_history,
        }))

    @database_sync_to_async
    def get_user_profile(self, user_id):
        return BasicUserProfile.objects.get(user__id=user_id)

    @database_sync_to_async
    def get_receiver_profile(self, username):
        return BasicUserProfile.objects.get(user__username=username)

    @database_sync_to_async
    def get_user_profile_by_username(self, username):
        return BasicUserProfile.objects.get(user__username=username)

    @database_sync_to_async
    def save_message(self, sender, message):
        chat = Chat.objects.create(sender=sender, content=message, receiver=self.receiver_profile)
        return chat

    @database_sync_to_async
    def get_chat_history(self):
        chat_history_feed = Chat.objects.filter(sender=self.sender_profile, receiver=self.receiver_profile) | \
                             Chat.objects.filter(sender=self.receiver_profile, receiver=self.sender_profile)
        chat_serializer = ChatSerializer(chat_history_feed, many=True)
        return chat_serializer.data

Problem:

Despite implementing JWT-based authentication via middleware, the WebSocket connection is always rejected with the message:
"Unauthenticated user attempted to connect.".

I am sending the JWT token as a Bearer token in the Authorization header of the WebSocket request, but the connection is not being authenticated successfully.


What I've Tried:

  1. Verified that the token is being passed correctly in the header.
  2. Checked that the CustomAuthMiddleware is correctly decoding the JWT and fetching the user.
  3. Confirmed that the JWT works correctly for API requests (outside of WebSocket).
  4. Debugged the middleware to ensure that the token is being received and decoded.

Expected Behavior:

The WebSocket connection should authenticate the user correctly using the JWT token and allow them to connect and chat with other users in real-time.


Question:

What could be causing the WebSocket to reject the connection with the message "Unauthenticated user attempted to connect." even though a valid JWT token is being sent in the header? Is there an issue with how I am handling the token authentication for WebSockets in Django Channels, or is there something else I might be missing?

Вернуться на верх