React Token Authentication Pt.2

View part 1 here: React Token Authentication Pt. 1

Recap

So far we are able to:

  • Create a new account, login with an existing account, and logout.
  • Store our user info in state.
  • Store our authorization token in Local Storage so that even if we refresh the page, we don't lose it.

What are we missing?

  • We cannot view or add to our todo list.
  • All our nav links are visible despite our login status.
  • We are not redirected to a new page after we signup/login/logout.

Interceptors

We receive an authorization error when we attempt to view or add todos because there is no token present in our GET/POST request header when we send it.

If you recall from the backend auth tutorial we simply placed the token into the headers via Postman. It looked like this:
.

The node package Axios makes it very easy to attach information to request headers using what are called interceptors. They are just middleware functions that do something to the outgoing request (or incoming response) before it gets sent.

In our case we just want to retrieve the token from Local Storage and set it to the value of 'Authorization', a property of the config.headers object:

// /actions/index.js
import axios from "axios";

axios.interceptors.request.use((config)=>{  
    const token = localStorage.getItem("token");
    config.headers.Authorization = `Bearer ${token}`;
    return config;
})

The config object is just an object containing metadata about the request.

Now, try logging in and adding a todo! You should see something like this:

Conditional Views

In our redux state we have a property called isAuthenticated. Its value gets toggled depending on whether we have logged in or out. We will be rendering the links in our navbar accordingly.

First, connect state to props in Navbar.js

const mapStateToProps = (state) => {  
    return state;
}

export default connect(mapStateToProps, { logout })(Navbar);  

Since we only want the signup and signin links to show if we AREN'T logged in we can write this:

class Navbar extends Component {  
    render() {
        const isAuthenticated = this.props.isAuthenticated;
        return (
            <div className="navbar-wrapper">
                {isAuthenticated ? null : <div className="nav-link"><Link to="/">Sign Up</Link></div>}
                {isAuthenticated ? null : <div className="nav-link"><Link to="/signin">Sign In</Link></div>}

// ... 
        )
    }
}

The ternary operator lets us return either null or <div> ... </div> based on the value of isAuthenticated.

Similarly, we want to see ONLY the logout, profile, and todos links if we ARE Logged in:

class Navbar extends Component {  
    render() {
        const isAuthenticated = this.props.isAuthenticated;
        return (
            <div className="navbar-wrapper">
                {isAuthenticated ? null : <div className="nav-link"><Link to="/">Sign Up</Link></div>}
                {isAuthenticated ? null : <div className="nav-link"><Link to="/signin">Sign In</Link></div>}
                {isAuthenticated ? <div className="nav-link"><Link to="/todos">Todos</Link></div> : null}
                {isAuthenticated ? <div className="nav-link"><Link to="/profile">Profile</Link></div> : null}
                {isAuthenticated ? <div className="nav-link"><button onClick={this.props.logout}>Logout</button></div> : null}
            </div>
        )
    }
}

Try logging in and logging out, you should see the navbar change!

Signed out view Signed in view

Protected Routes

There is still an obvious problem. Once we sign in or logout, we remain on the same URL. We need to tell our routes to render certain components based on the isAuthenticated property.

Auto-navigating from logout

We will be making a new file called ProtectedRoute.js inside our /routes folder, which will replace some of our normal <Route /> components. We will connect redux state so that we have access to isAuthenticated in state. Also, import Route and Redirect from "react-router-dom".

// /routes/ProtectedRoute.js
import React, { Component } from 'react';  
import { connect } from "react-redux";  
import { Route, Redirect } from "react-router-dom";

class ProtectedRoute extends Component {  
    render() {
        return (
            //logic to go here
        )
    }
}

const mapStateToProps = (state) => {  
    return state;
}
export default connect(mapStateToProps,{})(ProtectedRoute);  

The idea here is simple. Render a route to a component if user is authorized, otherwise redirect them to the login page.

ProtectedRoute takes two additional props:

  • component - the component we want to protect.
  • path - url endpoint to check for.
class ProtectedRoute extends Component {  
    render() {
        const isAuthenticated = this.props.isAuthenticated;
        const Component = this.props.component;
        const path = this.props.path;
        return (
            isAuthenticated ?
                <Route path={path} render={(props) => {
                    return <Component {...props} />
                }} /> :
                <Redirect to="/signin" />
        )
    }
}

