Graphene-django: How does login work with Django?

Created on 4 Sep 2017  路  13Comments  路  Source: graphql-python/graphene-django

Is there an example of how login works with Django and this package?

Let say you had a mobile application, and you had a login endpoint to authenticate a user. How is a session maintained for further graphql requests?

Most helpful comment

@patrick91 thanks for the example code! If one is already using the django rest framework JWT package I believe you could get away with something as short as this:

from rest_framework import exceptions
from rest_framework_jwt.authentication import JSONWebTokenAuthentication
from graphene_django.views import GraphQLView


class JWTGraphQLView(JSONWebTokenAuthentication, GraphQLView):

    def dispatch(self, request, *args, **kwargs):
        try:
            # if not already authenticated by django cookie sessions
            # check the JWT token by re-using our DRF JWT
            if not request.user.is_authenticated():
                request.user, request.token = self.authenticate(request)
        except exceptions.AuthenticationFailed as e:
            response = HttpResponse(
                json.dumps({
                    'errors': [str(e)]
                }),
                status=401,
                content_type='application/json'
            )
            return response
        return super().dispatch(request, *args, **kwargs)

All 13 comments

You can reuse your existing approach to authentication. GraphQLView is not special from this point of view.

So, be it API token you're mapping to session or plain session via cookie, it just works.

To elaborate just a little.

The default GraphQLView does not require a login or do any sort of authoriation.

If you take a look at the authorization docs, the section on "Adding login required" shows how to make a quick subclass of GraphQLView and Django's LoginRequiredMixin to achieve a private view that will utilize whatever authentication backends you have going in your app.

It is arguable that this requirement is common enough that even though it is only two lines of code, it's two lines of code that most projects will have to duplicate. Perhaps a PrivateGraphQLView or LoginRequiredGraphQLView should be included in this repo?

Okay. So I guess what is a popular method for authentication that works well with client side and backend? I get I can store a token on client and then use that to auth. But is that really the recommended way with GraphQL?

How does a cookie work with a mobile client?

That question is a bit out of scope for this repository. GraphQL will work with whatever Django authentication backend you choose. Whether you use a token-based authentication or have a mobile client consuming your API, it makes no difference to this repository.

Here are some links that might help you make a choice.

https://djangopackages.org/grids/g/authentication/
https://techstricks.com/api-authentication-django-and-android-apps/

Best of luck! Feel free to open another issue if you have a question specific to graphene-django.

I setup DRF Token Authentication and it works with REST. I thought that it would work with graphene-django.

With the following Query

class User(DjangoObjectType):
    class Meta:
        model = UserModel

class Query(graphene.ObjectType):
    users = graphene.List(User)
    me = graphene.Field(User)

    @graphene.resolve_only_args
    def resolve_users(self):
        return UserModel.objects.all()

    def resolve_me(self, info):
        print(info.context.user)
        if not info.context.user.is_authenticated():
            return None
        else:
            return info.context.user

schema = graphene.Schema(query=Query)

The me resolver always returns AnonymousUser for info.context.user. I am passing through the Token in Authorisation header.

So I am guessing it does not get context.user from the DRF middleware? Its using the Django middleware?

uhm, not familiar on how the DRF's authentication works in its details, but here's what I did for a work project:

authentication/views.py

Here's a simple class that extends the GraphQLView and a JWTTokenMixin, note that
you can use any authentication type you want, as long as the mixing works :)

from graphene_django.views import GraphQLView

from .mixins import JSONWebTokenAuthMixin


class JWTGraphQLView(JSONWebTokenAuthMixin, GraphQLView):
    pass

authentication/mixins.py

Here's the mixin I created in order to authenticate the user with JWT tokens, it is a
bit hacky, and it uses rest_frameworks stuff, but works :)

import json

import jwt

from django.contrib.auth import get_user_model
from django.contrib.auth.models import AnonymousUser
from django.http import HttpResponse
from django.utils.decorators import method_decorator
from django.utils.encoding import smart_text
from django.views.decorators.csrf import csrf_exempt

from rest_framework import exceptions
from rest_framework.authentication import get_authorization_header
from rest_framework_jwt.settings import api_settings

jwt_decode_handler = api_settings.JWT_DECODE_HANDLER
jwt_get_username_from_payload = api_settings.JWT_PAYLOAD_GET_USERNAME_HANDLER

