channels icon indicating copy to clipboard operation
channels copied to clipboard

Database query in custom authentication cause SynchronousOnlyOperation Error

Open AmirMahmood opened this issue 4 years ago • 19 comments

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.

AmirMahmood avatar Jan 07 '20 20:01 AmirMahmood

I'm observing the same issue and would appreciate a documentation update

jkupka avatar Jan 08 '20 09:01 jkupka

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

AmirMahmood avatar Jan 08 '20 10:01 AmirMahmood

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"]))

tiltec avatar Jan 10 '20 21:01 tiltec

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

Thank's for your workaround !

But I have a question: you don't use the close_old_connections as recommended into the documentation. Is there any reason to that ?

avallete avatar Mar 13 '20 12:03 avallete

@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.

AmirMahmood avatar Mar 13 '20 13:03 AmirMahmood

@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.

My bad, I missed that, it make a lot of sense ! Thank you for your answer !

avallete avatar Mar 13 '20 13:03 avallete

@AmirMahmood thank you for this snippet. But still, there is no PR for fixing it. Would you create it?

SerhiyRomanov avatar Apr 01 '20 10:04 SerhiyRomanov

@AmirMahmood Thank you ! You saved my day

maxcmoi89 avatar Apr 09 '20 17:04 maxcmoi89

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

AmirMahmood avatar Apr 11 '20 21:04 AmirMahmood

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

daxaxelrod avatar Apr 17 '20 02:04 daxaxelrod

@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:

frennkie avatar May 04 '20 20:05 frennkie

@frennkie Hey thanks for looking it over! You're right, the model filtered upon isn't of type Token but rather the user model. Changing my comment (and my code) to reflect this.

daxaxelrod avatar May 04 '20 21:05 daxaxelrod

@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

frennkie avatar May 05 '20 18:05 frennkie

thanks for gist @frennkie, but According to the documentation, AuthMiddlewareStack can handle session based authentication itself. using default django session based authentication system is enough in my opinion.

AmirMahmood avatar May 05 '20 20:05 AmirMahmood

Maybe my scenario is special. I have (technical) accounts. They have an ID (=username) and a Token (no password). They authenticate only by sending this Token. How does session based authentication help me here to avoid sending the Token in a Cookie with JavaScript WebSockets?

frennkie avatar May 05 '20 20:05 frennkie

@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.

AmirMahmood avatar May 05 '20 20:05 AmirMahmood

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

don't work, seems to be need SentryAsgiMiddleware? :(

lsaavedr avatar Jul 23 '20 14:07 lsaavedr

@maxizapata seems to be QueryAuthMiddleware

lsaavedr avatar Aug 17 '20 18:08 lsaavedr

I want to work on it, may I get the permission?

Rajeshwari0826 avatar Feb 01 '23 05:02 Rajeshwari0826