Session authentication with Django channels

后端 未结 3 509
心在旅途
心在旅途 2021-02-01 06:35

Trying to get authentication working with Django channels with a very simple websockets app that echoes back whatever the user sends over with a prefix \"You said: \"

相关标签:
3条回答
  • 2021-02-01 07:01

    Note: This answer is explicit to channels 1.x, channels 2.x uses a different auth mechanism.


    I had a hard time with django channels too, i had to dig into the source code to better understand the docs ...

    Question 1:

    The docs mention this kind of long trail of decorators relying on each other (http_session, http_session_user ...) that you can use to wrap your message consumers, in the middle of that trail it states this:

    Now, one thing to note is that you only get the detailed HTTP information during the connect message of a WebSocket connection (you can read more about that in the ASGI spec) - this means we’re not wasting bandwidth sending the same information over the wire needlessly. This also means we’ll have to grab the user in the connection handler and then store it in the session;....

    Its easy to get lost in all that, at least we both did ...

    You just have to remember that this happens when you use channel_session_user_from_http:

    1. It calls http_session_user
      a. calls http_session which will parse the message and give us a message.http_session attribute.
      b. Upon returning from the call, it initiates a message.user based on the information it got in message.http_session ( this will bite you later)
    2. It calls channel_session which will initiate a dummy session in message.channel_session and ties it to the message reply channel.
    3. Now it calls transfer_user which will move the http_session into the channel_session

    This happens during the connection handling of a websocket, so on subsequent messages you won't have acces to detailed HTTP information, so what's happening after the connect is that you're calling channel_session_user_from_http again, which in this situation (post-connect messages) calls http_session_user which will attempt reading the Http information but fails resulting in setting message.http_session to None and overriding message.user to AnonymousUser.
    That's why you need to use channel_session_user in this case.

    Question 2:

    Channels can use Django sessions either from cookies (if you’re running your websocket server on the same port as your main site, using something like Daphne), or from a session_key GET parameter, which works if you want to keep running your HTTP requests through a WSGI server and offload WebSockets to a second server process on another port.

    Remember http_session, that decorator that gets us the message.http_session data? it appears that if it doesn't find a session_key GET parameter it fails to settings.SESSION_COOKIE_NAME, which is the regular sessionid cookie, so whether you provide session_key or not, you'll still get connected if you're logged in, of course that happens only when your ASGI and WSGI servers are on the same domain (127.0.0.1 in this case), the port difference doesn't matter.

    I think the difference that the docs are trying to communicate but didn't expand on is that you need to setup session_key GET parameter when having your ASGI and WSGI servers on different domains since cookies are restricted by domain not port.

    Due to that lack of explanation i had to test running ASGI and WSGI on same port and different port and the result was the same, i was still getting authenticated, changed one server domain to 127.0.0.2 instead of 127.0.0.1 and the authentication was gone, set the session_key get parameter and the authentication was back again.

    Update: a rectification of the docs paragraph was just pushed to the channels repo, it was meant to mention domain instead of port like i mentioned.

    Question 3:

    my answer is the same as turbotux's but longer, you should use @channel_session_user_from_http on ws_connect and @channel_session_user on ws_receive and ws_disconnect, nothing from what you showed tells that it won't work if you do that change, maybe try removing http_user=True from your receive consumer? even thou i suspect it has no effect since its undocumented and intended only to be used by Generic Consumers...

    Hope this helps!

    0 讨论(0)
  • 2021-02-01 07:08

    To answer your first question you need to use the:

    channel_session_user
    

    decorator in the receive and disconnect calls.

    channel_session_user_from_http
    

    calls the transfer_user session during the connect method to transfer the http session to the channel session. This way all future calls may access the channel session to retrieve user information.

    To your second question I believe what you are seeing is that default web socket library passes the browser cookies over the connection.

    Third, I think your setup will be working quite well once have changed the decorators.

    0 讨论(0)
  • 2021-02-01 07:21

    I ran into this problem and I found that it was due to a couple of issues that might be the cause. I'm not suggesting this will solve your issue, but might give you some insight. Keep in mind I am using rest framework. First I was overriding the User model. Second when I defined the application variable in my root routing.py I didn't use my own AuthMiddleware. I was using the docs suggested AuthMiddlewareStack. So, per the Channels docs, I defined my own custom authentication middleware, which takes my JWT value from the cookies, authenticates it and assigns it to the scope["user"] like so:

    routing.py

    from channels.routing import ProtocolTypeRouter, URLRouter
    
    import app.routing
    from .middleware import JsonTokenAuthMiddleware
    
    application = ProtocolTypeRouter(
        {
            "websocket": JsonTokenAuthMiddleware(
                (URLRouter(app.routing.websocket_urlpatterns))
            )
        } 
    

    middleware.py

    from http import cookies
    from django.contrib.auth.models import AnonymousUser
    from django.db import close_old_connections
    from rest_framework.authtoken.models import Token
    from rest_framework_jwt.authentication import BaseJSONWebTokenAuthentication
    
    class JsonWebTokenAuthenticationFromScope(BaseJSONWebTokenAuthentication):
    
        def get_jwt_value(self, scope):
            try:
                cookie = next(x for x in scope["headers"] if x[0].decode("utf-8") 
                    == "cookie")[1].decode("utf-8")
                return cookies.SimpleCookie(cookie)["JWT"].value
            except:
                return None
    
    
    class JsonTokenAuthMiddleware(BaseJSONWebTokenAuthentication):
        def __init__(self, inner):
            self.inner = inner
    
        def __call__(self, scope):
    
            try:
                close_old_connections()
                user, jwt_value = 
                    JsonWebTokenAuthenticationFromScope().authenticate(scope)
                scope["user"] = user
            except:
                scope["user"] = AnonymousUser()
            return self.inner(scope)
    

    Hope this helps this helps!

    0 讨论(0)
提交回复
热议问题