Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Django 3.0 + Channels + ASGI + TokenAuthMiddleware

I upgraded to Django 3.0 and now I get this error when using websockets + TokenAuthMiddleware:

SynchronousOnlyOperation
You cannot call this from an async context - use a thread or sync_to_async.
like image 437
Max Malysh Avatar asked Jan 31 '20 18:01

Max Malysh


People also ask

What is AuthMiddlewareStack?

It allows read-only access to a user object in the scope . AuthMiddleware requires SessionMiddleware to function, which itself requires CookieMiddleware . For convenience, these are also provided as a combined callable called AuthMiddlewareStack that includes all three.

What is Django channels used for?

What is Django Channels? Django Channels (or just Channels) extends the built-in capabilities of Django allowing Django projects to handle not only HTTP but also protocols that require long-running connections, such as WebSockets, MQTT (IoT), chatbots, radios, and other real-time applications.

Is Django channels secure?

Django is a powerful Python framework for web development. It is fast, secure, and reliable.

What are Python channels?

Channels is a project that takes Django and extends its abilities beyond HTTP - to handle WebSockets, chat protocols, IoT protocols, and more. It's built on a Python specification called ASGI. Channels builds upon the native ASGI support in Django.


2 Answers

The problem is that you can't access synchronous code from an asynchronous context. Here is a TokenAuthMiddleware for Django 3.0:

# myproject.myapi.utils.py
from channels.auth import AuthMiddlewareStack
from channels.db import database_sync_to_async
from django.contrib.auth.models import AnonymousUser

from rest_framework.authtoken.models import Token


@database_sync_to_async
def get_user(headers):
    try:
        token_name, token_key = headers[b'authorization'].decode().split()
        if token_name == 'Token':
            token = Token.objects.get(key=token_key)
            return token.user
    except Token.DoesNotExist:
        return AnonymousUser()


class TokenAuthMiddleware:

    def __init__(self, inner):
        self.inner = inner

    def __call__(self, scope):
        return TokenAuthMiddlewareInstance(scope, self)


class TokenAuthMiddlewareInstance:
    """
    Yeah, this is black magic:
    https://github.com/django/channels/issues/1399
    """
    def __init__(self, scope, middleware):
        self.middleware = middleware
        self.scope = dict(scope)
        self.inner = self.middleware.inner

    async def __call__(self, receive, send):
        headers = dict(self.scope['headers'])
        if b'authorization' in headers:
            self.scope['user'] = await get_user(headers)
        inner = self.inner(self.scope)
        return await inner(receive, send)


TokenAuthMiddlewareStack = lambda inner: TokenAuthMiddleware(AuthMiddlewareStack(inner))

Use it like this:

# myproject/routing.py
from myapi.utils import TokenAuthMiddlewareStack
from myapi.websockets import WSAPIConsumer

application = ProtocolTypeRouter({
    "websocket": TokenAuthMiddlewareStack(
        URLRouter([
            path("api/v1/ws", WSAPIConsumer),
        ]),
    ),

})
application = SentryAsgiMiddleware(application)
like image 115
Max Malysh Avatar answered Nov 03 '22 17:11

Max Malysh


As @tapion stated this solution doesn't work anymore since channels 3.x

Newer solution can be a little bit tweaked:

class TokenAuthMiddleware:
    def __init__(self, inner):
        self.inner = inner

    async def __call__(self, scope, receive, send):
        headers = dict(scope['headers'])
        if b'authorization' in headers:
            scope['user'] = await get_user(headers)
        return await self.inner(scope, receive, send)
like image 3
Pavel Paranin Avatar answered Nov 03 '22 19:11

Pavel Paranin