Access Control in GraphQL
Jonas Helfer
Editor’s note: This post is from early 2016. We published an updated post on this topic in 2018, which you can read here:Authorization in GraphQLPractical methods for controlling access to the data in your APIdev-blog.apollodata.com
If you like, you can read the original content of this post below!
Last week, I wrote about how you might go about authentication in GraphQL. This week, I want to talk about authorization.
As a reminder, here’s the definition of authentication vs. authorization again:
Authentication means checking the identity of the user making a request.
Authorization refers to the set of rules that is applied to determine what a user is allowed to see / do.
Authorization
Now that we have some ideas for how to authenticate our users on the GraphQL server, let’s look at ways to check the user’s permissions to make sure we only return data the user is allowed to see, or perform actions that the user is allowed to perform.
While you can use good libraries and protocols for much of the hard stuff in authentication, the opposite is true of authorization: This is your app’s territory, and you’ll need to think hard about which user has permission to do what. But instead of saying it’s up to you, wishing you good luck and leaving it at that, let’s take a step back and look at what we’re dealing with. Please bear with me as things are about to get a bit philosophical…
A GraphQL schema defines types. Each type — except for scalar types like Int, Float or String — has fields which define the relationship between this type and other types (one to one, or one to many). If you think about your schema in terms of a graph, types are the nodes of your graph, and fields are edges. Scalar types have no fields, so they form the leaf nodes of your graph.
A GraphQL query is just an instruction for traversing the graph in a specific way, resulting in a tree.
When traversing a tree, you would start at the root, but a graph has no root so there is no logical starting point! That’s why every GraphQL schema needs to have a root query type: it’s the entry point into the graph. The fields of the root query type are links to the actual queries that your GraphQL server supports. This may sound confusing at first, but don’t worry about it, you can start using GraphQL just fine without understanding this detail.
Since a GraphQL query is just a set of instructions for how to traverse the graph, there are two natural options for enforcing permissions:
- Which edges the user can traverse
- Which nodes the user can visit.
Permissions on edges
Defining permission rules on edges may sound a bit strange at first, but it’s effectively how most systems implement permissions. For instance, a user can see a todo item if that item belongs to a list that the user can see. A user can see a list, if it is either public (no owner), or owned by them.
For graphql-js, authorization on a per-edge basis can easily be checked in the resolve function of a field. If the current user does not have the permission to traverse the edge, the resolver will either throw an error, resolve to null, or — in the case of a list type — the edge will be omitted from the set of edges returned. For the todo list example from above, it would look something like this:
let listType = new GraphQLObjectType({ name: 'List', fields: () => ({ id: { type: GraphQLString }, name: { type: GraphQLString }, // other field omitted for brevity ... todos: { type: new GraphQLList(todoType), resolve: function(list){ return DB.Lists.get_todos(list.id); } } }) });
You may have noticed that there is no permission check in there at all! Because we said a user can see a todo if it’s part of a list they can see, we don’t need to check anything in the todos resolver. The permission check is enforced when the user requests to traverse an edge to get to the list. In our case that happens when the user runs the getListById query. Here’s the schema for that:
let query = new GraphQLObjectType({ name: 'Query', fields: { getListById: { type: listType, description: "Get a specific todo list", args: { id: { type: GraphQLID } }, resolve: function(root, {id}, ctx){ return DB.Lists.get(id) .then( list => { if(list.owner_id && list.owner_id != ctx.userId){ throw new Error("Not authorized to see this list"); } else { return list; } }); } } } });
The approach sounds reasonable in theory, but it has a major disadvantage: There are often multiple edges that lead to a particular node. For example, apart from a getListById query, there might be a list field on user, which finds all the lists for that user. If we can access a list through the user node, we expect to be able to access it through getListById as well. Think about it this way: If you give someone the keys to your front door, you might as well give them the keys to your back door. What you really care about is whether that person can get into your house or not. Which door they are using is really not that important.
There’s another reason why defining permissions on edges isn’t practical. If you want to use a client-side cache like Relay, your GraphQL schema needs to provide a way to retrieve a node of any type by its ID. Effectively that means that your graph must have a query node which has a direct edge to all your object nodes in the graph!
So just like it would be useful to have a single key that works for all doors that lead into the same room, it would be useful to have one place where permissions are checked. That is: if we’re going to put the same rules on every edge in the graph leading to one node, we might as well put the rule on the node itself and save ourselves some unnecessary repetition which would only make our code more error-prone.
That brings us to defining authorization rules per node.
Permissions on nodes
The goal here is simple — put authorization logic into the nodes, such that they are enforced no matter how the node is reached in the graph. The problem is, graphql-js doesn’t let you do this easily. Fields have resolvers, but types (i.e. nodes) don’t. That leaves us with two possible solutions:
a) Write the authorization logic for list in one function, and make sure it’s called by every resolve function of a field that leads to a node of type list.
b) Write the authorization logic for list in one function, and make sure it’s called in every field on the list type before returning anything.
Clearly, none of these options is ideal. Option a) has the advantage of being pretty close to what we would naturally do in a resolve function: Fetch an item of type list from the backend and pass it on to the list type for dealing with it. What we’re essentially doing is enforcing authorization on the edges, but using the same rule on every edge that leads to list.
The downside of option a) is that you have to make sure that the function is called correctly from every edge that leads to list. Those edges could be anywhere, and new edges could be added by other programmers that don’t know about the authorization logic, making it both harder to test and harder to maintain.
Option b) doesn’t completely eliminate this downside, but it makes sure that the places in which you have to call the authorization function are grouped together. That makes it much easier to test, and it makes it less likely that someone will screw up.
A downside of option b) is that now every field on your type needs to have a resolve function, otherwise you’ll be leaking data. When using option a), the field name on list doesn’t need a resolve function because it’s identical to what’s returned from the database. If we’re using option b), we need to add the resolve function just to check permissions. Another downside of option b) is that we now might be doing extra work by checking a permission on every field when it would be sufficient to check it once for the whole node.
Given the downsides of both of these options, I think it would be very neat if graphql-js would let you define a function that is called whenever a node is entered during the execution of the query, a sort of type-level resolve function. Such a function would be the natural place to do authorization checks. It would also be the natural place to resolve from an id to an object instance. If you use something like Facebook’s Dataloader for batching and caching requests, the impact on performance should be negligible for most all applications. If you absolutely needed to optimize for speed, you could always make your `onEnter` function pass through its argument if it encounters anything other than an ID.
For now, I think the best way to implement either of the above, is to factor data fetching logic out of your resolvers and put it in a separate place. In my opinion, that’s something you should do anyway, because besides giving you a good place to enforce per-node permissions, it will factor out the glue between your GraphQL schema and your backend into a neat component, which can be easily changed in the future.
You should always factor data fetching logic out of your resolve functions.
We’ve all heard the DRY mantra many times, but I think it’s still worth repeating in the context of GraphQL.
Conclusion
So, that’s enough philosophy for now. Where does it all leave us? To summarize, here are the main points:
- Checking authorization per node makes the most sense
- graphql-js doesn’t have a type-level resolve function to do #1
- A decent solution is to factor out data fetching into a separate layer and do the authorization check there.
Below is an example that follows the principles outlined above. It’s for a query that fetches all todo lists that a user can see.
For the following query,
{ allLists { name } }
Don’t do this:
//in schema.js (just the essential bits) allLists: { resolve: (root, _, ctx) => { return sql.raw("SELECT * FROM lists WHERE owner_id is NULL or owner_id = %s", ctx.user_id); } }
Instead, I suggest you do this:
// in schema.js (just the essential bits) allLists: { resolve: (root, _, ctx) => { //factor out data fetching return DB.Lists.all(ctx.user_id) .then( lists => { //enforce auth on each node return lists.map(auth.List.enforce_read_perm(ctx.user_id) ); }); } }//in DB.js export const DB = { Lists: { all: (user_id) => { return sql.raw("SELECT id FROM lists WHERE owner_id is NULL or owner_id = %s, user_id); } } }//in auth.js export const auth = { List: { enforce_read_perm: (user_id) => { return (list) => { if(list.owner_id !== null && list.owner_id !== user_id){ throw new Error("User not authorized to read list"); } else { return list; } } } }
You may think that the DB.Lists.all function is already enforcing permissions, but the way I see it it’s just trying not to fetch too much data, the permissions themselves are enforced not on each node separately. That way you have the auth checks in one place and can be sure that they will be applied consistently, even if you fetch data in many different places.
Before we wrap up, there is actually one more option worth noting: Not dealing with authorization at all. Your GraphQL server does not necessarily need to deal with authorization, it could just pass through the authentication information and let your various backends deal with authorization. This is not a good choice if you’re connecting directly to a database, but if you are building on top on an existing infrastructure of REST services, this might be the fastest way to transition. Your REST services most likely already implement a bunch of complicated authorization logic, and moving all of it at once would not be practical.
My colleague Sashko Stubailo recently published a great Medium post in which he does exactly that to implement a GraphQL API on top of the popular forum software Discourse.
Alright, here’s the conclusion of the conclusion — the tldr:
- Permission checks are best implemented on nodes
- Permission checks are best separated from data fetching logic to make sure they are applied consistently, regardless of how the data is fetched.
As always, nothing written here should interpreted in a dogmatic way. These are not “hard and fast” rules. Think of them more as guidelines. When it comes to Authorization, there are more often than not many roads that lead to Rome, and the solution that fits best for your application might be quite different from what I’ve outlined above.
Nevertheless, my hope is that you will find this article helpful if you’re just getting started with a GraphQL and weren’t quite sure what to put where. If you already have experience with GraphQL and have implemented authentication and authorization, I’d be curious to hear if you came to the same or a different conclusion. Let me know in the comments below!
If you want to read more articles like this, you might like the Medium publication Building Apollo, which has many posts on similar topics by Sashko Stubailo and myself.