Protecting your React + GraphQL application with JWT authentication

Vladimir Kopychev
Level Up Coding
Published in
11 min readJan 29, 2018

--

Continuing the React + GraphQL tutorial by adding security with JWT

Secure your GraphQL API

Warning

The implementation provided in this tutorial is not production-ready code and it’s not secure enough to integrate in your application starightaway. The purpose of this post is to extend the information given in GraphQL documentation. You can read a lot of discussions (here or there) about using JWT, its advantages, disadvantages and different ways to store them.

Note: If you use Token Authentication in production you must ensure that your API is only available over https. And also CORS MUST be disabled (enabled here to simplify the example)

Introduction

In the first part of this article series we created Single Page Application (SPA) with GraphQL API on the Backend (using node JS) and React JS on the Frontend.

In the second part we enhanced our application by adding Redux as a State-management library and Redux-Thunk as a Middleware. We also added some features like StorageService to store data for the search form and React-Router-Redux to handle client-side routing in the ‘Redux’ way.

In this tutorial we will use all of those features and make some changes on the Backend side to protect our GraphQL API using authentication based on Json Web Tokens, also known as JWTs.

You can see a working tutorial in the repository. To launch it run npm install in both client and server folders and then you can use npm start to launch either server or client, or both.

Securing your API on the Backend

First of all we need to install an npm package useful for JWT tokens.

npm i -s jsonwebtoken

We will use it to create and verify JSON web tokens on the Backend side. You can see the full package.json file here.

Then we need to create our logic to work with JWT. We start by importing jsonwebtoken library, the data source for users, and an additional lodash function.

import jwt from 'jsonwebtoken'
import Users from './data/users'
import find from 'lodash/find'

The following constants work as configuration options for JWT:

const expiresIn = '3h' // time to live
const secret = 'samplejwtauthgraphql' // secret key
const tokenPrefix = 'JWT' // Prefix for HTTP header

Now we are ready to add methods responsible for authentication to the Backend side of our SPA. The first function will be responsible for creating a token. It will check if the user with given credentials exists in the data source, and return a token or return false otherwise. The jwt.sign function is used to generate a new token. For this tutorial we will use the user’s email and lowercased last name as credentials.

/**
* Use email as login, use password as password
* @param {string} email
* @param {string} password
*/
export const createToken = (email, password) => {
if (!email || !password) { // no credentials = fail
return false
}
const user = find(Users,
(user) => {
return user.email === email.toLowerCase()
&& user.last_name.toLowerCase() === password
}
);
if (!user) { // return false if not found
return false
}
const payload = {
username: user.email,
}
const token = jwt.sign(payload, secret, {
expiresIn
})
return token
}

The next function will extract the token from the authentication header (by the JWT prefix) and check if it’s valid. If it’s valid, it will return the token belonging to the user, or otherwise throw an error. jwt.verify method is used for this.

/**
* @returns {Object} - current user object
* @param {string} token header
*/
export const verifyToken = (token) => {
const [prefix, payload] = token.split(' ')
let user = null
if (!payload) { //no token in the header
throw new Error('No token provided')
}
if (prefix !== tokenPrefix) { //unexpected prefix or format
throw new Error('Invalid header format')
}
jwt.verify(payload, secret, (err, data) => {
if (err) { //token is invalid
throw new Error('Invalid token!')
} else {
user = find(Users, { email: data.username })
}
})
if (!user) { //user does not exist in DB
throw new Error('User doesn not exist')
}
return user
}

To make our API secure we need to create a few HTTP endpoints in the server.js file to handle user login and token verification.

import express from 'express'
import bodyParser from 'body-parser'
import schema from './schema'
import graphqlHTTP from 'express-graphql'
import { createToken, verifyToken } from './auth'
app.use('/login', jsonParser, (req, res) => {
if (req.method === 'POST') {
const token = createToken(req.body.email, req.body.password)
if (token) { //send successful token
res.status(200).json({ token })
} else {
res.status(403).json({ //no token - invalid credentials
message: 'Login failed! Invalid credentials!'
})
}
}
});
/**
* Verify token and return either error or valid user profile
*/
app.use('/verifyToken', jsonParser, (req, res) => {
if (req.method === 'POST') {
try {
const token = req.headers['authorization']
const user = verifyToken(token)
res.status(200).json({ user })
} catch (e) {
console.log(e.message)
res.status(401).json({ //unauthorized token
message: e.message
})
}
}
});

Also, we need to add some middleware for working on data prior to GraphQL API handling, in order to make our API protected by JWT token authentication.

// auth middleware
app.use('/graphql', (req, res, next) => {
const token = req.headers['authorization']
try {
req.user = verifyToken(token)
next()
} catch (e) {
res.status(401).json({ //unauthorized token
message: e.message
})
}
});

