I am currently developing a Spring Boot 3 application which provides a REST API. To consume this API, users have to be authenticated via an OAuth2 workflow of our identity provider keycloak. Therefore, I have used org.springframework.boot:spring-boot-starter-oauth2-resource-server. When I run the application, authentification and authorization works as expected.
Unfortunately, I am unable to write a WebMvcTest for the use case when the user does not provide a JWT for authentification. In this case I expect a HTTP response with status code 401 (unauthenticated) but I get status code 403 (forbidden). Is this event possible because MockMvc mocks parts of the response processing?
I have successfully written test cases for the following to use cases.
- The user provides a JWT with the expected claim => I expect status code 200 ✔
- The user provides a JWT without the expected claim => I expect status code 403 ✔
I have tried to follow everything from the Spring Security documentation: https://docs.spring.io/spring-security/reference/servlet/test/index.html
Here is my code.
@WebMvcTest(CustomerController.class)
@ImportAutoConfiguration(classes = {RequestInformationExtractor.class})
@ContextConfiguration(classes = SecurityConfiguration.class)
@Import({TestConfiguration.class, CustomerController.class})
public class PartnerControllerTest {
@Autowired
private WebApplicationContext context;
private MockMvc mockMvc;
@BeforeEach
public void setup() {
mockMvc = MockMvcBuilders
.webAppContextSetup(context)
.apply(springSecurity())
.build();
}
// runs successfully
@Test
void shouldReturnListOfCustomers() throws Exception {
mockMvc.perform(
post("/search")
.contentType(MediaType.APPLICATION_JSON)
.content("{" +
"\"searchKeyword\": \"Mustermann\"" +
"}")
.with(jwt()
.authorities(
new SimpleGrantedAuthority("basic")
)))
.andExpect(status().isOk());
}
// fails: expect 401 but got 403
@Test
void shouldReturn401WithoutJwt() throws Exception {
mockMvc.perform(
post("/search")
.contentType(MediaType.APPLICATION_JSON)
.content("{" +
"\"searchKeyword\": \"Mustermann\"" +
"}"))
.andExpect(status().isUnauthorized());
}
// runs successfully
@Test
void shouldReturn403() throws Exception {
mockMvc.perform(
post("/search")
.contentType(MediaType.APPLICATION_JSON)
.content("{" +
"\"searchKeyword\": \"Mustermann\"" +
"}")
.with(jwt()))
.andExpect(status().isForbidden());
}
}
@org.springframework.boot.test.context.TestConfiguration
public class TestConfiguration {
@Bean
public JwtDecoder jwtDecoder() {
SecretKey secretKey = new SecretKeySpec("dasdasdasdfgsg9423942342394239492349fsd9fsd9fsdfjkldasd".getBytes(), JWSAlgorithm.HS256.getName());
NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withSecretKey(secretKey).build();
return jwtDecoder;
}
}
@Configuration
@EnableWebSecurity
public class SecurityConfiguration {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.sessionManagement(sessionManagement -> sessionManagement.sessionCreationPolicy(STATELESS))
.authorizeHttpRequests((authz) -> authz
.requestMatchers("/actuator/**").permitAll()
.anyRequest().hasAuthority("Basic")
)
.oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt);
return http.build();
}
@Bean
public JwtAuthenticationConverter jwtAuthenticationConverter() {
JwtGrantedAuthoritiesConverter grantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter();
grantedAuthoritiesConverter.setAuthoritiesClaimName("groups");
grantedAuthoritiesConverter.setAuthorityPrefix("");
JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();
jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(grantedAuthoritiesConverter);
return jwtAuthenticationConverter;
}
}