Implementing Spring Security Across Microservices
I wanted to make this brief explanation for those who are new to microservices and Spring security and would like to leverage an existing security microservice across an entire microservices app.
For those who are unfamiliar with microservices, microservices are a way of modularizing the functionality of an application to improve its stability and scalability. In this example, I have two microservices: one that handles security and one that handles requests to the mock-store I’ve configured in my database.
Say, for instance, I have a security app setup with an AuthFilter that checks for the existence and validity of a JWT and a SecurityConfig that differentiates between different API requests like so:
AuthFilter
@Component
@RequiredArgsConstructor
public class JwtAuthFilter extends OncePerRequestFilter {
private final JwtService jwtService;
private final UserDetailsService userDetailsService;
@Override
protected void doFilterInternal(
@NonNull HttpServletRequest request,
@NonNull HttpServletResponse response,
@NonNull FilterChain filterChain
) throws ServletException, IOException {
final String authHeader = request.getHeader("Authorization");
final String jwtToken;
final String email;
if(authHeader == null || !authHeader.startsWith("Bearer ")){
filterChain.doFilter(request, response);
return;
}
jwtToken = authHeader.substring(7);
email = jwtService.extractUsername(jwtToken);
if(email != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = this.userDetailsService.loadUserByUsername(email);
if(jwtService.isTokenValid(jwtToken, userDetails)) {
UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(
userDetails,
null,
userDetails.getAuthorities()
);
authToken.setDetails(
new WebAuthenticationDetailsSource().buildDetails(request)
);
SecurityContextHolder.getContext().setAuthentication(authToken);
filterChain.doFilter(request, response);
}
}
}
}
SecurityConfig
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {
private final JwtAuthFilter jwtAuthFilter;
private final AuthenticationProvider authenticationProvider;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf()
.disable()
.authorizeHttpRequests()
.requestMatchers("/api/auth/permitAll/**")
.permitAll()
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authenticationProvider(authenticationProvider)
.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
}
Currently the SecurityConfig is setup to permit all requests to the "/api/auth/permitAll/**" url. This makes sense when we consider that this app is currently only setup with two APIs - one to register new users and one to log a user in. Neither of these APIs should require authentication because they are the end-user's first interaction with the app.
Now let's consider a second microservice that handles requests to a store's inventory. We want the APIs for this service to be accessed only by authenticated users and we need to find a way to leverage our existing security app to filter requests made to the store service. What we can do is create a protected endpoint within our security app and an AuthFilter within our store service that sends our API calls to this protected endpoint along with the user's credentials and JWT in order to validate the API call.
First, let's look at the controller in the security service
Recommended by LinkedIn
@RestController
@RequestMapping("/api/auth")
@RequiredArgsConstructor
public class AuthenticationController {
private final AuthenticationService service;
@PostMapping("/permitAll/register")
public RegisterResponse register(
@RequestBody RegisterRequest request
) {
return service.register(request);
}
@PostMapping("/permitAll/authenticate")
public AuthenticationResponse login(
@RequestBody AuthenticationRequest request
) {
return service.authenticate(request);
}
}
Currently, this controller only has the two non-protected endpoints I mentioned earlier. Let's add an endpoint that doesn't match the URL configured in the "permitAll" section of the SecurityConfig
@RestController
@RequestMapping("/api/auth")
@RequiredArgsConstructor
public class AuthenticationController {
private final AuthenticationService service;
@PostMapping("/permitAll/register")
public RegisterResponse register(
@RequestBody RegisterRequest request
) {
return service.register(request);
}
@PostMapping("/permitAll/authenticate")
public AuthenticationResponse login(
@RequestBody AuthenticationRequest request
) {
return service.authenticate(request);
}
@GetMapping("/validate")
public void validate() {
}
}
And then we need to change the SecurityConfig to account for this new URL
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {
private final JwtAuthFilter jwtAuthFilter;
private final AuthenticationProvider authenticationProvider;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf()
.disable()
.authorizeHttpRequests()
.requestMatchers("/api/auth/permitAll/**")
.permitAll()
.and()
.authorizeHttpRequests()
.requestMatchers("/api/auth/validate")
.authenticated()
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authenticationProvider(authenticationProvider)
.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
}
Now that we have a protected URL in our security service, we need to create a new AuthFilter that makes a call to our security service
@Component
@RequiredArgsConstructor
public class AuthFilter extends OncePerRequestFilter {
private final String AUTH_URL = "http://localhost:8080/api/auth/validate";
private final RestTemplate restTemplate = new RestTemplate();
@Override
protected void doFilterInternal(
HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain
) throws ServletException, IOException {
final String authHeader = request.getHeader("Authorization");
final String jwtToken;
if(authHeader == null || !authHeader.startsWith("Bearer ")){
response.setStatus(403);
return;
}
jwtToken = authHeader.substring(7);
HttpHeaders headers = new HttpHeaders();
headers.setBearerAuth(jwtToken);
HttpEntity<Void> entity = new HttpEntity<>(headers);
try{
ResponseEntity<Void> authResponse = restTemplate.exchange(AUTH_URL, HttpMethod.GET, entity, Void.class);
if(authResponse.getStatusCode().is2xxSuccessful()) {
filterChain.doFilter(request, response);
}
} catch (HttpClientErrorException e) {
response.setStatus(403);
return;
}
}
}
Let's break down what this AuthFilter is doing. The first thing to point out is the URL variable that holds the URL for the protected endpoint in our security service. (in a real app, you will likely store this as an environment variable as opposed to a local variable for security reasons) The filter takes the request, response and filterchain that are managed under the hood by Spring and uses them to parse the JWT. If no JWT is found, the response status is set to 403 (forbidden) and immediately returns out of the API call. Once a JWT has been found it is parsed and used in an HttpEntity for the API call that is made to our security service. Over in the security service, a request is received from the store service with user credentials and JWT. This request is immediately sent to the AuthFilter in the security service where the user credentials and JWT are checked for their validity before sending back a response to the store service. The last lines of the store service's AuthFilter check the response status code and either fulfills or denies the request.
I hope this was a good tutorial. Like and share if you found this helpful.