React Token Authentication Pt.1

Before you start, git clone https://github.com/VSchool/react-auth-series. Check the README for instructions on how to initialize the project.

NOTE: This tutorial assumes basic knowledge of React forms, Redux, and AJAX. Also we suggest you go through the back end authentication process in its entirety. Go HERE if you haven't yet.

Goal

We will be building a todo app in React that allows users to sign in, login, logout, view their profiles, and add/remove todos to their list.

Part 1 will go over how to write redux actions that will allow a user to signin, sign up and logout. The process goes something like this:

  • Make a post request containing user credentials
  • If there is an error, notify user
  • If they are valid, the server will send a token
  • Store token in local storage and store user info in redux state.
  • Send token with all outgoing requests.
  • Remove token from storage and reset state upon logout

Getting Started

Our project contains a simple navbar and several views. Our login and signup forms don't actually do anything except alert us with the info we just entered.
signup The todos and profile links don't show us much. Since we haven't implemented a way to send a token to the routes we protected in express, we will get an authorization error if we try to view todos or add them.
error

Signing Up

In our actions/index.js we need to write a function that makes a post request to our "/auth/signup" endpoint containing our user credentials:

const userUrl = "http://localhost:5000/auth/";

export function signup(credentials){  
    return (dispatch)=>{
        axios.post(userUrl + "signup", credentials)
        .then((response)=>{
            console.log(response.data);
        })
        .catch((err)=>{
            console.error(err);
        })
    }
}
//TODOS

Before you can signup you will have to send down this new function via props into your SignupContainer (/src/main/routes/signup/Container.js) and call it from inside handleSubmit:

import {signup} from "../../../redux/actions/index";  
class SignupContainer extends Component {  
...
handleSubmit(e) {  
        e.preventDefault();
        this.props.signup(this.state.inputs);
        this.clearInputs();
    }
...
}
export default connect(null,{signup})(SignupContainer);  

If you attempt to signup you should be able to see your user info, a token, and a success property in your console:
signup response

First let's store the token in localStorage:

axios.post(userUrl + "signup", credentials)  
        .then((response)=>{
            let {token, user, success} = response.data;
            localStorage.setItem("token", token);
        })

If you sign up now, look under Application in your dev tools and click on Local Storage. you should see something like this:
The next step is to make a redux action creator containing our user info so that our state has it. Later we will use it to populate our profile view and render certain components based on our login status.

function logon(success, user) {  
    return {
        type: "LOGON",
        success,
        user
    }
}

Dispatch logon after the code that stores your token:

let { token, user, success } = response.data;  
                localStorage.setItem("token", token);
                dispatch(logon(success, user));

In /reducers/index.js, our defaultState needs to be amended to include the user info and login status.

let defaultState = {  
    todos: [],
    user: {
        username: "",
        admin: false,
        _id: ""
    },
    isAuthenticated: false
}

Next, handle the logon action type:

