0

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;
    }
}
dur
  • 15,689
  • 25
  • 79
  • 125
LucasMoody
  • 403
  • 4
  • 10
  • 1
    What is your security configuration? And what is the exact content of the error response? You could face another error due to CORS or CSRF or whatever exception mapped to 403. – ch4mp Jan 04 '23 at 19:23
  • 1
    Also, you might find my [tutorials](https://github.com/ch4mpy/spring-addons/tree/master/samples/tutorials) and [samples](https://github.com/ch4mpy/spring-addons/tree/master/samples) usefull. All controller tests cover 200, 401 and 403 – ch4mp Jan 04 '23 at 19:27
  • Thank you @ch4mp for the tutorials and samples. I will look right into them. In the meantime, I already shared my security configuration in the code snippits. Unfortunately, I am not seeing any error in the test at all. Just that the response code is 403 instead of the expected 401. – LucasMoody Jan 05 '23 at 11:41
  • Hi @ch4mp, It worked after disabling csrf in the SecurityConfiguration. I thought that I already tried that but I must have done with a different configuration. Thank you so much. Would you like to write an answer to get the Stackoverflow points? Otherwise, I would do it. Thanks for you tutorials as well! – LucasMoody Jan 09 '23 at 10:17

1 Answers1

2

You probably have a 403 because an exception is thrown before access control is evaluated (CORS or CSRF or something).

For instance, in your security configuration, you disable sessions (session-creation policy to stateless) but not CSRF protection. Either disable CSRF in your conf (you can because CSRF attacks use sessions) or use MockMvc csrf() post-processor in your tests.

I have many demos of resource-servers with security configuration and tests (unit and integration) in my samples and tutorials. Most have references to my test annotations and boot starters (which enable to define almost all security conf from properties without Java conf), but this one is using nothing from my extensions. You should find useful tips for your security conf and tests there.

ch4mp
  • 6,622
  • 6
  • 29
  • 49