Implementing JWT authentication in MERN stack with GraphQL: Part I

Photo by Kevin Ku on Unsplash

In this tutorial, we will be building a simple web application from scratch that allows users to perform actions including sign up, login and logout using JWT authentication. In order to understand what JSON Web Tokens are and how they work, I suggest you to read this Medium blog post before starting this tutorial.

As for the technologies, we will be using React, Redux, React-Router v4, Node.js, Express.js, Mongoose, and GraphQL. It might be a bit difficult to follow up this tutorial if you haven’t yet used any of these technologies, so I would assume that you have some extent of experience in React and basic understanding of these technologies. Another exciting feature that will be covered in this tutorial is React Hooks, which came out officially after version 16.8.0, alongside with a React Hook for accessing state from a Redux store developed by the Facebook team called redux-react-hooks.

Environment Setup

First of all, make sure that you have already installed Node.js in your computer. If you haven’t, install it from the official website of Node.js.

As you install Node.js into your computer, npm will also be installed automatically.

Now, let’s first create our directory for this project:

mkdir mern-graphql-jwt/
cd mern-graphql-jwt/

Since we will be dealing with both client-side and server-side in this project, let’s separate these two parts into two folders. I will be using thecreate-react-app boilerplate for the client side, and create everything from scratch for the server-side. Let’s first deal with the server part!

mkdir server/
cd server/

After changing our directory into the server folder, let’s now initialize this project and create a package.json file by using the npm init command:

npm init

There is going to be a few questions popping up, it is totally up to you if you want to make any custom changes. In my case, I will just keep hitting the enter button and use the default options.

After the package.json file is being created, we can now start installing the dependencies required for this project:

npm i bcryptjs body-parser cors dotenv express express-graphql graphql jsonwebtoken mongoose && npm i --save-dev @babel/cli @babel/core @babel/preset-env @babel/node concurrently nodemon

Note: In case you are using Git, you might want to ignore all the packages and dependencies installed into the /node_modules folder by creating a .gitignore file and add /node_modules into this file.

When all the installations for the dependencies are done, let’s add some scripts in this package so that we can easily run our server without the need to write long and ugly commands every time. This is how the package.json file now looks like:

In the scripts property, we add another property called server which serves the purpose of running our server application through index.js. Nodemon is a very great CLI tool that restarts our server every time we make a change, so that we wouldn’t have to re-run our scripts all the time. Babel-node compiles our application with Babel presets and plugins before running it, therefore simplifies our tasks and allows us to write newer versions of JavaScript. It should be noted that babel-node isn’t meant for production use but only for development, and if you are curious about how to use Babel in a production deployment, you may check out this GitHub repository provided by Babel.

We also need to configure Babel in order to have it working. For that, we need to create a file named .babelrc in our server/ directory, and include the following:

//.babelrc
{
"presets": [
"@babel/preset-env"
]
}

This file basically allows Babel to use different configurations for different files during the same build. Adding these few lines will allow us to write the latest JavaScript without needing to micromanage which syntax transforms are needed by your target environment(s).

After dealing with all the configurations, let’s now jump in to actual coding by first creating an index.js file, which is going to be the main entry of the server-side of this application.

Here’s how our index.js file looks like:

//index.jsimport express from 'express';
import bodyParser from 'body-parser';
import expressGraphQL from "express-graphql";
import cors from "cors";
const app = express();function main() {
const port = process.env.PORT || 5000;
app.listen(port, () => console.log(`Server is listening on port: ${port}`));
}
main();

Nothing surprising so far, but we now have an actual working server running on localhost:5000 by simply typing npm run server in the terminal, and you should be able to see the message written in the console.log statement, indicating that your server is running successfully.

Setting Up the Middlewares

Middleware functions are functions that have access to the request object (req), the response object (res), and the next middleware function in the application’s request-response cycle.

In this application, there are some few basic middlewares that we need to setup. These include:

  • bodyParser: Node.js body parsing middleware
  • CORS: a specification that enables truly open access across domain-boundaries
  • express-graphql: provides a simple way to create an Express server that runs a GraphQL API

Let’s add these middlewares right after where we have declared our app variable.

//index.jsimport express from 'express';
import bodyParser from 'body-parser';
import expressGraphQL from 'express-graphql';
import cors from 'cors';
const app = express();app.use(
cors(),
bodyParser.json()
)
app.use(
"/graphql",
expressGraphQL({
schema: {},
rootValue: {},
graphiql: true
})
);
function main() {
const port = process.env.PORT || 5000;
app.listen(port, () => console.log(`Server is listening on port: ${port}`));
}
main();

For the time being, we’ll leave the schema and rootValue field as an empty object and decide what needs to be done as we proceed. By enabling graphiql, we are able to edit and test our GraphQL queries and mutations in a tabbed interface. You should be able to see the GraphiQL interface on localhost:5000/graphql, but since we haven’t defined any schema and resolver you will not be able to make any query for now.

Sample image of a GraphiQL interface

Database Configuration

Another configuration for this project is the connection to our database. We will be using mLab to connect to our MongoDB database in this project, but you may also choose other database service platforms such as ScaleGrid or MongoDB Atlas if you wish.

