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.
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.
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.
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
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
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.
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.
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
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.
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:
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.
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:
Next, we can login:
1mutation { 2 login(email: "johndoe@example.com", password: "password") 3 }
We should get a response as in the image below:
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:
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.
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:
The complete code is available on GitHub.
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.