Using JWT in Your React+Redux App for Authorization

Using JWT in Your React+Redux App for Authorization
An explanation and guide for how to handle JWT and Authentication in the frontend of your web app

The general idea is that you will save the current user’s information to the Redux store for easy access across your app. You will also save the JWT (JSON Web Token) associated with the user to localStorage so that their login can persist between sessions unless they explicitly logout.

This tutorial assumes you have the three following Rails API routes set up:

POST to /users 
POST to /login
GET to /profile

These three routes handle the three essential parts of authentication — when a user creates an account, when a user logs into that account, and when a logged-in user revisits your web app. I will go in that order, although the final part (handling when a user revisits your app) is where JWT’s usefulness shines.

Let’s get started!

Note: This tutorial is mostly intended for Mod 5 students of Flatiron School’s Web Immersive Program who have implemented JWT Authentication to their Rails API backend but are unsure of how to have their client-side React+Redux App handle authentication.
Hopefully, I can help clear up the workflow of Auth and how to apply it to your project!

1. User Signs Up (POST to /users)

When a new user visits your app, you may want them to sign up for an account. You will essentially run a standard POST request; you won’t have to do anything fancy to get this up and running.

If set up properly, your backend will create the user instance, salt the password using BCrypt, and then return an object with a user key and a jwt key. This object is the important part to Auth. You’ll see it later in this tutorial, but we will essentially take the user object and save it to your Redux store, then take the token associated with the user and save it to localStorage.

The steps for signing up new users and automatically logging them in are as follows.


Have a New User Submission Form

In your React App, you’ll want a form which, upon submission, will run your fetch in your actions.js file. You will be making use of Redux’s thunk here, so make sure you have it installed.

Create a controlled component that is a form for creating a new user. As an example, it may look like this:

import React, {Component} from 'react';
import {connect} from 'react-redux';
import {userPostFetch} from '../redux/actions';

class Signup extends Component {
  state = {
    username: "",
    password: "",
    avatar: "",
    bio: ""
  }

  handleChange = event => {
    this.setState({
      [event.target.name]: event.target.value
    });
  }

  handleSubmit = event => {
    event.preventDefault()
    this.props.userPostFetch(this.state)
  }

  render() {
    return (
      <form onSubmit={this.handleSubmit}>
        <h1>Sign Up For An Account</h1>

        <label>Username</label>
        <input
          name='username'
          placeholder='Username'
          value={this.state.username}
          onChange={this.handleChange}
          /><br/>

        <label>Password</label>
        <input
          type='password'
          name='password'
          placeholder='Password'
          value={this.state.password}
          onChange={this.handleChange}
          /><br/>

        <label>Avatar</label>
          <input
            name='avatar'
            placeholder='Avatar (URL)'
            value={this.state.avatar}
            onChange={this.handleChange}
            /><br/>

          <label>Bio</label>
          <textarea
            name='bio'
            placeholder='Bio'
            value={this.state.bio}
            onChange={this.handleChange}
            /><br/>

        <input type='submit'/>
      </form>
    )
  }
}

const mapDispatchToProps = dispatch => ({
  userPostFetch: userInfo => dispatch(userPostFetch(userInfo))
})

export default connect(null, mapDispatchToProps)(Signup);

Your component does not have to look exactly like this — the important part is the handleSubmit function.

Note where some unknown function named userPostFetch is being imported from actions.js and then added as a prop to the component using mapDispatchToProps. You can see above that this prop is invoked upon submission of the form. This will be the function that handles the fetch itself, as well as saving the user object to the Redux store and adding the token to localStorage. Next, we will write this function.


Have a function with the fetch request

In your actions.js file, it will look something like this:

