Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

@WithMockUser doesn't pick Spring Security auth credentials

I have setup basic authentication in my controller with Spring Security in the classic way as follows:

@EnableWebSecurity
@EnableGlobalMethodSecurity
public class MySecurityConfig extends WebSecurityConfigurerAdapter {


    @Autowired
    public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication()
                .withUser("user").password("user").roles("USER")
                .and()
                .withUser("admin").password("admin").roles("USER", "ADMIN");
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers(....);
    }
}

When it comes to the point of testing, I am using @WithMockUser to annotate my tests. A test might look like this for GET:

@RunWith(SpringRunner.class)
@WebMvcTest(controllers = SomeController.class)
public class SomeControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @Test
    public void test1() {
                  mockMvc.perform(get(...)).andExpect(...);
    }

or like this for POST:

@RunWith(SpringRunner.class)
@WebMvcTest(controllers = SomeController.class)
public class SomeControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @Test
    public void test1() {
                  mockMvc.perform(post(...)).andExpect(...);
    }

then something unexpected happens:

  • when a test method is not annotated with @WithMockUser, it fails because of 401 status (Unauthorized) which is reasonable, because no basic authentication has been fullfilled
  • when a test method is simply annotated with an empty @WithMockUser without specifying ANY credentials, it starts passing, which is not reasonable, because I did not provide the correct data for authentication (rather I left them empty)
  • at this point a test method is passing also when filling in some correct credentials like in @WithMockUser(username = "user", password = "user", roles = "USER")

QUESTION: what's going on? How to fix this misbehaviour?

Looks like Spring Security is activated, however my testing is not using the authentication that I would expect to be used. Do I have to mock the authentication data myself?

EDIT The full configure method is the following

@Override
protected void configure(HttpSecurity http) throws Exception {
    http.authorizeRequests()
            .antMatchers(URL1).hasAnyRole(ROLE_ADMIN)
            .antMatchers(URL2).hasAnyRole(ROLE_USER)
            .antMatchers(URL3).permitAll()
            .and()
            .httpBasic()
            .and()
            .csrf().disable();
}
like image 694
Johan Avatar asked Dec 01 '17 15:12

Johan


People also ask

How do I authenticate a password in Spring Security?

Simply put, Spring Security hold the principal information of each authenticated user in a ThreadLocal – represented as an Authentication object. In order to construct and set this Authentication object – we need to use the same approach Spring Security typically uses to build the object on a standard authentication.


1 Answers

There are two reasons behind this behavior:

  1. @WithMockUser annotation is not intended to execute authentication. It creates a user which is authenticated already. By default his credentials are user : password
  2. @WebMvcTest does not execute MySecurityConfig.java. This annotation creates Spring mockMvc object with Security defaults for testing. Those security defaults are applied by org.springframework.boot.test.autoconfigure.web.servlet.MockMvcSecurityAutoConfiguration You can double check this by putting break points on MySecurityConfig methods and rerunning your test in debug mode. Break points are not hit.

Solving issue 1

Simply change your approach to what @WithMockUser annotation does. It gives already logged-in user. It is still possible to test urls security and roles configuration with specifying concrete username, password and roles.

Solving issue 2
Create a base class for all Integration tests. It will configure mockMvc with Spring Security applied. Also note @SpringBootTest annotation. Now test will use MySecurityConfig.java

import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity;
import static org.springframework.test.web.servlet.setup.MockMvcBuilders.webAppContextSetup;

import org.junit.Before;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.security.web.FilterChainProxy;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.web.context.WebApplicationContext;

@RunWith(SpringRunner.class)
@SpringBootTest
public abstract class IT {
    @Autowired
    protected WebApplicationContext wac;

    @Autowired
    private FilterChainProxy springSecurityFilterChain;

    protected MockMvc mockMvc;

    @Before
    public void applySecurity() {
        this.mockMvc = webAppContextSetup(wac)
            .apply(springSecurity(springSecurityFilterChain))
            .build();
    }
}

Rewrite the test like this. Assuming you use http basic authentication. Credentials are provided inside the test. Note: no mock user annotation.

package com.example.demo;

import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.httpBasic;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;

import org.junit.Test;
import org.springframework.test.web.servlet.result.MockMvcResultMatchers;

public class SomeControllerIT extends IT {

  @Test
  public void test1() throws Exception {
    mockMvc.perform(get("/some")
        .with(httpBasic("user", "user")))
        .andExpect(MockMvcResultMatchers.content().string("hello"));
  }
}
like image 178
Etruskas Avatar answered Nov 17 '22 08:11

Etruskas