JWT Authentication with React + Redux

As part of my ongoing attempts to can has React + Redux, I spent some time yesterday building authentication flow into my CatBook React/Redux app, using JWT.

This post will take us through the sign-in flow for our React app, illustrate one way to build out a dynamic Header component that appropriately displays the log in/log out link, and implement authenticated routes--protecting some routes from being accessed by non-authenticated users.

You can check out the code for this post here.

What is JWT?

I've written a series of earlier posts on JWT authentication in Rails and Ember, so if you want to learn more about JWT authentication, these posts are a good place to start.

But, to sum up briefly here...

What is JWT Authentication?

JSON Web Token (JWT) Authentication is a compact, URL-safe means of representing claims to be transferred between two parties. The claims in a JWT are encoded as a JSON object that is used as the payload of a JSON Web Signature (JWS) structure or as the plaintext of a JSON Web Encryption (JWE) structure, enabling the claims to be digitally signed or integrity protected with a Message Authentication Code (MAC) and/or encrypted.*

In plain English, JWT allows us to authenticate requests between the client and the server by encrypting authentication information into a compact JSON object. Instead of, say, passing a user's own unique token (which we would need to persist to the database), or (god forbid), sending a user's raw email and password with every authentication request, JWT allows us to encrypt a user's identifying information, store it in a token, inside a JSON object, and include that object in every request that requires authentication.

Let's take a closer look at how that cycle might work, using the example of a React app + Rails API.

  • User fills out the log in form via the React app and hits "log in!"
  • React POSTs user's email and password to the Rails API.
  • Rails receives the POST request and and queries the database for the right user. If the user can be authenticated...
  • We'll use JWT to encrypt that user's unique ID into a compact and secure JSON Web Token.
  • This token is then included in the response that Rails sends back to React.
  • React stores the encrypted JWT token in session storage, retrieving it and sending it back to Rails, as the HTTP Authentication header, in any authenticated requests.

So, what's so great about this system?

Well, for one thing, we are not storing a unique user token in our database. Instead, we are encrypting the user's unique identifying info in our token, and decrypting it on the Rails side when we need to identify the "current user". Secondly, our server is not responsible for keeping track of the current user, as is the case when we use Rails' session object.

If you're not yet convinced of how great this is, check out the jwt.io documentation. It offers some very clear and concise info.

The Backend: JWT and Rails API

This post assumes that you've already set up a JWT authentication system on the backend. You can check out this post for an example JWT + Rails implementation.

This post assumes that your "log in" endpoint is http://localhost:5000/login, which would map to the Sessions#create controller action, authenticating the user with the payload sent by React, and sending back the encrypted JWT.

Okay, let's get started!

JWT Auth Flow in React

Let's break down the steps we'll need to enact in React to get our auth feature up and running.

  1. User fills out log in form on the LogInPage component and hits "submit"
  2. The LogInPage component dispatches an action, let's call it logInUser, that sends the request for authentication to the API.
  3. When the logInUser action receives a successful response that includes the JWT from the API, it will do two things: store the JWT in session storage, and dispatch another action that tells the session reducer we had a successful log in.
  4. The session reducer produces a new copy of state with a session property set equal to true.
  5. This triggers a re-render of our Header component which whose logged_in property, taken from application state's session property, controls whether the navbar is rendered with the "log in" or "log out" link.

So, we can see that there are actually two places in our application where we are keeping track of whether or not there is an authenticated user:

  • In session storage, where we actually store our JWT so that we can retrieve it to make future authenticated API requests.
  • In application state, so that the Header component can be aware of an authenticated user and render the correct navbar links.

We'll start by building the LogInPage component, session actions and session reducer to enact the first four steps of the process above.

The LogInPage component

Our LogInPage component needs to render the log in form. It also needs to be able to collect the information filled out in that form and respond to the user's click of the "submit" button by dispatching some action.

We'll give our LogInPage component an initial, internal, state that contains a credentials property.

this.state = {credentials: {email: '', password: ''}}

credentials contains email and password, and we'll update the object stored in this state in response to the user's filling out form fields.

For this, we'll need a function, onChange, that will be fired when a user fills out those form fields:

