Janik von Rotz


7 min read

Build an Apollo Graphql user authentication for your React app - part 1

I am currently building an Apollo Graphql API and a React web application. The application requires a user authentication functionality in order to enforce access restrictions on the Graphql endpoint. Apollo Graphql does not provide an out-of-the-box-solution and therefore I would like to present my solution.

This post requires some basic knowledge about building APIs with Apollo and later on combining the Apollo Client with React. First we are going to define the user schema and implement the resolvers. For the authentication mechanism we are going to implement a query that expects user credentials and returns a JSON Web Token as response. This token will be used by the React app and passed as an Bearer Authorization header to every sequentially API call. The Apollo server verifies the token and restricts access on certain calls if an invalid token has been provided. The access restriction is enforced on schema field level using a custom directive.

Files

My Apollo server implementation consists of the following files:

Note: This guide does not cover how user data is stored and retrieved. Comments in the code snippets will point out where to implement these functionalities. In short this guide can be read regardless of the storage solution.

Schema

Let us start with the type definitions. I have split the schema.js file into several parts.

schema.js

const { gql } = require('apollo-server-micro')

// GraphQL schema
const typeDefs = gql`

directive @isAuthenticated on FIELD_DEFINITION

scalar Date

The Date scalar is custom type, ignore it for now. The isAuthenticated directive will be discussed later.

Next follows the user type.

type User {
  id: String!
  email: String!
  password: String!
  firstname: String!
  lastname: String!

  created: Date
  created_by: String!
  updated: Date
  updated_by: String!
}

Then the token and default response.

type Token {
  token: String!
}

type Response {
  success: Boolean!
  message: String
}

Queries are restricted by the custom directive.

type Query {
  currentUser: User @isAuthenticated
  users: [User] @isAuthenticated
  loginUser(email: String!, password: String!): Token
}

And here are the mutation we need to actually create and manipulate a user.

type Mutation {
  createUser(email: String!, password: String!, firstname: String!, lastname: String!): User
  updateUser(id: String!, email: String, password: String, firstname: String, lastname: String): Response
  deleteUser(id: String!): Response
}
`

module.exports = typeDefs

Put everything together and you got the schema definitions.

Resolvers

In the resolvers file we map schema types and fields with functions that return the actual data. You can think of it as actually implementing the schema.

resolvers.js

const { GraphQLScalarType } = require('graphql')
const { AuthenticationError, ForbiddenError } = require('apollo-server-micro')
const bcrypt = require('bcrypt')
const jwt = require('jsonwebtoken')
const { Kind } = require('graphql/language')

// Hash configuration
const BCRYPT_ROUNDS = 12

// Resolve GraphQL queries, mutations and graph paths
const resolvers = {
  Query: {
    users: async (obj, args, context) => {
      const user // -> Access data layer and get user data
      return user
    },
    currentUser: async (obj, args, context) => {
      const user // -> Access data layer and get user data
      return user
    },
    loginUser: async (obj, args, context) => {
      // Find user by email and password
      const user // -> Access data layer and get user data

      // Compare hash
      if(user && await bcrypt.compare(args.password, user.password)) {
        // Generate and return JWT token
        const token = jwt.sign({ email: user.email, name: (user.firstname + ' ' + user.lastname) }, process.env.JWT_SECRET )
        return { token: token }
      } else {
        // Throw authentication error
        throw new AuthenticationError('Login failed.')
      }
    }
  },
  Mutation: {
    createUser: async (obj, args, context) => {
      // Check if user already exists
      const user // -> Access data layer and get user data
      if (user) {
        throw new ForbiddenError('User already exists.')
      }

      // Hash password
      args.password = await bcrypt.hash(args.password, BCRYPT_ROUNDS)
      args.created = new Date()
      args.created_by = context.name || "system"
      const user // -> Access data layer and store user data
      return user
    },
    updateUser: async (obj, args, context) => {
      args.updated = new Date()
      args.updated_by = context.name || "system"

      // Hash password if provided
      if(args.password){
        args.password = await bcrypt.hash(args.password, BCRYPT_ROUNDS)
      }

      // -> Access data layer and update user data
      return { 
        success: true
      }
    },
    deleteUser: async (obj, args, context) => {
      // -> Access data layer and delete user data
      return { 
        success: true
      }
    }
  },
  User: {
    // Hide password hash
    password() {
      return ''
    }
  }
}

