Authenticating users in Graphql with Django session authentication
We know that in JavaScript, cookies can travel over AJAX requests as long as the request comes from the same origin, and goes to the same origin. In other words, an AJAX request from https://www.pluto.com/
to https://www.pluto.com/api/
carries any cookie currently set in the browser, by sending them in a Cookie
header.
What this means in a Django project for example is that if a user is authenticated, and a template happens to make an AJAX request to the same backend, authentication credentials are transmitted by default.
In Django, the authentication cookie stored in the browser is called sessionid
by default.
In fact, by examining the headers of a WSGIRequest
or ASGIRequest
for an authenticated user in Django, we should be able to see something along these lines:
{ 'Cookie': 'sessionid=g9eflhxbeih1lgmslnybt5dn21zgk28t'; csrftoken=D3DtmfPKxriKMoy70eYikf8pUEVMTy3bDTczk8Ni0BNFVArAWg9oGat5V8PfKQW1 }
Such a request means that the user issuing the request is indeed authenticated. Here's the crazy idea: if you use GraphQL under Django session auth umbrella, you can use validate the sessionid
cookie in the resolver itself.
Here's how.
Validating sessionid in a GraphQL resolver
Consider the following Ariadne GraphQL resolver:
@mutation.field("replyUpdate")
def reply_update(_obj: Any, info: GraphQLResolveInfo, reply):
"""Resolver for reply update."""
request: ASGIRequest = info.context["request"]
# do work ...
The second parameter of the resolver is info
, which has access to the current request, be it an WSGIRequest
or ASGIRequest
(the same holds true for Strawberry).
How do we know that the user is authenticated in such a setup?
For some projects, it's not crazy to have the JavaScript frontend served by a Django template view, it could even be an SPA, or have a sprinkle of JavaScript in a template, making requests to the Django backend (Decoupled Django covers both approaches).
If you're running GraphQL in your Django project, and some of your JavaScript needs to make GraphQL requests to the backend, you can validate the session with get_user()
from django.contrib.auth
, as in the following example where I check that the user is both authenticated, and a staff member:
@mutation.field("replyUpdate")
def reply_update(_obj: Any, info: GraphQLResolveInfo, reply):
"""Resolver for reply update."""
request: ASGIRequest = info.context["request"]
user = get_user(request)
if user.is_authenticated and user.is_staff:
# do work, return the reply
return {"error": {"detail": "Authentication credentials were not provided."}}
If the user is not authenticated, I return a meaningful error to the client.
To give you some more context, here's also the JavaScript snippet in charge of sending out the GraphQL mutation:
form.addEventListener('submit', function (event) {
event.preventDefault();
const formData = new FormData(event.target);
const body = {
variables: {
reply: {
message: formData.get("edit-reply"),
id: replyId
}
},
query: `mutation replyUpdate($reply: ReplyInput!){
replyUpdate(reply: $reply) {
error { detail },
reply { message }
}
}`
}
fetch("{% url "support:graphql" %}", {
method: 'POST',
body: JSON.stringify(body),
headers: {
"Content-Type": "application/json"
}
})
.then( /* handle the response */ )
.then( /* handle GraphQL response*/ );
});
Also, from what I'm seeing, get_user()
works fine under ASGI without any trouble.
Thanks for reading!