Dave Roberts
Dave Roberts

Reputation: 300

Spring WebMvcTest with post returns 403

I'm wondering where the issue is with my code, every time I run a post test (irrespective of what controller it targets, or method), I return a 403 error, when in some cases I expect a 401, and in others a 200 response (with auth).

This is a snippet from my controller:

@RestController
@CrossOrigin("*")
@RequestMapping("/user")
class UserController @Autowired constructor(val userRepository: UserRepository) {
    @PostMapping("/create")
    fun addUser(@RequestBody user: User): ResponseEntity<User> {
        return ResponseEntity.ok(userRepository.save(user))
    }
}

And my unit test targeting this controller

@RunWith(SpringRunner::class)
@WebMvcTest(UserController::class)
class UserControllerTests {
    @Autowired
    val mvc: MockMvc? = null

    @MockBean
    val repository: UserRepository? = null

    val userCollection = mutableListOf<BioRiskUser>()

    @Test
    fun testAddUserNoAuth() {
        val user = BioRiskUser(
                0L,
                "user",
                "password",
                mutableListOf(Role(
                    0L,
                    "administrator"
                )))
        repository!!
        `when`(repository.save(user)).thenReturn(createUser(user))
        mvc!!
        mvc.perform(post("/create"))
                .andExpect(status().isUnauthorized)
    }

    private fun createUser(user: BioRiskUser): BioRiskUser? {
        user.id=userCollection.count().toLong()
        userCollection.add(user)
        return user
    }
}

What am I missing?

As requested, my security config...

@Configuration
@EnableWebSecurity
class SecurityConfig(private val userRepository: UserRepository, private val userDetailsService: UserDetailsService) : WebSecurityConfigurerAdapter() {
    @Bean
    override fun authenticationManagerBean(): AuthenticationManager {
        return super.authenticationManagerBean()
    }

    override fun configure(auth: AuthenticationManagerBuilder) {
        auth.authenticationProvider(authProvider())
    }

    override fun configure(http: HttpSecurity) {
        http
            .csrf().disable()
            .cors()
            .and()
            .httpBasic()
            .realmName("App Realm")
            .and()
            .authorizeRequests()
            .antMatchers("/img/*", "/error", "/favicon.ico", "/doc")
            .anonymous()
            .anyRequest().authenticated()
            .and()
            .logout()
            .invalidateHttpSession(true)
            .clearAuthentication(true)
            .logoutSuccessUrl("/user")
            .permitAll()
    }

    @Bean
    fun authProvider(): DaoAuthenticationProvider {
        val authProvider = CustomAuthProvider(userRepository)
        authProvider.setUserDetailsService(userDetailsService)
        authProvider.setPasswordEncoder(encoder())
        return authProvider
    }
}

and the auth provider

class CustomAuthProvider constructor(val userRepository: UserRepository) : DaoAuthenticationProvider() {
    override fun authenticate(authentication: Authentication?): Authentication {
        authentication!!
        val user = userRepository.findByUsername(authentication.name)
        if (!user.isPresent) {
            throw BadCredentialsException("Invalid username or password")
        }
        val result = super.authenticate(authentication)
        return UsernamePasswordAuthenticationToken(user, result.credentials, result.authorities)
    }


    override fun supports(authentication: Class<*>?): Boolean {
        return authentication?.equals(UsernamePasswordAuthenticationToken::class.java) ?: false
    }
}

Upvotes: 14

Views: 15105

Answers (4)

Anthony Raymond
Anthony Raymond

Reputation: 7872

Your problem comes from the CSRF, if you enable debug logging the problem will become obvious, and it comes from the fact that @WebMvcTest load only the web layer and not the whole context, your KeycloakWebSecurityConfigurerAdapter is not loaded.