User = get_user_model()


class JSONWebTokenAuthMixin():
    """Authenticated the user with JSONWebTokenAuthMixinBase only if
    the authentication was provided."""

    @method_decorator(csrf_exempt)
    def dispatch(self, request, *args, **kwargs):
        try:
            request.user, request.token = self.authenticate(request)
        except exceptions.AuthenticationFailed as e:
            response = HttpResponse(
                json.dumps({
                    'errors': [str(e)]
                }),
                status=401,
                content_type='application/json'
            )

            return response

        return super().dispatch(request, *args, **kwargs)

    def authenticate_credentials(self, payload):
        """
        Returns an active user that matches the payload's user id and email.
        """
        try:
            user_id = payload['user_id']

            if user_id:
                user = User.objects.get(pk=user_id, is_active=True)

                return user
            else:
                msg = 'Invalid payload'
                raise exceptions.AuthenticationFailed(msg)
        except User.DoesNotExist:
            msg = 'Invalid signature'
            raise exceptions.AuthenticationFailed(msg)

    def authenticate(self, request):
        # if user is already authenticated (for example via session)
        # skip the header auth

        if request.user.is_authenticated:
            return (request.user, None)

        auth = get_authorization_header(request).split()

        if not auth:
            return (AnonymousUser(), None)

        auth_header_prefix = api_settings.JWT_AUTH_HEADER_PREFIX.lower()

        if not auth or smart_text(auth[0].lower()) != auth_header_prefix:
            raise exceptions.AuthenticationFailed()

        if len(auth) == 1:
            msg = 'Invalid Authorization header. No credentials provided.'
            raise exceptions.AuthenticationFailed(msg)
        elif len(auth) > 2:
            msg = (
                'Invalid Authorization header. Credentials string '
                'should not contain spaces.'
            )
            raise exceptions.AuthenticationFailed(msg)

        try:
            payload = jwt_decode_handler(auth[1])
        except jwt.ExpiredSignature:
            msg = 'Signature has expired.'
            raise exceptions.AuthenticationFailed(msg)
        except jwt.DecodeError:
            msg = 'Error decoding signature.'
            raise exceptions.AuthenticationFailed(msg)

        user = self.authenticate_credentials(payload)

        return (user, auth[1])

Hope that helps, I'd like to write a detailed blog post about authentication and graphene, but I don't really have time right now :)

OK Thanks. Here is what worked for me:

I adapted some code from here: https://www.howtographql.com/graphql-python/4-authentication/

def get_user(context):
    token = context.session.get('token')
    if not token:
        return
    try:
        user = UserModel.objects.get(auth_token__key=token)
        return user
    except:
        raise Exception('User not found!')

class User(DjangoObjectType):
    class Meta:
        model = UserModel

class LogIn(graphene.Mutation):
    user = graphene.Field(User)

    class Arguments:
        email = graphene.String()
        password = graphene.String()

    @staticmethod
    def mutate(root, info, **input):
        user = authenticate(
            email=input.get('email'),
            password=input.get('password'),
        )

        if not user:
            raise Exception('Invalid username or password!')

        info.context.session['token'] = user.auth_token.key
        return LogIn(user=user)

class Query(graphene.ObjectType):
    users = graphene.List(User)
    me = graphene.Field(User)
    login = LogIn.Field()

    @graphene.resolve_only_args
    def resolve_users(self):
        return UserModel.objects.all()

    def resolve_me(self, info):
        context = info.context
        user = get_user(context)
        if not user:
            raise Exception('Not logged in!')

        return user

I should be able to reuse it to create a Mixin similar to yours.

@patrick91 is there a login method where the token is set? How does the user have the token in the first place?

@viperfx I probably forgot the most important thing :)

Again, depends on your use case, if it is fine to have the schema public you can do a mutation, like here:

import graphene
from graphene_django.rest_framework.mutation import SerializerMutation
from rest_framework_jwt.serializers import (
    JSONWebTokenSerializer,
    RefreshJSONWebTokenSerializer
)

class LoginMutation(SerializerMutation):
    token = graphene.String(description='JWT token')

    @classmethod
    def perform_mutate(cls, serializer, info):
        return cls(errors=[], token=serializer.object['token'])

    class Meta:
        serializer_class = JSONWebTokenSerializer