export const userPostFetch = user => {
  return dispatch => {
    return fetch("http://localhost:3000/api/v1/users", {
      method: "POST",
      headers: {
        'Content-Type': 'application/json',
        Accept: 'application/json',
      },
      body: JSON.stringify({user})
    })
      .then(resp => resp.json())
      .then(data => {
        if (data.message) {
          // Here you should have logic to handle invalid creation of a user.
          // This assumes your Rails API will return a JSON object with a key of
          // 'message' if there is an error with creating the user, i.e. invalid username
        } else {
          localStorage.setItem("token", data.jwt)
          dispatch(loginUser(data.user))
        }
      })
  }
}

const loginUser = userObj => ({
    type: 'LOGIN_USER',
    payload: userObj
})

Note the two separate functions: userPostFetch and loginUser. The function userPostFetch sends the user’s info to your backend to be verified. Upon success, it is expecting a response of a JSON object that looks like this:

{
  user: {
    username: "ImANewUser",
    avatar: "https://robohash.org/imanewuser.png",
    bio: "A new user to the app."
  },
  jwt: "aaaaaaa.bbbbbbbb.ccccccc"
}

In userPostFetch, this is what we named ‘data’ in the 2nd ‘then’ statement.


Save the token to localStorage

With the code we’ve written in our userPostFetch function, localStorage.setItem(“token”, data.jwt) will save the token (“aaaaaaa.bbbbbbbb.ccccccc”) to our user’s localStorage. This will be used later when we are persisting a user’s login between sessions.

To check that the token was saved successfully, run localStorage.token or localStorage.getItem(“token”) in your console.


Save user object to your Redux store

As for the user object, we see here that dispatch(loginUser(data.user)) is being ran. Presumably, your reducer will take the user object ({username: “ImANewUser”}) and save it to your Redux store. This will make it easy for any component in your React App to know who the current user is.

As an example, here is my reducer:

const initialState = {
  currentUser: {}
}

export default function reducer(state = initialState, action) {
    switch (action.type) {
      case 'LOGIN_USER':
        return {...state, currentUser: action.payload}
      default:
        return state;
    }
}

Here, the user object (action.payload) is being saved to the state under the key of currentUser. If you have Redux DevTools installed, you can check it after the successful creation of your user. You should see the user object.

You may notice here that I have a key of “reducer” that you might not have. This is because I have multiple reducers and named one of them “reducer”.

Note: You know how sometimes after you create an account on a website, it asks you to manually log in right after? You can have this for your website too — you don’t have to automatically sign users in after they create their accounts.
If you decide to do this, simply do nothing with the user object and JWT token. In fact, you can edit the logic from your backend so that it only returns status messages instead.

That’s it for signing up a new user. Next, we’ll check out how to log in an existing user.


2. User Logs In (POST to /login)

Logging in a user is very similar to the signup process, except you are sending only the login credentials to the backend. The backend will handle validating the user and then sending back the same object from sign up — an object with a user key and jwt key. Once again, you’ll save the user object to the Redux store and save the token to localStorage.

If you have a component dedicated to logging in, it will look similar to your Signup component with one major difference — it will be importing a different function from your actions.js file. It might look something like this:

import React, {Component} from 'react';
import {connect} from 'react-redux';
import {userLoginFetch} from '../redux/actions';

class Login extends Component {
  state = {
    username: "",
    password: ""
  }

  handleChange = event => {
    this.setState({
      [event.target.name]: event.target.value
    });
  }

  handleSubmit = event => {
    event.preventDefault()
    this.props.userLoginFetch(this.state)
  }

  render() {
    return (
      <form onSubmit={this.handleSubmit}>
        <h1>Login</h1>

        <label>Username</label>
        <input
          name='username'
          placeholder='Username'
          value={this.state.username}
          onChange={this.handleChange}
          /><br/>

        <label>Password</label>
        <input
          type='password'
          name='password'
          placeholder='Password'
          value={this.state.password}
          onChange={this.handleChange}
          /><br/>

        <input type='submit'/>
      </form>
    )
  }
}