The loaded config comes from org.springframework.boot.autoconfigure.security.servlet.DefaultConfigurerAdapter (= to org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter

WebSecurityConfigurerAdapter contains crsf().

As of today you have 3 options to resolve this:

Options 1

Create a WebSecurityConfigurerAdapter inside your test class.

The solution suits you if you have only few @WebMvcTest annotated class in your project.

@ExtendWith(SpringExtension.class)
@WebMvcTest(controllers = {MyController.class})
public class MyControllerTest {

    @TestConfiguration
    static class DefaultConfigWithoutCsrf extends WebSecurityConfigurerAdapter {
        @Override
        protected void configure(final HttpSecurity http) throws Exception {
            super.configure(http);
            http.csrf().disable();
        }
    }
    ...
}

Options 2

Create a WebSecurityConfigurerAdapter inside a superclass and make your test extend from it.

The solution suits you if you have multiple @WebMvcTest annotated class in your project.

@Import(WebMvcTestWithoutCsrf.DefaultConfigWithoutCsrf.class)
public interface WebMvcCsrfDisabler {

    static class DefaultConfigWithoutCsrf extends WebSecurityConfigurerAdapter {
        @Override
        protected void configure(final HttpSecurity http) throws Exception {
            super.configure(http);
            http.csrf().disable();
        }
    }
}
@ExtendWith(SpringExtension.class)
@WebMvcTest(controllers = {MyControllerTest .class})
public class MyControllerTest implements WebMvcCsrfDisabler {
    ...
}

Options 3

Use the spring-security csrf SecurityMockMvcRequestPostProcessors.

This solution is bulky and prone to error, checking for permission denial and forgeting the with(csrf()) will result in false positive test.

@ExtendWith(SpringExtension.class)
@WebMvcTest(controllers = {MyController.class})
public class MyControllerTest {
    ...
    @Test
    public void myTest() {
        mvc.perform(post("/path")
                .with(csrf()) // <=== THIS IS THE PART THAT FIX CSRF ISSUE
                .content(...)
                
        )
                .andExpect(...);
    }
}

Upvotes: 5

Dirk
Dirk

Reputation: 1194

In my case, the csrf-Protection seems to be still active in my WebMvcTest (even if disabled in your configuration).

So to workaround this, I simply changed my WebMvcTest to something like:

    @Test
    public void testFoo() throws Exception {

        MvcResult result = mvc.perform(
                    post("/foo").with(csrf()))
                .andExpect(status().isOk())
                .andReturn();

        // ...
    }

So the missing .with(csrf()) was the problem in my case.

Upvotes: 23

Branislav Lazic
Branislav Lazic

Reputation: 14806

Here's an issue:

override fun configure(http: HttpSecurity) {
    http
        .csrf().disable()
        .cors()
        .and()
        .httpBasic()
        .realmName("App Realm")
        .and()
        .authorizeRequests()
        .antMatchers("/img/*", "/error", "/favicon.ico", "/doc")
        .anonymous()
        .anyRequest().authenticated()
        .and()
        .logout()
        .invalidateHttpSession(true)
        .clearAuthentication(true)
        .logoutSuccessUrl("/user")
        .permitAll()
}

More particularly here:

.anyRequest().authenticated()

You're requiring for each request to be authenticated, therefore you get 403.

This tutorial explains well how to perform testing with mock user.

The easy way is to have something like this:

@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
class SecuredControllerRestTemplateIntegrationTest {

    @Autowired
    private val template: TestRestTemplate

    @Test
    fun createUser(): Unit {
        val result = template.withBasicAuth("username", "password")
          .postForObject("/user/create", HttpEntity(User(...)), User.class)
        assertEquals(HttpStatus.OK, result.getStatusCode())
    }
}

Upvotes: 0

Kushagra Goyal
Kushagra Goyal

Reputation: 272

You need to add @ContextConfiguration(classes=SecurityConfig.class) to the top of your UserControllerTests class after the @WebMvcTest(UserController::class) annotation.

Upvotes: 9

Related Questions