Securing the frontend

The authentication was implemented to the backend using JWT. In Chapter 5Securing and Testing Your Backend, we created JWT authentication, and the /login endpoint is allowed to everyone without authentication. In the frontend's login page, we have to first call the /login endpoint to get the token. After that, the token will be included in all requests we are sending to the backend, as demonstrated in Chapter 5Securing and Testing Your Backend.

Let's first create a login component that asks for credentials from the user to get a token from the backend:

  1. Create a new file, called Login.js, in the components folder. Now, the file structure of the frontend should be the following:
  1. Open the file in the VS Code editor view and add the following base code to the login component. We are also importing SERVER_URL, because it is required in a login request:
import React, { useState } from 'react';
import {SERVER_URL} from '../constants.js';

const Login = () => {
return (
<div>
</div>
);
}

export default Login;
  1. We need three state values for the authentication, two for the credentials (username and password), and one Boolean value to indicate the status of the authentication. The default value of the authentication status state is false. We introduce states using the useState function:
const [user, setUser] = useState({username: '', password: ''})
const [isAuthenticated, setAuth] = useState(false);
  1. In the user interface, we are going to use the Material-UI component library, as we did with the rest of the user interface. We need text field components for the credentials and a button to call a login function. Add imports for the components to the login.js file:
import TextField from '@material-ui/core/TextField';
import Button from '@material-ui/core/Button';
  1. Add imported components to a user interface by adding these to the return statement. We need two TextField components, one for the username and one for the password. One Button component is needed to call the login function that we are going to implement later in this section:
return (
<div>
<TextField name="username"
label="Username" onChange={handleChange} /><br/>
<TextField type="password" name="password"
label="Password" onChange={handleChange} /><br/><br/>
<Button variant="outlined" color="primary"
onClick={login}>
Login
</Button>
</div>
);
  1. Implement the change handler for the TextField components, in order to save typed values to the states:
const handleChange = (event) => {
setUser({...user, [event.target.name] : event.target.value})
}
  1. As shown in Chapter 5, Securing and Testing Your Backend, the login is done by calling the /login endpoint using the POST method and sending the user object inside the body. If authentication succeeds, we get a token in a response Authorization header. We will then save the token to session storage and set the isAuthenticated state value to true. The session storage is similar to local storage, but it is cleared when a page session ends. When the isAuthenticated state value is changed, the user interface is rerendered:
const login = () => {
fetch(SERVER_URL + 'login', {
method: 'POST',
body: JSON.stringify(user)
})
.then(res => {
const jwtToken = res.headers.get('Authorization');
if (jwtToken !== null) {
sessionStorage.setItem("jwt", jwtToken);
setAuth(true);
}
})
.catch(err => console.error(err))
}

  1. We can implement conditional rendering, which renders the Login component if the isAuthenticated state is false, or the Carlist component if the isAuthenticated state is true. We first have to import the Carlist component to the Login component:
import Carlist from './Carlist';

Then, we have to implement the following changes to the return statement:

if (isAuthenticated === true) {
return (<Carlist />)
}
else {
return (
<div>
<TextField name="username"
label="Username" onChange={handleChange} /><br/>
<TextField type="password" name="password"
label="Password" onChange={handleChange} /><br/><br/>
<Button variant="outlined" color="primary"
onClick={login}>
Login
</Button>
</div>
);
}
  1. To show the login form, we have to render the Login component instead of the Carlist component in the App.js file:
// App.js
import React from 'react';
import './App.css';
import AppBar from '@material-ui/core/AppBar';
import Toolbar from '@material-ui/core/Toolbar';
import Typography from '@material-ui/core/Typography';
import Login from './components/Login';

function App() {
return (
<div className="App">
<AppBar position="static" color="default">
<Toolbar>
<Typography variant="h6" color="inherit">
CarList
</Typography>
</Toolbar>
</AppBar>
<Login />
</div>
);
}

export default App;

Now, when your frontend and backend are running, your frontend should look like the following screenshot:

If you log in using the user/user or admin/admin credentials, you should see the CarList page. If you open the developer tools, you can see that the token is now saved to session storage:

The car list is still empty, but that is correct, because we haven't included the token to the request yet. That is required for JWT authentication, which we will implement in the next phase:

  1. Open the Carlist.js file in the VS Code editor view. To fetch the cars, we first have to read the token from the session storage and then add the Authorization header with the token value to the request. You can see the source code of the fetch function here:
