channels: Database query in custom authentication cause SynchronousOnlyOperation Error

In Custom Authentication section of documentation, there is database query in __call__ method:

def __call__(self, scope):
        ...
        user = User.objects.get(id=int(scope["query_string"]))
        ...

It’s OK with Django 2, but in Django 3 it cause django.core.exceptions.SynchronousOnlyOperation error. please update docs, and explain how to use database query in Custom Authentications. Thanks.

About this issue

  • Original URL
  • State: open
  • Created 4 years ago
  • Reactions: 26
  • Comments: 19 (8 by maintainers)

Most upvoted comments

@jkupka You can use below code, Until docs be updated.

from channels.db import database_sync_to_async

@database_sync_to_async
def get_user(user_id):
    try:
        return User.objects.get(id=user_id)
    except User.DoesNotExist:
        return AnonymousUser()

class QueryAuthMiddleware:
    """
    Custom middleware (insecure) that takes user IDs from the query string.
    """

    def __init__(self, inner):
        # Store the ASGI application we were passed
        self.inner = inner

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


class QueryAuthMiddlewareInstance:
    def __init__(self, scope, middleware):
        self.middleware = middleware
        self.scope = dict(scope)
        self.inner = self.middleware.inner

    async def __call__(self, receive, send):
        self.scope['user'] = await get_user(int(self.scope["query_string"]))
        inner = self.inner(self.scope)
        return await inner(receive, send)

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

Hey, thanks for the work done thus far! Adapted @AmirMahmood solution for django rest framework’s token auth based on the old impl. See below:

@database_sync_to_async
def get_user(token_key):
    try:
        return get_user_model().objects.get(auth_token__key=token_key)
    except get_user_model().DoesNotExist:
        return AnonymousUser()

class TokenAuthMiddlewareInstance:
    """
    Token authorization middleware for Django Channels 3
    """

    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:
            token_name, token_key = headers[b'authorization'].decode().split()
            if token_name == 'Token':
                self.scope['user'] = await get_user(token_key)
        inner = self.inner(self.scope)
        return await inner(receive, send)

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

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


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

Another way, based on AuthMiddleware:

from channels.db import database_sync_to_async
from channels.middleware import BaseMiddleware

@database_sync_to_async
def get_user(user_id):
    try:
        return User.objects.get(id=user_id)
    except User.DoesNotExist:
        return AnonymousUser()


class QueryAuthMiddleware(BaseMiddleware):
    def populate_scope(self, scope):
        if "user" not in scope:
            scope["user"] = UserLazyObject()

    async def resolve_scope(self, scope):
        scope["user"]._wrapped = await get_user(int(self.scope["query_string"]))

@avallete, According to the documentation @database_sync_to_async decorator cleans up database connections on exit. You can check this in the @database_sync_to_async source code.

@avallete, @SerhiyRomanov, @maxcmoi89 My pleasure. I created a PR for this issue. (#1442)

@frennkie, yes. it’s special case. I mentioned AuthMiddlewareStack for general scenarios, and for using session base authentication you must use a combination of ID, Token, etc … to login user and set session id. but it seems you don’t need login system in your case.

@daxaxelrod I would also like to use this from the browser - but Javascript does not seem to have a way to use Websockets with a custom Authorization header. Therefore I added an option to pass the Token in a Cookie: https://gist.github.com/frennkie/e79c52d10a8ed0c7af81226a559eada2

@daxaxelrod What happens if there is no User found with the given Token? I got an exception (django.contrib.auth.models.User.DoesNotExist: User matching query does not exist.) and had to replace except Token.DoesNotExist: with except get_user_model().DoesNotExist: