I'm developing Spring boot and security web application with security that is WebSecurityConfigurerAdapter
based. This application has two WebSecurityConfigurerAdapters
for two authorization types - login form and bearer with JWT.
There are some simple rest controllers with JWT protected endpoints. WebSecurityConfigurerAdapter
implementation for the bearer with JWT is as follows:
@Configuration
@Order(2)
public class SecurityConfigRest extends WebSecurityConfigurerAdapter {
protected void configure(HttpSecurity http) throws Exception {
http.requestMatcher(new AntPathRequestMatcher("/rest/**")).authorizeRequests()
.regexMatchers(HttpMethod.POST,"/rest/products/add/?").hasRole("ADMIN")
.antMatchers(HttpMethod.GET,"/rest/products/store/**").hasRole("ADMIN")
.regexMatchers(HttpMethod.POST,"/rest/store/add/?").hasRole("ADMIN")
.regexMatchers(HttpMethod.POST,"/rest/store/\\d/brands/?").hasRole("ADMIN")
.anyRequest().authenticated()
.and()
.csrf().disable()
.oauth2ResourceServer().jwt()
.jwtAuthenticationConverter(jwtAuthenticationConverter()); // - simple custom converter
}
}
I'm creating a unit test for those JWT protected endpoints:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.core.env.Environment;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
import java.math.BigDecimal;
import java.net.URI;
import java.util.Collections;
import java.util.Optional;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
@RunWith(SpringRunner.class)
@WebMvcTest(ProductControllerRest.class )
@ContextConfiguration(classes = WebMwcTestConfig.class)
@ActiveProfiles("test")
public class ProductControllerRestIntegrationTest {
@Autowired
private MockMvc mockMvc;
@Test
public void testProductAdd() throws Exception {
String scheme = env.getProperty(SERVER_SSL_ENABLED, Boolean.class, Boolean.TRUE) ? "https" : "http";
String port = Optional.ofNullable(env.getProperty(SERVER_PORT))
.map(":"::concat).orElse("");
StringBuilder uriBuilder = new StringBuilder(scheme).append("://").append("localhost").append(port)
.append("/rest/products/add/");
URI uri = URI.create(uriBuilder.toString());
MvcResult mvcResult = mockMvc.perform(post(uri)
.secure(true)
.accept(MediaType.APPLICATION_JSON)
.header(HttpHeaders.AUTHORIZATION, "Bearer test-token")
.contentType(MediaType.APPLICATION_JSON_VALUE)
.content(getStringJson(productDto))).andReturn();
verify(productRepository).save(productCaptor.capture());
......
}
}
JavaConfig contains aplication context moks for WebSecurityConfigurerAdapter
and JWT converter/decoder for the spring security filter chain:
import local.authorization.resource.server.controller.rest.ProductControllerRest;
import local.authorization.resource.server.security.SecurityConfigRest;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import java.util.Arrays;
import java.util.Collections;
@Configuration
public class WebMwcTestConfig {
@Bean
public SecurityConfigRest securityConfigRest() {
return new SecurityConfigRest();
}
@Bean // - add JwtDecoder mock to application context to be used in JwtAuthenticationProvider
public JwtDecoder jwtDecoderAdmin() {
return (token) -> createJwtToken("testAdmin", "ROLE_ADMIN");
}
@Bean
public UserDetailsService userDetailsService() {
User basicUserTest = new User("testUser","testUserPass", Arrays.asList(
new SimpleGrantedAuthority("USER")
));
User managerActiveUser = new User("testAdmin","testAdminPass", Arrays.asList(
new SimpleGrantedAuthority("ADMIN")
));
return new InMemoryUserDetailsManager(Arrays.asList(
basicUserTest, managerActiveUser
));
}
private Jwt createJwtToken(String userName, String role) {
String userId = "AZURE-ID-OF-USER";
String applicationId = "AZURE-APP-ID";
return Jwt.withTokenValue("test-token")
.header("typ", "JWT")
.header("alg", "none")
.claim("oid", userId)
.claim("user_name", userName)
.claim("azp", applicationId)
.claim("ver", "1.0")
.claim("authorities", Collections.singletonList(role))
.subject(userId)
.build();
}
}
But after MockHttpServletRequest
is passing the spring security filter chain and reaches rest controller, MockMvc returns MockHttpServletResponse
with 404 status:
MockHttpServletRequest:
HTTP Method = POST
Request URI = /rest/products/add/
Parameters = {}
Headers = [Content-Type:"application/json;charset=UTF-8", Accept:"application/json", Authorization:"Bearer test-token", Content-Length:"125"]
Body = {
"name" : "test_product_1_name",
"description" : "test_product_1_description",
"price" : 0.1,
"storeId" : 100
}
Session Attrs = {}
Handler:
Type = org.springframework.web.servlet.resource.ResourceHttpRequestHandler
Async:
Async started = false
Async result = null
Resolved Exception:
Type = null
ModelAndView:
View name = null
View = null
Model = null
FlashMap:
Attributes = null
MockHttpServletResponse:
Status = 404
Error message = null
Headers = [Vary:"Origin", "Access-Control-Request-Method", "Access-Control-Request-Headers", X-Content-Type-Options:"nosniff", X-XSS-Protection:"1; mode=block", Cache-Control:"no-cache, no-store, max-age=0, must-revalidate", Pragma:"no-cache", Expires:"0", Strict-Transport-Security:"max-age=31536000 ; includeSubDomains", X-Frame-Options:"DENY"]
Content type = null
Body =
Forwarded URL = null
Redirected URL = null
Cookies = []
It seems that the reason is that ProductControllerRest hasn't been mocked out and added to the application context by @WebMvcTest(controllers = ProductControllerRest.class)
. Here is indicated that @WebMvcTest
is the right way for controllers mocking.
@SpringBootTest(webEnvironment = MOCK)
@AutoConfigureMockMvc
is resulting in the same - being able to pass security chain, MockHttpServletResponse status is still 404
I was trying also :
@Autowired
private WebApplicationContext webAppContext;
mockMvc = MockMvcBuilders.webAppContextSetup(webAppContext).build(); // - init mockMvc with webAppContext Autowired
In this case security filter chain isn't being passed.
Are there other configurations and options how to mock a rest controller out with @WebMvcTest
or @SpringBootTest
?