onChange(event) {
  const field = event.target.name;
  const credentials = this.state.credentials;
  credentials[field] = event.target.value;
  return this.setState({credentials: credentials});
}

This onChange function will be passed as a callback function to the TextInput components that we'll use to construct our form. The TextInput components were built out in an earlier blog post, and you can check them out here. If you click this link and view the code, you'll notice that we've given the TextInput component an optional prop of type that defaults to "text". This way, we can create a password input field by passing in type="password" when we call the component below.

Lastly, we'll need an onSave function that fires when the user hits the "submit" button. This function will dispatch an action, logInUser, that we'll define next.

onSave(event) {
  event.preventDefault();
  this.props.actions.loginUser(this.state.credentials);
}

Let's put it all together:

// src/components/LogInPage.js

import React, {PropTypes} from 'react';
import TextInput from './common/TextInput';
import {bindActionCreators} from 'redux';
import {connect} from 'react-redux';
import * as sessionActions from '../actions/sessionActions';

class LogInPage extends React.Component {
  constructor(props) {
    super(props);
    this.state = {credentials: {email: '', password: ''}}
    this.onChange = this.onChange.bind(this);
    this.onSave = this.onSave.bind(this);
  }

  onChange(event) {
    const field = event.target.name;
    const credentials = this.state.credentials;
    credentials[field] = event.target.value;
    return this.setState({credentials: credentials});
  }

  onSave(event) {
    event.preventDefault();
    this.props.actions.logInUser(this.state.credentials);
  }

  render() {
    return (
      < div>
        < form>
          < TextInput
            name="email"
            label="email"
            value={this.state.credentials.email}
            onChange={this.onChange}/>

          < TextInput
            name="password"
            label="password"
            type="password"
            value={this.state.credentials.password}
            onChange={this.onChange}/>

          < input
            type="submit"
            className="btn btn-primary"
            onClick={this.onSave}/>
        
      
  );
  }
}

function mapDispatchToProps(dispatch) {
  return {
    actions: bindActionCreators(sessionActions, dispatch)
  };
}
export default connect(null, mapDispatchToProps)(LogInPage);

Okay, we're ready to define the logInUser action creator that we're importing from sessionActions here.

Session Actions: The logInUser Action Creator

This action creator will use Redux-Thunk to send an API request, with the payload of the credentials that the user filled out via our form, and dispatch another action in turn. Let's define our action creator first, then we'll take a step back and build out a helper module, SessionApi, to wrap up the API request.

// src/actions/sessionActions.js

import * as types from './actionTypes';
import sessionApi from '../api/SessionApi';

export function loginSuccess() {
  return {type: types.LOG_IN_SUCCESS}
}

export function logInUser(credentials) {
  return function(dispatch) {
    return sessionApi.login(credentials).then(response => {
      sessionStorage.setItem('jwt', response.jwt);
      dispatch(loginSuccess());
    }).catch(error => {
      throw(error);
    });
  };
}

