channels
channels copied to clipboard
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.
I'm observing the same issue and would appreciate a documentation update
@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))
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"]))
@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, 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, 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 !
@AmirMahmood thank you for this snippet. But still, there is no PR for fixing it. Would you create it?
@AmirMahmood Thank you ! You saved my day
@avallete, @SerhiyRomanov, @maxcmoi89 My pleasure. I created a PR for this issue. (#1442)
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 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 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 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
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.
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, 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.
@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
? :(
@maxizapata seems to be QueryAuthMiddleware
I want to work on it, may I get the permission?