switch (action.type) {  
        case "LOGON":
            return {
                ...state,
                user: action.user,
                isAuthenticated: action.success
            }
//...

Now, when you signup, a new user is created and stored in your redux state!
new user

Handling Errors This tutorial doesn't go deep into form validation, but we can at least inform the user if their credentials are invalid.

In /actions/index.js we'll create an action creator that returns an action containing a key and the error code return by the server.

function handleAuthErr(key, errCode) {  
    return {
        type: "HANDLE_AUTH_ERR",
        key,
        errCode
    }
}

Within the .catch callback, dispatch handleAuthErr:

.catch((err) => {
                console.error(err);
                dispatch(handleAuthErr("signup", err.response.status));
            })

Now we can handle the action in our reducer, which will simply set the error code and key into our state.

let defaultState = {  
    todos: [],
    user: {
        username: "",
        admin: false,
        _id: ""
    },
    authErrCode: {
        signup: "",
        signin: ""
    },
    isAuthenticated: false
}
switch (action.type) {  
        case "HANDLE_AUTH_ERR":
            return {
                ...state,
                authErrCode: {
                    ...state.authErrCode,
                    [action.key]: action.errCode
                }
            }

To display an appropriate message on the form in case of error, start by linking up state to the SignupContainer.

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

export default connect(mapStateToProps, { signup })(SignupContainer);  

Then in the render method we'll create the proper message based on what the error code is.

render() {  
        let authErrCode = this.props.authErrCode.signup;
        let errMsg = "";
        if (authErrCode < 500 && authErrCode > 399) {
            errMsg = "Invalid username or password!";
        } else if (authErrCode > 499) {
            errMsg = "Server error!";
        }
        return (
            <SignupComponent
                handleChange={this.handleChange.bind(this)}
                handleSubmit={this.handleSubmit.bind(this)}
                errMsg={errMsg}
                {...this.state.inputs} />
        )
    }

Finally, just add a <p> tag in the SignupComponent containing the message from props.

//...
<button type="submit">Create Account</button>  
<p>{this.props.errMsg}</p>  

If you try to signup with a name that already exists, this is what displays:
Since we want the error message to go away when we sign up or log in successfully we can easily reset the authErrCode object in thelogon case of the reducer:

case "LOGON":  
            return {
                ...state,
                user: action.user,
                isAuthenticated: action.success,
                authErrCode: {
                    signup: "",
                    signin: ""
                }
            }

Signing In

There is no substantial difference between signing in and signing up, so here's a quick run-through.

Create signin function in /actions/index.js.

export function signin(credentials) {  
    return (dispatch) => {
        axios.post(userUrl + "login", credentials)
            .then((response) => {
                let { token, user, success } = response.data;
                localStorage.setItem("token", token);
                dispatch(logon(success, user));
            })
            .catch((err) => {
                console.error(err);
                dispatch(handleAuthErr("signin", err.response.status));
            })
    }
}

Notice how we simply dispatch the logon action creator once we receive the token and user info. In case of error just dispatch the handleAuthErr function with a "signin" key. No need to mess with the reducer!

Next, connect signin to props in your LoginContainer.js and call it within handleSubmit.

handleSubmit(e) {  
        e.preventDefault();
        this.props.signin(this.state.inputs);
        this.clearInputs();
    }

Create a mapStateToProps so we can pass down error messages from the render function.

const mapStateToProps = (state)=>{  
    return state;
}
export default connect(mapStateToProps,{signin})(SigninContainer);  
render() {  
        let authErrCode = this.props.authErrCode.signin;
        let errMsg = "";
        if (authErrCode < 500 && authErrCode > 399) {
            errMsg = "Invalid username or password!";
        } else if (authErrCode > 499) {
            errMsg = "Server error!";
        }
        return (
            <SigninComponent
                handleChange={this.handleChange.bind(this)}
                handleSubmit={this.handleSubmit.bind(this)}
                errMsg={errMsg}
                {...this.state.inputs} />
        )
    }

Finally, in the SigninComponent add a <p> tag to display the error message.

<button type="submit">Submit</button>  
                    <p>{this.props.errMsg}</p>

Now you should be able to successfully (or unsuccessfully) sign in!

Or...

Logging Out

Logging out is as simple as removing the authorization token from local storage and resetting Redux's state back to default.

// actions/index.js
export function logout(){  
    localStorage.removeItem("token");
    return {
        type: "LOGOUT"
    }
}
// reducers/index.js
case "LOGOUT":  
            return {
                ...defaultState
            }

Since our logout button lives in our Navbar.js component we will have to link up Redux so that we can call logout from there.

import React, { Component } from 'react';  
import { Link } from "react-router-dom";  
import {connect} from "react-redux";  
import {logout} from "../redux/actions/index";

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

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

Now, when you click logout the token disappears form local storage, and your user info no longer appears in state!

Profile

Right now, the profile page looks blank. Let's give it some info to display.

// /src/main/routes/profile/Component.js
import {connect} from "react-redux";

class ProfileComponent extends Component {  
    render() {
        return (
            <div>
                <h2>Welcome, <i>@{this.props.user.username}</i></h2>
            </div>
        )
    }
}

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

export default connect(mapStateToProps, {})(ProfileComponent);  

Then, make sure to import and render ProfileComponent from within the Switch component in App.js:

<Switch>  
                    <Route exact path="/" component={SignupContainer} />
                    <Route path="/signin" component={SigninContainer} />
                    <Route path="/todos" component={TodosContainer}/>
                    <Route path="/profile" component={ProfileComponent}/>
                </Switch>

Next

Now that you can sign in, sign up, and logout, it is time to implement protected routes.

This means enabling the user to navigate to and from pages only which he/she is authorized.

In React Authentication Pt.2 we go over how to manipulate views using React Router.