How to test http status code 401 (unauthenticated) with MockMVC and Spring Boot OAuth2 Resource Server?

674 views Asked by At

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;
    }
}
1

There are 1 answers

0
ch4mp On BEST ANSWER

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.