How to authenticate a user in websocket connection

2019-03-22 04:01发布

问题:

I am using a frontend framework (Vuejs) and django-rest-framework for the REST API in my project. Also, for JSON web token authentication I am using django-rest-framework-jwt. After a successful login, the user is provided with a token. This token is passed into every request to fetch any API related stuff.

Now I would like to integrate django channels into my project. So, after successful login, when the token is received in the client side, I would like to initiate a websocket connection. Then on the server (consumer), I would like to check if the requested user is not anonymous. If the requested user is anonymous, I would like to close the connenction or else accept it.

This is how I have till now:

client side:

const socket = new WebSocket("ws://" + "dev.site.com"+ "/chat/");

routing.py:

channel_routing = [
    route("websocket.connect", ws_connect),
    ...
    ...
]

consumers:

def ws_connect(message):

    # if the user is no anonymous
    message.reply_channel.send({
        "accept": True
    })

    # else
    message.reply_channel.send({
        "close": True
    })

In the documentation there's a decorator @channel_session_user_from_http which will provide a message.user. But I am using a token instead of a session. How can I check a user on connection when using token authentication, so that I can accept or close connection. Or, if there is a better way could you please advise me with it. Thank you.

回答1:

The problem is that the browsers do not support passing jwt auth headers on websocket upgrade, so that's basically it. I faced this problem some time ago and came up with the solution of passing the token via query parameters - note that this is totally insecure without TLS as you expose the authentication in the URI. I don't have the access to the exact code anymore, but here is the idea:

from channels.generic.websockets import JsonWebsocketConsumer
from channels.handler import AsgiRequest

from rest_framework_jwt.serializers import VerifyJSONWebTokenSerializer
from jwt.exceptions import InvalidTokenError
from rest_framework.exceptions import ValidationError

class Consumer(JsonWebsocketConsumer):

    def connect(self, message, **kwargs):
        # construct a fake http-like request object from the message
        message.content.setdefault('method', 'FAKE')
        request = AsgiRequest(message)
        # validate the token
        try:
            VerifyJSONWebTokenSerializer().validate(request.GET)
            super().connect(message, **kwargs)
        except (KeyError, InvalidTokenError, ValidationError,):
            # token is either not available or invalid
            # so we disconnect the user
            message.reply_channel.send({'close': True})

Register the consumer with

channel_routing = [
    ...
    route_class(Consumer, path=r'^my-ws-endpoint$'),
]

On browser side, you can establish the websocket connection by passing the token as query parameter in the websocket URI:

let token: string = 'my-token'; // get the token
let wsHandler: $WebSocket = new $WebSocket('wss://example.com/my-ws-endpoint/?token=' + token, ...);

You can then extract the auth check code in a decorator similar to @channel_session_user_from_http and just decorate your connection routines, or extract the code to a mixin if you use class-based routes.

I would like to repeat though that this approach is totally insecure without using encryption, so in production you URIs should start with https/wss.


Edit: here is a pretty nice solution for DRF token auth, suitable for both function-based and class-based routes. It has pretty much the same approach as mine, constructing a request object and passing it to the authenticator.



回答2:

@hoefling's answer was my guide. I was confused about two things on authenticating a user.

  1. What to do with the token?

    • You can pass the token as a query string and get that query params. Read more about how to get the query params here.
    • Or if you are already passing it in the request's authorization header, you can get it from there like @hoefling did with his answer. Remeber to first fake that request.
  2. How validate that token and get the user?

    • Finally VerifyJSONWebTokenSerializer class was all I needed to validate the token, and get that token's user object. (Thanks @hoefling!) You can read the actual code of django-rest-framework-jwt here.

So, I ended up doing this way:

def ws_connect(message):
    message.content.setdefault('method', 'FAKE')
    django_request = AsgiRequest(message)
    token = django_request.GET['token'].split(' ')[1]
    try:
        data = {'token': token}
        valid_data = VerifyJSONWebTokenSerializer().validate(data)
        user = valid_data['user']
        ...
        ...
        message.reply_channel.send({
            "accept": True
        })
    except (KeyError, InvalidTokenError, ValidationError,):
        ...
        ...
        message.reply_channel.send({
            "text": "Authentication error",
            "close": True
        })