I have a Spring Boot + Spring Security application that has severalantMatchers
paths; some fullyAuthenticated()
, some permitAll()
.
How to I write a test that verifies SecurityConfiguration
has my endpoints under /api/**
(and ultimately others) secured correctly?
public class SecurityConfiguration extends WebSecurityConfigurerAdapter { protected void configure(HttpSecurity http) throws Exception { http //... .antMatchers("/api/**").fullyAuthenticated() } }
Using spring-boot-1.5.2.RELEASE
, spring-security-core-4.2.2-release
.
Clarification1: I want to as-directly-as-possible test the SecurityConfiguration
, as opposed to transitively testing via one of the /api/**
endpoints, which may have their own @PreAuthorize
security.
Clarification2: I would like something similar to this WebSecurityConfigurerAdapterTests.
3 Answers
Answers 1
I see below test case can help you achieve what you want. It is an Integration Test to test the Web Security configuration and we have similar testing done for all our code that is TDD driven.
@RunWith(SpringRunner.class) @SpringBootTest(classes = Application.class) @WebAppConfiguration public class WebConfigIT { private MockMvc mockMvc; @Autowired private WebApplicationContext webApplicationContext; @Autowired private FilterChainProxy springSecurityFilterChain; @Before public void setup() throws Exception { mockMvc = webAppContextSetup(webApplicationContext) .addFilter(springSecurityFilterChain) .build(); } @Test public void testAuthenticationAtAPIURI() throws Exception { mockMvc.perform(get("/api/xyz")) .andExpect(status.is3xxRedirection()); }
This though looks like doing an explicit testing of the end-point (which is anyways a testing one have to do if doing TDD) but this is also bringing the Spring Security Filter Chain in context to enable you test the Security Context for the APP.
Answers 2
MockMVC should be enough to verify you security configuration since the only thing it mocks is the Http layer. However if you really wish to test your Spring Boot application, Tomcat server and all, you need to use @SpringBootTest
, like this
@RunWith(SpringJUnit4ClassRunner.class) @SpringBootTest(webEnvironment= SpringBootTest.WebEnvironment.RANDOM_PORT) public class NoGoServiceTest { @LocalServerPort private int port; private <T> T makeDepthRequest(NoGoRequest request, NoGoResponse response, String path, Class<T> responseClass) { testService.addRequestResponseMapping(request, response); RestTemplate template = new RestTemplate(); HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); headers.setAccept(Lists.newArrayList(MediaType.APPLICATION_JSON)); headers.add("Authorization", "Bearer " + tokenProvider.getToken()); RequestEntity<NoGoRequest> requestEntity = new RequestEntity<>(request, headers, HttpMethod.POST, getURI(path)); ResponseEntity<T> responseEntity = template.exchange(requestEntity, responseClass); return responseEntity.getBody(); } @SneakyThrows(URISyntaxException.class) private URI getURI(String path) { return new URI("http://localhost:" +port + "/nogo" + path); } // Test that makes request using `makeDepthRequest` }
This code is a part on a test taken from an open source project (https://github.com/maritime-web/NoGoService). The basic idea is to start the test on a random port, which Spring will then inject into a field on the test. This allows you to construct URLs and use Springs RestTemplate
to make http request to the server, using the same DTO classes as your Controllers. If the authentication mechanism is Basic or Token you simply have to add the correct Authorization header as in this example. If you use Form authentication, then it becomes a bit harder, because you first have to GET /login
, then extract the CSRF token and the JSessionId cookie, and the POST
them with the credentials to /login
, and after login you have to extract the new JSessionId cookie, as the sessionId is changed after login for security reasons.
Hope this was what you needed.
Answers 3
If you want to programatically know which endpoints exist, you can autowire the List of RequestHandlerProvider
into your test and filter them based on the path they are exposed on.
@Autowired List<RequestHandlerProvider> handlerProviders; @Test public void doTest() { for (RequestHandlerProvider handlerProvider : handlerProviders) { for (RequestHandler requestHandler : handlerProvider.requestHandlers()) { for (String pattern : requestHandler.getPatternsCondition().getPatterns()) { // call the endpoint without security and check that you get 401 } } } }
Using the RequestHandlerProvider
is how SpringFox determines which endpoint are available and their signature, when it build the swagger definition for an API.
Unless you spend a long time building the correct input for each endpoint you will not get 200 OK back from the endpoint when including a valid security token, so you probably have to accept 400 as a correct response.
If you are already worried some developer would make security related mistakes when introducing a new endpoint, I would be equally worried about the logic of the endpoint, which is why I think you should have an integration test for each of them, and that would test your security as well.
0 comments:
Post a Comment