JJ Zabkar
JJ Zabkar

Reputation: 3689

Spring Security HttpSecurity Configuration Testing

I have a Spring Boot + Spring Security application that has severalantMatchers paths; some fullyAuthenticated(), some permitAll().

How to I write a test that verifies SecurityConfiguration has my endpoints under /api/** (and ultimately others) secured correctly?

public class SecurityConfiguration extends WebSecurityConfigurerAdapter {

    protected void configure(HttpSecurity http) throws Exception {
        http
            //...
            .antMatchers("/api/**").fullyAuthenticated()
    }

}

Using spring-boot-1.5.2.RELEASE, spring-security-core-4.2.2-release.

Clarification1: I want to as-directly-as-possible test the SecurityConfiguration, as opposed to transitively testing via one of the /api/** endpoints, which may have their own @PreAuthorize security.

Clarification2: I would like something similar to this WebSecurityConfigurerAdapterTests.

Clarification3: I would like to @Autowire something at the Spring Security layer, ideally HttpSecurity, to test.

Upvotes: 23

Views: 24658

Answers (6)

ch4mp
ch4mp

Reputation: 12659

The configuration may contain only part of the access control: you could as well have method security (@PreAuthorize, @PostFilter and alike) on any kind of component.

So it is actually a good idea to test access control rules with the components having security rules (and not just the conf in isolation or components without security):

  • @Controller endpoints with security in conf, method security or both
  • any other kind of @Component (@Service, @Repository or whatever) with method security

There is no need to change your security configuration to test access control. Just:

  • ensure that the runtime security configuration is imported (nothing to do in a @SpringBootTest)
  • mock or stub the Authentication in the test security context

For the authentication stubbing, Spring Security Test comes with some test annotations, MockMvc post-processors and WebTestClient mutators. I detailed usage in this Bealdung article.

You can also create an instance or mock of the Authentication implementation of your choice and manually set the test security context with it: TestSecurityContextHolder.getContext().setAuthentication(...)

Upvotes: 0

Klaus Groenbaek
Klaus Groenbaek

Reputation: 5035

So you want to ensure that if someone changes .antMatchers("/api/**") to .antMatchers("/WRONG_PATH/**") then you have a test that will figure it out ?

The rules you define using HttpSecurity will end up configuring a FilterChainProxy with one or more SecurityFilterChain, each with a list of filters. Each filter, such as UsernamePasswordAuthenticationFilter (used for form-based login), will have a RequestMatcher defined in the super class AbstractAuthenticationProcessingFilter. The problem is that RequestMatcher is an interface which currently have 12 different implementations, and this includes AndRequestMatcher and OrRequestMatcher, so the matching logic is not always simple. And most importantly RequestMatcher only has one method boolean matches(HttpServletRequest request), and the implementation often does not expose the configuration, so you will have to use reflection to access the private configurations of each RequestMatcher implementation (which could change in the future).

If you go down this path, and autowire FilterChainProxy into a test and use reflection to reverse-engineer the configuration, you have to consider all the implementation dependencies you have. For instance WebSecurityConfigurerAdapter has a default list of filters, which may change between releases, and unless disable it, and when it is disabled you have to define every filter explicitly. In addition new filters and RequestMatchers could be added over time, or the filter chain generated by HttpSecurity in one version of Spring Security may be slightly different in the next version (maybe not likely, but still possible).

Writing a generic test for your spring security configuration, is technically possible, but it is not exactly an easy thing to do, and the Spring Security filters certainly were not designed to support this. I have worked extensively with Spring Security since 2010, and I have never had the need for such a test, and personally I think it would be a waste of time trying to implement it. I think the time will be much better spent writing a test framework that makes it easy to write integration tests, which will implicitly test the security layer as well as the business logic.

Upvotes: 6

theINtoy
theINtoy

Reputation: 3668

Thinking outside the box a little, and answering the question in a different way, would it not be easier to simply define a static String[], e.g.

public class SecurityConfiguration extends WebSecurityConfigurerAdapter {

   public static final String[] FULLY_AUTH_PUBLIC_URLS = {"/api/**", "/swagger-resources/**", "/health", "/info" };

    protected void configure(HttpSecurity http) throws Exception {
        http
            //...
            .antMatchers(FULLY_AUTH_PUBLIC_URLS).fullyAuthenticated()
    }
}

...

And then if the purpose of the test is to ensure that no changes are made to the public urls simply test the known list?

The assumption here is that Spring Security works and has been tested so the only thing we are testing for is that the list of public URLs has not been changed. If they have changed a test should fail highlighting to the developer that there are dragons changing these values? I understand this does not cover the clarifications but assuming the supplied static public URLs are known to be accurate then this approach would provide a unit testable back stop if this is needed.

Upvotes: 0

Klaus Groenbaek
Klaus Groenbaek

Reputation: 5035

If you want to programatically know which endpoints exist, you can autowire the List of RequestHandlerProvider into your test and filter them based on the path they are exposed on.

@Autowired
List<RequestHandlerProvider> handlerProviders;

@Test
public void doTest() {
    for (RequestHandlerProvider handlerProvider : handlerProviders) {
        for (RequestHandler requestHandler : handlerProvider.requestHandlers()) {
            for (String pattern : requestHandler.getPatternsCondition().getPatterns()) {
                // call the endpoint without security and check that you get 401
            }
        }
    }
} 

Using the RequestHandlerProvider is how SpringFox determines which endpoint are available and their signature, when it build the swagger definition for an API.

Unless you spend a long time building the correct input for each endpoint you will not get 200 OK back from the endpoint when including a valid security token, so you probably have to accept 400 as a correct response.

If you are already worried some developer would make security related mistakes when introducing a new endpoint, I would be equally worried about the logic of the endpoint, which is why I think you should have an integration test for each of them, and that would test your security as well.

Upvotes: 1

Klaus Groenbaek
Klaus Groenbaek

Reputation: 5035

MockMVC should be enough to verify you security configuration since the only thing it mocks is the Http layer. However if you really wish to test your Spring Boot application, Tomcat server and all, you need to use @SpringBootTest, like this

@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest(webEnvironment= SpringBootTest.WebEnvironment.RANDOM_PORT)
public class NoGoServiceTest {

    @LocalServerPort
    private int port;

    private <T> T makeDepthRequest(NoGoRequest request, NoGoResponse response, String path, Class<T> responseClass) {
        testService.addRequestResponseMapping(request, response);

        RestTemplate template = new RestTemplate();
        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.APPLICATION_JSON);
        headers.setAccept(Lists.newArrayList(MediaType.APPLICATION_JSON));
        headers.add("Authorization", "Bearer " + tokenProvider.getToken());
        RequestEntity<NoGoRequest> requestEntity = new RequestEntity<>(request, headers, HttpMethod.POST, getURI(path));
        ResponseEntity<T> responseEntity = template.exchange(requestEntity, responseClass);
        return responseEntity.getBody();
    }

    @SneakyThrows(URISyntaxException.class)
    private URI getURI(String path) {
        return new URI("http://localhost:" +port + "/nogo" + path);
    }

    // Test that makes request using `makeDepthRequest`
}

This code is a part on a test taken from an open source project (https://github.com/maritime-web/NoGoService). The basic idea is to start the test on a random port, which Spring will then inject into a field on the test. This allows you to construct URLs and use Springs RestTemplate to make http request to the server, using the same DTO classes as your Controllers. If the authentication mechanism is Basic or Token you simply have to add the correct Authorization header as in this example. If you use Form authentication, then it becomes a bit harder, because you first have to GET /login, then extract the CSRF token and the JSessionId cookie, and the POST them with the credentials to /login, and after login you have to extract the new JSessionId cookie, as the sessionId is changed after login for security reasons.

Hope this was what you needed.

Upvotes: 2

Ashwin Gupta
Ashwin Gupta

Reputation: 940

I see below test case can help you achieve what you want. It is an Integration Test to test the Web Security configuration and we have similar testing done for all our code that is TDD driven.

@RunWith(SpringRunner.class)
@SpringBootTest(classes = Application.class)
@WebAppConfiguration
public class WebConfigIT {
    private MockMvc mockMvc;

    @Autowired
    private WebApplicationContext webApplicationContext;

    @Autowired
    private FilterChainProxy springSecurityFilterChain;

    @Before
    public void setup() throws Exception {
        mockMvc = webAppContextSetup(webApplicationContext)
                .addFilter(springSecurityFilterChain)
                .build();
    }

    @Test
    public void testAuthenticationAtAPIURI() throws Exception {
        mockMvc.perform(get("/api/xyz"))
                .andExpect(status.is3xxRedirection());
    }

This though looks like doing an explicit testing of the end-point (which is anyways a testing one have to do if doing TDD) but this is also bringing the Spring Security Filter Chain in context to enable you test the Security Context for the APP.

Upvotes: 2

Related Questions