I have some strange issue with my old tests after adding spring security configuration.
So, let's start from beginning to my already existing project I have added Spring Security and JWT Filter. The reason for that is that every request from UI, we are expecting JWT token and what we want to allow only users with given roles to access given endpoints.
Configuration code:
@Configuration
@EnableWebSecurity
public class SecurityConfig {
private final UserInfoHeaderFilter userInfoHeaderFilter;
public SecurityConfig(UserInfoHeaderFilter userInfoHeaderFilter) {
this.userInfoHeaderFilter = userInfoHeaderFilter;
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http
.addFilterBefore(userInfoHeaderFilter, BasicAuthenticationFilter.class)
.authorizeHttpRequests(auth -> auth
.requestMatchers("/swagger-ui/**", "/v3/api-docs/**", "/swagger-resources/**").permitAll()
.anyRequest().hasAnyAuthority("SUPER_ADMIN")
)
.csrf().disable()
.build();
}
}
just for example, I have changed it to have one role for each request.
JwtFilter code:
@Slf4j
@Component
public class UserInfoHeaderFilter extends OncePerRequestFilter {
public static final String HEADER_NAME = "Authorization";
private static final String NO_USER = "NO USER";
@Override
protected void doFilterInternal(HttpServletRequest request, @NotNull HttpServletResponse response, @NotNull FilterChain filterChain) throws ServletException, IOException {
String headerValue = request.getHeader(HEADER_NAME);
if (headerValue != null && headerValue.startsWith("Bearer ")) {
setAuthenticationInSecurityContext(headerValue.substring(7));
}
filterChain.doFilter(request, response);
}
private String setAuthenticationInSecurityContext(String token) {
try {
DecodedJWT jwt = JWT.decode(token);
String username = jwt.getClaim("preferred_username").asString();
List<String> roles = (List<String>) jwt.getClaim("realm_access").asMap().get("roles");
List<GrantedAuthority> authorities = roles.stream()
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList());
Authentication authentication = new UsernamePasswordAuthenticationToken(username, null, authorities);
SecurityContextHolder.getContext().setAuthentication(authentication);
log.debug("User [{}] has been logged", username);
return username;
} catch (JWTDecodeException e) {
log.error("Failed to decode JWT", e);
return NO_USER;
}
}
}
As you can probably see, in this filter we are only getting roles/username from jwt and putting them in securityContext. So no verification of jwt is needed.
And locally everything seems to be working correctly. But integration tests are failing (I have added to restTemplate interceptor, which will add every request correct jwt token)
This is simplified test, which is using TestRestTemplate
@BeforeEach
void setup() {
ClientHttpRequestInterceptor interceptor = (request, body, execution) -> {
request.getHeaders().add("Authorization", BEARER + TEST_JWT_TOKEN);
return execution.execute(request, body);
};
restTemplate.getRestTemplate().setInterceptors(Collections.singletonList(interceptor));
}
@Test
void stupidTest() {
restTemplate.execute(uri("/books"), HttpMethod.GET, req -> {
}, res -> {
Assertions.assertTrue(res.getStatusCode().is2xxSuccessful());
return IOUtils.toString(res.getBody(), StandardCharsets.UTF_8);
});
//Thread.sleep(10000);
AuthorRequest payload = AuthorRequest.builder()
.name("TEST")
.labels(Collections.emptySet())
.build();
restTemplate.postForEntity(uri("/author"), payload, String.class);
}
It is failing when second request is sent. When debugging code, it is never entering author endpoint nor filter for second time, but when adding Thread.sleep(1000); between those two calls it starts working correctly.
In logs I can see
2023-07-21 15:45:20 5.14.0 SEVERE Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Unable to handle the Spring Security Exception because the response is already committed.] with root cause org.springframework.security.access.AccessDeniedException: Access Denied
2023-07-21 15:45:20 5.14.0 SEVERE Exception Processing ErrorPage[errorCode=0, location=/error] jakarta.servlet.ServletException: Unable to handle the Spring Security Exception because the response is already committed.
I/O error on POST request for "http://localhost:61916/v1/author": localhost:61916 failed to respond org.springframework.web.client.ResourceAccessException: I/O error on POST request for "http://localhost:61916/v1/author": localhost:61916 failed to respond
Which seems to be saying that second request was denied, but even though why it is working correctly, with sleep? Hmm. Removing Spring Security Configuration make test works again.
Seems issue of parallel call between asynchrone call
restTemplate.execute(uri("/books"), HttpMethod.GET, req -> {
}, res -> {
Assertions.assertTrue(res.getStatusCode().is2xxSuccessful());
return IOUtils.toString(res.getBody(), StandardCharsets.UTF_8);
});
and next call which is execute in parallel
restTemplate.postForEntity(uri("/author"), payload, String.class);
Why not use restTemplate.getForEntity the 1st time to avoid parallel call and this issue of TestRestTemplate ?
But in fact to really analyse need all the Test class, especially the initialisation of TestRestTemplate
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With