How to build role-based authorization for your GraphQL server

Nathan Meibergen
The Startup
Published in
9 min readMay 11, 2020

--

Recently I started building a question answering tool with the likes of StackOverflow. As I truly believe in graph technology for many use cases, this seemed like one with great applicability. A great journey was ahead.

I won’t dive into Neo4j, nor in building a frontend (which I have done with React), nor in building the server-side code for such tool. Instead I want to focus on authorization. Authentication identifies a user and authorization determines what a user can and cannot do.

To proceed you will need a basic understanding of the following:

  • JavaScript
  • GraphQL
  • JWT encryption

If you don’t enjoy reading, feel free to dive into the my Github repo instead, where you can find the working example.

What I wanted

Before giving you an overview of how I went about creating role-based access control (RBAC), let’s start by what I needed.

In terms of flow: Whenever a user logs into our tool I want the user to retrieve a role. Based on that role the user is assigned a set of permissions which allows this user access to specific parts of our API. Similarly, if a user does not authenticate I want to provide that user with a ‘guest’ role, which again has a set of permissions attached to it.

The logged in user has a bunch of information in the JWT token, which I want access to within my resolvers, e.g. through accessibility in the context.

I want to have the ability to change permissions and not having to wait for JWT tokens to refresh: What we sometimes see is that specific permissions are saved in the JWT token. If at some point you want a (group of) user(s) to have less permissions, you might need to wait for tokens to expire, giving you less access control when you might need it.

I want to keep a nice separation between code development and access control management. That is, I want the ability for a developer to write code and assign permission names for all pieces of the API, while someone else can, without having to change anything in the code, assign any of these permissions to the roles.

This last point is another reason to include a guest role. You might decide that if you want to leave an endpoint accessible for anyone, you’d simply provide it no permissions in your GraphQL types. However, if at some point you’d like to grant more or less access for the guest, you will need to change the GraphQL schema. To avoid this I wanted to attach permissions to all queries/mutations, and leave it to the ‘access control manager’ to decide on permissions.

To summarise, what I needed:

  • Separating RBAC management from backend development
  • Ability to quickly change permissions for a role
  • User information throughout my whole backend application
  • Guest users (users that have not logged in) to have access to clearly defined pieces of the GraphQL API

High level resulting framework

The resulting framework heavily relies on a middleware that I have created, which will be described in more detail later.

How it will work

A user that comes into the tool without logging in doesn’t have a JWT token. A user that does log in will have one (if authentication succeeds that is). Any such user might try to access a GraphQL query/mutation in our API that requires some permissions. The next steps should be the result of a proper setup:

  1. If a token is provided, it will decode the token and extract the role or permissions attached to the user. If no token is provided it will attach a default role to the user, say “visitor”.
  2. If in the previous step a role was provided, the middleware will determine the permissions of this role, given a role-based authentication scheme.
  3. A check on whether the permissions are sufficient for the particular query/mutation that the user is trying to access.
  4. Finally the user information (if it exists) is attached to the context for further usage.

Setup steps

  1. Attach permissions/scopes to your queries and mutations. You can add them to all of them, also those that should be visible for unauthenticated users. You may also add them to any types and/or fields.
  2. Create a role-based authentication scheme, specifying permissions per user.
  3. Configure the middleware to pick up the authentication scheme and JWT secret among others.
  4. For a user, generate a token within your authentication service (Auth0 in my case) and when requesting some query/mutation hand it over to the middleware.

We shall walk through these setup steps in what comes next.

A final note before diving into the code setup is that this middleware is an extension of an existing package called graphql-auth-directives. A package that does the decoding and checks permissions/roles/authentication.

Great, but how do I implement this?

By means of an example I’d like to show you how you can setup the above framework. The result won’t be a fully working tool for it will be lacking a frontend and an authentication service. For the authentication service I used Auth0. How that fits in should become clear soon.

But before we start, let’s setup an example Apollo GraphQL server to work with.

Setup your GraphQL server

For the sake of this example I will use an index.js script with the following schema:

The schema thus allows for querying books and authors, and one may create new books. Whenever a book is created the name of the user that created it is attached to the book property addedBy.

1. Defining permissions in your GraphQL schema

First install the middleware package graphql-auth-user-directives. This is my extension ofgraphql-auth-directives. It is available on npm, so you can simply run yarn add graphql-auth-user-directives. To allow usage of the package we need to import the directives into the index.js file, i.e.

import { IsAuthenticatedDirective, HasRoleDirective, HasScopeDirective } from "graphql-auth-user-directives";

Next we need to make the new directives available in our schema. To do so add the directives in your typeDefs

directive @hasScope(scopes: [String]) on OBJECT | FIELD_DEFINITION
directive @hasRole(roles: [Role]) on OBJECT | FIELD_DEFINITION
directive @isAuthenticated on OBJECT | FIELD_DEFINITION

and extend the schema in makeExecutableSchema so that the schema instead becomes

