JSON Web Tokens authentication on Django Channels

A shared whiteboard implemented using Django Channels and JSON Web Tokens

Posted by Agustín Bartó 1 year, 3 months ago Comments

In this new blogpost we show you how to provide JWT Web Token based authentication on a Django Channels site. JWT is a widely supported open standard which provides for most (if not all) authentication and authorization needs. It is also fairly easy to implement on the server and client side.

We used our fork of django-jwt-auth (which allows token refresh) as the authentication provider.

It is not uncommon for projects to restrict the use of cookies and sessions. As of this writing, the only authentication mechanism provided in Channels is cookie based, and makes use of the existing Django authentication infrastructure. Our goal is to demonstrate how to implement custom authentication on channels using an industry standard as JWT as our basis.

The application

While researching for our introductory blogpost on channels, we came across a pattern on most forums discussing WebSockets: a lot of people question their usefulness. Besides chats and push notifications (which we’ve covered extensively on our site) we couldn’t find any other dominating use case for WebSockets.

We needed a use case to show you how to use JWT authentication with channels, and luckily, the inspiration was right there in our office: a whiteboard. We’re going to build an HTML5 based shared whiteboard on which all logged in users can draw something that is instantly visible to all others. This will allow us to fulfill the time-honored tradition of doodling nonsense on a clean surface:

Mission Accomplished!

Although technically it is almost exactly the same as a chat, the amount of events generated by the HTML5 canvas (provided by sketch.js) allowed us to also see the effects of having multiple channel workers (the greater the number of workers, the smoother the drawing experience).

The code for the application is available on GitHub.

The decorators

The authentication is provided by two decorators: @jwt_request_parameter and @jwt_message_text_field.

@jwt_request_parameter

This decorator is used on HTTP channels that need to be authenticated using a request parameter. On the sample application we use it to control the access to the websocket.connect channel. When the user opens up the WebSocket, we verify that the “token” request parameter is present and it is still valid. If all the checks pass, the original consumer is invoked. Otherwise, the reply channel is closed:

def jwt_request_parameter(func):
    """
    Checks the presence of a "token" request parameter and tries to
    authenticate the user based on its content.
    """
    @wraps(func)
    def inner(message, *args, **kwargs):
        # Taken from channels.session.http_session
        try:
            if "method" not in message.content:
                message.content['method'] = "FAKE"
            request = AsgiRequest(message)
        except Exception as e:
            raise ValueError("Cannot parse HTTP message - are you sure this is a HTTP consumer? %s" % e)

        token = request.GET.get("token", None)
        if token is None:
            _close_reply_channel(message)
            raise ValueError("Missing token request parameter. Closing channel.")

        user = authenticate(token)

        message.token = token
        message.user = user

        return func(message, *args, **kwargs)
    return inner

@jwt_message_text_field

This decorator is used to authenticate each message received through the websocket.receive channel. We check that the text field in the message payload contains a token field and that its contents are a valid JWT token. If the token is valid, we invoke the wrapped consumer, otherwise we close the reply channel.

def jwt_message_text_field(func):
    """
    Checks the presence of a "token" field on the message's text field and
    tries to authenticate the user based on its content.
    """
    @wraps(func)
    def inner(message, *args, **kwargs):
        message_text = message.get('text', None)
        if message_text is None:
            _close_reply_channel(message)
            raise ValueError("Missing text field. Closing channel.")

        try:
            message_text_json = loads(message_text)
        except ValueError:
            _close_reply_channel(message)
            raise

        token = message_text_json.pop('token', None)
        if token is None:
            _close_reply_channel(message)
            raise ValueError("Missing token field. Closing channel.")

        user = authenticate(token)

        message.token = token
        message.user = user
        message.text = dumps(message_text_json)

        return func(message, *args, **kwargs)
    return inner

The consumers

The consumers are exactly the same as you’ll see on a chat channels application (using a Group of channels). The only difference here is that instead of using the standard cookie based authentication decorators provided by channels, we used our own:

# consumers.py

from channels import Group

from .jwt_decorators import (
    jwt_request_parameter, jwt_message_text_field
)

# Connected to websocket.connect
@jwt_request_parameter
def websocket_connect(message):
    Group('shared_canvas').add(message.reply_channel)

# Connected to websocket.disconnect
def websocket_disconnect(message):
    Group('shared_canvas').discard(message.reply_channel)

# Connected to websocket.receive
@jwt_message_text_field
def websocket_receive(message):
    Group('shared_canvas').send({
        'text': message.content['text']
    })

As you can see, the code is free of any authentication code, as everything is handled by the decorators.

Conclusion

Implementing new authentication methods on channels, is straight forward. Is just a matter of being aware of the underlying protocol (HTTP and WebSockets in our case) and knowing how the user identifies itself with the system. The rest is just glue code.

Although the authentication methods demonstrated in this blogpost are specific to JSON Web Tokens, the same basic principles can be applied to other forms of authentication.

Vagrant

A Vagrant configuration file is included if you want to test the solutions.

Feedback

As usual, I welcome comments, suggestions and pull requests.


Previous / Next posts


Comments