Reputation: 300
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
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:
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();
}
}
...
}
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 {
...
}
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
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
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
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