19 Test Driven How to Test the Security of the System Based on Spring Security

19 Test-Driven How to Test the Security of the System Based on Spring Security #

As the last part of the entire course, we will discuss testing solutions based on Spring Security. Testing is a challenging aspect of security and often an overlooked technical system. When using Spring Security, how can we verify the correctness of the security features we are using? Today’s content will provide detailed answers.

How to test system security? #

Spring Security is a security development framework that provides infrastructure-level functions embedded in business systems. Therefore, it involves a large number of dependencies between components, which is the biggest challenge in testing security features and requires specific testing methods. Therefore, before introducing specific test cases, let us first clarify the methods of security testing and the testing solutions provided by Spring Security.

Security testing and Mock mechanism #

As mentioned earlier, the difficulty in verifying the correctness of security features lies in the dependencies between components. To clarify this relationship, we need to introduce a very important concept in the testing field, that is, Mocking. Regarding the external dependencies involved in the testing components, our focus is on the calling relationship between these components, the returned results, or the exceptions that occur, rather than the execution process inside the components . Therefore, a common technique is to use Mock objects to replace real dependent objects and simulate real calling scenarios.

Let’s take a common three-layer web service architecture as an example to further explain the implementation methods of Mocking. The Controller layer accesses the Service layer, and the Service layer accesses the Repository layer. When we verify the endpoints of the Controller layer, we need to simulate the functionality of the Service layer component. Similarly, when testing the Service layer component, we also need to assume that the results of the Repository layer component can be obtained, as shown below:

2.jpg

Diagram illustrating the components and Mock objects in web services

The principle shown in the above diagram is also applicable to Spring Security. For example, we can test the correctness of user authentication and authorization functions by simulating users. In the later part of this lecture, we will provide relevant code examples.

Testing solutions in Spring Security #

To conduct unit tests, integration tests, and Mock-based tests, a complete technical system is required. Like the Spring Boot 1.x version, Spring Boot 2.x also provides the spring-boot-starter-test component for testing. The method of integrating this component into Spring Boot is to add the following dependency in the pom file:

<dependency>
     <groupId>org.springframework.boot</groupId>
     <artifactId>spring-boot-starter-test</artifactId>
     <scope>test</scope>
</dependency>

With this dependency, a series of components are automatically introduced into the build path of the code project, including JUnit, JSON Path, AssertJ, Mockito, Hamcrest, etc., which are very useful for testing. At the same time, because the entry point of the Spring Boot program is the Bootstrap class, a @SpringBootTest annotation is specifically provided to test your Bootstrap class. All configurations will be loaded through the Bootstrap class, and this annotation can reference the configuration of the Bootstrap class.

On the other hand, Spring Security also provides the spring-security-test component specifically for testing security features, as shown below:

<dependency>
     <groupId>org.springframework.security</groupId>
     <artifactId>spring-security-test</artifactId>
     <scope>test</scope>
</dependency>

This component provides relevant annotations to simulate user login information or call user login methods. Let’s take a look together.

Testing Spring Security features #

Testing users #

When using Spring Security, the first thing to test is undoubtedly valid users. Suppose we have implemented a simple Controller as shown below:

@RestController
public class HelloController {

    @GetMapping("/hello")
    public String hello() {
        return "Hello";
    }
}

Once we enable Spring Security authentication, two types of tests can be performed on the above “/hello” endpoint, targeting authenticated and unauthenticated users, respectively. Let’s first look at the test method for unauthenticated users:

@SpringBootTest
@AutoConfigureMockMvc
public class HelloControllerTests {

    @Autowired
    private MockMvc mvc;

    @Test
    public void testUnauthenticatedUser() throws Exception {
        mvc.perform(get("/hello"))
                .andExpect(status().isUnauthorized());
	}

}

Here, an @AutoConfigureMockMvc annotation is introduced. By combining the @SpringBootTest annotation with the @AutoConfigureMockMvc annotation, the @AutoConfigureMockMvc annotation will automatically configure the MockMvc test tool in the Spring context environment loaded through the @SpringBootTest annotation.