To create our database, head to mLab’s website and create an account.

After signing in, go yo your dashboard and press the Create New button next to the header MongoDB Deployments.

Choose Amazon Web Services as your cloud provider and Sandbox as your plan type (if you are as broke as I am 😢). Press continue.

Here pick one of the given options and proceed. It doesn’t really matter which one you choose.

Now pick a name for your database. Just make it the same as our project’s name so it’s easy to understand. Press continue and then submit the order.

After creating the database you will be redirected back to your dashboard. Find your database and select it.

Under the Users tab, click the Add database user button. Fill in the necessary fields and press create. This will enable you to control your database as an administrator.

Now we’ve got all we need to connect our server to our database.

Copy the MongoDB URI of your own database and you will soon paste it in index.js by replacing <dbuser> and <dbpassword> with environmental variables.

Create a .env file under your server/ directory with the following lines:

//.envDB_USER=admin //username of the database user you just created
DB_PASS=myadminpass //password you have set

Now let’s modify index.js a little bit:

//index.jsimport express from 'express';
import bodyParser from 'body-parser';
import expressGraphQL from 'express-graphql';
import cors from 'cors';
import mongoose from 'mongoose';
require('dotenv').config();
const app = express();app.use(
cors(),
bodyParser.json()
)
app.use(
"/graphql",
expressGraphQL({
schema: {},
rootValue: {},
graphiql: true
})
);
function main() {
const port = process.env.PORT || 5000;
const uri = `mongodb://${process.env.DB_USER}:${process.env.DB_PASS}@ds135852.mlab.com:35852/mern-graphql-jwt`;
mongoose.connect(uri, { useNewUrlParser: true })
.then(() => {
app.listen(port, () => console.log(`Server is listening on port: ${port}`));
})
.catch(err => {
console.log(err);
})
}
main();

Modify the value of uri by pasting the MongoDB URI you have just copied, and replace <dbuser> as ${process.env.DB_USER} and <dbpassword> as ${process.env.DB_PASS}.

Now run npm run server again. If you see the console.log message, it means that you have successfully connected to your MongoDB database!

Defining GraphQL Schema

Before deciding on the schema and types for our queries and mutations, let’s first think of what different kinds of operations will be needed in the entire application.

Suppose that when a user accesses the web page, he/she should be able to perform actions such as login or sign up. As the user receives a JSON Web Token after being verified, we will be storing this token in the client’s localStorage. Therefore, when the user accesses the web page, we should first check whether the user has any token within the localStorage which was obtained previously when visiting the site: if yes, let the user login automatically; otherwise, display the original home page for unverified users.

According to the above scenario, we may be able to deduce some of the main queries and mutations required.

For our queries:

  • login: takes the user’s email and password as input, checks whether the email exists and the validity of the password in the resolver function, then returns the user’s information and a token if login is successful
  • verifyToken: takes a token string as input, checks the token’s validity and returns the user’s information if verification is successful

And one single mutation:

  • createUser: Takes the necessary information of the user and validates, then inserts this information to the database if all fields are valid and finally returns the user’s information, a token, and its id generated from the database

Let’s then create a folder called graphql/ under our server/ directory, and inside this folder create another two folders named as schema/ and resolvers/. We should first define the schema then deal with the resolvers, so create an index.js under folder and build our schema here.

// graphql/schema/index.js
import { buildSchema } from 'graphql';
export default buildSchema(`type User {
_id: ID!
email: String!
token: String!
}
input UserInput {
email: String!
password: String!
confirm: String!
}
type RootQuery {
login(email: String!, password: String!): User
verifyToken(token: String!): User
}
type RootMutation {
createUser(userInput: UserInput): User
}
schema {
query: RootQuery
mutation: RootMutation
}
`)

In our RootQuery, we have defined the queries the same as we have discussed earlier: login takes an email and password from the user as arguments, verifyToken takes a token string, then both returns back an object of User type, which consists of a generated id, email and token. In RootMutation, we take an object of type userInput, which consists of auser’s email, password, and confirm password, and return back the user’s information. Finally in the schema field, we combine both RootQuery and RootMutation.

Creating Mongoose Model

Now that we have defined our GraphQL schema, let’s then also create our mongoose schema and convert it into a model so that we may use it in our resolvers.

Create a folder called models/ under the server/ directory and create a file calleduser.js:

// models/user.jsimport mongoose, { Schema } from 'mongoose';const userSchema = new Schema({
email: {
type: String,
required: true
},
password: {
type: String,
required: true,
min: 8,
max: 32
}
})
export default mongoose.model('User', userSchema);

This seems pretty straightforward. Since we are only aiming for the basic authentication features, an email and password field should be enough for us. Now let’s get down to our business with the resolvers!

Building GraphQL Resolvers

Move to resolvers/ folder and create index.js. Since in the end of this file we will be exporting an object full of different functions, let’s create another folder within resolvers/ called handlerGenerators/ and create auth.js. The content of resolvers/index.js will be simply exporting all functions from files within handlerGenerators/ as one single object, so we’ll come back to it once we have finished building our resolvers.

