This chapter will explain how to implement authentication to our frontend when we are using JSON Web Token (JWT) authentication in the backend. In the beginning, we will switch on security in our backend to enable JWT authentication. Then, we will create a component for the login functionality. Finally, we will modify our CRUD functionalities to send the token in the request's authorization header to the backend. We will learn how to secure our application in this chapter.
In this chapter, we will cover the following topics:
The Spring Boot application that we created in Chapter 5, Securing and Testing Your Backend, is required (located on GitHub at https://github.com/PacktPublishing/Full-Stack-Development-with-Spring-Boot-2-and-React/tree/main/Chapter05), as is the React app that we used in Chapter 12, Styling the Frontend with React MUI (located on GitHub at https://github.com/PacktPublishing/Full-Stack-Development-with-Spring-Boot-2-and-React/tree/main/Chapter12).
The following GitHub link will also be required: https://github.com/PacktPublishing/Full-Stack-Development-with-Spring-Boot-and-React/tree/main/Chapter14.
Check out the following video to see the Code in Action: https://bit.ly/38RFO0X
We have implemented CRUD functionalities in our frontend using an unsecured backend. Now, it is time to switch on security for our backend and go back to the version that we created in Chapter 5, Securing and Testing Your Backend:
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable().cors().and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.
STATELESS).and()
.authorizeRequests()
.antMatchers(HttpMethod.POST, "/login").permitAll()
.anyRequest().authenticated().and()
.exceptionHandling()
.authenticationEntryPoint(exceptionHandler).and()
.addFilterBefore(authenticationFilter,
UsernamePasswordAuthenticationFilter.class);
}
Let's test what happens when the backend is secured again.
Now, we are ready to work with the frontend.
The authentication was implemented in the backend using JWT. In Chapter 5, Securing and Testing Your Backend, we created JWT authentication, and everyone is allowed access to the /login endpoint without authentication. On the frontend's login page, we have to first call the /login endpoint using the user credentials to get the token. After that, the token will be included in all requests that we send to the backend, as demonstrated in Chapter 5, Securing 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:
import React, { useState } from 'react';
import { SERVER_URL } from '../constants.js';
function Login() {
return(
<div></div>
);
}
export default Login;
const [user, setUser] = useState({
username: '',
password: ''
});
const [isAuthenticated, setAuth] = useState(false);
import Button from '@mui/material/Button';
import TextField from '@mui/material/TextField';
import Stack from '@mui/material/Stack';
return(
<div>
<Stack spacing={2} alignItems='center' mt={2}>
<TextField
name="username"
label="Username"
onChange={handleChange} />
<TextField
type="password"
name="password"
label="Password"
onChange={handleChange}/>
<Button
variant="outlined"
color="primary"
onClick={login}>
Login
</Button>
</Stack>
</div>
);
const handleChange = (event) => {
setUser({...user,
[event.target.name] : event.target.value});
}
const login = () => {
fetch(SERVER_URL + 'login', {
method: 'POST',
headers: { 'Content-Type':'application/json' },
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))
}
import Carlist from './Carlist';
if (isAuthenticated) {
return <Carlist />;
}
else {
return(
<div>
<Stack spacing={2} alignItems='center' mt={2} >
<TextField
name="username"
label="Username"
onChange={handleChange} />
<TextField
type="password"
name="password"
label="Password"
onChange={handleChange}/>
<Button
variant="outlined"
color="primary"
onClick={login}>
Login
</Button>
</Stack>
</div>
);
}
import './App.css';
import AppBar from '@mui/material/AppBar';
import Toolbar from '@mui/material/Toolbar';
import Typography from '@mui/material/Typography';
import Login from './components/Login';
function App() {
return (
<div className="App">
<AppBar position="static">
<Toolbar>
<Typography variant="h6">
Carshop
</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 car list page. If you open the developer tools' Application tab, you can see that the token is now saved to the session storage:
The car list is still empty, but that is correct because we haven't included the token in the GET request yet. That is required for the JWT authentication, which we will implement in the next phase:
const 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(data => setCars(data._embedded.cars))
.catch(err => console.error(err));
}
All other CRUD functionalities require the same modification to work correctly. The source code of the onDelClick function appears as follows, after the modifications:
// Carlist.js
const onDelClick = (url) => {
if (window.confirm("Are you sure to delete?")) {
const token = sessionStorage.getItem("jwt");
fetch(url, {
method: 'DELETE',
headers: { 'Authorization' : token }
})
.then(response => {
if (response.ok) {
fetchCars();
setOpen(true);
}
else {
alert('Something went wrong!');
}
})
.catch(err => console.error(err))
}
}
The source code of the addCar function appears as follows, after the modifications:
// Carlist.js
// Add a new car
const 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(response => {
if (response.ok) {
fetchCars()
}
else {
alert('Something went wrong!');
}
})
.catch(err => console.error(err))
}
Finally, the source code of the updateCar function looks like this:
// Carlist.js
// Update car
const updateCar = (car, link) => {
const token = sessionStorage.getItem("jwt");
fetch(link,
{
method: 'PUT',
headers: {
'Content-Type':'application/json',
'Authorization' : token
},
body: JSON.stringify(car)
})
.then(response => {
if (response.ok) {
fetchCars();
}
else {
alert('Something went wrong!');
}
})
.catch(err => console.error(err))
}
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 a user if authentication fails. We are using the Snackbar MUI component to show the message:
import Snackbar from '@mui/material/Snackbar';
const [open, setOpen] = useState(false);
<Snackbar
open={open}
autoHideDuration={3000}
onClose={() => setOpen(false)}
message="Login failed: Check your username and
password"
/>
const login = () => {
fetch(SERVER_URL + 'login', {
method: 'POST',
headers: { 'Content-Type':'application/json' },
body: JSON.stringify(user)
})
.then(res => {
const jwtToken = res.headers.get('Authorization');
if (jwtToken !== null) {
sessionStorage.setItem("jwt", jwtToken);
setAuth(true);
}
else {
setOpen(true);
}
})
.catch(err => console.error(err))
}
If you now log in with the wrong credentials, you will see the following message:
The logout functionality is much more straightforward to implement. You basically just have to remove the token from the session storage and change the isAuthenticated state value to false, as shown in the following source code:
const logout = () => {
sessionStorage.removeItem("jwt");
setAuth(false);
}
Now, we are ready with our car application.
In this chapter, we learned how to implement a login functionality for our frontend when we are using JWT authentication. Following successful authentication, we used session storage to save the token that we received from the backend. The token was then used in all requests that we sent to the backend; therefore, we had to modify our CRUD functionalities to work with authentication properly.
In the next chapter, we will deploy our application to Heroku, as we demonstrate how to create Docker containers.
Packt has other great resources available for learning about React. These are as follows:
3.144.41.148