As the name suggests, MockMvc is used to simulate the execution process of WebMVC. The basic methods provided by the MockMvc class are shown below.

  • perform: Executes a RequestBuilder request, automatically performs the SpringMVC process, and maps it to the corresponding Controller for processing.
  • get/post/put/delete: Declares the method of sending an HTTP request, obtains an HTTP request based on the URI template and URI variable values, and supports HTTP methods such as GET, POST, PUT, DELETE, etc.
  • param: Adds request parameters. This method cannot be used when sending JSON data, but should use the @ResponseBody annotation.
  • andExpect: Adds ResultMatcher verification rules to verify the correctness of the Controller execution result through judging the returned data.
  • andDo: Adds ResultHandler result processors, such as printing results to the console during debugging.
  • andReturn: Finally, returns the corresponding MvcResult, and then performs custom verification or asynchronous processing. In the code example above, we use the perform, accept, and andExpect methods to simulate the entire process of an HTTP request and verify that the return status is not authenticated.

Next, let’s simulate a test scenario for an authenticated user. The test code is as follows:

@Test
@WithMockUser
public void testAuthenticatedUser() throws Exception {
    mvc.perform(get("/hello"))
            .andExpect(content().string("Hello"))
            .andExpect(status().isOk());
}

Here, we see a new annotation called @WithMockUser. Please note that this annotation is provided by Spring Security and is used specifically to simulate an authenticated user. Now that we have an authenticated user, we can verify the response’s return value and status, as shown in the code above.

With the @WithMockUser annotation, we can also specify the user’s details. For example, the code snippet below simulates an authenticated user with the username “admin” and the roles “USER” and “ADMIN”:

@WithMockUser(username="admin",roles={"USER","ADMIN"})

Furthermore, we can provide custom user information by simulating the UserDetailsService. For this purpose, Spring Security provides the @WithUserDetails annotation. Here is an example:

@Test
@WithUserDetails("jianxiang")
public void testAuthenticatedUser() throws Exception {
    mvc.perform(get("/hello"))
            .andExpect(content().string("Hello"))
            .andExpect(status().isOk());
}

Testing Authentication #

After testing the user, let’s move on to testing the authentication process for the user. In order to have more customized authentication implementation, we provide an implementation class for the AuthenticationProvider interface called MyAuthenticationProvider, as shown below:

@Component
public class MyAuthenticationProvider implements AuthenticationProvider {

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        String username = authentication.getName();
        String password = String.valueOf(authentication.getCredentials());

        if ("jianxiang".equals(username) && "123456".equals(password)) {
            return new UsernamePasswordAuthenticationToken(username, password, Arrays.asList());
        } else {
            throw new AuthenticationCredentialsNotFoundException("Error!");
        }
    }

    @Override
    public boolean supports(Class<?> authenticationType) {
        return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authenticationType);
    }
}

Now, based on the HTTP basic authentication mechanism, we can write test cases as follows:

@SpringBootTest
@AutoConfigureMockMvc
public class AuthenticationTests {

    @Autowired
    private MockMvc mvc;

    @Test
    public void testAuthenticatingWithValidUser() throws Exception {
        mvc.perform(get("/hello")
                .with(httpBasic("jianxiang","123456")))
.andExpect(status().isOk());

}

@Test public void testAuthenticatingWithInvalidUser() throws Exception {

mvc.perform(get("/hello")
        .with(httpBasic("nonexistentuser","123456")))
        .andExpect(status().isUnauthorized());

}

}

Here we use the @AutoConfigureMockMvc annotation and the MockMvc utility class introduced earlier, and then use the httpBasic() method to implement HTTP basic authentication. We execute HTTP requests for the correct and incorrect username/password combinations respectively and verify the authentication results based on the returned status.

Testing Method Security #

The discussions so far have been about web applications, that is, testing is done on the HTTP endpoints. But how do we test method-level security?

For global method security, the @WithMockUser and @WithUserDetails annotations mentioned earlier can also be used. However, since we are now outside the web environment, the MockMvc utility class is obviously ineffective. In this case, all you need to do is directly inject the target method in the test case. Let’s take a look at the code example. Assume there is a Service class in a non-web application that looks like this:

@Service
public class HelloService {