// resolvers/handlerGenerators/auth.jsimport User from '../../../models/user';
import bcrypt from 'bcryptjs';
import jwt from 'jsonwebtoken';

These are the modules we need to import in this file.

bcrypt.js is a library that helps us hash passwords. As we are going to store user’s password into our database, it is considered more secure to hash them instead of storing plain password.

It’s time to define the resolvers for our queries and mutations. Let’s start off with createUser function:

// resolvers/handlerGenerators/auth.js...export async function createUser(args) {
try {
const {
email,
password,
confirm
} = args.userInput; //retrieve values from arguments
const existingUser = await User.findOne({ email });
if (existingUser) {
throw new Error('User already exists!');
}
if (password !== confirm) {
throw new Error('Passwords are inconsistent!');
}
const hashedPassword = await bcrypt.hash(password, 10); const user = new User({
email,
password: hashedPassword
}, (err) => { if (err) throw err });
user.save(); // if user is registered without errors
// create a token
const token = jwt.sign({ id: user._id }, "mysecret");

return { token, password: null, ...user._doc }
}
catch(err) {
throw err;
}
}

Note: JWT requires a secret or private key in order to generate and verify tokens, so you may store your secret key somewhere inside your .env file. In this tutorial, I am simply passing a “mysecret” string instead.

As we are using some of the Mongoose async operations, it would be easier to use ES7 async/await in this case.

Inside the args of createUser function is the User type we have defined earlier in our GraphQL schema, which consists of email, password and confirm. We can extract all these values by simply using ES6 object destructuring.

User.findOnewith a specific property value will return a user who matches the value given. In our case, we passed an email field with the email given from the user’s input and check whether there exists any user with the same email in the database: if yes, then throw an error message.

hashPassword is a result of user’s password hashed by bcrypt.js, we then pass this value and email while creating a new instance of a User model and finally save this object into our database. If nothing goes wrong, we then jwt.signto generate a token for the user and return it alongside with other user information.

After completing this function, let’s try it out and test it.

Open up graphql/resolvers/handleGenerators/index.js and insert the following code:

// graphql/resolvers/handleGenerators/index.jsimport * as authHandlers from './handlerGenerators/auth';export default {
...authHandlers
}

Now we can import both our GraphQL schema and resolvers from our main index.js file:

// index.js
...
import graphQLSchema from './graphql/schema';
import graphQLResolvers from './graphql/resolvers';
require('dotenv').config();
const app = express();app.use(
cors(),
bodyParser.json()
)
app.use(
"/graphql",
expressGraphQL({
schema: graphQLSchema,
rootValue: graphQLResolvers,
graphiql: true
})
);
...

Finally, go to localhost:5000/graphql and try to create a new user using the createUser mutation:

And…Ta-da! Working just as expected!

If you’d like to see whether it actually checks if your email is already registered or not, just run it again:

And you will get the error message back, as expected!

There aren’t much stuff to do left now except for those two queries which are simpler to implement than this.

Move back to our handler file and insert a login function:

// resolvers/handlerGenerators/auth.js...export async function login(args) {
try {
const user = await User.findOne({ email: args.email });
if (!user) throw new Error('Email does not exist');
const passwordIsValid = await bcrypt.compareSync(args.password, user.password);
if (!passwordIsValid) throw new Error('Password incorrect');
const token = jwt.sign({ id: user._id }, "mysecret"); return { token, password: null, ...user._doc }
}
catch (err) {
throw err;
}
}

This seems pretty similar to our createUserfunction. One notable difference here is that we are trying to find the existing user as well as checking the validity of the user’s password by using thebcrypt.comparseSync method. When everything goes successfully, the function returns the user’s information and a token.

Let’s test this out with GraphiQL by providing the same credentials we previously provided while creating a new user:

And if we input our password incorrectly:

Working perfectly fine.

Lastly, let’s add our verifyToken function and make it work as elegant as the previous functions:

// resolvers/handlerGenerators/auth.js...export async function verifyToken(args) {
try {
const decoded = jwt.verify(args.token, "mysecret");
const user = await User.findOne({ _id: decoded.id })
return { ...user._doc, password: null };
}
catch (err) {
throw err;
}
}

As you might remember, we have passed the user’s id when generating our JSON Web Token, so we are now retrieving the user’s id from the token by passing it into jwt.verify and use it to retrieve the specific user’s information from the database.

Let’s copy the token we have obtained previously from running the queries and test it on GraphiQL:

That’s it! We have completed the necessary authentication process and now ready to build the client side by connecting it to our server.

Wrap up

Although we have implemented the basic functionalities for user authentication, there are still a few problems which you have to decide on how to tackle, such as email validity, password limitations, error handling, and so on. As these are commonly encountered problems you may want to choose some JavaScript modules to handle them. One of the solutions would be to use validator.js for data validations.

If you have encountered any problem while following this tutorial, I have uploaded the entire project on a Git repository.

This article is the first of a two-part tutorial. In the next part, we will be building the user interfaces with React libraries. Here’s the second article:

Thank you so much for reading. If you liked this article, feel free to share this article with your friends.

Frontend Engineer @Giftpack. Senior student at Bilkent University. Big fan of React.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store