Token Authentication and Authorization with GraphQL and Django
I've enjoyed playing around with GraphQL. The GraphiQL in-browser IDE makes it easy to iterate on query . Graphene and its associated plugins make it easier to integrate GraphQL with Django applications, and with the recent 2.0 update seemingly making things a little more intuitive in more complex use cases, I've been experimenting with adding a GraphQL API to existing Django projects. The graphene documentation provides solutions for a handful of Django authorization scenarios, but doesn't cover how to integrate with any authentication methods beyond the standard Django LoginRequiredMixin. This is nice for a web application, but what if I want to add GraphQL to (and, presumably, eventually replace) some REST backend API?
In my case, I wanted to use my existing Django Rest Framework (DRF) Token authentication endpoints alongside GraphQL. I'll be using a class-based view approach for Django, DRF, and Graphene.
Authentication
To implement the necessary authentication endpoints, let's just follow the DRF documentation. I prefer to isolate authentication in a seperate Django app. Per the DRF docs, this app includes the following files:
# authentication/models.py
from django.conf import settings
from django.db.models.signals import post_save
from django.dispatch import receiver
from rest_framework.authtoken.models import Token
@receiver(post_save, sender=settings.AUTH_USER_MODEL)
def create_auth_token(sender, instance=None, created=False, **kwargs):
if created:
Token.objects.create(user=instance)
The provided ObtainAuthToken view, by default, returns a JSON response containing the token, but this class can be extended to implement a custom response if more information is necessary to the client:
# authentication/views.py
from rest_framework.authtoken.views import ObtainAuthToken
from rest_framework.response import Response
from rest_framework.authtoken.models import Token
from users.serializers import UserSerializer
class CustomObtainAuthToken(ObtainAuthToken):
def post(self, request, *args, **kwargs):
serializer = self.serializer_class(data=request.data)
serializer.is_valid(raise_exception=True)
user = serializer.validated_data['user']
token, created = Token.objects.get_or_create(user=user)
user_serializer = UserSerializer(user)
return Response({'token': token.key, 'user': user_serializer.data})
GraphQL
Credit to those involved in this discussion for this solution.
The key step in allowing for token-based authentication and proper authorization is extending the graphene-django GraphQLView
, the built-in class-based view. By default, our GraphQL endpoint is exposed, and we need to add the necessary permissions and mechanisms for a token-based approach. This can be done with DRF's decorators:
from graphene_django.views import GraphQLView
from rest_framework.permissions import IsAuthenticated
from rest_framework.authentication import TokenAuthentication
from rest_framework.decorators import authentication_classes, permission_classes, api_view
class DRFAuthenticatedGraphQLView(GraphQLView):
# custom view for using DRF TokenAuthentication with graphene GraphQL.as_view()
# all requests to Graphql endpoint will require token for auth, obtained from DRF endpoint
# https://github.com/graphql-python/graphene/issues/249
@classmethod
def as_view(cls, *args, **kwargs):
view = super(DRFAuthenticatedGraphQLView, cls).as_view(*args, **kwargs)
view = permission_classes((IsAuthenticated,))(view)
view = authentication_classes((TokenAuthentication,))(view)
view = api_view(['POST'])(view)
return view
Hook this up via urls.py
. Then, when hitting the GraphQL endpoint with the GraphQL client of choice, include the token in the Authorization header. For example, with the Apollo GraphQL client, configure the networkInterface
to use the appropriate header:
// assuming a our DRFAuthenticatedGraphQLView is exposed at /graphql
const networkInterface = createNetworkInterface({ uri: 'http://localhost:8000/graphql' });
const authMiddleware = {
applyMiddleware(req, next) {
if (!req.options.headers) {
req.options.headers = {}; // create headers object if needed
}
req.options.headers['authorization'] = getToken();
},
};
networkInterface.use([authMiddleware]);
const apolloClient = new ApolloClient({
networkInterface,
});
This post is mostly for my own reference, and any feedback or suggestions more than welcome.