Scaling Authorization in GraphQL

ยท

7 min read

One of the de facto standards of tackling how decoupled systems interact together is by building APIs. In recent years, GraphQL has gained popularity in developing web APIs. GraphQL spec uses a declarative query language that favors flexible queries to your client's demands. On the backend, you create a strongly typed schema to resolve data so the clients can send arbitrary queries for the exact data they want, which you will validate against your schema.

However, building GraphQL APIs differs from architectural styles like REST. In REST, everything is a resource defined by a URL. The REST architecture leverages stateless operations such as POST, PUT, GET, and DELETE. When you need to combine data from multiple endpoints in REST, you run numerous round-trip requests. Each round-trip will have the overhead of establishing a network request making the API inflexible. GraphQL, on the other hand, exposes a single endpoint for clients to request the exact data they need and get it back in a single response.

This article will teach you the best practices for implementing access control patterns in GraphQL APIs.

## How is Authorization Different in GraphQL

Unlike the REST API environment, where there's a single entry point for authorization, the [GraphQL spec](https://spec.graphql.org/) is unopinionated about how authorization is implemented.

For flexibility, GraphQL ensures that you have ad-hoc requests and, therefore, can have multiple layers where you can delegate authorization, such as field resolvers and the data layer.

## Ways of Implementing Authorization in GraphQL

This section will dive into various methods of handling authorization logic in a GraphQL API.

Before controlling access to data, a user's identity has to be verified. Use [this example](https://www.howtographql.com/graphql-js/6-authentication/) to learn how to put authenticated user info into the context object that is then passed to every executed resolver. The resolvers can then use the object to determine if a user has access to specific data.

## API-Wide Authorization

API-wide authorization is commonly referred to as the all-or-nothing approach since it permits or restricts access to the entire API. Once a request is received from a client, you can deny them the ability to execute a query based on their role. Performing this kind of authorization requires you to modify the context function as shown in this code snippet:

```js

context: ({ req }) => {

// extract the user token from the HTTP headers

const token = req.headers.authentication || '';

// try to retrieve a user associated with the token

const user = getUser(token);

// logic for checking the user roles can be implemented here

if (!user) throw new AuthorizationError('login required');

return { user };

},

```

API-wide authorization can be easily implemented in private environments where the organization doesn't want to expose the schema or fields to the public. However, this approach locks queries, making your API inflexible when you need to implement field-level accessibility.

## Authorization in Resolvers

Resolvers are functions that define how you perform actions against your data from the fields on the type definitions. GraphQL offers granular control over data by delegating authorization to individual field resolvers. This allows the developer to create individual field resolvers that check respective user roles and return the appropriate data for each user.

Unlike the API-wide approach, implementing Authorization in the resolvers offers enormous flexibility. You have the logic close to the components the end-users will interact with, and all you need to do is to grant queries and mutations based on roles. When developing large systems, we reduce the baggage of hunting down the logic that controls access. However, this approach has the potential drawback of needing to repeat code across multiple resolvers.

You must use the user information attached to the context object to implement this approach.

"`js

books: (root, args, context) => {

if (!context.user || !context.user.roles.includes('admin')) return null;

return ['book 1', 'book 2โ€™'];

}

```

The above example shows a field in the schema named books, which returns a list of books. The if statement checks the context generated from the request for a user roles array with the admin role, and if one doesn't exist, it returns null for the whole field. This dictates that only users with the admin role can access the list of books.

This approach allows you to limit possible errors that could expose sensitive data by short-circuiting resolvers and not calling lookup functions when the permissions to use them are unavailable.

On the other hand, it can become tedious since you need to write authorization checks on every resolver, thereby replicating logic.

### Authorization in Models

This approach is practical when you have the same fetching logic in multiple places.

Most GraphQL APIs will have a schema that returns nearly similar data. Moving the data fetching and transformation logic from the resolvers to centralized model objects in such cases. This helps clean up the resolver by delegating authorization to models.

You need to add the models to the context to delegate authorization to models. For example, to generate models with a function, the code can look like this:

```js

