14

I'm trying out Channels in Django 1.10 and set up a few consumers.

I tried creating a login_required decorator for it that closes the connection before executing it to prevent guests from entering this private socket. Also integrated unit tests afterwards to test it and they keep failing because it keeps letting guests in (AnonymousUser errors everywhere).

Also, sometimes when logging in and logging out the session doesn't clear and it lets the old user in.

The decorator:

def login_required_websocket(func):
    """
    If user is not logged in, close connection immediately.
    """
    @functools.wraps(func)
    def inner(message, *args, **kwargs):
        if not message.user.is_authenticated():
            message.reply_channel.send({'close': True})
        return func(message, *args, **kwargs)
    return inner

Here's the consumer code:

def ws_connect(message, slug):
    message.reply_channel.send({ 'accept': True })
    client = message.reply_channel
    client.send(signal.message("Welcome"))
    try:
        # import pdb; pdb.set_trace()
        Room.objects.get(name=slug)
    except Room.DoesNotExist:
        room = Room.objects.create(name=slug)
        room.users.add(message.user)
        room.turn = message.user.id
        room.save()
        story = Story(room=room)
        story.save()


    # We made sure it exists.
    room = Room.objects.get(name=slug)
    message.channel_session['room'] = room.name

    # Check if user is allowed here.
    if not room.user_allowed(message.user):
        # Close the connection. User is not allowed.
        client.send(Signal.error("User isn't allowed in this room."))
        client.send({'close': True})

The strange thing is, when commenting out all the logic between client.send(signal.message)) forwards, it works just fine and unit tests pass (meaning guests are blocked and auth code does not run [hence AnonymousUser errors]). Any ideas?

Here's the tests too:

class RoomsTests(ChannelTestCase):

    def test_reject_guest(self):
        """
        This tests whether the login_required_websocket decorator is rejecting guests.
        """
        client = HttpClient()
        user = User.objects.create_user(
            username='test', password='password')

        client.send_and_consume('websocket.connect',
                                path='/rooms/test_room', check_accept=False)
        self.assertEqual(client.receive(), {'close': True})

    def test_accept_logged_in(self):
        """
        This tests whether the connection is accepted when a user is logged in.
        """
        client = HttpClient()
        user = User.objects.create_user(
            username='test', password='password')
        client.login(username='test', password='password')

        client.send_and_consume('websocket.connect', path='/rooms/test_room')

Am I approaching this wrong, and if I am, how do I do this (require auth) properly?

EDIT: Integrated an actions system to try something out, looks like Django channels is simply not picking up any sessions from HTTP at all.

@enforce_ordering
@channel_session_user_from_http
def ws_connect(message, slug):
    message.reply_channel.send({'accept': True})
    message.reply_channel.send(Action.info(message.user.is_authenticated()).to_send())

Just returns false.

EDIT2: I see it works now, I tried changing localhost to 127.0.0.1 and turns out it works now. Is there a way to make it detect localhost as a valid domain so it ports over the sessions?

EDIT3: Turns out I found the localhost vs 127.0.0.1 cookie issue haha. To not waste the bounty, how would you personally implement auth login_required in messages/channels?

edit4: While I still don't know why the thing didn't work, here's how I eventually changed my app around the issue:

I created an actions system. When entering in, the socket does nothing until you send it an AUTHENTICATE action through JSON. I separated logged in actions in guest_actions and user_actions. Once authenticated, it sets the session and you are able to use user_actions.

Sergio E. Diaz
  • 396
  • 3
  • 15

3 Answers3

2

Django Channels already supports session authentication:

# In consumers.py
from channels import Channel, Group
from channels.sessions import channel_session
from channels.auth import channel_session_user, channel_session_user_from_http

# Connected to websocket.connect
@channel_session_user_from_http
def ws_add(message):
    # Accept connection
    message.reply_channel.send({"accept": True})
    # Add them to the right group
    Group("chat-%s" % message.user.username[0]).add(message.reply_channel)

# Connected to websocket.receive
@channel_session_user
def ws_message(message):
    Group("chat-%s" % message.user.username[0]).send({
        "text": message['text'],
    })

# Connected to websocket.disconnect
@channel_session_user
def ws_disconnect(message):
    Group("chat-%s" % message.user.username[0]).discard(message.reply_channel)

http://channels.readthedocs.io/en/stable/getting-started.html#authentication

Chris Montanaro
  • 16,948
  • 4
  • 20
  • 29
  • 2
    That does not answer my question. I suggest you read the question again. I know it supports auth, I want to prevent channel access to guests only and I'm looking for the best async-compatible way to do this. – Sergio E. Diaz Mar 29 '17 at 06:26
1

Your function worked "as-is" for me. Before I walk through the details, there was a bug (now resolved) that was preventing sessions from being closed which may explain your other issue.

I use scarce quotes around "as-is" because I was using a class-based consumer so I had to add self to the whole stack of decorators to test it explicitly:

class MyRouter(WebsocketDemultiplexer):
    # WebsocketDemultiplexer calls raw_connect for websocket.connect
    @channel_session_user_from_http
    @login_required_websocket
    def raw_connect(self, message, **kwargs):
        ...

After adding some debug messages to verify the sequence of execution:

>>> ws = create_connection("ws://localhost:8085")

# server logging
channel_session_user_from_http.run
login_required_websocket.run
user: AnonymousUser

# client logging
websocket._exceptions.WebSocketBadStatusException: Handshake status 403

>>> ws = create_connection("ws://localhost:8085", cookie='sessionid=43jxki76cdjl97b8krco0ze2lsqp6pcg')

# server logging
channel_session_user_from_http.run
login_required_websocket.run
user: admin

As you can see from my snippet, you need to call @channel_session_user_from_http first. For function-based consumers, you can simplify this by including it in your decorator:

def login_required_websocket(func):
    @channel_session_user_from_http
    @functools.wraps(func)
    def inner(message, *args, **kwargs):
        ...

On class-based consumers, this is handled internally (and in the right order) by setting http_user_and_session:

class MyRouter(WebsocketDemultiplexer):
    http_user_and_session = True

Here's the full code for a self-respecting decorator that would be used with it:

def login_required_websocket(func):
    """
    If user is not logged in, close connection immediately.
    """
    @functools.wraps(func)
    def inner(self, message, *args, **kwargs):
        if not message.user.is_authenticated():
            message.reply_channel.send({'close': True})
        return func(self, message, *args, **kwargs)
    return inner
claytond
  • 1,061
  • 9
  • 22
0

My suggestion is that you can require a session key or even better take the username/password input within your consumer method. Then call the authenticate method to check if the user exists. On valid user object return, you can broadcast the message or return and invalid login details message.

from django.contrib.auth import authenticate

@channel_session_user
def ws_message(message):
    user = authenticate(username=message.username, password=message.password')
     if user is not None: 
        Group("chat-%s" % message.user.username[0]).send({
              "text": message['text'],
              })
     else:
           # User is not authenticated so return an error message.
Community
  • 1
  • 1
IVI
  • 1,893
  • 1
  • 16
  • 19