Authorization in GraphQL

GraphQL is a great new technology. It fills many gaps the REST leaves. However, there are challengs with GraphQL. One of those challenges are authorization and authentication.

June 8th, 2020 in GraphQL

Unlike REST, where access control is typically done endpoint by endpoint, GraphQL has a single endpoint with many entities. With a typical REST setup using something like Express, Flask, or Laravel, you typically set up your controls using middleware. For each request, your middleware is triggered once and your permissions are checked and applied there. If you are allowed to access some endpoint, this middleware will allow the request to continue. If you are not allowed to access the resource, the middleware will reject your request.

The life cycle of express JS. Any of the middleware can reject a request before it hits the main task
The life cycle of express JS. Any of the middleware can reject a request before it hits the main task

This works in REST because typically, a single endpoint refers to a single resource or action. For example /:userID/orders refers to actions related to a users’s orders. A GET /:userID/orders request could have a single permission like read:orders, POST /:userID/orders could have create:orders. So on and so forth. This is insanely easy to wrap our heads around: does this request have permission to talk to this endpoint.

But now lets think about how this fits in to a GraphQL api. On its face, this looks like a harder problem. After all, instead of having an API comprised of multiple endpoints, we have an a single endpoint that represents our entire API. We can’t check the permissions on an endpoint by endpoint basis. But this problem isn’t actually all that different. We still check permissions at the entity level, its the representation of those entities that change. REST represents entities at the endpoint level, GraphQL represents entities at the Schema level.

Translating Authorization from REST to GraphQL

Similar to REST, there are a few ways to check permissions in GraphQL. You could use JWTs to represent access and permissions, you could use token based authentication, or you can go session based. The mechanism for getting information about the current request and the permissions associated with that request doesn’t have to be any different from what you are doing in REST. However, the interpretation of that mechanism must be different.

A single GraphQL request could be comprised of multiple entities, which means that interpretting your Authorization mechanism per entity will absolutely lead to performance issues. Unlike REST, where you can do a simple look up in a database for a permission once in a request for the entity you are looking for, a GraphQL server may have three or four entities that all need the same check. This means that a single request could trigger revalidating and re authorizing a request three or four times at least. If you are joining multiple entities together in a request, this number can go up exponentially, a classic n+1 problem.

Take for example this schema:

type User {
    orders: [Order!]!
}
type Order {
    items: [Item!]!
}
type Item {
    sku: String!
}

If in order to read items, you need a permissions called read:items and for reading orders you need a permission read:orders, everytime a user asks for an order and items on that order, you will check to see if they have that permission. Now lets say that a user has 25 orders, and every order has at least 25 items. Your check for 2 permissions on two different entities becomes 625 checks! If for each of these checks you are going to the database, that is 625 different times you are going to the database for a single request! A huge performance implication for sure.

Fast it ain't
Soooooooo slow

Seperating Authorization from Authentication

In order to work around this obvious bottleneck, first you need to seperate Authentication from Authorization a bit. Authentication is the idea that this person is they are who they say they are, where as authorization is that this person has permission to do these things. In REST, its really easy to marry these two concepts. It is all mostly a single request after all. In GraphQL, marrying these two leads to checking someones identity 625 times. By seperating these two concepts, you can do a lot of frontloading of identity data.

Luckily, GraphQL has this idea of a context object, which is shared accross all of the resolvers in a single execution. Information about the user/current request should be put on this object. If you are using token based auth, building your context is where you should reach into the database and load information about the user (name, id, and yes permissions). For more granular access controls (like this user can view this Item on that order, but not that item on this other order), you can reuse the dataloader pattern to load the permissions when you need them and then cache them for the life cycle of the request.

Of course, this means that authentication is done first, and authorization is still done on the entity its self. But where should we add this logic?

Strategies for Describing Access in GraphQL

When you have access controls, you need a way to describe this access. This is typically done by some middleware function like hasPermission("read:orders"). In GraphQL, this is a little more complicated. You have three places you can define access: the Schema, the Resolver, and the Model. Which is the best place for this control?

Defining Access in the Schema

Since GraphQL has directives, it seems like this could be a good place to define what the permissions needed to access that entity are. And for simple applications, it is. Being able to define your API schema and the permissions needed to access it in a single place is great. It shifts the focus from thinking about the actual code, to engineers thinking more about who should be accessing this resource. But when the model gets more complicated, like when a user may have different level of access to different resources, this becomes harder to maintain. Especially when there are more complicated authentication needs. For example in a single request, the ability for a user to see some information on one order, but not on the other order means you need to have control around the user, and the entity. How do you represent this in your schema? You need to tell your directive about the context of the object like your parent, your current arguments, the overall context, etc. Then that context shifts based on the location in the schema.

Defining Access in the Resolver

Instead of defining a Schema directive, you move this logic to the resolver. This allows you to keep your authorization logic close to the code that a user will actually interface with. This reduces the load for hunting down logic that will "keep users out." You also gain all the context of a single resolver like the parent, the actual context, info on the current execution and arguments. There is however a draw back in the fact that there may be a lot of repeated code. For example, lets look at this schema (which is the schema we defined above, but extended):

type User {
    orders: [Order!]!
}
type Order {
    items: [Item!]!
    user: User!
}
type Item {
    sku: String!
    owner: User
}

type Query {
    users: [User]!
    orders: [Orders]!
    items: [Items]!
}

If we needed the read:users permission to access users, and we put that logic in the resolver, then we would need to replicate that logic accross 3 different resolvers. Once in the query resolver, once in the User resolver for Orders, and once for the User resolve in Items.

Defining Access in Models

This is more philosophical, but in general, resolvers should be as dumb as possible. The should be mostly relegated to grabbing data from some other class. Not only does that help by not replicating logic to grab an entity accross multiple resolvers, but it helps keep concerns seperate. For example, the schema above should have a User, Order, and Item model. These models should then be responsible for grabbing the information from its datasource, checking the permissions, and doing any kind of business logic. Now in the resolver for Users in Item, all we do is call the User Model to get the user information for us. This allows us to keep the authorization logic close to the code that actually fetches the data, and reuse the code across an application.

To throw or not to throw

On of the last major considerations for authorization in GraphQL is whether to throw an error if the user is not authorized, or to simply return a null value. This has its roots in the 404 vs 403 debate in REST.. In GraphQL, this takes a whole new meaning. Throwing an error in a resolver could result in an entire request being thrown out, even if other parts of the request were valid. This is made worse when you have a client that asks for broad data, with some data the user can not access. For example, if you have access to read some of the User‘s order, but not all of them, if one user throws an error, that means the entire request may be thrown out, leading to no data being returned to the client. Instead, resolvers should return a nullish value if they don’t have access to the resource.

If done intelegently howerver, you could use the errors in the GraphQL response to denote that the user doesn’t have access and still return a partial response. However, differently libraries handle errors differently which could lead to the problem I spoke about above. For example, Apollo Server will accept certian errors as allowing partial responses, but not all errors. I have had issues with Apollo throwing away all data when I threw a AuthenticationError from resolvers in the past. Really its up to you how you want to handle access problems.

The Wrap up

Authentication and Authorization in GraphQL isn’t anymore difficult than it is in REST. It is a different way of thinking for sure. We are used to this in REST, and have many different ways of handling this securely and safely. GraphQL, being a newer standard, doesn’t have the experience yet. However, we can take the experience that we gained from REST and apply it to GraphQL pretty easily.



Popular Posts