Handling authentication in GraphQL Part 2: JWT

handling-authentication-in-graphql-jwt-header.png

This is part 2 of a 3 part tutorial. You can find part 1 here and part 3 here. In the first part of this series, we looked at an overview of authentication, how it is done in REST and how it can be done in GraphQL.

Introduction

You can find part 1

In the first part of this series, we looked at an overview of authentication, how it is done in REST and how it can be done in GraphQL. Today, we’ll be taking a more practical approach by building a GraphQL server and then add authentication to it.

What we’ll be building

To demonstrate things, we’ll be building a GraphQL server that has a kind of authentication system. This authentication system will include the ability for users to signup, login and view their profile. The authentication system will make use of JSON Web Tokens (JWT). The GraphQL server will be built using the Express framework.

This tutorial assumes you already have Node.js (at least version 8.0) installed on your computer.

Create a new project

We’ll start by creating a new Node.js project. Create a new folder called graphql-jwt-auth. Open the newly created folder in the terminal and run the command below:

1npm init -y

? The -y flag indicates we are selecting yes to all the npm init options and using the defaults.

Then we’ll update the dependencies section of package.json as below:

1// package.json
2
3    "dependencies": {
4      "apollo-server-express": "^1.3.2",
5      "bcrypt": "^1.0.3",
6      "body-parser": "^1.18.2",
7      "dotenv": "^4.0.0",
8      "express": "^4.16.2",
9      "express-jwt": "^5.3.0",
10      "graphql": "^0.12.3",
11      "graphql-tools": "^2.19.0",
12      "jsonwebtoken": "^8.1.1",
13      "mysql2": "^1.5.1",
14      "sequelize": "^4.32.2"
15    }

These are all the dependencies for our GraphQL server and the authentication system. We’ll go over each of them as we begin to use them. Enter the command below to install them:

1npm install

Database setup

For the purpose of this tutorial we’ll be using MySQL as our database and Sequelize as our ORM. To make using Sequelize seamless, let’s install the Sequelize CLI on our computer. We’ll install it globally:

1npm install -g sequelize-cli

?You can also install the CLI locally to the node_modules folder with: npm install sequelize-cli --save. The rest of the tutorial assumes you installed the CLI globally. If you installed it locally, you will need to run CLI with ./node_modules``/.bin/sequelize.

Once that’s installed, we can use the CLI to initialize Sequelize in our project. In the project’s root directory, run the command below:

1sequelize init

The command will create some folders within our project root directory. The config, models and migrations folders are the ones we are concerned with in this tutorial.

The config folder contains a config.json file. We’ll rename this file to config.js. Now, open config/config.js and paste the snippet below into it:

1// config/config.js
2
3    require('dotenv').config()
4
5    const dbDetails = {
6      username: process.env.DB_USERNAME,
7      password: process.env.DB_PASSWORD,
8      database: process.env.DB_NAME,
9      host: process.env.DB_HOST,
10      dialect: 'mysql'
11    }
12
13    module.exports = {
14      development: dbDetails,
15      production: dbDetails
16    }

We are using the dotenv package to read our database details from a .env file. Let’s create the .env file within the project’s root directory and paste the snippet below into it:

1//.env
2
3    NODE_ENV=development
4    DB_HOST=localhost
5    DB_USERNAME=root
6    DB_PASSWORD=
7    DB_NAME=graphql_jwt_auth

Update accordingly with your own database details.

Since we have changed the config file from JSON to JavaScript file, we need to make the Sequelize CLI aware of this. We can do that by creating a .sequelizerc file in the project’s root directory and pasting the snippet below into it:

1// .sequelizerc
2
3    const path = require('path')
4
5    module.exports = {
6      config: path.resolve('config', 'config.js')
7    }

Now the CLI will be aware of our changes.

One last thing we need to do is update models/index.js to also reference config/config.js. Replace the line where the config file is imported with the line below:

1// models/index.js
2
3    var config = require(__dirname + '/../config/config.js')[env]

With the setup out of the way, let’s create our model and migration. For the purpose of this tutorial, we need only one model which will be the User model. We’ll also create the corresponding migration file. For this, we’ll be using the Sequelize CLI. Run the command below:

1sequelize model:generate --name User --attributes username:string,email:string,password:string

This creates the User model and its attributes/fields: username, email and password. You can learn more on how to use the CLI to create models and migrations in the docs.

A user.js file will be created in the models folder and a migration file with name like
TIMESTAMP-create-user.js in the migrations folder.

Now, let’s run our migration:

1sequelize db:migrate

Creating the GraphQL server

With the dependencies installed and database setup, let’s begin to flesh out the GraphQL server. Create a new server.js file and paste the code below into it:

1// server.js
2
3    const express = require('express')
4    const bodyParser = require('body-parser')
5    const { graphqlExpress } = require('apollo-server-express')
6    const schema = require('./data/schema')
7
8    // create our express app
9    const app = express()
10
11    const PORT = 3000
12
13    // graphql endpoint
14    app.use('/api', bodyParser.json(), graphqlExpress({ schema }))
15
16    app.listen(PORT, () => {
17      console.log(`The server is running on http://localhost:${PORT}/api`)
18    })

We import some of the dependencies we installed earlier: express is the Node.js framework, body-parser is used to parse incoming request body and graphqlExpress is the express implementation of Apollo server which will be used to power our GraphQL server. Then, we import our GraphQL schema which we’ll created shortly.

Next, we define the route (in this case /api) for our GraphQL server. Then we add body-parser middleware to the route. Also, we add graphqlExpress passing along our GraphQL schema.

Finally, we start the server and listen on a specified port.

Defining GraphQL schema

Now, let’s define our GraphQL schema. We’ll keep the schema simple. Create a folder name data and, within this folder, create a new schema.js file then paste the code below into it:

1// data/schema.js
2
3    const { makeExecutableSchema } = require('graphql-tools')
4    const resolvers = require('./resolvers')
5
6    // Define our schema using the GraphQL schema language
7    const typeDefs = `
8      type User {
9        id: Int!
10        username: String!
11        email: String!
12      }
13
14      type Query {
15        me: User
16      }
17
18      type Mutation {
19        signup (username: String!, email: String!, password: String!): String
20        login (email: String!, password: String!): String
21      }
22    `
23    module.exports = makeExecutableSchema({ typeDefs, resolvers })

We import apollo-tools which allows us to define our schema using the GraphQL schema language. We also import our resolvers which we’ll create shortly. The schema contains just one type which is the User type. Then a me query which will be used to fetch the profile of the currently authenticated user. Then, we define two mutations; for users to signup and login respectively.

Lastly we use makeExecutableSchema to build the schema, passing to it our schema and the resolvers.

Writing resolver functions

Remember we referenced a resolvers.js file which doesn’t exist yet. Now, let’s create it and define our resolvers. Within the data folder, create a new resolvers.js file and paste the following code into it:

1// data/resolvers.js
2
3    const { User } = require('../models')
4    const bcrypt = require('bcrypt')
5    const jsonwebtoken = require('jsonwebtoken')
6    require('dotenv').config()
7
8    const resolvers = {
9      Query: {
10        // fetch the profile of currently authenticated user
11        async me (_, args, { user }) {
12          // make sure user is logged in
13          if (!user) {
14            throw new Error('You are not authenticated!')
15          }
16
17          // user is authenticated
18          return await User.findById(user.id)
19        }
20      },
21
22      Mutation: {
23        // Handle user signup
24        async signup (_, { username, email, password }) {
25          const user = await User.create({
26            username,
27            email,
28            password: await bcrypt.hash(password, 10)
29          })
30
31          // return json web token
32          return jsonwebtoken.sign(
33            { id: user.id, email: user.email },
34            process.env.JWT_SECRET,
35            { expiresIn: '1y' }
36          )
37        },
38
39        // Handles user login
40        async login (_, { email, password }) {
41          const user = await User.findOne({ where: { email } })
42
43          if (!user) {
44            throw new Error('No user with that email')
45          }
46
47          const valid = await bcrypt.compare(password, user.password)
48
49          if (!valid) {
50            throw new Error('Incorrect password')
51          }
52
53          // return json web token
54          return jsonwebtoken.sign(
55            { id: user.id, email: user.email },
56            process.env.JWT_SECRET,
57            { expiresIn: '1d' }
58          )
59        }
60      }
61    }
62
63    module.exports = resolvers