context: ({ req }) => {

// extract the user token from the HTTP headers

const token = req.headers.authentication || '';

// try to retrieve a user associated with the token

const user = getUser(token);

// logic for checking the user roles can be implemented here

if (!user) throw new AuthorizationError('login required');

return {

user,

models: {

User: generateUserModel({ user }),

}

};

},

```

This solution gives all the model methods in the User model access to the user information, which can be used to check permissions directly without using the resolvers.

The downside of this approach is that it does not allow you to know authorization rules at a high level when you need to return early.

## Using an ABAC system like Cerbos

A common approach to adopt for authorization is Role-Based Access Control (RBAC). With RBAC, your access and actions are assigned to users depending on their roles in your organization. However, this solution becomes cumbersome as we need to redesign and add more roles.

Another alternative to implementing authorization is using attribute-based access control (ABAC). The access model provides security rules applied as object attributes with this approach. These attributes act as scope metadata to infer whether the user should be granted specific actions against the resource. For example, we can have an attribute about a location that limits managers to access information based on their current of a large organization.

One of the popular tools that provide Attribute-Based Access Control is Cerbos. Cerbos is a self-hosted, open-source access control provider that allows you to offload tedious authorization decisions from your backend infrastructure. This approach makes it easier to decouple authorization logic as configuration scripts to ensure consistency between various services.

Running Cerbos requires you to configure a server by defining the port and the location for storage of local policies. A simple configuration file takes this format:

"`yaml

---

server:

httpListenAddr: ":3592"

storage:

driver: "disk"

disk:

directory: /policies

```

After you've defined the configuration, Cerbos can be run as (a Docker container or binary)[https://book.cerbos.dev/02-running-locally/index.html].

Cerbos also requires you to define policies that dictate the permissions allowed for a given user. A simple policies file looks like this:

"`yaml

---

apiVersion: api.cerbos.dev/v1

resourcePolicy:

version: "default"

resource: "Project"// Name of the resource whose permissions are defined.

rules: // list of rules on the resource. In this case, a user is only allowed to create and read a project, while an admin can also delete and update the project.

- actions:

- create

- read

effect: EFFECT_ALLOW

roles:

- user

- actions:

- create

- read

- update

- delete

effect: EFFECT_ALLOW

roles:

- admin

```

With the user actions defined, Cerbos can be called inside GraphQL resolvers as discussed in the Cerbos GraphQL demo on [Github](https://github.com/cerbos/demo-graphql).

From the policies defined above, making a delete request as a user who is not allowed will result in an error:

![An error message for an unauthorized user](https://i.imgur.com/VixInwy.jpeg)

For an admin, the delete action is allowed, and there won't be an error:

![A successful query for an authorized user](https://i.imgur.com/71zAwrt.jpeg)

With ABAC and Cerbos, you have the flexibility of fine-grained control over your authorization, where policy updates do not require redeploying the application in every change.

Any part of the application stack can check to see if a user is authorized to access a particular resource. Additionally, Cerbos is stateless and lightweight, which makes communication overhead negligible, especially with modern networks. However, you may need custom access requests in different stages when processing a request, possibly even on each field level, for granular permissions of each field in a query.

## Conclusion

This article has covered what makes GraphQL very different from REST. It has also discussed multiple places where you can implement authorization in your GraphQL API with their pros and cons.

As GraphQL adoption continues, decoupling and centrally managing authorization logic becomes vital. Cerbos is a popular open-source solution that lets you define and consistently share your authorization policies using declarative YAML files, which can integrate seamlessly with any environment.

Learn more about Cerbos in the [offical documentation](https://cerbos.dev/) and check out how to integrate Cerbos in your GraphQL API from the [offical Github page](https://github.com/cerbos/demo-graphql).

Did you find this article valuable?

Support Wilson Gichuhi by becoming a sponsor. Any amount is appreciated!