I have a custom implementation of WebSecurityConfigurerAdapter where I override the config() method to authorize requests with matchers.
I need to create unit tests that use mock mvc to send requests to my controllers to make sure that they are being blocked properly. But when I run my tests, they don't load my implentation of WebSecurityConfigurerAdapter.
Overriden WebSecurityConfigurerAdapter::configure() method from my SecurityConfigSso.class:
#Override
protected void configure( HttpSecurity http ) throws Exception {
http.authorizeRequests()
.antMatchers( "/img/**", "lib/**", "/api/event**", "/api/event/**","/login/cas**" ).permitAll()
.antMatchers(HttpMethod.GET, "/**").hasAnyAuthority(AvailableRoles.ANY)
.antMatchers(HttpMethod.POST, "/**").hasAnyAuthority(AvailableRoles.ADMIN, AvailableRoles.GIS_ANALYST)
.antMatchers(HttpMethod.PUT, "/**").hasAnyAuthority(AvailableRoles.ADMIN, AvailableRoles.GIS_ANALYST)
.antMatchers(HttpMethod.DELETE, "/**").hasAnyAuthority(AvailableRoles.ADMIN, AvailableRoles.GIS_ANALYST)
.anyRequest().authenticated();
}
Here is my unit test:
#RunWith(SpringJUnit4ClassRunner.class)
#WebAppConfiguration
#ContextConfiguration(classes = { SecurityConfigSso.class })
public class SecurityTestControllerTests {
private final String SECURITY_URL = "/security";
private MockMvc mockMvc;
#Autowired
private WebApplicationContext context;
#Before
public void init() {
Assert.assertNotNull(context);
mockMvc = MockMvcBuilders.webAppContextSetup(context).build();
}
#Test
public void postMethodShouldBeForbiddenToGuest() throws Exception {
this.mockMvc.perform(post(SECURITY_URL).with(user("test").roles(AvailableRoles.GUEST)))
.andExpect(status().isForbidden()).andReturn();
}
}
The result of this test should be a 403 from the server, but it's still 200... :(
You need to add security to the mockMvc:
mockMvc = MockMvcBuilders.webAppContextSetup(context)
.apply(org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity())
.build();
For an example, have a look at https://github.com/spring-projects/spring-security/blob/master/test/src/test/java/org/springframework/security/test/web/servlet/showcase/secured/SecurityRequestsTests.java
Related
I have the following controller:
#CrossOrigin
#RestController
#RequestMapping("api")
public class MyController {
#GetMapping("/principal")
public void principalEndpoint(Principal user) {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
System.out.println(user);
System.out.println(authentication);
}
}
and the corresponding integration test whcich uses #WithMockUser as described in the docs:
/**
* implementation according to https://docs.spring.io/spring-security/reference/servlet/test/index.html
*/
#ExtendWith(SpringExtension.class)
#SpringBootTest
#WebAppConfiguration
#ContextConfiguration
public class MyControllerIT {
#Autowired
private WebApplicationContext context;
private MockMvc mvc;
#BeforeEach
public void setup() {
mvc = MockMvcBuilders
.webAppContextSetup(context)
.apply(springSecurity())
.build();
}
#Test
#WithMockUser(value = "Dani", username = "TAATIDA3")
public void testWithPrincipal() throws Exception {
mvc.perform(get("/api/principal").principal(new PrincipalImpl()))
.andExpect(status().isOk());
}
}
PrincipalImpl is a simple implementation of Principal:
public class PrincipalImpl implements Principal {
#Override
public String getName() {
return "MOCKUSER";
}
}
I also have the following SpringBoot configuration to authorize requests under the /api path:
#Configuration
#EnableResourceServer
#EnableCaching
#EnableScheduling
#EnableMongoAuditing
public class MyApiConfig extends ResourceServerConfigurerAdapter {
#Override
public void configure(HttpSecurity http) throws Exception {
http.antMatcher("/api/**").authorizeRequests()
.antMatchers("/api/**").not().anonymous();
}
}
My problem is that the request dispatched by mockMvc in MyControllerIT fails because a HTTP Status 401 is returned (not authorized). It would work if I change the HttpSecurity configuration to this
http.antMatcher("/api/**").authorizeRequests()
.antMatchers("/api/**").permitAll();
then the request succeeds (HTTP status 200), but no Principal is injected and the Authentication object from SecurityContextHolder.getContext().getAuthentication() is from an anonymous user:
null
AnonymousAuthenticationToken [Principal=anonymousUser, Credentials=[PROTECTED], Authenticated=true, Details=WebAuthenticationDetails [RemoteIpAddress=127.0.0.1, SessionId=null], Granted Authorities=[ROLE_ANONYMOUS]]
If I change the paths in MyApiConfig e.g. to this:
http.antMatcher("/someOtherApi/**").authorizeRequests()
.antMatchers("/someOtherApi/**").permitAll();
then the call from MyControllerIT succeeds and also a Principal is injected, which is what I want. However, in this case the actual api under /api/** is not secured anymore...
I'm quite new to the concepts of Spring Boot Security. Somehow I would have to override the MyApiConfig to configure HttpSecurity differently for tests (or use a separate configuration for test while at the same time excluding MyApiConfig). How do I do that, or what's the best way to make the HttpSecurity setup not interfere with MockMvc setup?
In my app I've got a custom filter added in WebSecurityConfigurerAdapter extension:
#Configuration
#EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
private static final RequestMatcher PROTECTED_URLS = new AntPathRequestMatcher("/v1/**");
#Override
public void configure(HttpSecurity http) throws Exception {
http
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.addFilterBefore(authenticationFilter(), AnonymousAuthenticationFilter.class)
.authorizeRequests()
.requestMatchers(PROTECTED_URLS)
.authenticated()
.and()
.csrf().disable()
.formLogin().disable()
.httpBasic().disable()
.logout().disable();
}
#Bean
AuthenticationFilter authenticationFilter() throws Exception {
final AuthenticationFilter filter = new AuthenticationFilter(PROTECTED_URLS);
// filter setup...
filter.setAuthenticationManager(authenticationManager());
return filter;
}
}
The filter itself, which is responsible for validating the access token by calling an external authorization server is defined as:
public class AuthenticationFilter extends AbstractAuthenticationProcessingFilter {
AuthenticationFilter(final RequestMatcher requiresAuth) {
super(requiresAuth);
}
#Override
public Authentication attemptAuthentication(HttpServletRequest httpServletRequest,
HttpServletResponse httpServletResponse)
throws AuthenticationException, IOException, OAuth2Exception {
try {
// Get Authorization header.
String token = httpServletRequest.getHeader(AUTHORIZATION);
// Check if the token is valid by calling an external authorization server.
// Returns some Authentication if successful.
} catch (OAuth2Exception exception) {
// Return 401
} catch (Exception exception) {
// All other errors are 500s
}
}
#Override
protected void successfulAuthentication(final HttpServletRequest request,
final HttpServletResponse response,
final FilterChain chain,
final Authentication authResult)
throws IOException, ServletException {
SecurityContextHolder.getContext().setAuthentication(authResult);
chain.doFilter(request, response);
}
}
What I'm trying to do is to perform integration test on the controller defined as:
#RestController
#RequestMapping(value = "/v1", produces = "application/json")
public class SomeController {
#Autowired
private SomeService someService;
#ResponseStatus(OK)
#PostMapping(value = "/a/path")
public SomeSuccessResponse pathHandlerMethod() {
return someService.someServiceMethod();
}
}
Finally, my test setup is as following:
#RunWith(SpringRunner.class)
#WebMvcTest(SomeController.class)
#Import(SecurityConfig.class)
#ContextConfiguration
#WebAppConfiguration
public class SomeControllerTest {
private MockMvc mockMvc;
#Autowired
private ObjectMapper objectMapper;
#Autowired
private WebApplicationContext context;
#MockBean
private SomeService someService;
#Before
public void setup() {
mockMvc = MockMvcBuilders
.webAppContextSetup(context)
.apply(springSecurity()) // When I comment out this line I'm getting 404 errors instead.
.build();
}
#Test
#WithMockUser
public void performIntegrationTest() throws Exception {
mockMvc.perform(post("/v1/a/path")).andExpect(status().isOk());
}
}
I'd like the authentication to be either turned off or somehow mocked out for this scenario - the actual code in AuthenticationFilter sholdn't be invoked at all. In order to achieve this, in SomeControllerTest class I've tried:
annotating test methods with #WithMockUser
setting mockMvc with MockMvcBuilders (see the setup() method above) with .apply(springSecurity()) and without it
annotating the SomeControllerTest class with #AutoConfigureMockMvc (with both secure and addFilters parameters set to false)
annotating the SomeControllerTest class with #ContextConfiguration and #WebAppConfiguration (I don't know if it changes anything)
None of these approaches disable the authentication. When I run the test, the AuthenticationFilter's attemptAuthentication() method calling external service is still invoked which I don't want to happen.
Disabling the filter sounds contradictory for an integration test, imho. Have you considered mocking the filter instead?
Create a
public class MockAuthenticationFilter implements Filter {
// return mock data for different use cases.
}
Then register this filter in your test.
#Before
public void setup() {
mockMvc = MockMvcBuilders.webAppContextSetup(context)
.apply(springSecurity(new MockAuthenticationFilter()))
.build();
}
This would also allow you to test different use cases where the filter acts one way or an other.
Today I updated my project from Spring Boot 1.5.9 to 2.1.1, and some of my tests stopped working. When i start the tests, error pops on console:
Field authEntryPoint in com.example.rest.config.SecurityConfig required a bean of type 'com.example.rest.service.auth.entrypoints.AuthenticationEntryPoint' that could not be found.
The problem is I have bean of this type defined in my SecurityConfig class, but I am overriding this configuration in my test package in TestApplication class. Security config is defined there as static inner class. I have tried different approaches including Spring profiles and #Primary annotation, but nothing seems to work and Spring doesn't pick my test configuration like it did before. Only thing that worked was when I deleted the non-test version of SecurityConfig class and test version became only bean of this type.
Can someone tell me how do I override this original configuration or how to turn off Spring Security just for testing? Or maybe there is a way to force Spring not to pick up that non-test #Configuration bean?
SecurityConfig.class
#Configuration
#EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired
AuthenticationEntryPoint authEntryPoint;
#Autowired
BasicAuthenticationProvider basicAuthProvider;
#Autowired
PreAuthenticatedUserDetailsService preAuthUserDetailsService;
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/rest/query/id/*/user/*",
"/rest/files/**/*").hasAnyRole("CLIENT", "SYSTEM")
.antMatchers("/public/api/management/**/*").hasRole("SYSTEM")
.antMatchers("/public/api/**/*").hasAnyRole("SYSTEM", "USER")
.antMatchers("/rest/**/*").hasRole("SYSTEM")
.and()
.x509()
.userDetailsService(preAuthUserDetailsService)
.and()
.httpBasic()
.authenticationEntryPoint(authEntryPoint)
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and().csrf().disable();
}
#Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(basicAuthProvider);
}
#Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/").antMatchers("/rest/files/name/**");
}
}
Test SpringBootClass with SecurityConfig inside
#SpringBootApplication
public class TestApplication {
#Configuration
#EnableWebSecurity
public static class SecurityConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().antMatchers("/**").permitAll()
.and().csrf().disable();
}
}
}
Example test from the suite
#RunWith(SpringRunner.class)
#WebMvcTest(DocumentManagementController.class)
public class DocumentManagementControllerTests {
#Autowired
MockMvc mvc;
#MockBean
SystemMetadataService systemMetadataService;
#MockBean
CustomMetadataService customMetadataService;
#MockBean
PrinterService printerService;
#MockBean
EventLoggerService eventLoggerService;
#Captor ArgumentCaptor<String> systemCaptor;
#Captor ArgumentCaptor<String> clientCaptor;
#Captor ArgumentCaptor<Boolean> holdCaptor;
#Captor ArgumentCaptor<String> retentionCaptor;
#Captor ArgumentCaptor<String> objectPathCaptor;
#Captor ArgumentCaptor<Boolean> accessCaptor;
#Captor ArgumentCaptor<Boolean> manualProcessingCaptor;
#Captor ArgumentCaptor<Boolean> incorrectCaptor;
#Captor ArgumentCaptor<Integer> statusCaptor;
#Captor ArgumentCaptor<Boolean> noTemplateCaptor;
#Test
public void setDocumentAccess_givenProperData_shouldReturnOk() throws Exception {
when(customMetadataService.setDocumentAccess(anyString(), anyBoolean()))
.then(inv -> new HcpCreateObjectResult(inv.getArgument(0)));
Boolean accessForbidden = true; String objectPath = "path";
mvc.perform(get("/rest/management/access/forbid/"+accessForbidden+"?objectPath="+objectPath))
.andExpect(status().isOk());
verify(customMetadataService).setDocumentAccess(objectPathCaptor.capture(), accessCaptor.capture());
assertThat(objectPathCaptor.getValue(), is(equalTo(objectPath)));
assertThat(accessCaptor.getValue(), is(equalTo(accessForbidden)));
}
I managed to do make this work using #Profile and #ActiveProfiles. But i had to extract my static inner #Configuration class to another java file and then it automagically started to work. Still haven't found why it worked in earlier version of Spring Boot
Below is my test class. The hello-world endpoint simply returns an HTML page containing text i.e. Hello Stranger!
#RunWith(SpringRunner.class)
#SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class HelloWorldTest {
#Autowired
private HelloWorldController controller;
#Autowired
private TestRestTemplate restTemplate;
#LocalServerPort
private int port;
#Test
public void contextLoads() throws Exception {
assertThat(controller).isNotNull();
}
#Test
public void greetingShouldReturnDefaultMessage() throws Exception {
String baseUrl = "http://localhost:" + port;
assertThat(this.restTemplate.getForObject(baseUrl+"/hello-world", String.class))
.contains("Hello Stranger!");
}
}
This is my Security Config:
#Configuration
#EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/", "/home").permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login")
.permitAll()
.and()
.logout()
.permitAll();
}
}
It simply redirects all authenticated users to the login page
I have tried adding #WithMockUser annotation or adding another security config class in my test directory to override the default config. But so far nothing has seemed to work.
Any help or suggestions on documentation to read is appreciated!
Another way to do it that worked for me was to override the normal security configation for running the integration test like so:
#TestConfiguration
#Order(-101)
#EnableWebSecurity
class TestSecurityConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity security) throws Exception {
security.httpBasic().and().formLogin().disable();
}
}
I have managed to solve this issue by first creating another web security config without requiring login/authorization, then by adding #Profile to my config class and production/dev/test profile via application.properties in my test directory (i.e. adding "spring.profiles.active=test").
Not sure if this is the best way to solve this issue, but it works for now.
I'm unable to configure correctly the security in my tests.
My web security configuration:
#Configuration
#EnableWebMvcSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/api/**").hasRole("USER")
.and()
.httpBasic()
;
}
}
And my test class:
#RunWith(SpringJUnit4ClassRunner.class)
#SpringApplicationConfiguration
#ContextConfiguration(classes = {Application.class, AppConfig.class, WebMvcConfig.class, WebSecurityConfig.class})
#WebAppConfiguration
public class TestControllerTest {
#Autowired
private WebApplicationContext wac;
private MockMvc mockMvc;
#Before
public void setUp() {
MockitoAnnotations.initMocks(this);
this.mockMvc = webAppContextSetup(wac).dispatchOptions(true).build();
}
#Test
public void getTest() throws Exception {
mockMvc
.perform(get("/api/test"))
.andExpect(status().isForbidden())
;
}
}
I get a 404 status code meaning the security layer is not executed, so it is not configured correctly in my test class.
I tried to switch the classes from #ContextConfiguration to #SpringApplicationConfiguration without success.
Make the following modifications to your code:
#Autowired
private FilterChainProxy filterChainProxy;
#Before
public void setUp() {
MockitoAnnotations.initMocks(this);
this.mockMvc = webAppContextSetup(wac).dispatchOptions(true).addFilters(filterChainProxy).build();
}
As said in reference for Spring Security 4.0.4:
In order to use Spring Security with Spring MVC Test it is necessary to add the Spring Security FilterChainProxy as a Filter. It is also necessary to add Spring Security’s TestSecurityContextHolderPostProcessor to support Running as a User in Spring MVC Test with Annotations. This can be done using Spring Security’s SecurityMockMvcConfigurers.springSecurity().
Example:
import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.*;
#RunWith(SpringJUnit4ClassRunner.class)
#ContextConfiguration
#WebAppConfiguration
public class TestControllerTest {
#Autowired
private WebApplicationContext wac;
private MockMvc mockMvc;
#Before
public void setup() {
mockMvc = MockMvcBuilders
.webAppContextSetup(wac)
.apply(springSecurity()) //will perform all of the initial setup to integrate Spring Security with Spring MVC Test
.build();
}