Search code examples
djangodjango-channelsasgi

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.

Solution

  • 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)