Let's build Java Full-Stack Spring boot & React app: Backend REST API / 3 - Securing the REST API with Spring Security & JWT (b)
Hello,
We added some security features to our REST API in previous post thanks to Spring Security and JWT. In this tutorial, we'll be validating token and secure the API endpoints.
Overview
Here are what we'll be doing in this post:
- JWT token validation
- How to grant/deny access to Spring Data REST Repository method
JWT Token validation
In order to catch and validate token coming from a client, we have to create a request Filter class extending the Spring Web Filter OncePerRequestFilter class. For any incoming request, this Filter class will be executed. It checks if the request has a valid JWT token. If it has a valid JWT Token, then it sets the authentication in context to specify that the current user is authenticated.
Create a class, AuthRequestFilter.java in security.filter package.
package com.codeurinfo.easytransapi.security.filter;
import com.codeurinfo.easytransapi.security.UserDetailsImpl;
import com.codeurinfo.easytransapi.security.UserDetailsServiceImpl;
import com.codeurinfo.easytransapi.security.util.JwtUtil;
import java.io.IOException;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
@Component
public class AuthRequestFilter extends OncePerRequestFilter {
@Autowired
private UserDetailsServiceImpl userDetailsService;
@Autowired
private JwtUtil jwtUtil;
@Override
protected void doFilterInternal(
HttpServletRequest request,
HttpServletResponse response,
FilterChain chain
)
throws ServletException, IOException {
final String authorizationHeader = request.getHeader("Authorization");
String username = null;
String jwt = null;
if (
authorizationHeader != null && authorizationHeader.startsWith("Bearer ")
) {
// get the jwt token
jwt = authorizationHeader.substring(7);// authorizationHeader is like "Bearer jwttoken", so 7 caracters before jwttoken
username = jwtUtil.extractUsername(jwt);// get username for the given token
}
if (
username != null &&
SecurityContextHolder.getContext().getAuthentication() == null
) {
UserDetailsImpl userDetails =
(UserDetailsImpl) this.userDetailsService.loadUserByUsername(username);
if (jwtUtil.validateToken(jwt, userDetails)) {
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(
userDetails,
null,
userDetails.getAuthorities()
);
usernamePasswordAuthenticationToken.setDetails(
new WebAuthenticationDetailsSource().buildDetails(request)
);
SecurityContextHolder
.getContext()
.setAuthentication(usernamePasswordAuthenticationToken);
}
}
chain.doFilter(request, response);
}
}
Then, let's add this Filter to the SecurityConfig.java class in the configure method like this:
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf()
.disable()
.authorizeRequests()
.antMatchers("/api/auth/**").permitAll() // Grant access to every /api/auth based url
.anyRequest().authenticated() // All other based url wil be allowed if authenticated
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS);//Force server not to never create an HttpSession
http.addFilterBefore(
authRequestFilter,
UsernamePasswordAuthenticationFilter.class
);
}
Reminder: You must first declare authRequestFilter as a variable in the SecurityConfig.java class
@Autowired
private AuthRequestFilter authRequestFilter;
At this point, our REST API is almost secured. Let's run and test it in Postman.
1 . Make a simple GET request
http:
As you can see, we have an Access Denied error.
2 . Make a login POST request
http:
Remember to use the same credentials you set up for users in the preload DatabaseLoader.java If a valid username/password is provided, a JWT token is returned.
3 . Try a simple GET request again
http:
- Before execute the GET request in Postman, copy the JWT token returned in step 2
- Open Header tab, add a key : Authorization , type the value : Bearer yourCopiedToken
- Execute the request, you might have a good result as below.
From now on, you must add the Authorization key and value in the Header tab before executing any other request apart from the login
4 . Make a POST request to save a route
http:
- Open the Header tab and Authorization key and value as proceed in step 3
- Open the Body tab to add your payload data
{
"name": "Rex-Adidogome",
"start": "Grand marche - Rex",
"terminus": "Adidogome"
}
- Then execute to have a result as below:
Congratulations! You configured Spring Security and JWT to secure a REST API so that only authenticated consumers can access endpoints for requests.
But ... in the real world, every logged-in user should have the ability to access some endpoints while others no. Let's strengthen our API security.
Spring Security: method level security
Remember, we added @EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true) annotation to the SecurityConfig.java class. This annotation enables method level security.
In the RouteRepository.java class, suppose we want only ROLE_ADMIN users to have the ability to access the findRoutesByName method.
Add @PreAuthorize("hasRole('ROLE_ADMIN')") annotation to that method to grant access to ROLE_ADMIN users. Updated code of RouteRepository.java:
package com.codeurinfo.easytransapi.repository; import com.codeurinfo.easytransapi.model.Route;
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.repository.query.Param;
import org.springframework.data.rest.core.annotation.RepositoryRestResource;
import org.springframework.data.rest.core.annotation.RestResource;
import org.springframework.security.access.prepost.PreAuthorize; @RepositoryRestResource
public interface RouteRepository extends JpaRepository<Route, Long> { @PreAuthorize("hasRole('ROLE_ADMIN')") @RestResource(path = "routesByName") Optional<Route> findRoutesByName(@Param("name") String name);
}
Let's test the API again.
1 . Make sure you preloaded two different users, one with the ROLE_ADMIN and the other no.
2 . Make a POST request to log in with the admin user credentials, then copy the token
3 . Try to make a POST request to save route, GET request to search route, etc. using the Authorization token in Header tab, you should have successful responses.
4 . Now login with the simple user (who had no ROLE_ADMIN role) credentials and copy the token
5 . Using Authorization token in Header tab, all POST and GET methods should be successful except the GET request for search route!
http://localhost:9000/api/routes/search/routesByName?name=Rex-Adidogome
We got a Forbidden error stating that the logged-in user is not allowed to query this method.
In the next post, we will be adding new features to our EasyTrans app. Hope you learned something new. If so, don't forget to hit the Like button and subscribe to this blog to be up to date with new posts.