fanout / django-eventstream

Server-Sent Events for Django
MIT License
650 stars 85 forks source link

Troubles getting user with django Oauth2 #40

Closed MrVhek closed 4 years ago

MrVhek commented 4 years ago

Hello, I've an issue making authentication work in the channels. I made a fairly simple channel manager :

from logging import info
from django_eventstream.channelmanager import DefaultChannelManager

class MyChannelManager(DefaultChannelManager):
    def can_read_channel(self, user, channel):
        info(user)
        # Require Auth
        if user is None:
           return False
        return True

The problem is, I'm using Oauth2 for authentication, and the user given in the can_read_channel is always None. In my views, I get the user with request.user without problems by passing the token in the headers but not in the channel manager.

Do I need some extra step to get the user ? I've followed the tutorial given by the readme so I have :

urlpatterns = [
    url(r'^v1/gateways-events/gateway/(?P<obj_id>\w+)/', AuthMiddlewareStack(
        URLRouter(django_eventstream.routing.urlpatterns)
    ), {'format-channels': ['gateway-{obj_id}']}),
    url(r'', AsgiHandler),
]

In a routing.py file


from channels.routing import ProtocolTypeRouter, URLRouter
import apps.myhardware.routing

application = ProtocolTypeRouter({
    'http': URLRouter(apps.myhardware.routing.urlpatterns),
})

In the main routing.py file...

Thanks in advance for your help !

jkarneges commented 4 years ago

Strange. I wonder if it's an issue with the channels AuthMiddlewareStack. The user object should be provided in the asgi consumer scope field. You can see the user value is extracted from the scope in django_eventstream/consumers.py.

Maybe you can log the scope value in your code or by modifying django_eventstream, to confirm whether it has a user field.

MrVhek commented 4 years ago

You seem to be right about that, here what I've done :

        request = AsgiRequest(self.scope, body)
        from logging import info
        info("Asgi : ")
        info(self.scope['user'])

        gm = GripMiddleware()
        gm.process_request(request)

        from logging import info
        info("Asgi + GM : ")
        info(self.scope['user'])
        if 'user' in self.scope:
            request.user = self.scope['user']

And it logged this :

HTTP b'GET' request for ['127.0.0.1', 51315]
Asgi : 
AnonymousUser
Asgi + GM :
AnonymousUser
HTTP 200 response started for ['127.0.0.1', 51315]
None

So yes, ASGI request doesn't seems to have anything... Here the self.scope values, I could maybe retrieve the user via the token but it is really ugly :

{'type': 'http', 'http_version': '1.1', 'method': 'GET', 'path': '/v1/gateways-events/gateway/1/', 'root_path': '', 'scheme': 'http', 'query_string': b'', 'headers': [(b'authorization', b'Bearer QmEvB9PpDnoXkXnrhN4SdxH5gOM66c'), (b'user-agent', b'PostmanRuntime/7.19.0'), (b'accept', b'*/*'), (b'cache-control', b'no-cache'), (b'postman-token', b'ce5e5e9f-24c5-463d-b2bc-8456da33fcd6'), (b'host', b'127.0.0.1:8000'), (b'content-type', b'multipart/form-data; boundary=--------------------------721020527187888896297434'), (b'accept-encoding', b'gzip, deflate'), (b'content-length', b'278'), (b'connection', b'keep-alive')], 'client': ['127.0.0.1', 51336], 'server': ['127.0.0.1', 8000], 'path_remaining': '', 'url_route': {'args': (), 'kwargs': {'obj_id': '1', 'format-channels': ['gateway-{obj_id}']}}, 'cookies': {}, 'session': <django.utils.functional.LazyObject object at 0x000002115BC5B668>, 'user': <channels.auth.UserLazyObject object at 0x000002115BC5B780>}

Here my middlewares, in case you think of a wrong order :

MIDDLEWARE = [
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'oauth2_provider.middleware.OAuth2TokenMiddleware',
    'django_grip.GripMiddleware',
    'corsheaders.middleware.CorsMiddleware',
    'django.middleware.security.SecurityMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
MrVhek commented 4 years ago

Ok, so I've found the solution, as you stated the AuthMiddlewareStack doesn't support Oauth2 nor token authentication, so I've wrapped the middleware in my own to have the user :

from channels.auth import AuthMiddlewareStack
from oauth2_provider.models import AccessToken
from django.contrib.auth.models import AnonymousUser
from django.db import close_old_connections

class TokenAuthMiddleware:
    """
    Token OAuth2 authorization middleware for Django Channels 2
    """

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

    def __call__(self, scope):
        headers = dict(scope['headers'])
        if b'authorization' in headers:
            try:
                token_name, token_key = headers[b'authorization'].decode().split()
                if token_name == 'Bearer'or token_name == 'Token':
                    token = AccessToken.objects.get(token=token_key)
                    scope['user'] = token.user
                    close_old_connections()
            except AccessToken.DoesNotExist:
                scope['user'] = AnonymousUser()
        return self.inner(scope)

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

Maybe you should add a few line in documentation for people wondering about this problem.

Inspired by : https://gist.github.com/rluts/22e05ed8f53f97bdd02eafdf38f3d60a

jkarneges commented 4 years ago

Glad to hear you got it working! And thanks for the tip. I've added a note to the readme.