Storing JWT tokens in your React Frontend

This is an entry in a larger series of blog posts about JWT authentication using Tornado and Neo4J. It’s not strictly necessary to read the previous post, but please check it out if you’re interested! Here’s the link.

Our front end here is relatively simple, after hitting the GET or POST user endpoints in our API, we want to be able to store the the JWT as part of the state. I am using redux in my app, but hypothetically you can do this however you want. Within redux I have an authentication reducer that stores this token.

Here’s some code which fires on submit of a login attempt:

  handleSubmit = (evt) => {
    evt.preventDefault();
    const { email, password } = this.state;
    fetch(`http://localhost:8888/registration?email=${email}&password=${password}`, {
      method: 'GET',
    }).then(res => res.json()).then((response) => {
      if (!response.error) {
        this.props.onAuthenticate(response.session_token);
      } else {
        this.setState({ error: JSON.stringify(response.error.message) });
      }
    }).catch(error => this.setState({ error: 'Something went wrong' }));
  }

The fetch’s response passes the token to an authentication action which uses redux’s connect function to connect it to the component.

const mapDispatchToProps = dispatch => ({
  onAuthenticate: (token) => {
    dispatch(authenticationAction(token));
  },
});

const mapStateToProps = state => ({
  authentication: state.authenticationReducer,
  user: state.user,
});

export default connect(mapStateToProps, mapDispatchToProps)(Login);

The reducer can then save that type of action and store the token in the state.

import { AUTHENTICATE, LOGOUT } from '../actionTypes/ActionTypes';

export const initialAuthenticationState = {};

const authenticationReducer = (state = initialAuthenticationState, action) => {
  switch (action.type) {
    case AUTHENTICATE:
      return Object.assign({}, state, {
        token: action.token,
      });
    case LOGOUT:
      return Object.assign({}, state, {
        token: null,
      });
    default:
      return state;
  }
};


export { authenticationReducer };

We can then control if a component renders by checking if there’s a token stored within the state. Of course, this is manipulable by the user if they inspect the state and such, but is an initial level layer of security.

By storing it within the state, we can easily add it to any further fetch request that needs the token to authenticate as follows:

import React, { Component } from 'react';
import { connect } from 'react-redux';
import FileUploader from '../../components/FileUploader';
import Navbar from '../../components/Navbar';
import uploadActions from '../../store/actionTypes/upload';
import Registration from '../../components/Registration/registration';
import './uploadData.css';

class UploadData extends Component {
  render() {
    const { onDrop, token, isAuthenticated } = this.props;
    return (
      <div className="UploadData">
        { !isAuthenticated
                && <Registration />
                }
        <Navbar />
        <div className="instructions">
          First, upload your data and we'll make some models for you.
        </div>
        <div className="file-uploader-wrapper">
          <FileUploader pending={this.props.pending} onDrop={onDrop(token)} />
        </div>
      </div>
    );
  }
}

const mapStateToProps = state => ({
  isAuthenticated: !!state.authenticationReducer.token,
  token: state.authenticationReducer.token,
  pending: state.uploadReducer.pending,
});

const mapDispatchToProps = dispatch => ({
  onDrop: token => (acceptedFiles) => {
    const uploadedFile = acceptedFiles[0];
    dispatch({
      type: uploadActions.UPLOAD,
      file: uploadedFile,
    });
    const form = new FormData();
    form.append('csv', uploadedFile);
    console.log('TIME TO CALL THE API WITH THE FILE', form);
    console.log('TOKEN', token);
    fetch('http://localhost:8888/csv', {
      headers: {
        Authorization: `Bearer ${token}`,
      },
      method: 'POST',
      body: form,
    });
  },
});


export default connect(mapStateToProps, mapDispatchToProps)(UploadData);

Here we are currying functions to allow us to pass the token as an argument and have access to it when the dispatch triggers. This then hits our back end, where we decode the token and if the email is correct, allow the action to trigger. The only thing to add now is some extra usability using cookies.

Let’s Celebrate our Journey with some Cookies

We can save the user some time, and allow him to not have to log in on every visit by storing the JWT as a cookie. When the app loads it can check the cookie store for anything and load it into the state. This allows any logic that is checking if the JWT exists to execute without a login.

First we will create a cookie_store.js file that looks like this

export const loadState = () => {
  try {
    const serializedState = localStorage.getItem('state');
    if (serializedState === null) {
      return undefined;
    }
    return JSON.parse(serializedState);
  } catch (error) {
    console.log('No cookies for you!!', error);
    return undefined;
  }
};

export const saveState = (state) => {
  try {
    const serializedState = JSON.stringify(state);
    localStorage.setItem('state', serializedState);
  } catch (error) {
    console.log("We couldn't save your cookies", error);
  }
};

We can then import this in the file where you are setting up your redux store and pass in loadState() to the configureStore() function. We then use saveState() to save cookies based on a timer. In our case we are saving the current authentication reducer’s state to the user’s cookies every one second. Here is an example of my store.js file:

import {
  applyMiddleware, compose, createStore, combineReducers,
} from 'redux';
import throttle from 'lodash/throttle';
import uploadReducer from './reducers/upload';
import { authenticationReducer } from './reducers/authentication';
import { usersReducer } from './reducers/users';
// import thunk from 'redux-thunk';
import { loadState, saveState } from './cookie_store';

const middlewares = [];

const combinedReducers = combineReducers({
  uploadReducer,
  authenticationReducer,
  usersReducer,
});

export function configureStore(initialState) {
  const createdStore = createStore(combinedReducers, initialState,
    compose(applyMiddleware(...middlewares)));
  return createdStore;
}

const store = configureStore(loadState());

store.subscribe(throttle(() => {
  saveState({
    authenticationReducer: store.getState().authenticationReducer,
  });
}, 1000));

export { store };

There’s a lot of ways to set up your redux store so you can easily apply this logic to what you have.
If you have any questions, queries, quandaries or concerns feel free to email me at [email protected]. Moreover, if you think any of the code can be improved also feel free to email me, as I am always on a mission to improve my code.

Cheers!