Handling authorization in GraphQL
You will need Node installed on your machine. Basic knowledge of JavaScript and GraphQL will be helpful.
Authorization occurs after a successful authentication, it checks the access levels or privileges of the user, which will determine what the user can see or do with the application. Some time ago, I did a tutorial series on handling authentication in GraphQL. So in this tutorial, I will be covering authorization.
Prerequisites
This tutorial assumes the following:
- Node.js and NPM installed on your computer
- Basic knowledge of JavaScript and Node.js
- Basic knowledge of GraphQL
- Understanding of handling authentication in GraphQL with JWT. You can check out this tutorial.
What we’ll be building
We will be building on where we left off from handling authentication in GraphQL – Part 2: JWT. To demonstrate authorization, we will add two new features: fetching a list of all users and allowing users to edit their post. Only an admin user will be able to fetch a list of all users. Also, we will make it so users can only edit their own posts.
Getting started
To speed things up, we will start by cloning a boilerplate, which I have created for this tutorial:
$ git clone --branch starter https://github.com/ammezie/graphql-authorization.git
Next, let’s install the project dependencies:
$ cd graphql-authorization
$ npm install
Next, rename .env.example
to .env
then enter your JWT secret:
// .env
JWT_SECRET=somereallylongsecretkey
We will be using SQLite, so create a new database.sqlite3
file in the project’s root directory.
$ touch database.sqlite3
Lastly, run the migration:
$ node_modules/.bin/sequelize db:migrate
If you followed from the handling authentication in GraphQL series, you will already be familiar with the project. I made some few changes though. The project has been migrated to Apollo Server 2 and the User
model now has an is_admin
column as well as a corresponding isAdmin
field on the User
type schema definition. Also, a new Post
model and Post
type have been added, as well a query to fetch a single post and mutation for creating a new post.
Creating dump data
To test out what we will be building, we need to have some data to play with. So let’s create some. First, let’s start the server:
$ npm run dev
The project has nodemon as a dev dependency, which will watch our files for changes and restarts the server. So we will leave this running for the rest of the tutorial.
The server should be running on http://localhost:4000/api. Apollo Server 2 now comes with Playground. Visiting the URL should load it up as seen in the image below:
Let’s create two users and a new post created by one of the users. In Playground enter the mutations below one after the other.
// Create first user
mutation {
signup (username: "mezie", email: "chimezie@tutstack.io", password: "password")
}
// Create second user
mutation {
signup (username: "johndoe", email: "johndoe@example.com", password: "password")
}
Next, log in as one of the user:
// Log in as the first user
mutation {
login (email: "chimezie@tutstack.io", password: "password")
}
The mutation above will return a JWT, which we will attach as an Authorization
header in our subsequent requests.
Click on HTTP HEADERS at the bottom of Playground, then enter the JWT copied from above:
{
"Authorization": "Bearer ENTER JWT HERE"
}
Now, we can create a new post as the logged in user:
// Create a new post
mutation {
createPost (title: "Intro to GraphQL", content: "This is an intro to GraphQL."){
title
content
}
}
The rest of this tutorial assumes you have at least two users and a post created by one of the users.
Using a resolver function
We will be looking at two different methods with which we can handle authorization in GraphQL. This first method is to add the authorization logic directly inside the resolver function, which is pretty straightforward. We will be using this method to implement editing a post.
First, let’s define the mutation for editing a post. Open schemas/index.js
and add the code below inside the Mutation
object:
// schemas/index.js
type Mutation {
...
editPost(id: Int!, title: String, content: String): Post
}
This mutation accepts three arguments: the ID of the post, the title of the post and the content of the post. Only the id
argument is required.
Next, let’s write the resolver function for this mutation. Inside resolvers/index.js
, add the code below immediately after the createPost
resolver function in the Mutation
object:
// resolvers/index.js
async editPost (root, { id, title, content }, { user }) {
if (!user) {
throw new Error('You are not authenticated!')
}
const post = await Post.findById(id)
if (!post) {
throw new Error('No post found')
}
if (user.id !== post.user_id) {
throw new Error('You can only edit the posts you created!')
}
await post.update({ title, content })
return post
}
Here, we first check to make sure the user is authenticated. Then we get the post matching the supplied ID. If no match was found, we throw an appropriate error. Then we check to make sure the authenticated user trying to edit the post is the author of the post by checking the user ID against the user_id
on the post
object. If the authenticated user is not the author of the post, we throw an appropriate error. Otherwise, we update the post with the supplied details and return the newly updated post.
Let’s test this out. First, let’s trying editing a post we didn’t create. We should get an error as in the image below:
// Editing a post user didn’t create
mutation {
editPost (id:1, title: "GraphQL 101", content: "This is an intro to GraphQL.") {
title
content
author {
username
}
}
}
We should get an error like below:
{
...
"errors": [
{
"message": "You can only edit the posts you created!",
...
}
]
}
But if we trying to edit our own post, then we should see the updated post:
{
"data": {
"editPost": {
"title": "GraphQL 101",
"content": "This is an intro to GraphQL.",
"author": {
"username": "mezie"
}
}
}
}
Using custom directives
Now, let’s allow an admin to fetch a list of users that have signed up. For this, we will be using the second method, which is using custom directives. A GraphQL directive starts with the @
symbol. The core GraphQL specification includes two directives: @include()
and @skip()
. Visit the GraphQL directives page to learn more about directives.
Let’s create the schema for fetching all users. Add the code below inside schemas/index.js
:
// schemas/index.js
const typeDefs = gql`
directive @isAdmin on FIELD_DEFINITION
...
type Query {
allUsers: [User]! @isAdmin
...
}
...
`
First, we define a new directive called @isAdmin
, which will be added to a field (hence, FIELD_DEFINITION
). Then we define the query for fetching all users and use the @isAdmin
directive on it. This means only admin users will be able to perform this query.
Now, let’s create the @isAdmin
implementation. Create a new directives
directory in the project’s root. Then inside the directives
directory, create a new isAdmin.js
file and paste the code below in it:
// directives/isAdmin.js
const { SchemaDirectiveVisitor } = require('apollo-server-express')
const { defaultFieldResolver } = require('graphql')
class IsAdminDirective extends SchemaDirectiveVisitor {
visitFieldDefinition (field) {
const { resolve = defaultFieldResolver } = field
field.resolve = async function (...args) {
// extract user from context
const { user } = args[2]
if (!user) {
throw new Error('You are not authenticated!')
}
if (!user.is_admin) {
throw new Error('This is above your pay grade!')
}
return resolve.apply(this, args)
}
}
}
module.exports = IsAdminDirective
Apollo Server 2 makes it easy to create custom directives by using SchemaDirectiveVisitor
. We create a new IsAdminDirective
class which extends SchemaDirectiveVisitor
. Since we want the directive to be added to a field, we override the visitFieldDefinition()
, which accepts the field the directive was added to. Inside the resolve function of the field, we get the authenticated user from the context. Then we perform the authentication and authorization checks and throw any appropriate errors.
Next, let’s write the resolver function for the query. Inside resolvers/index.js
, add the code below immediately after the post
resolver function in the Query
object:
// resolvers/index.js
async allUsers (root, args, { user }) {
return User.all()
}
Before we test this out, let’s make our server be aware of the custom directive. Update server.js
to reflect the changes below:
// server.js
...
const IsAdminDirective = require('./directives/isAdmin')
...
const server = new ApolloServer({
typeDefs,
resolvers,
schemaDirectives: {
isAdmin: IsAdminDirective
},
context: ({ req }) => ({
user: req.user
})
})
...
We import the custom directive, then we add a new schemaDirectives
object (which contains our custom directive) to the object passed to ApolloServer
.
To test this out, let’s set one of the users we created earlier as an admin. To keep things simple, we will do this manually directly in the database. Just change the is_admin
value of the user from 0
to 1
.
If we try to perform the fetch all users query as a non-admin user:
// fetching all users as a non-admin user
{
allUsers {
username
email
}
}
We will get an error as below:
{
...
"errors": [
{
"message": "This is above your pay grade!",
...
}
]
}
Otherwise, we should get an array of all users:
{
"data": {
"allUsers": [
{
"username": "mezie",
"email": "chimezie@tutstack.io"
},
{
"username": "johndoe",
"email": "johndoe@example.com"
}
]
}
}
Conclusion
In this tutorial, we saw how to handle authorization in GraphQL. We looked at two different methods of achieving it. Using custom directives has some advantages over using resolver function, which include reducing repetition in your resolver function, which in turn keeps your them lean. Another advantage is that it promotes reusability and it’s easier to maintain.
The complete code is available on GitHub.
6 November 2018
by Chimezie Enyinnaya