Four Methods to Disable Spring Security in JUnit Tests

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 API’s 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 falls 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 approch, we will not actually disable the security. Instead, we will be running the tests with mock user 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 falls 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 class 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 a Http POST endpoint which 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

Order REST Controller Unit Test Result

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 an UsernamePasswordAuthenticationToken with our custom LocalUser and password and set it in the SecurityContextHolder.
  • 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 database. Hence we have created a PasswordEncoder mock using @MockBean and stubbed the matches() method to always return to true.

Run Junit Tests

Authentication REST Controller Unit Test Result

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 authenction for JUnit tests.

Thank you for reading.

Leave a Reply