Graphql server authentication with JWT

ahmdtalat

Ahmd Talat

Posted on February 19, 2020

Graphql server authentication with JWT

Welcome, we will continue working on our Graphql to-do API, by setting up authorization for our app using JWT.

Getting Started

This tutorial will be focusing on:

  • What is JWT and how to use it
  • Hashing the password with bcryptjs
  • How to control who can interact with the data

Step 1: Install dependencies

You'll need to install jsonwebtoken and bcryptjs by running the following in the terminal:

yarn add jsonwebtoken bcryptjs
Enter fullscreen mode Exit fullscreen mode

or using npm

npm i jsonwebtoken bcryptjs
Enter fullscreen mode Exit fullscreen mode

So, what are JSON Web Tokens and bcryptjs, and Why?

  1. JWT

JSON Web Token (JWT) is a compact, URL-safe means of representing claims to be transferred between two parties. more info. passing jwt tokens will take this formAlt Text

  1. bcryptjs

Is a way to hash the user info before hitting the DB. and why hashing? just a safety precaution.

Step 2: Re-organize project structure

Our app is growing and we need a proper structure to avoid confusion.

So, we'll put resolvers and typeDefs both in separate folders and create an index.js in each one. then you can invoke them as Node modules. const resolvers = require('./resolvers').

index.js will look something like this

  • typeDefs/index.js
const typesDefs = require('./typeDefs');
module.exports = typesDefs;
Enter fullscreen mode Exit fullscreen mode
  • resolvers/index.js
const todosResolvers = require('./todos');
const userResolvers = require('./user'); // Will create this file later.
module.exports = {
  Query: {
    ...todosResolvers.Query
  },
  Mutation: {
    ...todosResolvers.Mutation,
    ...userResolvers.Mutation
  }
};
Enter fullscreen mode Exit fullscreen mode

Alt Text

Step 3: Create a User model

We need a new MongoDB collection for the users.

const { Schema, model } = require('mongoose');

const userSchema = new Schema({
  username: String,
  password: String,
  email: String,
  created: String
});

module.exports = model('User', userSchema);
Enter fullscreen mode Exit fullscreen mode

And we need to update our Todo model as well.

const todoSchema = new Schema({
  text: String,
  created: String,
  username: String, // who created it
  user: {
    // referencing the User docutment
    type: Schema.Types.ObjectId,
    ref: 'users'
  }
});
Enter fullscreen mode Exit fullscreen mode

Step 4: Create a Schema and resolvers for User

  1. we will update our typeDefs
const { gql } = require('apollo-server');

module.exports = gql`
  type Todo {
    id: ID!
    username: String! // new! add a username
    text: String!
    created: String!
  }
  // new! types of user data
  type User {
    id: ID!
    username: String!
    email: String!
    created: String!
    token: String!
  }
  // new! we use input instead of type
  // to pass the whole user info as an object
  input RegisterUserInput {
    username: String!
    password: String!
    confirmPassword: String!
    email: String!
  }
  type Query {
    getTodos: [Todo]
  }
  type Mutation {
    createTodo(text: String!): Todo!
    deleteTodo(todoId: ID!): String!
    // new! A user needs to be logged in before changing data.
    LoginUser(username: String!, password: String!): User!
    RegisterUser(user: RegisterUserInput!): User!
  }
`;
Enter fullscreen mode Exit fullscreen mode
  1. update the ApolloServer parameters

Per the documentation: there are a number of ways to handle authentication of users. And we'll be using a token in an HTTP authorization header.

const server = new ApolloServer({
  typeDefs,
  resolvers,
  // a () => {} to build our context object.
  context: ({ req }) => {
    // get the authorization from the request headers
    // return a context obj with our token. if any!
    const auth = req.headers.authorization || '';
    return {
      auth
    };
  }
});
Enter fullscreen mode Exit fullscreen mode

The context object is one that gets passed to every single resolver at every level, so we can access it anywhere in our schema code.

  1. create the user resolvers