Our logInUser action does three things:

  • Call on the session api to make the authentication request. (We'll build out the session api and its login function next).
  • If we get a positive response back from the API, store the JWT from that response in session storage, and
  • Dispatch the loginSuccess action, which will get sent to the reducer. We'll teach our reducer how to handle that action coming up.

Notice that our loginSuccess function utilizes an action type, LOG_IN_SUCCESS. Let's add that to our action type constants now.

// src/actions/actionTypes.js

export constant LOG_IN_SUCCESS = 'LOG_IN_SUCCESS'

Now we're ready to define our the session api class and its login function.

The SessionApi Class

 // src/api/sessionApi.js;

class SessionApi {
  static login(credentials) {
    const request = new Request('http://localhost:5000/login', {
      method: 'POST',
      headers: new Headers({
        'Content-Type': 'application/json'
      }), 
      body: JSON.stringify({auth: credentials})
    });


    return fetch(request).then(response => {
      return response.json();
    }).catch(error => {
      return error;
    });
  } 
}

export default SessionApi;

Great, now we're ready to teach our reducer to handle to LOG_IN_SUCCESS action by creating a new copy of state with a property that indicates that there is an authenticated user.

The Session Reducer and Tracking Authentication in State

We'll create a new reducer, the session reducer, to handle the dispatch of the LOG_IN_SUCCESS action. We'll also update our initialState object so it include a property, session. The state's session property should read true or false depending on whether or not there is an authenticated user.

How did we tell our application that we have an authenticated user? We stored the JWT we got back from the API in session storage!

So, in order for our application's initialState to be properly set, it will simply ask sessionStorage whether or not it contains a value at a key of jwt. (Recall that we stored in our token in sessionStorage under {jwt: "xxxxxxx"}).

Our session reducer will ensure that the state's session property is set to true when it receives the LOG_IN_SUCCESS function--changing it from it's previous false value if there was no authenticated user prior to this.

Updating Initial State

Our initial state is set in src/reducers/initialState.js.

Let's add a property, session. The value of this property will be determine by whether or not there is a jwt key/value pair in sessionStorage. session should point to true if such a pair is present, false if not. We'll use this double bang operator, !!, to coerce the presence/absence of this key/value pair into a true/false value.

// src/reducers/initialState.js

export default {
  cats: [],
  hobbies: [],
  session: !!sessionStorage.jwt
}

Next up, we'll teach our session reducer how to update the state's session property when receiving the LOG_IN_SUCCESS action.

Defining the Session Reducer

We'll define our session reducer in src/reducers/sessionReducer.js, and we'll tell our root reducer to use it:

// src/reducers/rootReducer.js

import {combineReducers} from 'redux';
import cats from './catReducer';
import hobbies from './hobbyReducer';
import session from './sessionReducer';

const rootReducer = combineReducers({
  // short hand property names
  cats,
  hobbies,
  session
})

export default rootReducer;

Our session reducer will look like this:

import * as types from '../actions/actionTypes';
import initialState from './initialState';
import {browserHistory} from 'react-router';

export default function sessionReducer(state = initialState.session, action) {
  switch(action.type) {
    case types.LOG_IN_SUCCESS:
      browserHistory.push('/')
      return !!sessionStorage.jwt
    default: 
      return state;
  }
}

When it receives the LOG_IN_SUCCESS action, it will redirect to the home page, and return a new copy of state, with the session value of state to true (assuming that the JWT was correctly added to the store by our logInUser action creator function).

Now that we can properly log in a user and store their JWT in session storage, we can update our API classes to make authenticated requests.

Making Authenticated Requests to the API

Our API authenticates users by looking for the following request header:

{"Authorization": "Bearer <jwt token>"}

So, we'll have to update the necessary API class functions to include this header with every request.

In an earlier post, we defined a CatApi and a HobbyApi class to contain functions for making CRUD-related API requests. We'll update just one of those functions to include the necessary authentication headers, and you can check out the full set of changes here.

// src/api/catApi.js

class CatApi {

  static requestHeaders() {
    return {'AUTHORIZATION': `Bearer ${sessionStorage.jwt}`}
  }

  static getAllCats() {
    const headers = this.requestHeaders();
    const request = new Request('http://localhost:5000/api/v1/cats', {
      method: 'GET',
      headers: headers
    });

    return fetch(request).then(response => {
      return response.json();
    }).catch(error => {
      return error;
    });
  }

  ...

You can see that we are retrieving the JWT from session storage and using it to construct the request headers.

Now that we're making all these lovely authenticated web requests, let's make sure that unauthenticated users can't visit pages and view components that the API won't serve data to.

Building Authenticated Routes with React Router

If our API is protecting the /cats and /hobbies routes from unauthenticated requests, we certainly don't want unauthenticated users to be able to access any of the views that would try to render that data.

So, we'll use React Router's onEnter callback to prevent any of the following pages from being visited by an unauthenticated user:

  • /cats
  • /cats/:id
  • /cats/new

Currently our Router looks like this:

// src/routes.js

import React from 'react';
import { Route, IndexRoute } from 'react-router';
import App from './components/App';
import HomePage from './components/home/HomePage';
import CatsPage from './components/cats/CatsPage';
import AboutPage from './components/about/AboutPage';
import CatPage from './components/cats/CatPage';
import NewCatPage from './components/cats/NewCatPage';
import LogInPage from './components/LogInPage';

export default (
  <Route path="/" component={App}>
    <IndexRoute component={HomePage} />
    <Route path="/login" component={LogInPage} />
    <Route path="/cats" component={CatsPage}>
      <Route path="/cats/new" component={NewCatPage} />
      <Route path="/cats/:id" component={CatPage} />
    </Route>
  </Route>
);

We can see that all of the routes we wish to authenticate are nested beneath the /cats route. So, we can add our onEnter callback function to to the parent route, /cats, and it will hold for all the children.

React Router's onEnter Callback

The onEnter callback function can be added to any <Route>. It will be automatically invoked when the route is about to be entered. It provides the next router state, available as an argument, nextState, and a function to redirect to another path.

We'll define our onEnter function to redirect if there is not authenticated user. How does our app know if there is an authenticated user? By asking sessionStorage if it contains a jwt key/value pair!

Let's define our onEnter function and add it to the /cats route.

...
export default (
  <Route path="/" component={App}>
    <IndexRoute component={HomePage} />
    <Route path="/login" component={LogInPage} />
    <Route path="/cats" component={CatsPage} 
      onEnter={requireAuth}>
      <Route path="/cats/new" component={NewCatPage} />
      <Route path="/cats/:id" component={CatPage} />
    </Route>
    <Route path="/about" component={AboutPage} />
  </Route>
);

function requireAuth(nextState, replace) {
  if (!sessionStorage.jwt) {
    replace({
      pathname: '/login',
      state: { nextPathname: nextState.location.pathname }
    })
  }
}

Great! Now, if someone who is not logged in tries to visit any of our authenticated routes, they'll get bumped over to the /login path, which renders our LogInPage component.

Now we're ready to build a dynamic header with appropriate links to "log in" and "log out", along with our log out flow.

Log Out Flow and The Dynamic Header Component

We need to enable our users to log out of this fabulous app when they are done interacting with it. We'll add a "log out" link to our header component and have the click of that link dispatch a logout action.

Before we enact the log out flow, though, let's make our Header component dynamically render the "log in" link when the application has an authenticated user and the "log out" user when we don't have any such user.

Recall that our application state has a property, session, that is set to true or false depending on the presence of the jwt key/value pair in sessionStorage. So, we'll connect our Header component to the store, tell it to take the session attribute from the store as a prop, and have it render different navbar links depending on that prop.

Let's take a look:

import React, {PropTypes} from 'react';
import { Link, IndexLink } from 'react-router';
import {connect} from 'react-redux';

class Header extends React.Component {
  render() {
    if (this.props.logged_in) {
      return (
        <nav>
          <IndexLink to="/" 
            activeClassName="active">Home</IndexLink>
          {" | "}
          <Link to="/cats" activeClassName="active">Cats</Link>
          {" | "}
          <Link to="/about" 
            activeClassName="active">About</Link>
          {" | "}
          <a href="/logout">log out</a>
        </nav>
      );
    } else {
      return (
        <nav>
          <IndexLink to="/" 
            activeClassName="active">Home</IndexLink>
          {" | "}
          <Link to="/cats" activeClassName="active">Cats</Link>
          {" | "}
          <Link to="/about" 
            activeClassName="active">About</Link>
          {" | "}
          <Link to="/login" activeClassName="active">
            log in</Link>
        </nav>
      );
    }
  }
}

Header.propTypes = {
  actions: PropTypes.object.isRequired
}

function mapStateToProps(state, ownProps) {
  return {logged_in: state.session};
}

export default connect(mapStateToProps)(Header);

Our mapStateToProps function takes the application state's session value and sets it as a prop on our component, logged_in.

Then, our render function implements an if condition that states: if the logged_in prop is true, render navbar links including the "log out" link, otherwise, render navbar links including the "log in" link.

Our log out link doesn't do anything yet though. Let's fix that now.

Log Out Feature

What does it mean to "log out" our user? Well, if our application knows that there is an authenticated user by looking for the jwt key/value pair in sessionStorage, we'll "log out" a user by removing that pair from sessionStorage.

However, when a user logs out, we need to do more than just remove their token from session storage. We need to redirect them to the home page and we need to update our application's state, letting it know that its session property should be set to false.

This second step is necessary because our Header component uses application state to set a property, logged_in, which determines which navbar links to show.

Let's break down the flow:

  1. Authenticated user will click "log out" link
  2. Header component will dispatch a logOutUser action that removes the user's token from session storage and dispatches to the session reducer.
  3. The session reducer will create a new copy of state with the session property set to false
  4. This will trigger a re-invocation of our container components' mapStateToProps functions, which, in the case of the Header component, will set the logged_in prop to false. Then, when the Header component re-renders, it will render the correct nav bar links.

Okay, let's build it!

The logOut Component Function

We'll build a function, logOut, and tell it to fire on the click of the "log out" link.

import React, {PropTypes} from 'react';
import { Link, IndexLink } from 'react-router';
import {connect} from 'react-redux';
import {bindActionCreators} from 'redux';
import * as sessionActions from '../../actions/sessionActions';


class Header extends React.Component {
  constructor(props) {
    super();
    this.logOut = this.logOut.bind(this);
  }

  logOut(event) {
    event.preventDefault();
    this.props.actions.logOutUser();
  }

  render() {
    if (this.props.logged_in) {
      return (
        <nav>
          <IndexLink to="/" 
            activeClassName="active">Home</IndexLink>
          {" | "}
          <Link to="/cats" activeClassName="active">Cats</Link>
          {" | "}
          <Link to="/about" 
            activeClassName="active">About</Link>
          {" | "}
          <a href="/logout" onClick={this.logOut}>log out</a>
        </nav>
      );
    } else {
      return (
        <nav>
          <IndexLink to="/" 
            activeClassName="active">Home</IndexLink>
          {" | "}
          <Link to="/cats" activeClassName="active">Cats</Link>
          {" | "}
          <Link to="/about" 
            activeClassName="active">About</Link>
          {" | "}
          <Link to="/login" activeClassName="active">
            log in</Link>
        </nav>
      );
    }
  }
}

Header.propTypes = {
  actions: PropTypes.object.isRequired
}

function mapStateToProps(state, ownProps) {
  return {logged_in: state.session};
}

export default connect(mapStateToProps)(Header);

A couple of things to note here:

  • We had to bind the this keyword for our logOut function in our constructor function, since we're working with es6 component definitions.
  • We've imported sessionActions, and we're passing them to our components as props via the mapDispatchToProps function. We need to do this so that we can dispatch the logOutUser action, which we'll define next.

The logOutUser Action Creator

This action creator has two jobs:

  • "log out" the user by removing the JWT from session storage
  • return an object that will get sent to the reducers, so that the session reducer can produce a new copy of state with the session property set to false. This will trigger a re-render of any components connected to the store, including the Header component.

Let's take a look at our action creator:

// src/actions/sessionActions.js

export function logOutUser() {
  sessionStorage.removeItem('jwt');
  return {type: types.LOG_OUT}
}

We'll add the LOG_OUT action type constant to our exported constants:

// src/actions/actionTypes.js
...
export const LOG_IN_SUCCESS = 'LOG_IN_SUCCESS';
export const LOG_OUT = 'LOG_OUT';

We'll teach our session reducer to respond to the LOG_OUT action by creating a new copy of state, with session properly set to false.

The Session Reducer, Log Out Edition

import * as types from '../actions/actionTypes';
import initialState from './initialState';
import {browserHistory} from 'react-router';

export default function sessionReducer(state = initialState.session, action) {
  switch(action.type) {
    case types.LOG_IN_SUCCESS:
      browserHistory.push('/')
      return !!sessionStorage.jwt
    case types.LOG_OUT:
      browserHistory.push('/')
      return !!sessionStorage.jwt
    default: 
      return state;
  }
}

See that our reducer responds to the LOG_OUT action by redirecting to the root path and creating a new copy of state with the session property set to false (assuming we've properly removed the JWT from sessionSotrage).

And that's it! This new state will cause the Header component to re-render, properly displaying the "log in" link in the navbar.

subscribe and never miss a post!

Blog Logo

Sophie DeBenedetto

comments powered by Disqus
comments powered by Disqus