MockMvc seems to be clear SecurityContext after performing request (java.lang.IllegalArgumentException: Authentication object cannot be null)
Asked Answered
O

5

6

I'm trying to run some integration test using SpringBoot + Spring Data Mongo + SpringMVC

I've simplified and generified the code but it should be able to reproduce the behavior with the following test.

As you can see from BookRepository interface I want the user to be able to retrieve only the books that he owns (@Query("{ 'ownerName' : '?#{principal?.username})) and I'm writing a test to perform a POST to save a Book and then verify the book has the owner set appropriately.

For the purpose of the question here I've simplified the test to just to a GET and then calling findAll()

Problem

After performing any MockMvc request, the SecurityContext is cleared using ThreadLocalSecurityContextHolderStrategy#clearContext() which cause the following exception to be thrown when I try to call repository.findAll();

java.lang.IllegalArgumentException: Authentication object cannot be null

BookRepository.java

@RepositoryRestResource
public interface BookRepository extends MongoRepository<Book, String> {
      
    @Query("{ 'ownerName' : ?#{principal?.username} }")
    List<Book> findAll();  
 
}

BookCustomRepositoryIntegrationTest.java

/**
 * Integrate data mongo + mvc
 */
@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureMockMvc
public class BookCustomRepositoryIntegrationTest {
    
    @Autowired
    BookRepository repository;

    @Autowired
    MockMvc mockMvc;  

    @Test
    @WithMockUser
    public void reproduceBug() throws Exception {

        repository.findAll(); //Runs allright

        mockMvc.perform(get("/books")
                .contentType(APPLICATION_JSON_UTF8))
                .andExpect(status().isOk());

        repository.findAll(); //Throws exception: java.lang.IllegalArgumentException: Authentication object cannot be null


    }

}
Outandout answered 31/7, 2018 at 21:25 Comment(0)
U
11

Your case does not work, because SecurityContextPersistenceFilter and FilterChainProxy filters clear SecurityContextHolder, but the TestSecurityContextHolder (filled by WithSecurityContextTestExecutionListener) still contains SecurityContext.

Try this approach:

@Test
@WithMockUser
public void reproduceBug() throws Exception {
    repository.findAll();
    mockMvc.perform(get("/books")
            .contentType(APPLICATION_JSON_UTF8))
            .andExpect(status().isOk());
    SecurityContextHolder.setContext(TestSecurityContextHolder.getContext());
    repository.findAll();
}
Undermost answered 17/8, 2018 at 16:18 Comment(2)
Hi @seregamorph, than you for your suggestion. This is the best workaround I've seen so far, so I'll upvote it. However, I'm not convinced that manually setting the security context after each interaction with MockMvc makes for a very readable test so I wouldn't consider it a definitive solution.Outandout
I used this workaround as well, as I am maintaining a huge codebase with many tests that look like that.Mattock
Q
2

I just now found a nice solution to this problem. You can register a MockMvcBuilderCustomizer bean in your test configuration and all works fine.

public class MockMvcTestSecurityContextPropagationCustomizer implements MockMvcBuilderCustomizer {

@Override
public void customize(ConfigurableMockMvcBuilder<?> builder) {
    builder.alwaysDo(result -> {
        log.debug("resetting SecurityContextHolder to TestSecurityContextHolder");
        SecurityContextHolder.setContext(TestSecurityContextHolder.getContext());
    });
}

}

[spring-boot]

Quadrate answered 31/3, 2021 at 7:47 Comment(0)
R
2

Inspired by @brass-monkey, as it's the simplest solution, here is the code that works for me:

    @BeforeEach
    void setUp() {
        mockMvc = MockMvcBuilders
                .webAppContextSetup(applicationContext)
                .apply(springSecurity())
                .alwaysDo(result -> SecurityContextHolder.setContext(TestSecurityContextHolder.getContext()))
                .build();
    }
Reredos answered 21/6, 2023 at 13:1 Comment(0)
D
1

a good practice would be to separate the tests with the RestController from the tests of the other layers of the application. In your case i think you should not mix rest calls tests with respositories calls tests ..

i guess you should make a mock mvc call to test your "repository.findAll()"

I'm working with Spring Boot 2.6.7 ( spring-security-test-5.6.3.jar ) and Junit 5 and the TestSecurityContextHolder holds the security context even after performing a MockMVC http request.

@ExtendWith(SpringExtension.class)
@SpringBootTest(classes = MyApp.class, webEnvironment = WebEnvironment.RANDOM_PORT)
@AutoConfigureMockMvc
public class MyTests {

...

    @Test
    @WithMockUser
    public void test_make_many_mockmvc_calls() {
    
            //The first call
            mockMvc.perform(MockMvcRequestBuilders.get("/books")//
                .contentType(MediaType.APPLICATION_JSON)//
                .accept(MediaType.APPLICATION_JSON))//
                .andExpect(MockMvcResultMatchers.status().isOk());
    
            //Another call
            mockMvc.perform(MockMvcRequestBuilders.get("/books")//
                .contentType(MediaType.APPLICATION_JSON)//
                .accept(MediaType.APPLICATION_JSON))//
                .andExpect(MockMvcResultMatchers.status().isOk());
    }
...
}

but if you wont a good workaround u need to add some stuff.. in fact your repository.findAll() will resolve security informations using the SecurityEvaluationContextExtension witch use the main SecurityContextHolder ( not the TestSecurityContextHolder ).

So i guess you should make something like this :

public class MyCustomSecurityEvaluationContextExtension implements EvaluationContextExtension {
...
    @Override
    public SecurityExpressionRoot getRootObject() {
        
        //Override the way you retrieve the Authentication object
    }
...
}
Domeniga answered 2/10, 2022 at 14:51 Comment(0)
L
0

I think instead of using the AutoConfigureMockMvc annotation you could configure MockMvc manually and configure Spring security as follows:

import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.*;

@RunWith(SpringRunner.class)
@SpringBootTest
public class BookCustomRepositoryIntegrationTest {

    @Before
    public void setup() {
        mockMvc = MockMvcBuilders
                .webAppContextSetup(context)
                .apply(springSecurity()) 1
                .build();
    }
    // ...
}

As the documentation states:

In order to use Spring Security with Spring MVC Test it is necessary to add the Spring Security FilterChainProxy as a Filter. It is also necessary to add Spring Security’s TestSecurityContextHolderPostProcessor to support Running as a User in Spring MVC Test with Annotations. This can be done using Spring Security’s SecurityMockMvcConfigurers.springSecurity().

Lipocaic answered 31/7, 2018 at 21:54 Comment(1)
The whole security filter chain is there, in fact the authentication context is there when I call repository.findAll(). But MockMvc for some reason cleans it and that makes a perfectly reasonable test case not possible to execute. Anyway, I tried your approach but I get the same error. Thanks a lot for trying to helpOutandout

© 2022 - 2024 — McMap. All rights reserved.