Token Auth with JWTs Part 2 - React + Redux
This is part 2 of a multi-part series on setting up user authentication in a MERN-stack application using JSON Web Tokens. If you haven't been following along, please visit the other posts for a full understanding of the current project:
Once you've gone through those, you should check out the part-2-react-auth-beginning
branch to start from the same place we'll be starting from for this tutorial. To do that:
# Add and commit your code if you've been typing along
git add -A
git commit -m '<your message>'
# Whether you've been typing along or not, follow these steps:
git checkout part-2-react-auth-beginning
# cd into the "client" folder
npm install
# Just in case, cd back into the project's root folder
npm install
# Run the server
nodemon index.js
# In separate terminal window, run the react app from the client folder
cd client && npm start
Again, spend some time looking around the React code to make sure you understand everything before moving on. This tutorial assumes you've written at least smallish apps in React before.
As a recap, some things to note about the starter code:
- We're using
redux-thunk
as middleware so we can asynchronously dispatch actions - We're using a slightly-modified version of the "ducks" redux organization
- The parent
index.js
file in theredux
folder is solely in charge of combining separated reducers and using the combined reducers to create and export the store. - We've added a
"proxy"
line to the client'spackage.json
so that all outgoing HTTP requests will be proxied to port 5000. This way we can avoid any CORS issues.- If you make a change to this
proxy
, like if you chose to run your server on a port other than 5000, you'll need to manually stop and start your React dev server. Otherwise the change won't take effect.
- If you make a change to this
Goal
Building off the todo app from part 1, we'll be adding the ability for users to sign up, login, logout, view their profiles, and add/remove todos.
First we'll go over how to write redux actions that will allow a user to login, sign up, and logout.
The process goes something like this:
- Make a POST request containing user's login credentials.
- If the credentials are valid, the server will send a token and information about the user. We already built this functionality in part 1 of this series.
- Store token in local storage and (limited) user information in redux state.
- Send token with all outgoing requests in an HTTP header called
Authorization
. - If the user logs out, remove the token and user info from local storage and redux state.
Getting Started
Our project contains a simple navbar and several views. Currently, our login and signup forms don't actually do anything except alert us with the info we just entered.
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.
Signing Up
Right now we only have a redux reducer and action creators for todos, so let's write one for authentication.
In the redux
folder, create a new file called auth.js
. Now your redux folder should look like this:
|_ redux/
|_ auth.js
|_ todos.js
|_ index.js
In our redux/auth.js
file, we need to write a function that makes a POST request to our /auth/signup
endpoint containing our user credentials (chosen username and password):
import axios from "axios";
export function signup(userInfo) {
return dispatch => {
axios.post("/auth/signup", userInfo)
.then(response => {
// We'll come back to this to dispatch an action to the reducer instead of just this console.log
console.log(response.data);
})
.catch(err => {
console.error(err);
})
}
}
With this action creator written, now you will have to connect
your SignupFormContainer
so it can dispatch this action.
Open your SignupFormContainer
(/src/main/Signup/index.js
), and make the following additions:
...
import {connect} from "react-redux";
import {signup} from "../../redux/auth";
class SignupFormContainer extends Component {
...
handleSubmit(e) {
e.preventDefault();
this.props.signup(this.state.inputs);
this.clearInputs();
}
...
}
export default connect(null, {signup})(SignupFormContainer);
If you attempt to sign up (make sure to use a different username than the one you used when testing this endpoint in Postman from the previous section of this tutorial) you should be able to see your user info, a token, and a success property in your console:
Once again, it's bad to store and transmit plain text passwords. We're going to remedy this in part 3 of this series, so make sure to complete that section after this one!
Next let's store the token and user's information in localStorage
. Back in your redux/auth.js
file, update the console.log
to store the response data in localStorage
:
...
axios.post("/auth/signup", userInfo)
.then(response => {
const {token, user} = response.data
localStorage.setItem("token", token)
localStorage.setItem("user", JSON.stringify(user))
})
...
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 redux global state has it. Later we will use it to populate our profile view and render certain components based on our login status.
Underneath your signup
action creator in redux/auth.js
, add a new authenticate
action creator:
export function authenticate(user) {
return {
type: "AUTHENTICATE",
user // pass the user for storage in Redux store
}
}
And dispatch authenticate
in your signup
action creator after the code that stores your token:
...
.then(response => {
const {token, user} = response.data
localStorage.setItem("token", token)
localStorage.setItem("user", JSON.stringify(user))
dispatch(authenticate(user))
})
...
We don't have anything telling the Redux store to keep information about the user, only the list of todos. We'll need to visit a couple places to add user
to our Redux store:
1. redux/index.js
...
import todos from "./todos";
import user from "./auth";
const reducer = combineReducers({
todos,
user
});
...
That reducer hasn't been written yet, so let's head to...
2. redux/auth.js
Underneath your authenticate
action creator:
const initialState = {
username: "",
isAdmin: false,
isAuthenticated: false
}
export default function reducer(state = initialState, action) {
switch (action.type) {
case "AUTHENTICATE":
return {
...state,
...action.user,
isAuthenticated: true
}
default:
return state;
}
}
Now, when you signup, a new user is created in the database, and the token is stored in localStorage and the user's info is stored in both the Redux store and localStorage!
Logging In
There is no substantial difference between logging in and signing up, so here's a quick run-through.
Create login
function in redux/auth.js
.
export function login(credentials) {
return dispatch => {
axios.post("/auth/login", credentials)
.then(response => {
const {token, user} = response.data;
localStorage.setItem("token", token)
localStorage.setItem("user", JSON.stringify(user))
dispatch(authenticate(user))
})
.catch((err) => {
console.error(err);
});
}
}
Notice how we simply dispatch the authenticate
action creator once we receive the token and user info.
Next, use connect
to connect the login
action creator to props in your LoginFormContainer
and call it within handleSubmit
. In main/Login/index.js
:
...
import {connect} from "react-redux";
import {login} from "../../redux/auth";
...
handleSubmit(e) {
e.preventDefault();
this.props.login(this.state.inputs);
this.clearInputs();
}
...
export default connect(null, {login})(LoginFormContainer);
Now you should be able to successfully sign in!
Logging Out
Logging out is as simple as removing the authorization token and user info from local storage and resetting Redux's user
property in state back to default.
// redux/auth.js
...
export function logout() {
localStorage.removeItem("token")
localStorage.removeItem("user")
return {
type: "LOGOUT"
}
}
...
// (In the reducer's switch)
case "LOGOUT":
return initialState;
It also makes sense to remove the list of todos from the Redux store. Over in redux/todos.js
, add a case
in the reducer that deals with the todos array to handle the "LOGOUT"
action as well.
...
case "LOGOUT":
return initialTodos; // an empty array
Notice that every action will run through every reducer. You can have one action affect multiple parts of the Redux store simply by adding a
case
in any reducer's switch statement.
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 from 'react';
import {Link} from "react-router-dom";
import {connect} from "react-redux";
import {logout} from "../redux/auth";
function Navbar(props) {
return (
<div className="navbar-wrapper">
<div className="nav-link"><Link to="/">Sign Up</Link></div>
<div className="nav-link"><Link to="/login">Log 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={props.logout}>Logout</button>
</div>
</div>
)
}
export default connect(null, {logout})(Navbar);
Now when you click the "Logout" button, the token and user info disappears from localStorage
, and your user info no longer appears in state!
Next
Now that you can sign in, sign up, and logout, it is time to be able for a user to be add and see just their own todos. This means enabling the user to navigate to and from pages only which he/she is authorized.
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 Part 1 of this series, 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:
Add the following code to the top of the file.
// /redux/todos.js
import axios from "axios";
let todoAxios = axios.create();
todoAxios.interceptors.request.use((config)=>{
const token = localStorage.getItem("token");
config.headers.Authorization = `Bearer ${token}`;
return config;
})
Then replace all instances of axios
with todoAxios
Also make sure that your todoUrl
is set to /api/todo/
and not just /todo/
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:
You can now continue to part 3 here