Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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
    }
}
like image 417
Dave Roberts Avatar asked Oct 25 '18 16:10

Dave Roberts


3 Answers

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.

like image 106
Dirk Avatar answered Nov 20 '22 11:11

Dirk


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

like image 3
Kushagra Goyal Avatar answered Nov 20 '22 12:11

Kushagra Goyal


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(...);
    }
}
like image 1
Anthony Raymond Avatar answered Nov 20 '22 10:11

Anthony Raymond