In this article, we are gonna explore various options for Disabling Spring Security in JUnit Tests and Executing the tests with mock authentication. Basically, there are four approaches that we are gonna explore and they can be categorized as follows:
- Disable Spring Security based on Configuration class. In this approach, we will configure Spring Security to permit all requests without authentication. However, if your REST APIs are having method based security enabled using annotations like
@PreAuthorize
,@PostAuthorize
and@Secured
, then that method-level security will not be disabled. The following methods fall under this category:- Disable Security with Test Security Configuration
- Disable Security with a Spring Profile
- Execute the tests with Spring Security using Mock Authentication. In this approach, we will not actually disable the security. Instead, we will be running the tests with mock users and roles. Hence, we can unit test REST services with method-based security as well. This will help us to make sure that only the authenticated user with the required role can access a specific endpoint. The following methods fall under this category:
- Test with Mock User
- Test with Mock Authentication
Let’s explore each method in detail in the following sections.
Disable Security with Test Security Configuration
Create Test Security Configuration
TestSecurityConfig
class extends the WebSecurityConfigurerAdapter
and overrides the HttpSecurity
configuration to permit all incoming requests without authentication.
@TestConfiguration
can be used to define additional beans or customizations for a test. Unlike regular @Configuration
classes the use of @TestConfiguration
does not prevent auto-detection of @SpringBootConfiguration
.
@Order
defines the sort order for an annotated component. The value
is optional and represents an order value as defined in the Ordered
interface. Lower values have higher priority. The default value is Ordered.LOWEST_PRECEDENCE
, indicating the lowest priority (losing to any other specified order value).
TestSecurityConfig.java
package com.javachinna.config;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.core.annotation.Order;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
@TestConfiguration
@Order(1)
public class TestSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
// Disable CSRF
httpSecurity.csrf().disable()
// Permit all requests without authentication
.authorizeRequests().anyRequest().permitAll();
}
}
Understanding The Spring Security Filter Chain
Spring Security maintains a filter chain internally where each of the filters has a particular responsibility and filters are added or removed from the configuration depending on which services are required.
When we define several security configuration classes, each of those classes adds a filter chain, and the first one that matches is executed. The @Order
annotation can be used to influence the order of the filter chains to make sure that the right one is executed first.
At the end of the filter chain is the FilterSecurityInterceptor
that checks if the requested resource requires authentication and if the one that is set conforms to the requested roles.
Since we have specified the order value as 1 which is the highest precedence for the TestSecurityConfig
class, it will add a security filter first. Then the WebSecurityConfig
security filter will be added. Hence, when a request comes in, the first security filter matching the supplied URL which is our TestSecurityConfig will get executed. Thus, it allows all the requests without authentication.
Unit Testing Order Controller
OrderController
exposes an HTTP POST endpoint that requires authentication. So, in order to disable the authentication and test the endpoint, we are gonna create a test class and annotate it with @SpringBootTest(classes = TestSecurityConfig.class)
OrderControllerTest.java
package com.javachinna.controller;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.http.MediaType;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.test.web.servlet.MockMvc;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.javachinna.config.TestSecurityConfig;
import com.javachinna.dto.payment.PaymentResponse;
import com.javachinna.service.OrderService;
@SpringBootTest(classes = TestSecurityConfig.class)
@AutoConfigureMockMvc
class OrderControllerTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private OrderService orderService;
@MockBean
private PasswordEncoder passwordEncoder;
private static ObjectMapper mapper = new ObjectMapper();
@Test
public void testUpdateOrder() throws Exception {
PaymentResponse paymentResponse = new PaymentResponse();
paymentResponse.setRazorpayPaymentId("22445566");
String json = mapper.writeValueAsString(paymentResponse);
mockMvc.perform(put("/api/order").contentType(MediaType.APPLICATION_JSON).characterEncoding("utf-8").content(json).accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk()).andExpect(jsonPath("$.success").value("true")).andExpect(jsonPath("$.message").isNotEmpty());
}
}
Run JUnit Tests
Disable Security with a Spring Profile
@Profile
annotation indicates that a component is eligible for registration when one or more specified profiles are active.
A profile is a named logical grouping that may be activated programmatically via ConfigurableEnvironment.setActiveProfiles
or declaratively by setting the spring.profiles.active
property as a JVM system property, as an environment variable, or as a Servlet context parameter in web.xml
for web applications. Profiles may also be activated declaratively in integration tests via the @ActiveProfiles
annotation.
The @Profile
annotation may be used in any of the following ways:
- as a type-level annotation on any class directly or indirectly annotated with
@Component
, including@Configuration
classes - as a meta-annotation, for the purpose of composing custom stereotype annotations
- as a method-level annotation on any
@Bean
method
If a @Configuration
class is marked with @Profile
, all of the @Bean
methods and @Import
annotations associated with that class will be bypassed unless one or more of the specified profiles are active. A profile string may contain a simple profile name (for example "p1"
) or a profile expression. A profile expression allows for more complicated profile logic to be expressed, for example "p1 & p2"
.
Read more on how to disable Spring Security for a Profile.
Test with Mock User
We can use annotations like @WithMockUser
, @WithUserDetails
or create a custom annotation as per our need and use that to run the tests as a specific user.
Test with Mock Authentication
This is another simple approach in which we will create a mock user and put it in the SecurityContextHolder.
AuthControllerTest2.java
This test class is just a modified version of our existing AuthControllerTest
class. Here, we are directly mocking the authentication principal instead of using the aforesaid MockUser
annotations.
@BeforeEach
is used to signal that the annotated method should be executed before each @Test
, @RepeatedTest
, @ParameterizedTest
, @TestFactory
, and @TestTemplate
method in the current test class. Since each REST call requires authentication to be set in the security context, we have used this annotation instead of @BeforeAll
annotation which is used to signal that the annotated method should be executed before all tests in the current test class.
package com.javachinna.controller;
import static org.mockito.ArgumentMatchers.any;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInstance;
import org.junit.jupiter.api.TestInstance.Lifecycle;
import org.mockito.Mockito;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.http.MediaType;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.test.web.servlet.MockMvc;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.javachinna.config.MockUserUtils;
import com.javachinna.dto.LocalUser;
import com.javachinna.dto.LoginRequest;
import com.javachinna.dto.SignUpRequest;
import com.javachinna.dto.SocialProvider;
import com.javachinna.exception.UserAlreadyExistAuthenticationException;
import com.javachinna.model.User;
import com.javachinna.service.UserService;
import dev.samstevens.totp.code.CodeVerifier;
@SpringBootTest
@TestInstance(Lifecycle.PER_CLASS)
@AutoConfigureMockMvc
class AuthControllerTest2 {
@Autowired
private MockMvc mockMvc;
@MockBean
private UserService userService;
@MockBean
private CodeVerifier verifier;
@MockBean
private PasswordEncoder passwordEncoder;
private static User user = MockUserUtils.getMockUser("JavaChinna");
private static ObjectMapper mapper = new ObjectMapper();
@BeforeEach
public void init() {
LocalUser localUser = LocalUser.create(user, null, null, null);
SecurityContextHolder.getContext().setAuthentication(new UsernamePasswordAuthenticationToken(localUser, "secret"));
Mockito.when(userService.findUserByEmail(Mockito.anyString())).thenReturn(user);
Mockito.when(passwordEncoder.matches(Mockito.anyString(), Mockito.anyString())).thenReturn(true);
}
@Test
public void testAuthenticateUser() throws Exception {
LoginRequest loginRequest = new LoginRequest(user.getEmail(), user.getPassword());
String json = mapper.writeValueAsString(loginRequest);
mockMvc.perform(post("/api/auth/signin").contentType(MediaType.APPLICATION_JSON).characterEncoding("utf-8").content(json).accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk()).andExpect(jsonPath("$.authenticated").value("true")).andExpect(jsonPath("$.accessToken").isNotEmpty());
// Test when user 2fa is enabled
user.setUsing2FA(true);
mockMvc.perform(post("/api/auth/signin").contentType(MediaType.APPLICATION_JSON).characterEncoding("utf-8").content(json).accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk()).andExpect(jsonPath("$.authenticated").value("false")).andExpect(jsonPath("$.user").doesNotExist());
}
@Test
public void testRegisterUser() throws Exception {
SignUpRequest signUpRequest = new SignUpRequest("1234", "JavaChinna", user.getEmail(), user.getPassword(), user.getPassword(), SocialProvider.FACEBOOK);
// Test when user provided email already exists in the database
Mockito.when(userService.registerNewUser(any(SignUpRequest.class))).thenReturn(user);
String json = mapper.writeValueAsString(signUpRequest);
mockMvc.perform(post("/api/auth/signup").contentType(MediaType.APPLICATION_JSON).characterEncoding("utf-8").content(json).accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk()).andExpect(jsonPath("$.success").value("true")).andExpect(jsonPath("$.message").value("User registered successfully"));
// Test when user provided email already exists in the database
Mockito.when(userService.registerNewUser(any(SignUpRequest.class))).thenThrow(new UserAlreadyExistAuthenticationException("exists"));
json = mapper.writeValueAsString(signUpRequest);
mockMvc.perform(post("/api/auth/signup").contentType(MediaType.APPLICATION_JSON).characterEncoding("utf-8").content(json).accept(MediaType.APPLICATION_JSON))
.andExpect(status().isBadRequest()).andExpect(jsonPath("$.success").value("false")).andExpect(jsonPath("$.message").value("Email Address already in use!"));
}
@Test
public void testVerifyCodeWhenCodeIsNotValid() throws Exception {
Mockito.when(verifier.isValidCode(Mockito.anyString(), Mockito.anyString())).thenReturn(false);
String json = mapper.writeValueAsString("443322");
mockMvc.perform(post("/api/auth/verify").contentType(MediaType.APPLICATION_JSON).characterEncoding("utf-8").content(json).accept(MediaType.APPLICATION_JSON))
.andExpect(status().isBadRequest()).andExpect(jsonPath("$.success").value("false")).andExpect(jsonPath("$.message").value("Invalid Code!"));
}
@Test
public void testVerifyCodeWhenCodeIsValid() throws Exception {
Mockito.when(verifier.isValidCode(Mockito.anyString(), Mockito.anyString())).thenReturn(true);
String json = mapper.writeValueAsString("443322");
mockMvc.perform(post("/api/auth/verify").contentType(MediaType.APPLICATION_JSON).characterEncoding("utf-8").content(json).accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk()).andExpect(jsonPath("$.authenticated").value("true")).andExpect(jsonPath("$.accessToken").isNotEmpty())
.andExpect(jsonPath("$.user").exists());
}
}
Points to be noted:
- In the
init()
method, we have created anUsernamePasswordAuthenticationToken
with our customLocalUser
andpassword
and set it in theSecurityContextHolder
. - During authentication, the spring security DaoAuthenticationProvider will retrieve the user by invoking the UserDetailsService.loadUserByUsername() method. In our custom LocalUserDetailService implementation, it will call our UserService.findUserByEmail(email) method to fetch the user details by email from the database. Hence, we have stubbed this method to return our user object using
Mockito.when(...).thenReturn(...)
. - Once the user is retrieved, the PasswordEncoder.matches() method will be invoked to verify if the password supplied in the UsernamePasswordAuthenticationToken matches with the one returned from the database. Hence we have created a
PasswordEncoder
mock using@MockBean
and stubbed thematches()
method to always return totrue
.
Run JUnit Tests
References
https://docs.spring.io/spring-security/site/docs/3.0.x/reference/security-filter-chain.html
Source Code
https://github.com/JavaChinna/angular-spring-boot-razorpay-integration
Conclusion
That’s all folks. In this article, we have explored various options for disabling spring security or mocking authentication for JUnit tests.
It would be nice to print/mention actual output to understand what is success , message etc
mockMvc….. .andExpect(status().isOk()).andExpect(jsonPath(“$.success”).value(“true”)).andExpect(jsonPath(“$.message”).isNotEmpty());
}
I have no clue now
my end points return DTO and not ResponseEntity , hence no $.success