A Guide on GraphQL Authorization
Written by Bahaa Zidan
The GraphQL spec is very open-ended. It leaves many concerns to be implemented by programmers as they see fit. Among the concerns that are not strictly outlined in the spec are Authentication and Authorization. In this article, I’ll walk you through one possible approach to building a great authorization framework for use in your GraphQL API.
I’ll assume that the reader already have a basic understanding of backend development in general and have built at least basic GraphQL APIs before. If you haven’t, please take a quick look at the official graphql tutorial.
Authentication
Before we get into authorization, let’s have a super basic authentication setup in our GraphQL server. We won’t get into the login/signup flows. But we should at the very least have a way to identify who exactly is using our GraphQL API.
Here we’re using apollo-server. The same can be implemented using any other spec-compliant graphql server. Implementing getAuthenticatedUser
function is beyond the scope of this article. But no matter how it’s implemented. It should return a Promise of a user object. That way, queries won’t have to be blocked on authenticated user unless it is necessary.
Possible Approaches
Now when it comes to authorization, there are many approaches out there. Most of them can be boiled down to these 3:
Imperative Approach
The simplest way to start implementing authorization is to have your authorization checks write there in the resolver code. Let’s run with an example of a graphql mutation called editArticle
.
This approach is fine for toy projects. But it becomes a hell to maintain very quickly if you’re building something serious. Imagine you want to add more auth checks. Maybe implement banning features. Imagine a change of auth policy is required to be implemented system-wide, you’d have to go and read the resolver code for every single query and mutation you have in your API. It doesn’t scale with the size and/or complexity of your application.
Middleware
A much better approach is using middlewares. Continuing with our editArticle
example, let’s see how can this approach be better. I’m gonna be using graphql-shield for these examples. But the same can be achieved using any middleware library.
We first start by defining our rules. These are functions that return a boolean. If true, execution will continue.
Then we create the permissions map.
Then we use applyMiddleware
from the graphql-middleware package to apply these rules to our schema.
And since we’ve externalized all our auth checks into their own functions and middlewares, we can delete all these imperative checks from our resolver code.
Now we have a clean resolver code that acts as a routing layer that maps our operation to a data source. This approach is already a massive improvement over the imperative approach. But while decoupling authorization code from the resolver implementation led a much leaner code and allowed for reusing auth rules, it also introduced a major flaw. Now we’re developing our authorization in a vacuum and then attaching it to our api implementation. We’re now treating authorization as if it’s a concern separate from our API and that can lead to poor readability down the line. That’s where our next approach comes to the rescue.
The Auth Directive
Directives are a great tool to dynamically change the behaviour or even shape of your schema. Let’s see how we can use them to create a powerful authorization framework while still being very simple to implement.
Continuing with our editArticle
mutation example, assume that this is the schema we have:
As of now, this schema only defines the types our GraphQL API exposes, our implementation is in the resolvers, and our authorization is defined using middlewares somewhere completely separate in code. Using Directives we can define our authorization rules right there in the schema itself. And instead of middlewares, we’re going to make a declarative authorization framework using directives.
To create a directive we need to define its’ types in our schema:
First we define an enum called Scope
this enum is going to hold all the possible scopes that we’re going to check against. Here we define one scope called write_article
that represents having write access to an article.
Then we define our directive. Its’ name, arguments, and what kind of objects can this directive be applied to. For now this directive can be applied to objects and field definitions in our graphql schema. For more information about the directive syntax, consult the graphql-tools documentation.
Now let’s provide an implementation of this directive. The current directives API provided by graphql-tools is very simple to understand but a bit gnarly to implement :D. All we’re actually doing is we’re making a transformation function that takes in an executable schema and returns an executable schema that is slightly modified.
It may look like much, but all this code does is checking whether or not our directive has been applied to a certain field. And if so, we’re going to redefine the resolver of that field to run our authorization checks before we run the original field resolver.
Now let’s add those checks:
All that’s left is to run the transformer function on our schema before we export it:
Finally we can use the requireAuth
directive anywhere in our schema.
We can extend the directive by defining more scopes. We can also have one query depend on multiple scopes at once by having the requireAuth
directive take an array of scopes as an argument.
So we get the same composability we had using middlewares. But now we define our authorizations declaratively within our schema without having to worry about implementation or order of execution. Everything we need declared in one place: the schema.
Conclusion
Any of these approaches can work. It all depends on the size and complexity of your project. If you’re building a toy project then having imperative auth checks right there in your resolvers might not be so bad. If you’re building something serious, I invite you to consider either the middleware or the directive approach. Personally, I tend to prefer the auth directive as it allows me to include all API concerns in the schema. I think of it the same way I think of css-in-js and JSX allowing frontend devs to include everything a component needs in one place. In future articles, I’ll show you how I use directives to handle data validation as well.