This middleware is checking a token and adding a user object to request the object if the token is valid, and returning a 401 http header otherwise. If the token is valid it calls the next() function, which leads to an existing GraphQL handler. We can pass this user object to the GraphQL schema using the ‘context’ key.

app.use('/graphql', graphqlHTTP((req, res) => ({
schema,
graphiql: true,
context: {
user: req.user,
}
}));

Adding authentication and token-verification logic on the Frontend

We need to make some changes on the Frontend side for it to work with authentication and tokens. You can find the source code in the client folder of the repository.

First we need to add some methods in the ApiService for new endpoint calls.

The following method is a generic function for calling non-GraphQL APIs. It just adds more syntactic sugar for the Javascript fetch functionality.

/**
* Generic API call (for non-graphql endpoints)
* @param {string} url
* @param {object} params
*/
async apiCall(url, params = {}, method = 'POST', token = false) {
const res = await fetch(`${this.baseUrl}${url}/`, {
method,
mode: 'cors',
headers: this.buildHeaders(token),
body: JSON.stringify(params),
})
if (!res.ok) {
throw new Error(res.status)
}
return res.json()
}

We also need to define a buildHeaders method which assumes that we may have an Authentication header containing our token. If we pass the token as a parameter it creates an Authorization HTTP header, which contains our token prefixed with ‘JWT’.

    /**
* Build http headers object
* @param {string|boolean} token
*/
buildHeaders(token = false) {
let headers = new Headers();
headers.append('Content-type', 'application/json');
if (token) {
headers.append('Authorization', `JWT ${token}`);
}
return headers;
}

Then we need methods to call the login and verifyToken endpoints.

/**
* Login user and return jwt token or throw error in
* case of fail
* @param {string} login
* @param {string} password
*/
async login(params) {
const res = await this.apiCall('/login', params)
console.log(res)
return res.token
}
/**
* Verify current token and return current user or throw error
* @param {string} token
*/
async verifyToken(token) {
const res = await this.apiCall(
'/verifyToken',
{},
'POST',
token
)
return res.user
}

And finally, our method for the GraphQL API should have the ability to use our Authentication token in its HTTP Headers. Also, please note that we now pass tokens to all protected API calls as a parameter.

/**
* Generic function to fetch data from server via graphql API
* @param {string} query
* @returns {unresolved}
*/
async getGraphQlData(resource, params, fields, token = false) {
const query = `{${resource} ${this.paramsToString(params)}
${fields}}`
const res = await fetch(this.apiUrl, {
method: 'POST',
mode: 'cors',
headers: this.buildHeaders(token),
body: JSON.stringify({ query }),
});
if (res.ok) {
const body = await res.json();
return body.data;
} else {
throw new Error(res.status);
}
}
/* .... */ /**
*
* @param {object} params
* @returns {array} users list or empty list
*/
async getTodos(params = {}, token) {
const data = await this.getGraphQlData(
'todos', params, this.todoFields, token
);
//return todos list
return data.todos;
}

Now it’s time to move on to the React-Redux related logic. We need to define a reducer to handle the parts of our state related to authentication.

// shape of bit or state related to authentication
const initialState = {
isAuthenticated: false,
isFailure: false,
isLoading: true,
current_user: null,
}

This reducer will handle all cases of successful and failed authentication and it will load user profile data as well. We use the isLoading flag to let UI components know that loading is in progress.

The actions file will handle all those state transitions. The most interesting part of it is actually the middleware, which process API calls to get/verify tokens and dispatch certain actions depending on the result. For example, the following login function will make an API call with given parameters, then store the token in storage and dispatch a loginSuccess() action in case of success, or display an error and dispatch loginFailure() in case of failed authentication. Please note that we also use the push() method from react-redux-router to redirect in the ‘redux way.’

export const login = (params) => async dispatch => {
try {
const token = await ApiService.login(params)
StorageService.setToken(token)
dispatch(loginSuccess())
dispatch(push('/'))
} catch (e) {
console.error(e.message)
dispatch(loginFailure())
}
}
export const logout = () => dispatch => { //destroy token and logout
StorageService.removeToken()
dispatch(logoutAction())
dispatch(push('/login'))
}

The next action is used to verify our token from storage on page load and fetch the user’s profile if the token is valid, or redirect to the login page if not.

export const verifyToken = () => async dispatch => {
if (!StorageService.getToken()) { //if no token - logout
dispatch(logoutAction())
return
}
try {
dispatch(requestProfile())
const user =await ApiService.verifyToken(
StorageService.getToken()
)
dispatch(receiveProfile(user))
dispatch(loginSuccess())
} catch (e) {
//remove token and logout if invalid
console.error(e.message)
StorageService.removeToken()
dispatch(logoutAction())
}

Handling Private and Public routes on the client-side

We have many different components in our application and we need some of them (Users list, Todo list) to be available for authorized users only, but some of the others (Login form) should be available for non-authorized users. It’s not really efficient to check this inside of every component, so we will create special containers to handle it on the Route level. The first one allows only guest access and redirects to the main page if the user is already logged in.

/**
* Router for only guest stuff like Login/Register
* If not guest - redirects to home
*/
class GuestRoute extends React.Component {
render() {
const {
isAuthenticated,
component: Component,
...props
} = this.props
return (
<Route
{...props}
render={props =>
!isAuthenticated
? <Component {...props} />
: (
<Redirect to={{
pathname: '/',
state: { from: props.location }
}} />
)
}
/>
)
}
}
const mapStateToProps = ({ auth }) => {
const isAuthenticated = auth.isAuthenticated
return {
isAuthenticated,
}
}
export default connect(mapStateToProps)(GuestRoute)

The second one is PrivateRoute which allows access only for authorized users, and redirect to the Login page is the user is not logged in.

/**
* Private route to navigate over private routes
* If not logged in - goes to login
* If not admin but required - throws an error!
*/
class PrivateRoute extends React.Component {componentDidMount() {
this.props.dispatch(verifyToken())
}
logoutHandler() {
this.props.dispatch(logout())
}
render() {
const {
isAuthenticated,
component: Component,
current_user,
...props
} = this.props
if (this.props.isLoading) {
return <Loading />
}
if (isAuthenticated && !current_user) {
return null
}
return (
<Route
{...props}
render={props =>
isAuthenticated
?
<main>
<Header
current_user={current_user}
logout={this.logoutHandler.bind(this)}
/>
<Component {...props} />
</main>
: (
<Redirect to={{
pathname: '/login',
state: { from: props.location }
}} />
)
}
/>
)
}
}
const mapStateToProps = ({ auth }) => {
const current_user = auth.current_user;
const isAuthenticated = auth.isAuthenticated;
return {
isAuthenticated,
current_user,
isLoading: auth.isLoading,
}
}
export default connect(mapStateToProps)(PrivateRoute)

On component Mount we check the authentication status by dispatching the verifyToken action, and then either pass current User profile to the state of our application, or redirect to the Login page. The isLoading flag is used for the transition state.

For this container we also add more features like a common Header with a welcome message and a link for Logout, which will be displayed on all pages when the user is logged in.

/**
* Header with greetings and links
* @param {object} props
*/
const Header = (props) => {
return <header className="header">
<p className="header__greeting">
<span>
Welcome
<a className="header__link" href="">
{props.current_user.first_name}
</a>
</span>
</p>
<p className="header__logout">
<a className="header__link" href="" onClick={(e) => { e.preventDefault(); props.logout(); }}>
Logout
</a>
</p>
</header>
}
export default Header

As you can see those Route Containers are higher order components which wrap a generic react-router Route component so we can use them inside our application routing in the main App file. All public and guest routes are here.

const App = () => {
return <Switch>
<GuestRoute exact path='/login'
component={LoginContainer}
/>
<PrivateRoute exact path='/' component={UserListContainer}/>
<PrivateRoute path='/todos/:userId'
component={TodoListContainer}
/>
</Switch>
};

Wrapping up

After going through all the source code examples, let’s see how our application actually works.

When we run our application and open the page in a browser — we’re not logged in and the check in PrivateRoute is redirecting us to the Login form.

If we enter some valid credentials from the Users data source (email and lowercase last name) we will be logged in and redirected to the Users list page.

We send a request to the /login endpoint and if our credentials are valid, it returns us a valid JWT authentication token. You can see it in your localStorage using Chrome dev tools.

One you refresh the page the token is still there, and you can see verifyToken method was called to get information about the currently logged in user. The token passes verification via that endpoint and token verification logic behind it, and our server sends us back the the current user’s profile.

If our token is expired or removed, or we change it to something not valid like:

we will see our error message, and GraphQL API request will return non-authorized 401 HTTP Response because token verification has failed.

While we are logged in our application should work as usually, until we preform a log out, whereupon the token will be removed from localStorage, and we will be redirected to the Login page again. To logout using JWT you simply need to remove your token from the client side.

That’s how we can use GraphQL API with JWT authentication in Node JS with React + Redux on the Frontend side. I hope it will be helpful. Feel free to clone the Repository and run/test/modify it.

That’s more or less it for this tutorial. :)

--

--

Full Stack Engineer with passion for Frontend and UI solutions, PhD, coding at @udemy