First, we import the User model and other dependencies. bcrypt will be used for hashing users passwords, jsonwebtoken will be used to generate a JSON Web Token (JWT) which will be used to authenticate users and dotenv will be used to read from our .env file.

The me query will be used to fetch the profile of the currently authenticated user. This query accepts a user object as the context argument. user will either be an object or null depending on whether a user is logged in or not. If the user is not logged in, we simply throw an error. Otherwise, we fetch the details of the user by ID from the database.

The signup mutation accepts the user’s username, email and password as arguments then create a new record with these details in the users database. We use the bcrypt package to hash the users password. The login mutation checks if a user with the email and password supplied exists in the database. Again, we use the bcrypt package to compare the password supplied with the password hash generated while creating the user. If the user exists, we generate a JWT that contains the user’s ID and email. This JWT will be used to authenticate the user.

That’s all for our resolvers. Noticed we use JWT_SECRET from the environment variable which we are yet to define. Add the line below to .env:

1// .env
2
3    JWT_SECRET=somereallylongsecretkey

Adding authentication middleware

We’ll use an auth middleware to validate incoming requests made to our GraphQL server. Open server.js and add the code below to it:

1// server.js
2
3    const jwt = require('express-jwt')
4    require('dotenv').config()
5
6    // auth middleware
7    const auth = jwt({
8      secret: process.env.JWT_SECRET,
9      credentialsRequired: false
10    })

First, we import the express-jwt and the dotenv packages. Then we create an auth middleware which uses the express-jwt package to validate the JWT from the incoming requests Authorization header and set the req.user with the attributes (id and email) encoded in the JWT. It is worth mentioning that req.user is not the Sequelize User model object. We set credentialsRequired to false because we want users to be able to at least signup and login first.

Then update the route as below:

1// server.js
2
3    // graphql endpoint
4    app.use('/api', bodyParser.json(), auth, graphqlExpress(req => ({
5      schema,
6      context: {
7        user: req.user
8      }
9    }))
10    )

We add the auth middleware created above to the route. This makes the route secured as it will check to see if there is an Authorization header with a JWT on incoming requests. express-jwt adds the details of the authenticated user to the request body so we simply pass req.user as context to GraphQL. This way, user will be available as the context argument across our GraphQL server.

Testing it out

For the purpose of testing out our GraphQL, we’ll be making use of Insomnia. Of course you can make use of other HTTP clients that supports GraphQL or even GraphiQL.

Start the GraphQL server:

1node server.js

It should be running on http://localhost:3000/api.

Then start Insomnia:

start insomnia

Click on create New Request. Give the request a name if you want, then select POST as the request method, then select GraphQL Query. Finally, click Create.

new request in insomnia

Next, enter http://localhost:3000/api. in the address bar.

Let’s try signing up:

1mutation {
2      signup (username: "johndoe", email: "johndoe@example.com", password: "password")
3    }

We should get a response as in the image below:

signup mutation

Next, we can login:

1mutation {
2      login(email: "johndoe@example.com", password: "password")
3    }

We should get a response as in the image below:

authentication login

A JWT is returned on successful login.

Now, if we try fetching the user’s profile using the me query, we’ll get the You are not authenticated! error message as in the image below:

not authenticated

Remember we said the auth middleware will check the incoming request for an Authorization header. So, we need to set the Authorization: Bearer <token> header to authenticate the request.

From Auth dropdown, select Bearer Token and paste the token (JWT) above in the field provided.

authorization header

Now we can fetch the profile of the currently authenticated user:

1{
2      me {
3        id
4        username
5        email
6      }
7    }

We should get a response as in the image below:

user profile

The complete code is available on GitHub.

Conclusion

That’s it! In this tutorial, we have seen how to add authentication using JWT to a GraphQL server. We also saw how to test our GraphQL server using Insomnia.

In the next part in this series, we’ll cover how to add authentication to GraphQL using a third-party authentication service like Auth0.

You can find part 1