const schema = makeExecutableSchema({
typeDefs,
resolvers,
schemaDirectives: {
isAuthenticated: IsAuthenticatedDirective,
hasRole: HasRoleDirective,
hasScope: HasScopeDirective
}
});

Now that we can use the directives, we will attach permissions to our scheme, that is, for this particular example we append the queries and mutations as follows:

Now anyone that tries to access the queries or the mutation must have the specified permissions.

2. Define a role-based authorization scheme

We don’t want everyone to be able to create new books and view the authors, that should be available for admins only. To manage our role-based permissions we create a JSON file with our authorization scheme:

Here the visitor corresponds to a user that is not authenticated. If a user is not authenticated our middleware package graphql-auth-user-directives will attach the role visitor to the user, if you’d like to name it differently you can configure the middleware to rename it, check out the configuration section below.

So, now we have

  1. permissions/scopes for all our queries and mutations, and
  2. an authorization scheme specified in JSON format

3. Configure the middleware

Specify the autorisation scheme

All that is left is for the middleware to recognise that a certain role has a set of permissions attached to them, so that based on the required permissions the middleware will only let you in if the role has the required permissions.

This is done by providing the authorization scheme as an environmental variable. As a JSON is not a very convenient form for such variables we base64 encode it. The above authorization scheme thus becomes

Now create a new environmental variable called PERMISSIONS, e.g. add

export PERMISSION=<base64 encoded authorization scheme>

Users that are not authenticated are automatically provided with the role visitor, however if in your case that is not a suitable name you can change it by specifying the environmental variable DEFAULT_ROLE.

Add your JWT secret

JWT authentication works by encrypting a claim, that is, information for a specific user. This encryption results in a token that can only be decrypted given a ‘secret’ code, the JWT secret.

In order for decryption to be successful within the middleware you’ll need to provide the JWT secret as an environmental variable, e.g.

export JWT_SECRET=<you secret>

Managing your meta data, e.g. roles and scopes

You can add roles and/or scopes to your (decrypted) token. To assign roles you can use the property name role, roles, Role or Roles. If instead you’d like to use scopes, you may call these scope, scopes, Scope, or Scopes. In some cases you will have property names formatted as a url, e.g.

"https://www.mysite.com/role" : "admin"

The middleware can help you extract the property role if you provide role as a meta data property. If you do so it will remove the url part and attaches "role": "admin" to your user object instead. You can let the middleware know that role is such meta data by setting adding it to your environmental variables as

export USER_METAS="role"

To add more of these meta data, you simply provide them comma separated to the USER_METAS variable.

4. Generate a token and let the middleware know

I won’t spend too much into how you should generate a token as that is part of your authentication service. In my tool I have implemented Auth0 which allows you to create roles and attach these to users with an account. As a critic might note, in Auth0 you can also setup permissions, so why bother with our JSON file? Well, because

  1. We also want to manage the permissions of our visitors, those that do not have an account and
  2. if you change permissions in Auth0, all users that are logged in already, and thus already have a token will still retain their old permissions.

When a user requests a query or mutation, the middleware will need to know what role this particular user has. This is all stored in the JWT claim and therefore it should be send together with your request in the header as follows:

Authorization: "Bearer <The user JWT token goes here>"

Of course, if the user is not authenticated you do not need to provide a token. The middleware will understand that there is a visitor instead.

Let’s try an example

Configure the middleware with the following environmental variables:

This sets a JWT secret and permissions as specified above. Next create a JWT token based on the following claim:

This should be encrypted with your JWT secret. You can create such JWT claim with for example an online JWT builder, see here. The token I created looks as follows:

Now, start up your server and send the following mutation:

You will probably get an error because you are not authenticated/authorised to perform this action, as we specified that adding books is only for admins. If now you provide the above token in your header as described in the previous section you will see that a new book is successfully created and your result should look like:

Again this example is fully worked out in my Github repo here.

That’s all there is to it

At the start of this article I specified a few of my desires and I can now show you how these are all satisfied.

First of all separation between RBAC management and backend development is guaranteed. If RBAC manager wants to provide a visitor with additional permissions, the backend developer will not have to remove scopes from the GraphQL schema, instead one can simply adapt the rule based permissions JSON instead. For example, if you’d like visitors to be able to add books in the example, simply add it to the JSON and you’re done!

And indeed, guests, or visitors, however you’d like to call them, have clearly defined permissions as specified in the permissions JSON. Instead of having to refer to the GraphQL scheme to see these permissions everything is collected in a single place.

Whenever such changes are performed these can ‘instantly be turned on’ for end users of your tool by changing the environmental variable PERMISSIONS on your server or virtual machine or however you do your hosting. In my case I use a Kubernetes cluster and therefore will have to perform a restart on my pod that hosts the GraphQL server.

Whenever authorization is successful the user information is attached to the context and can thus be used immediately in your resolver. This is also shown in the example above.

Thank you for reading through, and if you have any feedback, please let me know. Writing good code is a never ending learning journey!

--

--

Nathan Meibergen
The Startup

I am a mathematician, enthusiastic about creating analytics driven tools using state of the art analytics models and development tools.