How is user/password authentication done with channels / websockets?

I am making an IOS and Android app that will communicate with Django real time through websockets but I am at a complete loss on how to do user/password authentication when opening a websockets stream. I built the chat demo, but of course the problem is that there is no authentication in it at all. I noticed if I log into the admin panel with my user, then scope[‘user’] will get populated in my AuthMiddleware / Consumer, but since I will be creating a websocket connection from a native mobile app I need a way to pass the user/password without using a Django login form.

I suppose I could pass the username/password with a URL query but this would be highly insecure and the password may be logged in cleartext if done in this matter. I also tried the http basic auth method http://user:pass@127.0.0.1:8000/ws/chat/lobby but these creds don’t seem to make it into the session at all.

Surely someone has done this, can anyone provide example code? I see a lot of examples where the user is provided in query text and in the middleware there is a lookup for the token that the user owns but I don’t see how this could ever be secure without providing a password somewhere. I’m coming from crossbar WAMP server and there were several methods to authenticate with that.

What am I missing? Thanks for the help.

Here is my current code:

asgi.py:

routes = chat.routing.websocket_urlpatterns

application = ProtocolTypeRouter(
{
“http”: django_asgi_app,
“websocket”: AllowedHostsOriginValidator(
DualAuthMiddleware(URLRouter(routes))
),
}
)

DualAuthMiddleware:

class DualAuthMiddleware:
“”"
Custom middleware (insecure) that takes user IDs from the query string.
“”"

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 (you should also do things like
    # checking if it is a valid user ID, or if scope["user"] is already
    # populated).
    print(chat.routing.websocket_urlpatterns)
    if scope['path'].startswith("/ws/chat"):
        print("REGULAR AUTH")
        return await AuthMiddlewareStack(self.app)(scope, receive, send)
    else:
        print("NO AUTH")
        return await AuthMiddlewareStack(self.app)(scope, receive, send)


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

ChatConsumer

class ChatConsumer(AsyncWebsocketConsumer):
async def connect(self):
user = self.scope[“user”]
print(“HEREHEREHERE2”,user,user.is_authenticated,self.scope)
if user.is_authenticated:
#else:
# await self.close()
print(“HEREHEREHERE”,user,user.is_authenticated)
self.room_name = self.scope[“url_route”][“kwargs”][“room_name”]
self.room_group_name = “chat_%s” % self.room_name

    # Join room group
        await self.channel_layer.group_add(self.room_group_name, self.channel_name)

        await self.accept()
    else:
        await self.close()

async def disconnect(self, close_code):
    # Leave room group
    try:
        await self.channel_layer.group_discard(self.room_group_name, self.channel_name)
    except AttributeError:
        pass

# Receive message from WebSocket
async def receive(self, text_data):
    text_data_json = json.loads(text_data)
    message = text_data_json["message"]

    # Send message to room group
    await self.channel_layer.group_send(
        self.room_group_name, {"type": "chat_message", "message": message}
    )

# Receive message from room group
async def chat_message(self, event):
    message = event["message"]

    # Send message to WebSocket
    await self.send(text_data=json.dumps({"message": message}))

routing.py:

from django.urls import re_path

from . import consumers

websocket_urlpatterns = [
re_path(r"ws/chat/(?P<room_name>\w+)/$", consumers.ChatConsumer.as_asgi()),
]

room.html:

Chat Room



{{ room_name|json_script:"room-name" }} const roomName = JSON.parse(document.getElementById('room-name').textContent);
    const chatSocket = new WebSocket(
        'ws://'
        + 'testuser:testpass@127.0.0.1:9000'
        + '/ws/chat/'
        + roomName
        + '/'
    );

    chatSocket.onmessage = function(e) {
        const data = JSON.parse(e.data);
        document.querySelector('#chat-log').value += (data.message + '\n');
    };

    chatSocket.onclose = function(e) {
        console.error('Chat socket closed unexpectedly');
    };

    document.querySelector('#chat-message-input').focus();
    document.querySelector('#chat-message-input').onkeyup = function(e) {
        if (e.keyCode === 13) {  // enter, return
            document.querySelector('#chat-message-submit').click();
        }
    };

    document.querySelector('#chat-message-submit').onclick = function(e) {
        const messageInputDom = document.querySelector('#chat-message-input');
        const message = messageInputDom.value;
        chatSocket.send(JSON.stringify({
            'message': message
        }));
        messageInputDom.value = '';
        /*const usernameInputDom = document.querySelector("#username-input");
        const passwordInputDom = document.querySelector("#password-input");
        document.querySelector('#chat-log').value = (usernameInputDom.value + '\n');
        document.querySelector('#chat-log').value += (passwordInputDom.value + '\n');*/
    };
</script>

You actually have a variety of options here - see the docs at Authentication — Channels 4.0.0 documentation.

More appropriate would be to post those credentials to the login view. You would still to a GET to get the CSRF token, but you can post the credentials to a custom view to perform the authentication. (You could actually do this in a manner similar to DRF’s authentication).

Is there a way to set post variables in the websocket instance creation? I didn’t see anything in the Authorization page about that.

I am not aware of any facility within the ws (or wss) negotiation that allows for post data to be included. (I’m far from an expert on this though.)

If you’re interested in pursuing it, you may find these links useful:

Keep in mind that your application must, at any time, need to re-establish the websocket connection. One advantage of doing the authentication in Django is that the cookie is going to remain even if the websocket drops.

I wrote up a pretty thorough article on this, along with a companion git.

https://medium.com/@matt_31770/authentication-for-django-channels-using-a-database-cluster-4800dbfcb07e

https://gitlab.com/sourcesolver1/django_wss_auth_v1

Even if you don’t want to adopt the code I wrote, the article explains well the issues that need to be addressed in accomplishing this…