    @PreAuthorize("hasAuthority('write')")
    public String hello() {
        return "Hello";
    }
}

Here, the method is restricted to only users with the “write” authority using the @PreAuthorize annotation.

Now let’s write the first test case for method access security, as shown below:

@Autowired
private HelloService helloService;

@Test
void testMethodWithNoUser() {
    assertThrows(AuthenticationException.class, () -> helloService.hello());
}

When we access the hello() method of helloService without authentication, an AuthenticationException should be thrown. The above test case verifies this. So what happens when we access this method with an authenticated user with different authorities? The corresponding test case is shown below:

@Test
@WithMockUser(authorities = "read")
void testMethodWithUserButWrongAuthority() {
    assertThrows(AccessDeniedException.class, () -> helloService.hello());
}

You may notice that the @WithMockUser annotation is used here to simulate an authenticated user with the “read” authority. But because the @PreAuthorize annotation specifies that only users with the “write” authority can access this method, an AccessDeniedException will be thrown.

Finally, let’s test the result in the normal flow. The test case is as follows:

@Test
@WithMockUser(authorities = "write")
void testMethodWithUserButCorrectAuthority() {
    String result = helloService.hello();
    assertEquals("Hello", result);
}

Testing CSRF and CORS Configuration #

Based on the discussion in the previous article “Defense Against Attacks: How to Implement CSRF Protection and CORS?”, for HTTP POST, PUT, DELETE, etc. requests, we need to add security protection for CSRF. To test the correctness of the CSRF configuration, let’s assume there is an HTTP endpoint, note that its HTTP method is POST:

@RestController
public class HelloController {

    @PostMapping("/hello")
    public String postHello() {
        return "Post Hello!";
    }
}

Now, let’s initiate a POST request using the MockMvc utility class to test:

@Test
public void testHelloUsingPOST() throws Exception {
    mvc.perform(post("/hello"))
            .andExpect(status().isForbidden());
}

Please note that this post request does not include a CSRF token, so the response status should be HTTP 403 Forbidden.

Now, let’s refactor the above test case as follows:

@Test
public void testHelloUsingPOSTWithCSRF() throws Exception {
    mvc.perform(post("/hello").with(csrf()))
            .andExpect(status().isOk());
}

The csrf() method adds a CSRF token to the request. Obviously, the response at this point should be correct.

After discussing CSRF, let’s take a look at CORS. In the previous article “Defense Against Attacks: How to Implement CSRF Protection and CORS?”, we have set the HTTP response headers through CorsConfiguration, as shown below:

@Override
protected void configure(HttpSecurity http) throws Exception {
    http.cors(c -> {
        CorsConfigurationSource source = request -> {
            CorsConfiguration config = new CorsConfiguration();
            config.setAllowedOrigins(Arrays.asList("*"));
            config.setAllowedMethods(Arrays.asList("*"));
            return config;
        };
        c.configurationSource(source);
    });
    ...
}

Testing the above configuration is also easy. We initiate a request using MockMvc, and then verify the response headers. The test case is as follows:

@SpringBootTest
@AutoConfigureMockMvc
public class MainTests {

    @Autowired
    private MockMvc mvc;

    @Test
    public void testCORSForTestEndpoint() throws Exception {
        mvc.perform(options("/hello")
                .header("Access-Control-Request-Method", "POST")
                .header("Origin", "http://www.test.com")
        )
                .andExpect(header().exists("Access-Control-Allow-Origin"))
                .andExpect(header().string("Access-Control-Allow-Origin", "*"))
                .andExpect(header().exists("Access-Control-Allow-Methods"))
                .andExpect(header().string("Access-Control-Allow-Methods", "POST"))
                .andExpect(status().isOk());
    }
}

As you can see, we get the “Access-Control-Allow-Origin” and “Access-Control-Allow-Methods” response headers and verify them.

Summary and Next #

For an application, regardless of whether it is a web service or not, we need to test its security. Spring Security provides a dedicated testing solution for this, which relies heavily on the proper application of the Mock mechanism. In this article, we have designed test cases for user, authentication, global method security, CSRF, and CORS in Spring Security, and provided corresponding example code.