const mapDispatchToProps = dispatch => ({
  userLoginFetch: userInfo => dispatch(userLoginFetch(userInfo))
})

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

Note that the only thing that changed about the form itself was the removal of the avatar and bio input fields.

We haven’t yet written the userLoginFetch function, but again, its appearance is similar to the fetch that handled sign up. See below:

export const userLoginFetch = user => {
  return dispatch => {
    return fetch("http://localhost:3000/api/v1/login", {
      method: "POST",
      headers: {
        'Content-Type': 'application/json',
        Accept: 'application/json',
      },
      body: JSON.stringify({user})
    })
      .then(resp => resp.json())
      .then(data => {
        if (data.message) {
          // Here you should have logic to handle invalid login credentials.
          // This assumes your Rails API will return a JSON object with a key of
          // 'message' if there is an error
        } else {
          localStorage.setItem("token", data.jwt)
          dispatch(loginUser(data.user))
        }
      })
  }
}

Note here that we are reusing the loginUser action in order to save our user object to our state.

Surprisingly, that’s it for logging a user in! When a user’s object is saved to the state and their token is saved to localStorage, you can consider your user logged in.

Now let’s do the third and final part: persisting your user’s login between sessions.


3. User Revisits (GET to /profile)

The point of saving a token to localStorage is to persist a login. When your user revisits your site, you want them to feel as if they are continuing their session from before.

Remember though that the token saved into localStorage is just a string. It in itself does not equal a logged-in user. You as the developer must take the token and translate it into a persisting login.

To do this, you will want to run your fetch (GET to /profile) every time your app is accessed if the user has a token saved into their localStorage. Running this logic in componentDidMount in your App component is a good choice, as it will definitely run when your app is accessed.

import React, { Component } from 'react';
import { Switch, Route } from 'react-router-dom';
import {connect} from 'react-redux';
import {getProfileFetch} from './redux/actions';
import Signup from './components/Signup';
import Login from './components/Login';


class App extends Component {
  componentDidMount = () => {
    this.props.getProfileFetch()
  }

  render() {
    return (
      <div>
        <Switch>
          <Route path="/signup" component={Signup}/>
          <Route path="/login" component={Login}/>
        </Switch>
      </div>
    );
  }
}

const mapDispatchToProps = dispatch => ({
  getProfileFetch: () => dispatch(getProfileFetch())
})

export default connect(null, mapDispatchToProps)(App);

Your App component won’t look exactly like this (you can especially ignore the Switch and Routes parts), but the key part here is the getProfileFetch function being given as a prop to App and then invoked in componentDidMount.

We’re importing a function from actions.js called getProfileFetch, which is ran immediately when the App component mounts.

What getProfileFetch will do is run a standard GET request, except with an Authorization header with the token, which you handle in your actions.js file. Your backend should be set up to receive the token, decode it, and then return its associated user object. You then save this to the Redux store as usual. You already have the token saved to localStorage, so you don’t have to worry about it.

The function will look something like this:

export const getProfileFetch = () => {
  return dispatch => {
    const token = localStorage.token;
    if (token) {
      return fetch("http://localhost:3000/api/v1/profile", {
        method: "GET",
        headers: {
          'Content-Type': 'application/json',
          Accept: 'application/json',
          'Authorization': `Bearer ${token}`
        }
      })
        .then(resp => resp.json())
        .then(data => {
          if (data.message) {
            // An error will occur if the token is invalid.
            // If this happens, you may want to remove the invalid token.
            localStorage.removeItem("token")
          } else {
            dispatch(loginUser(data.user))
          }
        })
    }
  }
}

The function getProfileFetch first checks if there is a token saved into localStorage before attempting to persist a login. This way, you won’t run an unnecessary fetch.

Note here again that we are reusing the loginUser action and that we are not re-saving the token. The user already has it in their localStorage, so there’s no need to save it again.

And there you have it — you have functioning authorization!


*** BONUS: Logging Out

