Skip to content

This issue was moved to a discussion.

You can continue the conversation there. Go to discussion →

What middleware for Django's session authentication for graphene-django? #1153

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
thomergil opened this issue Mar 27, 2021 · 1 comment
Closed
Labels

Comments

@thomergil
Copy link

First off, thanks for graphene-django!

Question: how does graphene-django work with Django's session authentication?

For a number of reasons I need to move away from JWT authentication (which was working fine). I implemented a login mutation as follows.

class Login(graphene.Mutation):
    # Note: this code is highly simplified for brevity; not safe for production
    def mutate(self, info, username, password):
        user = authenticate(info.context, username=username, password=password)
        if user:
            login(info.context, user)

I see that a fresh sessionid is returned. I also see that subsequent calls to graphql include sessionid in the Cookie HTTP header. So far so good. However, subsequent queries and mutations fail to set info.context.user.

I presumably need to set some MIDDLEWARE on GRAPHENE to ensure that request.user is set based on the sessionid for incoming requests. But what is that middleware?

In other words, what is the equivalent of graphql_jwt.middleware.JSONWebTokenMiddleware for Django's session authentication?

I see some previous discussions that come close (for example, #476). However, none seem to answer this question directly.

Some other relevant settings:

AUTHENTICATION_BACKENDS = [
    'django.contrib.auth.backends.ModelBackend',
]

MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
]

GRAPHENE = {
    'SCHEMA': 'backend.schema.schema',
    'SCHEMA_OUTPUT': 'schema/schema.json',  # defaults to schema.json,
}
@thomergil
Copy link
Author

thomergil commented Mar 29, 2021

I took a stab at this myself, partly inspired and stolen from other similar libraries.

A proof of concept implementation would look as follows. I'm pretty sure there are still problems all over the place, but it seems to correctly set request.user based on the configured AUTHENTICATION_BACKENDS, which in the case of django.contrib.auth.backends.ModelBackend means using sessionid in the Cookie header.

First add MIDDLEWARE to GRAPHENE, to point at the class that implements the middleware. In this case, that's bmt.auth.middleware.AuthenticationMiddleware.

 GRAPHENE = {
   # ...
    'MIDDLEWARE': ['bmt.auth.middleware.AuthenticationMiddleware',],
}

Then bmt/auth/middleware.py would look like this:

from django.contrib import auth
from django.utils.functional import SimpleLazyObject

def get_user(request):
    if not hasattr(request, '_cached_user'):
        request._cached_user = auth.get_user(request)
    return request._cached_user

class AuthenticationMiddleware:
    def resolve(self, next, root, info, **args):
        context = info.context
        context.user = SimpleLazyObject(lambda: get_user(context))
        return next(root, info, **args)

And, as a bonus, bmt/auth/decorators.py:

from functools import wraps
# v2
from graphql.execution.executor import ResolveInfo
# v3
# from graphql.execution.execute import GraphQLResolveInfo
from django.core.exceptions import PermissionDenied

__all__ = [
    'user_passes_test',
    'login_required',
    'staff_member_required',
    'superuser_required',
    'permission_required',
]

def context(f):
    def decorator(func):
        def wrapper(*args, **kwargs):
            info = next(arg for arg in args if isinstance(arg, ResolveInfo))
            return func(info.context, *args, **kwargs)
        return wrapper
    return decorator

def user_passes_test(test_func, exc=PermissionDenied):
    def decorator(f):
        @wraps(f)
        @context(f)
        def wrapper(context, *args, **kwargs):
            if test_func(context.user):
                return f(*args, **kwargs)
            raise exc
        return wrapper
    return decorator

login_required = user_passes_test(lambda u: u.is_authenticated)
staff_member_required = user_passes_test(lambda u: u.is_staff)
superuser_required = user_passes_test(lambda u: u.is_superuser)

def permission_required(perm):
    def check_perms(user):
        if isinstance(perm, str):
            perms = (perm,)
        else:
            perms = perm
        return user.has_perms(perms)
    return user_passes_test(check_perms)

@zbyte64 zbyte64 closed this as completed Apr 14, 2021
@graphql-python graphql-python locked and limited conversation to collaborators Apr 14, 2021

This issue was moved to a discussion.

You can continue the conversation there. Go to discussion →

Labels
Projects
None yet
Development

No branches or pull requests

2 participants