Skip to main content
- Class Components
- The React Lifecycle
- Composing and Sharing Non-Visual Behavior
- Testing Web Applications
- Class Components
- The React Lifecycle
- Composing and Sharing Non-Visual Behavior
- Testing Web Applications
We will learn how to work with a server that uses tokens for authentication
At the end of this module, you should be able to:
- handle authentication with tokens in a React app
- implement protected routes using an authentication token and Redirect
As you think [positively or negatively], so shall you become.
Bruce Lee
Review each preclass resource before class.
Many services out in the wild require the client (our React app, for example) to provide proof that it’s authenticated with them. The server running these services can issue a JWT (JSON Web Token) as the authentication token, in exchange for correct login credentials. So how do we handle this token from then on, inside our Single Page Application?
In general, servers don’t remember anything about us after each interaction. Clients need to tack a reminder to every request, and this is commonly done automatically with cookies. Cookies are just small pieces of data generated by the browser to store state information.
Modern web services dealing with JSON data often use Jason Web Tokens (JWT)s instead. These tokens, issued by the server, are strings of cryptic text, which can be stored on the client-side using local storage or session storage. The server can readily tell the client that it issued the token. It can also read the token and make decisions for data transfer based on the client’s permission.
A common pattern is for a login endpoint to exist, which takes a payload of username and password. If the credentials are known, the server responds with a fresh JWT. From then on, it’s the application’s responsibility to add an Authorization: <token> header to every request, to be allowed access to protected resources that require authentication.
We are going to create an Axios configuration that attaches an Authorization: <token> header to requests. To do that, we will create a new file called axiosAuth.js. Whenever the application needs to exchange data with a protected endpoint, it imports this module, instead of the usual import axios from "axios";.
import axios from 'axios';
export const axiosWithAuth =() => {
const token = localStorage.getItem('token');
return axios.create({
headers: {
Authorization: token,
},
});
};
Note: Some API’s require you to use a slightly different syntax. Make sure you understand the API you are working with:
Authorization: `Bearer ${token}`,
After the user properly authenticates (properly logs in), the server returns the token. Your application needs to save the returned token (the permissions) to localStorage, so that the above axiosWithAuth module can grab it for other calls that require the Authorization header.
const login = () => {
axios.post('endpoint/here', userCredentials)
.then(res => {
localStorage.setItem('token', res.data.token);
props.history.push('/dashboard');
}
}
Now that we have the token, we can do an AJAX request to an endpoint using the axiosWithAuth.js module.
import { axiosWithAuth } from '../../path/to/axiosAuth.js';
// etc
axiosWithAuth().get('endpoint/path/here').then(data => /* do something with the data */);
If you inspect the request on the Network tab in Chrome Devtools, you should see Authorization: eyJ1c2VySWQiOiJiMDhmODZhZi0zNWRhLTQ4ZjItOGZhYi1jZWYzOTA0NjYwYmQifQ among the request headers.
Improve our custom Axios configuration using the library axios-retry. The idea is to automatically retry the request three times, if there are errors or timeouts.
Most of the web apps we will work on will have some sort of authentication system. We want to protect any routes in our app from being rendered if the user is not authenticated with a token from the server.
As we build our web apps, we will most likely have some “protected” routes - routes that should only render with authentication. Normally, the client will make a login request, sending the server the user’s username and password. Then, the server will check those credentials against the database, and if it can authenticate the user, it will return a token. Once we have this token, we can add two layers of protection to our app. One uses protected routes, and the other sends an authentication header with our API calls (as we learned in the above objective).
Let’s see what this looks like in code. We will use some React Router components, and build a custom <PrivateRoute /> component to protect the route.
In our App component, let’s add a couple of public routes and a couple of links:
function App() {
return (
<div>
<ul>
<li>
<Link to="/public">Public Page</Link>
</li>
<li>
<Link to="/protected">Protected Page</Link>
</li>
</ul>
<Route path="/public" component={Public} />
<Route path="/login" component={Login} />
</div>
);
}
Notice the links? Anyone can click on the “Public Page” link, but if they click on the “Protected Page” link without authorization, they will be routed to the login page instead.
Next step will be to add a <PrivateRoute /> route:
<Route path="/public" component={Public}/>
<Route path="/login" component={Login}/>
<PrivateRoute path='/protected' component={Protected} />
Here are the requirements for our PrivateRoute component.
- It has the same API as
<Route />. - It renders a
<Route />and passes all the props through to it. - It checks if the user is authenticated, if they are, it renders the “component” prop. If not, it redirects the user to /login.
With those requirements in mind, let’s build it out.
// Requirement 1.
// It has the same API as `<Route />`
const PrivateRoute = ({ component: Component, ...rest }) => (
)
This code means that the component can accept a component Prop, just like <Route /> does, and take any other prop that gets passed into it by spreading in ...rest.
// Requirement 2.
// It renders a `<Route />` and passes all the props through to it.
const PrivateRoute = ({ component: Component, ...rest }) => (
<Route {...rest} render={} />
)
We are passing in all of the props passed to <PrivateRoute /> in the App component.
// Requirement 3.
// It checks if the user is authenticated, if they are,
// it renders the "component" prop. If not, it redirects
// the user to /login.
const PrivateRoute = ({ component: Component, ...rest }) => (
<Route
{...rest}
render={props =>
localStorage.getItem("token") ? (
<Component {...props} />
) : (
<Redirect to="/login" />
)
}
/>
);
This is using the render props pattern. We won’t go too much into how this works here, but it would be a great weekend learning objective for you to look into if you so desire.
Also, notice the <Redirect /> component? That is from React Router. It does exactly what you think - redirects the app to the supplied route.
At this point, you can see the PrivateRoute component in action if you try to click on the Protected Page link. You should be redirected to “/login” instead of being taken to the “/protected” page.
Let’s think about the login page now. Our login page is going to be a form that takes in a user’s credentials, calls the login endpoint with a POST request, and then redirects the user to the protected route when the login API call returns. Let’s set it up and practice our authentication skills!
import React, { useState } from 'react';
import { axiosWithAuth } from '../path/to/module';
const Login = (props) => {
const [credentials, setCredentials] = useState({});
const login = e => {
e.preventDefault();
axiosWithAuth().post('login/endpoint', credentials)
.then(res => {
localStorage.setItem('token', res.data.token);
this.props.history.push('/');
})
}
const handleChange = e => {
setCredentials: {
...credentials,
[e.target.name]: e.target.value,
}
}
return (
<div>
<form onSubmit={this.login}>
<input
type="text"
name="username"
value={credentials.username}
onChange={this.handleChange}
/>
<input
type="password"
name="password"
value={credentials.password}
onChange={this.handleChange}
/>
<button>Log in</button>
</form>
</div>
)
}
export default Login;
Great work providing a good experience for your amazing users!
Add some loggingIn state, and error state. Solve for both potential states in your login form. For example, if loggingIn is true, show a spinner in the button. If there is an error, display an error message.
-
Build an app that renders list of friends retrieved from a server that uses tokens for authentication
You can use class recordings to help you master the material.
-
Client-Side Authentication for Web35 w/Warren Longmire
Client-Side Authentication
To demonstrate mastery of this module, you need to complete and pass a code review on each of the following:
-
Objective challenge:
Improve our custom Axios configuration using the library
axios-retry. The idea is to automatically retry the request three times, if there are errors or timeouts. -
Objective challenge:
Add some
loggingInstate, anderrorstate. Solve for both potential states in your login form. For example, ifloggingInis true, show a spinner in the button. If there is an error, display an error message. -
Project: Auth Friends