An authentication process example for a GraphQL API powered by Apollo, using Cookies and JWT

In this tutorial, I’ll explain how to handle a login mechanism for a GraphQL API using Apollo.

We’ll create a private area that will display different information based on the user’s login status.

Here are the steps we’ll follow:

  1. Create a login form on the client
  2. Send the login data to the server
  3. Authenticate the user and send a JWT back
  4. Store the JWT in a cookie
  5. Use the JWT for further requests to the GraphQL API

The code for this tutorial is available on GitHub at https://github.com/flaviocopes/apollo-graphql-client-server-authentication-jwt

Let’s get started.

WARNING! This tutorial is old. Apollo now uses @apollo/xxx rather than apollo-xxx. Do your research until I update it :)

Setting up the Client Application

Create the client-side part using create-react-app. Run npx create-react-app client in an empty folder.

Then navigate into the client folder and install the required dependencies:

npm install apollo-client apollo-boost apollo-link-http apollo-cache-inmemory react-apollo apollo-link-context @reach/router js-cookie graphql-tag

Creating the Login Form

Create a Form.js file in the src folder and add the following code:

import React, { useState } from 'react'
import { navigate } from '@reach/router'

const url = 'http://localhost:3000/login'

const Form = () => {
  const [email, setEmail] = useState('')
  const [password, setPassword] = useState('')

  const submitForm = event => {
    event.preventDefault()

    const options = {
      method: 'post',
      headers: {
        'Content-type': 'application/x-www-form-urlencoded; charset=UTF-8'
      },
      body: `email=${email}&password=${password}`
    }

    fetch(url, options)
      .then(response => {
        if (!response.ok) {
          if (response.status === 404) {
            alert('Email not found. Please retry')
          }
          if (response.status === 401) {
            alert('Email and password do not match. Please retry')
          }
        }
        return response
      })
      .then(response => response.json())
      .then(data => {
        if (data.success) {
          document.cookie = 'token=' + data.token
          navigate('/private-area')
        }
      })
  }

  return (
    <div>
      <form onSubmit={submitForm}>
        <p>Email: <input type="text" onChange={event => setEmail(event.target.value)} /></p>
        <p>Password: <input type="password" onChange={event => setPassword(event.target.value)} /></p>
        <p><button type="submit">Login</button></p>
      </form>
    </div>
  )
}

export default Form

Here, we assume that the server will run on localhost on port 3000. This code handles form submission using fetch API, and if the login is successful, it stores the JWT token in a cookie and navigates to the /private-area URL.

Adding the Form to the App

Edit the index.js file in the src folder and add the following code:

import React from 'react'
import ReactDOM from 'react-dom'
import { Router } from '@reach/router'
import Form from './Form'

ReactDOM.render(
  <Router>
    <Form path="/" />
  </Router>,
  document.getElementById('root')
)

Setting up the Server

Create a server folder and run npm init -y to create a package.json file.

Install the required dependencies:

npm install express apollo-server-express cors bcrypt jsonwebtoken

Create an app.js file and add the following code:

const express = require('express')
const cors = require('cors')
const bcrypt = require('bcrypt')
const jwt = require('jsonwebtoken')
const cookieParser = require('cookie-parser')
const app = express()

app.use(cors())
app.use(express.urlencoded({ extended: true }))
app.use(cookieParser())

const SECRET_KEY = 'secret!'

const users = [{
  id: 1,
  name: 'Test user',
  email: '[[email protected]](/cdn-cgi/l/email-protection)',
  password: '$2b$10$ahs7h0hNH8ffAVg6PwgovO3AVzn1izNFHn.su9gcJnUWUzb2Rcb2W' // = ssseeeecrreeet
}]

const todos = [
  {
    id: 1,
    user: 1,
    name: 'Do something'
  },
  {
    id: 2,
    user: 1,
    name: 'Do something else'
  },
  {
    id: 3,
    user: 2,
    name: 'Remember the milk'
  }
]

app.post('/login', async (req, res) => {
  const { email, password } = req.body
  const theUser = users.find(user => user.email === email)

  if (!theUser) {
    res.status(404).send({
      success: false,
      message: `Could not find account: ${email}`,
    })
    return
  }

  const match = await bcrypt.compare(password, theUser.password)
  if (!match) {
    res.status(401).send({
      success: false,
      message: 'Incorrect credentials',
    })
    return
  }

  const token = jwt.sign(
    { email: theUser.email, id: theUser.id },
    SECRET_KEY,
  )

  res.cookie('token', token, {
    httpOnly: true
  })

  res.send({
    success: true
  })
})

const context = ({ req }) => {
  const token = req.cookies['token'] || ''

  try {
    return { id, email } = jwt.verify(token, SECRET_KEY)
  } catch (e) {
    throw new Error('Authentication token is invalid, please log in')
  }
}

const {
  ApolloServer,
  gql,
} = require('apollo-server-express')

const typeDefs = gql`
  type User {
    id: ID!
    email: String!
    name: String!
    password: String!
  }

  type Todo {
    id: ID!
    user: Int!
    name: String!
  }

  type Query {
    todos: [Todo]
  }
`

const resolvers = {
  Query: {
    todos: () => {
      return todos.filter(todo => todo.user === id)
    }
  }
}

const server = new ApolloServer({ typeDefs, resolvers, context })
server.applyMiddleware({ app, cors: false })

app.listen(3000, () => {
  console.log('Server listening on port 3000')
})

Creating the Private Area

Create a PrivateArea.js file in the src folder and add the following code:

import React from 'react'
import { gql } from 'apollo-boost'
import { Query } from 'react-apollo'
import { navigate } from '@reach/router'

const PrivateArea = () => {
  return (
    <div>
      <Query
        query={gql`
          query {
            todos {
              id
              name
            }
          }
        `}
      >
        {({ loading, error, data }) => {
          if (loading) return <p>Loading...</p>
          if (error) {
            navigate('/')
            return null
          }
          return <ul>{data.todos.map(item => <li key={item.id}>{item.name}</li>)}</ul>
        }}
      </Query>
    </div>
  )
}

export default PrivateArea

This code uses the Query component from Apollo to fetch the todos from the server and display them. If there’s a loading state, it displays a “Loading…” message. If there’s an error, it navigates back to the login form. Otherwise, it renders a list of todos.

Edit the index.js file in the src folder and add the following code:

import React from 'react'
import ReactDOM from 'react-dom'
import { Router } from '@reach/router'
import Form from './Form'
import PrivateArea from './PrivateArea'

ReactDOM.render(
  <Router>
    <Form path="/" />
    <PrivateArea path="/private-area" />
  </Router>,
  document.getElementById('root')
)

Conclusion

Now you have a basic understanding of how to authenticate using GraphQL, Cookies, and JWTs in your Apollo-powered application. You can use this knowledge as a starting point to build more complex authentication systems.

Tags: GraphQL, Apollo, Authentication, Cookies, JWT