Testing integration with Spring Security configurations for endpoints
Testing integration with Spring Security for method-level security
The legend says that writing unit and integration tests started with a short verse:
Track one down, patch it around,
There’s 113 little bugs in the code.”
With time, software became more complex, and teams became larger. Knowing all the functionalities implemented over time by others became impossible. Developers needed a way to make sure they didn’t break existing functionalities while correcting bugs or implementing new features.
While developing applications, we continuously write tests to validate that the functionalities we implement work as desired. The main reason why we write unit and integration tests is to make sure we don’t break existing functionalities when changing code for fixing a bug or for implementing new features. This is also called regression testing.
Nowadays, when a developer finishes making a change, they upload the changes to a server used by the team to manage code versioning. This action automatically triggers a continuous integration tool that runs all existing tests. If any of the changes break an existing functionality, the tests fail, and the continuous integration tool notifies the team (figure 20.1). This way, it’s less likely to deliver changes that affect existing features.
NOTE By using Jenkins in this figure, I say neither that this is the only continuous integration tool used or that it’s the best one. You have many alternatives to choose from like Bamboo, GitLab CI, CircleCI, and so on.
When testing applications, you need to remember it’s not only your application code that you need to test. You need to also make sure you test the integrations with the
frameworks and libraries you use, as well (figure 20.2). Sometime in the future, you may upgrade that framework or library to a new version. When changing the version of a dependency, you want to make sure your app still integrates well with the new version of that dependency. If your app doesn’t integrate in the same way, you want to easily find where you need to make changes to correct the integration problems.
So that’s why you need to know what we’ll cover in this chapter--how to test your app’s integration with Spring Security. Spring Security, like the Spring framework ecosystem in general, evolves quickly. You probably upgrade your app to new versions, and you certainly want to be aware if upgrading to a specific version develops vulnerabilities, errors, or incompatibilities in your application. Remember what we discussed right from the first chapter: you need to consider security from the first design for the app, and you need to take it seriously. Implementing tests for any of your security configurations should be a mandatory task and should be defined as part of your definition of “done.” You shouldn’t consider a task finished if security tests aren’t ready.
In this chapter, we’ll discuss several practices for testing an app’s integration with Spring Security. We’ll go back to some of the examples we worked on in previous chapters, and you’ll learn how to write integration tests for implemented functionality. Testing, in general, is an epic story. But learning this subject in detail brings many benefits.
In this chapter, we’ll focus on testing integration between an application and Spring Security. Before starting our examples, I’d like to recommend a few resources that helped me understand this subject deeply. If you need to understand the subject more in detail, or even as a refresher, you can read these books. I am positive you’ll find these great!
JUnit in Action, 3rd ed. by Cătălin Tudose et al. (Manning, 2020)
Unit Testing Principles, Practices, and Patterns by Vladimir Khorikov (Manning, 2020)
Testing Java Microservices by Alex Soto Bueno et al. (Manning, 2018)
Our adventure in writing tests for security implementations starts with testing authorization configurations. In section 20.1, you’ll learn how to skip authentication and define mock users to test authorization configuration at the endpoint level. Then, in section 20.2, you’ll learn how to test authorization configurations with users from a UserDetailsService
. In section 20.3, we’ll discuss how to set up the full security context in case you need to use specific implementations of the Authentication
object. And finally, in section 20.4, you’ll apply the approaches you learned in the previous sections to test authorization configuration on method security.
Once we complete our discussion on testing authorization, section 20.5 teaches you how to test the authentication flow. Then, in sections 20.6 and 20.7, we’ll discuss testing other security configurations like cross-site request forgery (CSRF) and cross-origin resource sharing (CORS). We’ll end the chapter in section 20.8 discussing integration tests of Spring Security and reactive applications.
In this section, we discuss using mock users to test authorization configuration. This approach is the most straightforward and frequently used method for testing authorization configurations. When using a mock user, the test completely skips the authentication process (figure 20.3). The mock user is valid only for the test execution, and for this user, you can configure any characteristics you need to validate a specific scenario. You can, for example, give specific roles to the user (ADMIN, MANAGER, and so on) or use different authorities to validate that the app behaves as expected in these conditions.
NOTE It’s important to know which components from the framework are involved in an integration test. This way, you know which part of the integration you cover with the test. For example, a mock user can only be used to cover authorization. (In section 20.5, you’ll learn how to deal with authentication.) I sometimes see developers getting confused on this aspect. They thought they were also covering, for example, a custom implementation of an AuthenticationProvider
when working with a mock user, which is not the case. Make sure you correctly understand what you’re testing.
To prove how to write such a test, let’s go back to the simplest example we worked on in this book, the project ssia-ch2-ex1. This project exposes an endpoint for the path /hello with only the default Spring Security configuration. What do we expect to happen?
When calling the endpoint without a user, the HTTP response status should be 401 Unauthorized.
When calling the endpoint having an authenticated user, the HTTP response status should be 200 OK, and the response body should be Hello!
.
Let’s test these two scenarios! We need a couple of dependencies in the pom.xml file to write the tests. The next code snippet shows you the classes we use throughout the examples in this chapter. You should make sure you have these in your pom.xml file before starting to write the tests. Here are the dependencies:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> <exclusions> <exclusion> <groupId>org.junit.vintage</groupId> <artifactId>junit-vintage-engine</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-test</artifactId> <scope>test</scope> </dependency>
NOTE For the examples in this chapter, we use JUnit 5 for writing tests. But don’t be discouraged if you still work with JUnit 4. From the Spring Security integration point of view, the annotations and the rest of the classes you’ll learn work the same. Chapter 4 of JUnit in Action by Cătălin Tudose et al. (Manning, 2020), which is a dedicated discussion about migrating from JUnit 4 to JUnit 5, contains some interesting tables that show the correspondence between classes and annotations of versions 4 and 5. Here’s the link: https://livebook.manning.com/book/junit-in-action-third-edition/chapter-4.
In the test folder of the Spring Boot Maven project, we add a class named MainTests
. We write this class as part of the main package of the application. The name of the main package is com.laurentiuspilca.ssia
. In listing 20.1, you can find the definition of the empty class for the tests. We use the @SpringBootTest
annotation, which represents a convenient way to manage the Spring context for our test suite.
@SpringBootTest ❶
public class MainTests {
}
❶ Makes Spring Boot responsible for managing the Spring context for the tests
A convenient way to implement a test for the behavior of an endpoint is by using Spring’s MockMvc. In a Spring Boot application, you can autoconfigure the MockMvc utility for testing endpoint calls by adding an annotation over the class, as the next listing presents.
@SpringBootTest @AutoConfigureMockMvc ❶ public class MainTests { @Autowired private MockMvc mvc; ❷ }
❶ Enables Spring Boot to autoconfigure MockMvc. As a consequence, an object of type MockMvc is added to the Spring context.
❷ Injects the MockMvc object that we use to test the endpoint
Now that we have a tool we can use to test endpoint behavior, let’s get started with the first scenario. When calling the /hello endpoint without an authenticated user, the HTTP response status should be 401 Unauthorized.
You can visualize the relationship between the components for running this test in figure 20.4. The test calls the endpoint but uses a mock SecurityContext
. We decide what we add to this SecurityContext
. For this test, we need to check that if we don’t add a user that represents the situation in which someone calls the endpoint without authenticating, the app rejects the call with an HTTP response having the status 401 Unauthorized. When we add a user to the SecurityContext
, the app accepts the call, and the HTTP response status is 200 OK.
The following listing presents this scenario’s implementation.
@SpringBootTest
@AutoConfigureMockMvc
public class MainTests {
@Autowired
private MockMvc mvc;
@Test
public void helloUnauthenticated() throws Exception {
mvc.perform(get("/hello")) ❶
.andExpect(status().isUnauthorized());
}
}
❶ When performing a GET request for the /hello path, we expect to get back a response with the status Unauthorized.
Mind that we statically import the methods get()
and status()
. You find the method get()
and similar methods related to the requests we use in the examples of this chapter in this class:
org.springframework.test.web.servlet.request.MockMvcRequestBuilders
Also, you find the method status()
and similar methods related to the result of the calls that we use in the next examples of this chapter in this class:
org.springframework.test.web.servlet.result.MockMvcResultMatchers
You can run the tests now and see the status in your IDE. Usually, in any IDE, to run the tests, you can right-click on the test’s class and then select Run. The IDE displays a successful test with green and a failing one with another color (usually red or yellow).
NOTE In the projects provided with the book, above each method implementing a test, I also use the @DisplayName
annotation. This annotation allows us to have a longer, more detailed description of the test scenario. To occupy less space and allow you to focus on the functionality of the tests we discuss, I took the @DisplayName
annotation out of the listings in the book.
To test the second scenario, we need a mock user. To validate the behavior of calling the /hello endpoint with an authenticated user, we use the @WithMockUser
annotation. By adding this annotation above the test method, we instruct Spring to set up a SecurityContext
that contains a UserDetails
implementation instance. It’s basically skipping authentication. Now, calling the endpoint behaves like the user defined with the @WithMockUser
annotation successfully authenticated.
With this simple example, we don’t care about the details of the mock user like its username, roles, or authorities. So we add the @WithMockUser
annotation, which provides some defaults for the mock user’s attributes. Later in this chapter, you’ll learn to configure the user’s attributes for test scenarios in which their values are important. The next listing provides the implementation for the second test scenario.
@SpringBootTest @AutoConfigureMockMvc public class MainTests { @Autowired private MockMvc mvc; // Omitted code @Test @WithMockUser ❶ public void helloAuthenticated() throws Exception { mvc.perform(get("/hello")) ❷ .andExpect(content().string("Hello!")) .andExpect(status().isOk()); } }
❶ Calls the method with a mock authenticated user
❷ In this case, when performing a GET request for the /hello path, we expect the response status to be OK.
Run this test now and observe its success. But in some situations, we need to use a specific name or give the user specific roles or authorities to implement the test. Say we want to test the endpoints we defined in ssia-ch5-ex2. For this example, the endpoints return a body depending on the authenticated user’s name. To write the test, we need to give the user a known username. The next listing shows how to configure the details of the mock user by writing a test for the /hello endpoint in the ssia-ch5-ex2 project.
@SpringBootTest
@AutoConfigureMockMvc
public class MainTests {
// Omitted code
@Test
@WithMockUser(username = "mary") ❶
public void helloAuthenticated() throws Exception {
mvc.perform(get("/hello"))
.andExpect(content().string("Hello, mary!"))
.andExpect(status().isOk());
}
}
❶ Sets up a username for the mock user
In figure 20.5, you find a comparison between how using annotations to define the test security environment differs from using a RequestPostProcessor
. The framework interprets annotations like @WithMockUser
before it executes the test method. This way, the test method creates the test request and executes it in an already configured security environment. When using a RequestPostProcessor
, the framework first calls the test method and builds the test request. The framework then applies the RequestPostProcessor
,
which alters the request or the environment in which it’s executed before sending it. In this case, the framework configures the test dependencies, like the mock users and the SecurityContext
, after building the test request.
Like setting up the username, you can set the authorities and roles for testing authorization rules. An alternative approach to creating a mock user is using a RequestPostProcessor
. We can provide a RequestPostProcessor
the with()
method, as listing 20.6 presents. The class SecurityMockMvcRequestPostProcessors
provided by Spring Security offers us lots of implementations for RequestPostProcessor
, which helps us cover various test scenarios.
In this chapter, we also discuss the frequently used implementations for Request-PostProcessor
. The method user()
of the class SecurityMockMvcRequestPostProcessors
returns a RequestPostProcessor
we can use as an alternative to the @WithMockUser
annotation.
@SpringBootTest
@AutoConfigureMockMvc
public class MainTests {
// Omitted code
@Test
public void helloAuthenticatedWithUser() throws Exception {
mvc.perform(
get("/hello")
.with(user("mary"))) ❶
.andExpect(content().string("Hello!"))
.andExpect(status().isOk());
}
}
❶ Calls the /hello endpoint using a mock user with the username Mary
As you observed in this section, writing tests for authorization configurations is fun and simple! Most of the tests you write for Spring Security integration with functionalities of your application are for authorization configurations. You might be wondering now why didn’t we also test authentication. In section 20.5, we’ll discuss testing authentication. But in general, it makes sense to test authorization and authentication separately. Usually, an app has one way to authenticate users but might expose dozens of endpoints for which authorization is configured differently. That’s why you test authentication separately with a handful of tests and then implement these individually for each authorization configuration for the endpoints. It’s a loss of execution time to repeat authentication for each endpoint tested, as long as the logic doesn’t change.
In this section, we discuss obtaining the user details for tests from a UserDetailsService
. This approach is an alternative to creating a mock user. The difference is that, instead of creating a fake user, this time we need to get the user from a given UserDetailsService
. You use this approach if you want to also test integration with the data source from where your app loads the user details (figure 20.6).
To demonstrate this approach, let’s open project ssia-ch2-ex2 and implement the tests for the endpoint exposed at the /hello path. We use the UserDetailsService
bean that the project already adds to the context. Note that, with this approach, we need to have a UserDetailsService
bean in the context. To specify the user we authenticate from this UserDetailsService
, we annotate the test method with @WithUserDetails
. With the @WithUserDetails
annotation, to find the user, you specify the username. The following listing presents the implementation of the test for the /hello endpoint using the @WithUserDetails
annotation to define the authenticated user.
@SpringBootTest
@AutoConfigureMockMvc
public class MainTests {
@Autowired
private MockMvc mvc;
@Test
@WithUserDetails("john") ❶
public void helloAuthenticated() throws Exception {
mvc.perform(get("/hello"))
.andExpect(status().isOk());
}
}
❶ Loads the user John using the UserDetailsService for running the test scenario
Generally, when using a mock user for a test, you don’t care which class the frame-work uses to create the Authentication
instances in the SecurityContext
. But say you have some logic in the controller that depends on the type of the object. Can you somehow instruct the framework to create the Authentication
object for the test using a specific type? The answer is yes, and this is what we discuss in this section.
The logic behind this approach is simple. We define a factory class responsible for building the SecurityContext
. This way, we have full control over how the SecurityContext
for the test is built, including what’s inside it (figure 20.7). For example, we can choose to have a custom Authentication
object.
Let’s open project ssia-ch2-ex5 and write a test in which we configure the mock SecurityContext
and instruct the framework on how to create the Authentication
object. An interesting aspect to remember about this example is that we use it to prove the implementation of a custom AuthenticationProvider
. The custom AuthenticationProvider
we implement in our case only authenticates a user named John. However, as in the other two previous approaches we discussed in sections 20.1 and 20.2, the current approach skips authentication. For this reason, you see at the end of the example that we can actually give any name to our mock user. We follow three steps to achieve this behavior (figure 20.8):
Write an annotation to use over the test similarly to the way we use @WithMockUser
or @WithUserDetails
.
Write a class that implements the WithSecurityContextFactory
interface. This class implements the createSecurityContext()
method that returns the mock SecurityContext
the framework uses for the test.
Link the custom annotation created in step 1 with the factory class created in step 2 via the @WithSecurityContext
annotation.
In listing 20.8, you find the definition of the custom annotation we define for the test, named @WithCustomUser
. As properties of the annotation, you can define whatever details you need to create the mock Authentication
object. I added only the username
here for my demonstration. Also, don’t forget to use the annotation @Retention (RetentionPolicy.RUNTIME)
to set the retention policy to runtime. Spring needs to read this annotation using Java reflection at runtime. To allow Spring to read this annotation, you need to change its retention policy to RetentionPolicy.RUNTIME
.
@Retention(RetentionPolicy.RUNTIME) public @interface WithCustomUser { String username(); }
The second step consists in implementing the code that builds the SecurityContext
that the framework uses for the test’s execution. Here’s where we decide what kind of Authentication
to use for the test. The following listing demonstrates the implementation of the factory class.
public class CustomSecurityContextFactory ❶ implements WithSecurityContextFactory<WithCustomUser> { @Override ❷ public SecurityContext createSecurityContext( WithCustomUser withCustomUser) { SecurityContext context = ❸ SecurityContextHolder.createEmptyContext(); var a = new UsernamePasswordAuthenticationToken( withCustomUser.username(), null, null); ❹ context.setAuthentication(a); ❺ return context; } }
❶ Implements the WithSecurityContextFactory annotation and specifies the custom annotation we use for the tests
❷ Implements createSecurityContext() to define how to create the SecurityContext for the test
❸ Builds an empty security context
❹ Creates an Authentication instance
❺ Adds the mock Authentication to the SecurityContext
Using the @WithSecurityContext
annotation, we now link the custom annotation we created in step 1 to the factory class for the SecurityContext
we implemented in step 2. The following listing presents the change to our @WithCustomUser
annotation to link it to the SecurityContext
factory class.
@Retention(RetentionPolicy.RUNTIME) @WithSecurityContext(factory = CustomSecurityContextFactory.class) public @interface WithCustomUser { String username(); }
With this setup complete, we can write a test to use the custom SecurityContext
. The next listing defines the test.
@SpringBootTest
@AutoConfigureMockMvc
public class MainTests {
@Autowired
private MockMvc mvc;
@Test
@WithCustomUser(username = "mary") ❶
public void helloAuthenticated() throws Exception {
mvc.perform(get("/hello"))
.andExpect(status().isOk());
}
}
❶ Executes the test with a user having the username “mary”
Running the test, you observe a successful result. You might think, “Wait! In this example, we implemented a custom AuthenticationProvider
that only authenticates a user named John. How could the test be successful with the username Mary?” As in the case of @WithMockUser
and @WithUserDetails
, with this method we skip the authentication logic. So you can use it only to test what’s related to authorization and onward.
In this section, we discuss testing method security. All the tests we wrote until now in this chapter refer to endpoints. But what if your application doesn’t have endpoints? In fact, if it’s not a web app, it doesn’t have endpoints at all! But you might have used Spring Security with global method security as we discussed in chapters 16 and 17. You still need to test your security configurations in such scenarios.
Fortunately, you do this by using the same approaches we discussed in the previous sections. You can still use @WithMockUser
, @WithUserDetails
,
or a custom annotation to define your own SecurityContext
. But instead of using MockMvc
, you directly inject from the context the bean defining the method you need to test.
Let’s open project ssia-ch16-ex1 and implement the tests for the getName()
method in the NameService
class. We protected the getName()
method using the @PreAuthorize
annotation. In listing 20.12, you find the implementation of the test class with its three tests, and figure 20.9 represents graphically the three scenarios we test:
Calling the method without an authenticated user, the method should throw AuthenticationException
.
Calling the method with an authenticated user that has an authority different than the expected one (write), the method should throw AccessDeniedException
.
Calling the method with an authenticated user that has the expected authority returns the expected result.
@SpringBootTest class MainTests { @Autowired private NameService nameService; @Test void testNameServiceWithNoUser() { assertThrows(AuthenticationException.class, () -> nameService.getName()); } @Test @WithMockUser(authorities = "read") void testNameServiceWithUserButWrongAuthority() { assertThrows(AccessDeniedException.class, () -> nameService.getName()); } @Test @WithMockUser(authorities = "write") void testNameServiceWithUserButCorrectAuthority() { var result = nameService.getName(); assertEquals("Fantastico", result); } }
We don’t configure MockMvc
anymore because we don’t need to call an endpoint. Instead, we directly inject the NameService
instance to call the tested method. We use the @WithMockUser
annotation as we discussed in section 20.1. Similarly, you could have used the @WithUserDetails
as we discussed in section 20.2 or designed a custom way to build the SecurityContext
as discussed in section 20.3.
In this section, we discuss testing authentication. Previously, in this chapter, you learned how to define mock users and test authorization configurations. But what about authentication? Can we also test the authentication logic? You need to do this if, for example, you have custom logic implemented for your authentication, and you want to make sure the entire flow works. When testing authentication, the test implementation requests work like normal client requests, as presented in figure 20.10.
For example, going back to project ssia-ch2-ex5, can we prove that the custom authentication provider we implemented works correctly and secure it with tests? In this project, we implemented a custom AuthenticationProvider
, and we want to make sure that we secure this custom authentication logic as well with tests. Yes, we can test the authentication logic as well.
The logic we implement is straightforward. Only one set of credentials is accepted: the username "john"
and the password "12345"
. We need to prove that, when using valid credentials, the call is successful, whereas when using some other credentials, the HTTP response status is 401 Unauthorized. Let’s again open project ssia-ch2-ex5 and implement a couple of tests to validate that authentication behaves correctly.
@SpringBootTest @AutoConfigureMockMvc public class AuthenticationTests { @Autowired private MockMvc mvc; @Test public void helloAuthenticatingWithValidUser() throws Exception { mvc.perform( get("/hello") .with(httpBasic("john","12345"))) ❶ .andExpect(status().isOk()); } @Test public void helloAuthenticatingWithInvalidUser() throws Exception { mvc.perform( get("/hello") .with(httpBasic("mary","12345"))) ❷ .andExpect(status().isUnauthorized()); } }
❶ Authenticates with the correct credentials
❷ Authenticates with the wrong credentials
Using the httpBasic()
request postprocessor, we instruct the test to execute the authentication. This way, we validate the behavior of the endpoint when authenticating using either valid or invalid credentials. You can use the same approach to test the authentication with a form login. Let’s open project ssia-ch5-ex4, where we used form login for authentication, and write some tests to prove authentication works correctly. We test the app’s behavior in the following scenarios:
When authenticating with a valid set of credentials, but the user doesn’t have a valid authority according to the implementation we wrote in the AuthenticationSuccessHandler
When authenticating with a valid set of credentials and a user that has a valid authority according to the implementation we wrote in the AuthenticationSuccessHandler
In listing 20.14, you find the implementation for the first scenario. If we authenticate using invalid credentials, the app doesn’t authenticate the user and adds the header “failed” to the HTTP response. We customized an app and added the “failed” header with an AuthenticationFailureHandler
when discussing authentication back in chapter 5.
@SpringBootTest
@AutoConfigureMockMvc
public class MainTests {
@Autowired
private MockMvc mvc;
@Test
public void loggingInWithWrongUser() throws Exception {
mvc.perform(formLogin() ❶
.user("joey").password("12345"))
.andExpect(header().exists("failed"))
.andExpect(unauthenticated());
}
}
❶ Authenticates using form login with an invalid set of credentials
Back in chapter 5, we customized authentication logic using an Authentication-SuccessHandler
. In our implementation, if the user has read authority, the app redirects them to the /home page. Otherwise, the app redirects the user to the /error page. The following listing presents the implementation of these two scenarios.
@SpringBootTest @AutoConfigureMockMvc public class MainTests { @Autowired private MockMvc mvc; // Omitted code @Test public void loggingInWithWrongAuthority() throws Exception { mvc.perform(formLogin() .user("mary").password("12345") ) .andExpect(redirectedUrl("/error")) ❶ .andExpect(status().isFound()) .andExpect(authenticated()); } @Test public void loggingInWithCorrectAuthority() throws Exception { mvc.perform(formLogin() .user("bill").password("12345") ) .andExpect(redirectedUrl("/home")) ❷ .andExpect(status().isFound()) .andExpect(authenticated()); } }
❶ When authenticating with a user that doesn’t have read authority, the app redirects the user to path /error.
❷ When authenticating with a user that has read authority, the app redirects the user to path /home.
In this section, we discuss testing the cross-site request forgery (CSRF) protection configuration for your application. When an app presents a CSRF vulnerability, an attacker can fool the user into taking actions they don’t want to take once they’re logged into the application. As we discussed in chapter 10, Spring Security uses CSRF tokens to mitigate these vulnerabilities. This way, for any mutating operation (POST, PUT, DELETE), the request needs to have a valid CSRF token in its headers. Of course, at some point, you need to test more than HTTP GET requests. Depending on how you implement your application, as we discussed in chapter 10, you might need to test CSRF protection. You need to make sure it works as expected and protects the endpoint that implements mutating actions.
Fortunately, Spring Security provides an easy approach to test CSRF protection using a RequestPostProcessor
. Let’s open the project ssia-ch10-ex1 and test that CSRF protection is enabled for an endpoint /hello when called with HTTP POST in the following scenarios:
If we don’t use a CSRF token, the HTTP response status is 403 Forbidden.
If we send a CSRF token, the HTTP response status is 200 OK.
The following listing shows you the implementation of these two scenarios. Observe how we can send a CSRF token in the response simply by using the csrf()
RequestPostProcessor
.
@SpringBootTest @AutoConfigureMockMvc public class MainTests { @Autowired private MockMvc mvc; @Test public void testHelloPOST() throws Exception { mvc.perform(post("/hello")) ❶ .andExpect(status().isForbidden()); } @Test public void testHelloPOSTWithCSRF() throws Exception { mvc.perform(post("/hello").with(csrf())) ❷ .andExpect(status().isOk()); } }
❶ When calling the endpoint without a CSRF token, the HTTP response status is 403 Forbidden.
❷ When calling the endpoint with a CSRF token, the HTTP response status is 200 OK.
In this section, we discuss testing cross-origin resource sharing (CORS) configurations. As you learned in chapter 10, if a browser loads a web app from one origin (say, example.com), the browser won’t allow the app to use an HTTP response that comes from a different origin (say, example.org). We use CORS policies to relax these restrictions. This way, we can configure our application to work with multiple origins. Of course, as for any other security configurations, you need to also test the CORS policies. In chapter 10, you learned that CORS is about specific headers on the response whose values define whether the HTTP response is accepted. Two of these headers related to CORS specifications are Access-Control-Allow-Origin
and Access-Control-Allow-Methods
. We used these headers in chapter 10 to configure multiple origins for our app.
All we need to do when writing tests for the CORS policies is to make sure that these headers (and maybe other CORS-related headers, depending on the complexity of your configurations) exist and have the correct values. For this validation, we can act precisely as the browser does when making a preflight request. We make a request using the HTTP OPTIONS method, requesting the value for the CORS headers. Let’s open project ssia-ch10-ex4 and write a test to validate the values for the CORS headers. The following listing shows the definition of the test.
@SpringBootTest @AutoConfigureMockMvc public class MainTests { @Autowired private MockMvc mvc; @Test public void testCORSForTestEndpoint() throws Exception { mvc.perform(options("/test") ❶ .header("Access-Control-Request-Method", "POST") .header("Origin", "http://www.example.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()); } }
❶ Performs an HTTP OPTIONS request on the endpoint requesting the value for the CORS headers
❷ Validates the values for the headers according to the configuration we made in the app
In this section, we discuss testing the integration of Spring Security with functionalities developed within a reactive app. You won’t be surprised to find out that Spring Security provides support for testing security configurations also for reactive apps. As in the case of non-reactive applications, security for reactive apps is a crucial aspect. So testing their security configurations is also essential. To show you how to implement tests for your security configurations, we go back to the examples we worked on in chapter 19. With Spring Security for reactive applications, you need to know two approaches for writing your tests:
Using the @WithMockUser
annotation is straightforward because it works the same as for non-reactive apps, as we discussed in section 20.1. The definition of the test is different, however, because being a reactive app, we can’t use MockMvc
anymore. But this change isn’t related to Spring Security. We can use something similar when testing reactive apps, a tool named WebTestClient
. In the next listing, you find the implementation of a simple test making use of a mock user to verify the behavior of a reactive endpoint.
@SpringBootTest @AutoConfigureWebTestClient ❶ class MainTests { @Autowired ❷ private WebTestClient client; @Test @WithMockUser ❸ void testCallHelloWithValidUser() { client.get() ❹ .uri("/hello") .exchange() .expectStatus().isOk(); } }
❶ Requests Spring Boot to autoconfigure the WebTestClient we use for the tests
❷ Injects the WebTestClient instance configured by Spring Boot from the Spring context
❸ Uses the @WithMockUser annotation to define a mock user for the test
❹ Makes the exchange and validates the result
As you observe, using the @WithMockUser
annotation is pretty much the same as for non-reactive apps. The framework creates a SecurityContext
with the mock user. The application skips the authentication process and uses the mock user from the test’s SecurityContext
to validate the authorization rules.
The second approach you can use is a WebTestClientConfigurer
. This approach is similar to using the RequestPostProcessor
in the case of a non-reactive app. In the case of a reactive app, for the WebTestClient
we use, we set a WebTestClientConfigurer
, which helps mutate the test context. For example, we can define the mock user or send a CSRF token to test CSRF protection as we did for non-reactive apps in section 20.6. The following listing shows you how to use a WebTestClientConfigurer
.
@SpringBootTest
@AutoConfigureWebTestClient
class MainTests {
@Autowired
private WebTestClient client;
// Omitted code
@Test
void testCallHelloWithValidUserWithMockUser() {
client.mutateWith(mockUser()) ❶
.get()
.uri("/hello")
.exchange()
.expectStatus().isOk();
}
}
❶ Before executing the GET request, mutates the call to use a mock user
Assuming you’re testing CSRF protection on a POST call, you write something similar to this:
client.mutateWith(csrf()) .post() .uri("/hello") .exchange() .expectStatus().isOk();
Writing tests is a best practice. You write tests to make sure your new implementations or fixes don’t break existing functionalities.
You need to not only test your code, but also test integration with libraries and frameworks you use.
Spring Security offers excellent support for implementing tests for your security configurations.
You can test authorization directly by using mock users. You write separate tests for authorization without authentication because, generally, you need fewer authentication tests than authorization tests.
It saves execution time to test authentication in separate tests, which are fewer in number, and then test the authorization configuration for your endpoints and methods.
To test security configurations for endpoints in non-reactive apps, Spring Security offers excellent support for writing your tests with MockMvc
.
To test security configurations for endpoints in reactive apps, Spring Security offers excellent support for writing your tests with WebTestClient
.
You can write tests directly for methods for which you wrote security configurations using method security.
3.141.30.162