// Carlist.js 
// Fetch all cars
fetchCars = () => {
// Read the token from the session storage
// and include it to Authorization header
const token = sessionStorage.getItem("jwt");
fetch(SERVER_URL + 'api/cars',
{
headers: {'Authorization': token}
})
.then((response) => response.json())
.then((responseData) => {
this.setState({
cars: responseData._embedded.cars,
});
})
  .catch(err => console.error(err)); 
}
  1. If you log in to your frontend, you should see the car list populated with cars from the database:
  1. Check the request content from the developer tools; you can see that it contains the Authorization header with the token value:

All other CRUD functionalities require the same modification to work correctly. The source code of the delete function appears as follows, after the modifications:

// Delete car
onDelClick = (link) => {
if (window.confirm('Are you sure to delete?')) {
const token = sessionStorage.getItem("jwt");
fetch(link,
{
method: 'DELETE',
headers: {'Authorization': token}
})
.then(res => {
toast.success("Car deleted", {
position: toast.POSITION.BOTTOM_LEFT
});
this.fetchCars();
})
.catch(err => {
toast.error("Error when deleting", {
position: toast.POSITION.BOTTOM_LEFT
});
console.error(err)
})
}
}

The source code of the add function appears as follows, after the modifications:

// Add new car
addCar(car) {
const token = sessionStorage.getItem("jwt");
fetch(SERVER_URL + 'api/cars',
{ method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': token
},
body: JSON.stringify(car)
})
.then(res => this.fetchCars())
.catch(err => console.error(err))
}

Finally, the source code of the update function looks like this:

// Update car
updateCar(car, link) {
const token = sessionStorage.getItem("jwt");
fetch(link,
{ method: 'PUT',
headers: {
'Content-Type': 'application/json',
'Authorization': token
},
body: JSON.stringify(car)
})
.then(res => {
toast.success("Changes saved", {
position: toast.POSITION.BOTTOM_LEFT
});
this.fetchCars();
})
.catch(err =>
toast.error("Error when saving", {
position: toast.POSITION.BOTTOM_LEFT
})
)
}

Now, all the CRUD functionalities will be working after you have logged in to the application.

In the final phase, we are going to implement an error message that is shown to an end user if authentication fails. We are using the react-toastify component to show the message:

  1. Add the following import to the Login.js file:
import { ToastContainer, toast } from 'react-toastify';
import 'react-toastify/dist/ReactToastify.css';
  1. Add ToastContainer to the render() method:
<ToastContainer autoClose={1500} /> 
  1. Show the toast message if authentication fails:
const login = () => {
fetch(SERVER_URL + 'login', {
method: 'POST',
body: JSON.stringify(user)
})
.then(res => {
const jwtToken = res.headers.get('Authorization');
if (jwtToken !== null) {
sessionStorage.setItem("jwt", jwtToken);
setAuth(true);
}
else {
toast.warn("Check your username and password", {
position: toast.POSITION.BOTTOM_LEFT
})
}
})
.catch(err => console.error(err))
}

If you now log in with the wrong credentials, you will see the toast message:

The logout functionality is much more straightforward to implement. You basically just have to remove the token from session storage and change the isAuthenticated state value to false, as shown in the following source code:

const logout = () => {
sessionStorage.removeItem("jwt");
setAuth(false);
}

Then, with conditional rendering, you can render the Login component instead of Carlist

If you want to implement a menu using React Router, it is possible to implement so-called secured routes that can be accessed only when a user is authenticated. The following source code shows the secured route that presents the routed component if the user is authenticated; otherwise, it redirects to a login page:

const SecuredRoute = ({ component: Component, ...rest, isAuthenticated }) => (
<Route {...rest} render={props => (
isAuthenticated ? (
<Component {...props}/>
) : (
<Redirect to={{
pathname: '/login',
state: { from: props.location }
}}/>
)
)}/>
)

Here is an example of a Switch router that is using SecuredRoute, which we defined in the previous example:

 <Switch>
<Route path="/login" component={Login} />
<Route path="/contact" component={Contact} />
<SecuredRoute isAuthenticated={this.state.isAuthenticated}
path="/shop" component={Shop} />
<Route render={() => <h1>Page not found</h1>} />
</Switch>

Now, the Login and Contact components can be accessed without authentication, but Shop requires authentication.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.22.79.82