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:
- Create a login form on the client
- Send the login data to the server
- Authenticate the user and send a JWT back
- Store the JWT in a cookie
- 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 thanapollo-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