From App.js, import ProtectedRoute and replace the appropriate Route components with it (the ones rendering the TodoContainer and ProfileComponent):

import ProtectedRoute from "./routes/ProtectedRoute";

export default class App extends Component {  
    render() {
        return (
            <div className="app-wrapper">
                <Navbar />
                <Switch>
                    <Route exact path="/" component={SignupContainer} />
                    <Route path="/signin" component={SigninContainer} />
                    <ProtectedRoute path="/todos" component={TodosContainer}/>
                    <ProtectedRoute path="/profile" component={ProfileComponent}/>
                </Switch>
            </div>
        )
    }
}

We're almost there! If you click logout while you are viewing the profile or todos page, you will instantly be transferred to the login page.

Auto-navigating out of login/signup

Now we must render the login/signup the pages based on the isAuthenticated property. If we are logged in we will Redirect the user to the endpoint "/profile". Otherwise we will render it like normal.

All you need to do is import Redirect and withRouter from "react-router-dom" into App.js and connect to redux.

import { Route, Switch, withRouter, Redirect } from "react-router-dom";  
// ...
import {connect} from "react-redux";

class App extends Component {  
// ...
}

const mapStateToProps = (state) => {  
    return state;
}

export default withRouter(connect(mapStateToProps,{})(App));  

withRouter() is necessary to let App know about changes in the pathname.

Right now we are directly rendering LoginContainer and SignupContainer from within the Switch component. We need to make a slight adjustment so that each Route checks whether the user is authenticated before rendering the component.

<Route /> components can take an additional prop called render. We are going to set it to a callback function that will do the above check.

// App.js
// ...
render() {  
        const isAuthenticated = this.props.isAuthenticated;
        return (
            <div className="app-wrapper">
                <Navbar />
                <Switch>
                    <Route exact path="/" render={(props)=>{
                       return  isAuthenticated ?
                        <Redirect to= "/profile"/> :
                        <SignupContainer {...props}/>
                    }}/>
                    <Route path="/signin" render={(props)=>{
                       return  isAuthenticated ?
                        <Redirect to= "/profile"/> :
                        <SigninContainer {...props}/>
                    }} />
                    <ProtectedRoute path="/todos" component={TodosContainer}/>
                    <ProtectedRoute path="/profile" component={ProfileComponent}/>
                </Switch>
            </div>
        )
    }

That's it! You should now see your profile page when you login or sign up.

Refresh

Try logging in and then refreshing the page while you are in a protected route. What happens? It sends you back to your login page! That's kind of annoying. Luckily, it's an easy fix.

In /actions/index.js We're going to write an function that sends a GET request to the "/profile/verify" endpoint, which will send us our user info and login status.

// /actions/index.js
const todoUrl = "http://localhost:5000/todo/";  
const userUrl = "http://localhost:5000/auth/";

// add the new url
const profileUrl = "http://localhost:5000/profile/";  
// create and export new action creator
export function verify() {  
    return (dispatch) => {
        axios.get(profileUrl + "verify")
            .then((response) => {
                let { success, user } = response.data;
                dispatch(logon(success, user));
            })
            .catch((err) => {
                console.error(err);
            })
    }
}

Basically all this function does is send a GET request which, because of our interceptor, contains the authorization token in the header. The server verifies it and sends back user info. Then, logon function gets dispatched and state is updated.

Now all we have to do is call it when our App component mounts.

Import verify into App.js:

//...
import {verify} from "../redux/actions/index";

class App extends Component {  
 //...
}

export default withRouter(connect(mapStateToProps,{verify})(App));  

Call it from ComponentDidMount()

componentDidMount(){  
        this.props.verify();
    }

Try refreshing while you are logged in. You will still be authorized! Notice that it will always redirect you back to your profile page. This is a minor inconvenience, but is beyond the scope of this post. As an exercise, try displaying a ...Loading message while awaiting all your AJAX responses!

Conclusion

Congratulations! You made it! You have the necessary tools to make a basic token authentication system in React. From here you can try to add on more security features such as email verification, password reset, and profile edits!