As you test your app, you’ll notice that currently the only way to log out is to clear the JWT token in your localStorage by typing localStorage.removeItem(“token”) into your console and pressing refresh to clear the Redux store. We should create a button for your clients to log themselves out.

Somewhere in your app, you’ll need a logout button which will do the above. While I wouldn’t place this randomly in the App.js file, here is an example of a logout button in App.js for sake of example.

import React, { Component } from 'react';
import { Switch, Route } from 'react-router-dom';
import {connect} from 'react-redux';
import {getProfileFetch, logoutUser} from './redux/actions';
import Signup from './components/Signup';
import Login from './components/Login';


class App extends Component {
  componentDidMount = () => {
    this.props.getProfileFetch()
  }

  handleClick = event => {
    event.preventDefault()
    // Remove the token from localStorage
    localStorage.removeItem("token")
    // Remove the user object from the Redux store
    this.props.logoutUser()
  }

  render() {
    return (
      <div>
        <Switch>
          <Route path="/signup" component={Signup}/>
          <Route path="/login" component={Login}/>
        </Switch>
          {this.props.currentUser.username
            ? <button onClick={this.handleClick}>Log Out</button>
            : null
          }
      </div>
    );
  }
}

const mapStateToProps = state => ({
  currentUser: state.reducer.currentUser
})

const mapDispatchToProps = dispatch => ({
  getProfileFetch: () => dispatch(getProfileFetch()),
  logoutUser: () => dispatch(logoutUser())
})

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

You should notice a few new things: for one, there is a new action being imported called logoutUser, which we will write shortly. We also now have mapStateToProps being used in order for the App component to receive a prop called currentUser.

Note: Because I have multiple reducers, I had to access my Redux store’s current user by typing state.reducer.currentUser in mapStateToProps. If you only have one reducer, you will likely only have to write state.currentUser.

There is now also a ternary operator which checks if the currentUser prop received from the Redux store has a username key (as in, if the currentUser object is empty or not). If it does, it renders a ‘Log Out’ button, which will invoke logoutUser when clicked. It will also remove the token from localStorage.

logoutUser will just be a simple action:

export const logoutUser = () => ({
  type: 'LOGOUT_USER'
})

This action will do the following in the reducer, replacing the currentUser with an empty object:

const initialState = {
  currentUser: {}
}

export default function reducer(state = initialState, action) {
    switch (action.type) {
      case 'LOGIN_USER':
        return {...state, currentUser: action.payload}
      case 'LOGOUT_USER':
        return {...state, currentUser: {} }
      default:
        return state;
    }
}

And there you have it! You should see the button appearing and disappearing when you log in and log out.

How to Check?

You may have followed the above and are questioning if your authorization is working. You can test it by signing up, logging in, pressing refresh, and logging out. If everything works as it should, you should see the user object saved to your Redux store under the key of currentUser using your Redux DevTools and a token saved to your localStorage which you can view in the console.


Now What?

As for where to go from here, you can run checks in your components to essentially kick a user out if they are not logged in. You can do this with Redirect from react-router-dom or the push function from connected-react-router. These are the resources I prefer to use.

You can also now pass your currentUser object from your Redux store to any component in your app, as we saw earlier. For example, if you wanted your Navbar to show the user’s avatar, you could use mapStateToProps to pass the currentUser object to the Navbar and then render the avatar.

Thanks for reading ❤

If you liked this post, share it with all of your programming buddies!

Follow me on Facebook | Twitter

Learn More

React - The Complete Guide (incl Hooks, React Router, Redux)

Modern React with Redux [2019 Update]

React Native - The Practical Guide

MERN Stack Front To Back: Full Stack React, Redux & Node.js

Build Progressive Web Apps with React

Learn React - React Crash Course 2019 - React Tutorial with Examples

React + Webpack + TypeScript Project Setup

Progressive Web App vs Native app

Getting Started With React.js, Babel 7, Webpack 4, and MobX 5