Hope that helps :)

Here is my final implementation for Token Auth for reference, for anyone needing this:
http://www.django-rest-framework.org/api-guide/authentication/#tokenauthentication

mixins.py

import json

from django.contrib.auth import get_user_model
from django.contrib.auth.models import AnonymousUser
from django.http import HttpResponse
from django.utils.decorators import method_decorator
from django.utils.encoding import smart_text
from django.views.decorators.csrf import csrf_exempt

from rest_framework import exceptions
from rest_framework.authentication import get_authorization_header
from rest_framework.authtoken.models import Token
User = get_user_model()


class TokenAuthMixin():
    """
    Simple token based authentication.
    Clients should authenticate by passing the token key in the "Authorization"
    HTTP header, prepended with the string "Token ".  For example:
        Authorization: Token 401f7ac837da42b97f613d789819ff93537bee6a
    """

    keyword = 'Token'
    model = None

    @method_decorator(csrf_exempt)
    def dispatch(self, request, *args, **kwargs):
        try:
            request.user, request.token = self.authenticate(request)
        except exceptions.AuthenticationFailed as e:
            response = HttpResponse(
                json.dumps({
                    'errors': [str(e)]
                }),
                status=401,
                content_type='application/json'
            )

            return response

        return super().dispatch(request, *args, **kwargs)


    def get_model(self):
        if self.model is not None:
            return self.model

        return Token

    """
    A custom token model may be used, but must have the following properties.
    * key -- The string identifying the token
    * user -- The user to which the token belongs
    """

    def authenticate(self, request):
        auth = get_authorization_header(request).split()

        if not auth or auth[0].lower() != self.keyword.lower().encode():
            return None

        if len(auth) == 1:
            msg = 'Invalid token header. No credentials provided.'
            raise exceptions.AuthenticationFailed(msg)
        elif len(auth) > 2:
            msg = 'Invalid token header. Token string should not contain spaces.'
            raise exceptions.AuthenticationFailed(msg)

        try:
            token = auth[1].decode()
        except UnicodeError:
            msg = 'Invalid token header. Token string should not contain invalid characters.'
            raise exceptions.AuthenticationFailed(msg)

        return self.authenticate_credentials(token)

    def authenticate_credentials(self, key):
        model = self.get_model()
        try:
            token = model.objects.select_related('user').get(key=key)
        except model.DoesNotExist:
            raise exceptions.AuthenticationFailed('Invalid token.')

        if not token.user.is_active:
            raise exceptions.AuthenticationFailed('User inactive or deleted.')

        return (token.user, token)

    def authenticate_header(self, request):
        return self.keyword

Pass header Authorization with Token .... to authenticate requests against user.

@patrick91 for some reason with that code I am getting an error saying
Exception: serializer_class is required for the SerializerMutation
despite being on the latest version of 2.0. Did you deal with this/were you able to fix it?

@brunoprela yeah, that code is for the version before 2.0 (that I think wasn't released). I'll see if I get some time to update it :)

@patrick91 thanks for the example code! If one is already using the django rest framework JWT package I believe you could get away with something as short as this:

from rest_framework import exceptions
from rest_framework_jwt.authentication import JSONWebTokenAuthentication
from graphene_django.views import GraphQLView


class JWTGraphQLView(JSONWebTokenAuthentication, GraphQLView):

    def dispatch(self, request, *args, **kwargs):
        try:
            # if not already authenticated by django cookie sessions
            # check the JWT token by re-using our DRF JWT
            if not request.user.is_authenticated():
                request.user, request.token = self.authenticate(request)
        except exceptions.AuthenticationFailed as e:
            response = HttpResponse(
                json.dumps({
                    'errors': [str(e)]
                }),
                status=401,
                content_type='application/json'
            )
            return response
        return super().dispatch(request, *args, **kwargs)
Was this page helpful?
0 / 5 - 0 ratings

Related issues

timothyjlaurent picture timothyjlaurent  路  3Comments

hyusetiawan picture hyusetiawan  路  4Comments

ZuluPro picture ZuluPro  路  3Comments

artofhuman picture artofhuman  路  3Comments

Northshoot picture Northshoot  路  4Comments