In our case, We'll be creating two resolvers. Login & Register.

Register takes a userinput (username, pass,confirmPass, email) and returns the user info with a token.

But, First, We need import jsonwebtoken and bcrypt, So if the userinput is valid ( non-empty fields - some basic validation ), then hash the password, generate a token for that user.

const jwt = require('jsonwebtoken');
const bcrypt = require('bcryptjs');

const {
  UserInputError, // Throw an error if empty fields.
  AuthenticationError
} = require('apollo-server');

const User = require('../models/User');
// some string as a secret to generate toekns.
const { SECRET } = require('../config');

const getToken = ({ id, username, email }) =>
  jwt.sign(
    {
      id,
      username,
      email
    },
    SECRET,
    { expiresIn: '1d' }
  );

module.exports = {
  Mutation: {
    async LoginUser(_, { username, password }) {
      // validateLogin is a simple func that checks for empty fields
      // and return valid = false if any.
      const { errors, valid } = validateLogin(username, password);
      if (!valid) throw new UserInputError('Error', { errors });

      // check if that user already exists.
      const user = await User.findOne({ username });
      if (!user) throw new AuthenticationError('this user is not found!');

      const match = await bcrypt.compare(password, user.password);
      if (!match) throw new AuthenticationError('wrong password!');

      const token = getToken(user); // generate a token if no erros.
      return {
        id: user._id, // set an id
        ...user._doc, // spread the user info (email, created at, etc)
        token
      };
    },

    async RegisterUser(
      _,
      { user: { username, password, confirmPassword, email } }
    ) {
      const { errors, valid } = validateRegister(
        username,
        password,
        confirmPassword,
        email
      );
      if (!valid) throw new UserInputError('Error', { errors });

      const user = await User.findOne({ username });
      if (user) throw new ValidationError('This username is not valid!');

      password = await bcrypt.hash(password, 10); // hashing the password
      const newUser = new User({
        username,
        password,
        email,
        created: new Date().toISOString()
      });
      const res = await newUser.save();
      const token = getToken(res);

      return {
        id: res._id,
        ...res._doc,
        token
      };
    }
  }
};
Enter fullscreen mode Exit fullscreen mode

Finally

Now, We'll update the todo resolvers (createTodo - deleteTodo), So We can control who can create and delete todos.

// Simply take an auth header and returns the user.
const getUser = async auth => {
  if (!auth) throw new AuthenticationError('you must be logged in!');

  const token = auth.split('Bearer ')[1];
  if (!token) throw new AuthenticationError('you should provide a token!');

  const user = await jwt.verify(token, SECRET, (err, decoded) => {
    if (err) throw new AuthenticationError('invalid token!');
    return decoded;
  });
  return user;
};


// The mutation takes 3 args (parent,args,context)
// in our case, we don't need parent
// destructure the todo text from args, and auth from context.
async createTodo(_, { text }, { auth }) {
      const user = await getUser(auth);
      if (user) {
        try {
          const newTodo = new Todo({
            user: user.id,
            username: user.username,
            text,
            created: new Date().toISOString()
          });
          const todo = await newTodo.save();
          return todo;
        } catch (err) {
          throw new Error(err);
        }
      }
    },
    async deleteTodo(_, { todoId }, { auth }) {
      const user = await getUser(auth);
      try {
        const todo = await Todo.findById(todoId);
        if (todo) {
          if (todo.username === user.username) await todo.delete();
          else
            throw new AuthenticationError(
              `you're not allowed to delete this todo!!`
            );
        } else throw new Error('todo is not found');
        return 'Todo deleted!';
      } catch (err) {
        throw new Error(err);
      }
    }
  }
Enter fullscreen mode Exit fullscreen mode

That's it, I hope you find it easy and simple. If you have any questions, please let me know.

The cover image is from undraw.co.

💖 💪 🙅 🚩
ahmdtalat
Ahmd Talat

Posted on February 19, 2020

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related