module.exports = resolvers

The most important part of the resolver files are the bcrypt and jwt method calls. If a user is created the plaintext password is hashed and stored in the database. If the loginUser query is called the user is retrieved and the password hash compared. If successful a JWT token is generated and returned.

Context

We assume that during the login process in the client app the Apollo client calls the loginUser query, retrieves token and stores it in the local storage. If set the token is passed in the Bearer Authentication header with each API call. On the Apollo server side the token must be verified and its claims added to the Apollo context.

So what is the context? To put it simply the context is an object that is available to all resolvers. From the context user claims can be accessed and e.g. passed to a database query.

context.js

const { AuthenticationError } = require('apollo-server-micro')
const jwt = require('jsonwebtoken')

const context = ({ req }) => {
  // Get the user token from the headers
  let token = req.headers.authorization ? req.headers.authorization.split(' ')[1] : null

  // Verify token if available
  if (token) {
    try {
      token = jwt.verify(token, process.env.JWT_SECRET)
    } catch (error) {
      throw new AuthenticationError(
        'Authentication token is invalid, please log in.'
      )
    }
  }

  return {
    email: token ? token.email : null,
    name: token ? token.name : null
  }
}

module.exports = context

If the token is set in the header and has been verified, the two claims email and name are then added to the context.

Directive

As I mentioned we enforce schema field access with a custom directive. In the isAuthenticated directive we simply check if the email property is set in the context object.

const { SchemaDirectiveVisitor, ForbiddenError } = require('apollo-server-micro')
const { defaultFieldResolver } = require('graphql')

// Custom directive to check if user is authenicated
class isAuthenticated extends SchemaDirectiveVisitor {
  // Field definition directive
  visitFieldDefinition (field) {
    // Get field resolver
    const { resolve = defaultFieldResolver } = field

    field.resolve = async function (...args) {
      // Check if user email is in context
      if (!args[2].email) {
        throw new ForbiddenError('You are not authorized for this ressource.')
      }

      // Resolve field
      return resolve.apply(this, args)
    }
  }
}

module.exports = { isAuthenticated: isAuthenticated }

Whenever a field declared with the isAuthenticated directive is queried, the directive will also ensure the user has a valid token.

Server

In the index.js we stitch everything together.

const { ApolloServer } = require('apollo-server-micro')
const cors = require('micro-cors')()
const typeDefs = require('./schema')
const resolvers = require('./resolvers')
const context = require('./context')
const directives = require('./directives')

// Initialize Apollo server
const server = new ApolloServer({
  typeDefs,
  resolvers,
  context: context,
  introspection: true,
  playground: true,
  schemaDirectives: directives
})

// Export server as handler
module.exports = cors((req, res) => {
  // Workaround abort on Options method
  if (req.method === 'OPTIONS') {
    res.end()
    return
  }
  return server.createHandler({ path: '/api' })(req, res)
})

To those who noticed that I import apollo-server-micro instead of apollo-server, it is because I am deploying my apps as serverless functions with Zeit Now using their HTTP microservice.

Test

Start the Apollo server and open the Graphql playground.

Create a user with the createUser mutation.

mutation { 
  createUser(
    email: "login@example.com",
    password: "password",
    firstname: "Login",
    lastname: "Example"
  ) { id } }

Login with this user.

{ loginUser(email: "login@example.org", password: "password") {
  token
} }

Copy the token in to the HTTP header field.

{
    "Authorization": "Bearer YOUR_TOKEN_HERE"
}

Query the user info.

{ currentUser {
  firstname
  lastname
  email
  password
  id
} }

What the final query should look like:

If this worked you have reached the end of part 1 😊.

Next

In part 2 I will show how we implement the user authentication mechanism in a React app.

Additions

I try to keep my guides as slick as possible and therefore intentionally do not cover certain parts. But at least I wanna let know you which parts I ignored.

In order to provide a full user accounting module of course more queries are required. You would at least have to implement the following ones:

If you have any questions, feel free to ask below 👋.

Updates

2019/08/30: Updated all code snippets after applying JavaScript Standard Style guide. Removed unnecessary await statements.

Categories: JavaScript development
Tags: apollo , graphql , authentication , react , json web token , directive
Improve this page
Show statistic for this page