Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Spring Boot Authentication for Integration Tests

I'm trying to run an integration test for my controller but I am running into issues if I don't authenticate. Here's my controller:

@RunWith(SpringRunner.class) @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) @TestPropertySource(properties = {"security.basic.enabled=false", "management.security.enabled=false"}) @EnableAutoConfiguration(exclude = {org.springframework.boot.autoconfigure.security.SecurityAutoConfiguration.class}) public class HelloControllerIT {     private final ObjectMapper mapper = new ObjectMapper();     @Autowired private TestRestTemplate template;      @Test     public void test1() throws Exception {         ObjectNode loginRequest = mapper.createObjectNode();         loginRequest.put("username","name");         loginRequest.put("password","password");         JsonNode loginResponse = template.postForObject("/authenticate", loginRequest.toString(), JsonNode.class);          HttpHeaders headers = new HttpHeaders();         headers.setAccept(Arrays.asList(MediaType.APPLICATION_JSON));         headers.add("X-Authorization", "Bearer " + loginResponse.get("token").textValue());         headers.add("Content-Type", "application/json");         return new HttpEntity<>(null, headers);          HttpEntity request = getRequestEntity();         ResponseEntity response = template.exchange("/get",                                                     HttpMethod.GET,                                                     request,                                                     new ParameterizedTypeReference<List<Foo>>() {});         //assert stuff     } } 

When I run this, everything works. But if I comment out the line:

headers.add("X-Authorization", "Bearer " + loginResponse.get("token").textValue()); 

I get the error:

org.springframework.http.converter.HttpMessageNotReadableException: Could not read JSON document: Can not deserialize instance of java.util.ArrayList out of START_OBJECT token  at [Source: java.io.PushbackInputStream@272a5bc6; line: 1, column: 1]; nested exception is com.fasterxml.jackson.databind.JsonMappingException: Can not deserialize instance of java.util.ArrayList out of START_OBJECT token  at [Source: java.io.PushbackInputStream@272a5bc6; line: 1, column: 1]      at org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter.readJavaType(AbstractJackson2HttpMessageConverter.java:234)     at org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter.read(AbstractJackson2HttpMessageConverter.java:219)     at org.springframework.web.client.HttpMessageConverterExtractor.extractData(HttpMessageConverterExtractor.java:95)     at org.springframework.web.client.RestTemplate$ResponseEntityResponseExtractor.extractData(RestTemplate.java:917)     at org.springframework.web.client.RestTemplate$ResponseEntityResponseExtractor.extractData(RestTemplate.java:901)     at org.springframework.web.client.RestTemplate.doExecute(RestTemplate.java:655)     at org.springframework.web.client.RestTemplate.execute(RestTemplate.java:613)     at org.springframework.web.client.RestTemplate.exchange(RestTemplate.java:559)     at org.springframework.boot.test.web.client.TestRestTemplate.exchange(TestRestTemplate.java:812)     at com.test.HelloControllerIT.test1(HelloControllerIT.java:75)     at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)     at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)     at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)     at java.lang.reflect.Method.invoke(Method.java:498)     at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50)     at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)     at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47)     at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)     at org.junit.internal.runners.statements.RunBefores.evaluate(RunBefores.java:26)     at org.springframework.test.context.junit4.statements.RunBeforeTestMethodCallbacks.evaluate(RunBeforeTestMethodCallbacks.java:75)     at org.springframework.test.context.junit4.statements.RunAfterTestMethodCallbacks.evaluate(RunAfterTestMethodCallbacks.java:86)     at org.springframework.test.context.junit4.statements.SpringRepeat.evaluate(SpringRepeat.java:84)     at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325)     at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:252)     at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:94)     at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)     at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)     at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)     at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)     at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)     at org.springframework.test.context.junit4.statements.RunBeforeTestClassCallbacks.evaluate(RunBeforeTestClassCallbacks.java:61)     at org.springframework.test.context.junit4.statements.RunAfterTestClassCallbacks.evaluate(RunAfterTestClassCallbacks.java:70)     at org.junit.runners.ParentRunner.run(ParentRunner.java:363)     at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.run(SpringJUnit4ClassRunner.java:191)     at org.junit.runner.JUnitCore.run(JUnitCore.java:137)     at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:68)     at com.intellij.rt.execution.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:51)     at com.intellij.rt.execution.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:242)     at com.intellij.rt.execution.junit.JUnitStarter.main(JUnitStarter.java:70) Caused by: com.fasterxml.jackson.databind.JsonMappingException: Can not deserialize instance of java.util.ArrayList out of START_OBJECT token  at [Source: java.io.PushbackInputStream@272a5bc6; line: 1, column: 1]     at com.fasterxml.jackson.databind.JsonMappingException.from(JsonMappingException.java:270)     at com.fasterxml.jackson.databind.DeserializationContext.reportMappingException(DeserializationContext.java:1234)     at com.fasterxml.jackson.databind.DeserializationContext.handleUnexpectedToken(DeserializationContext.java:1122)     at com.fasterxml.jackson.databind.DeserializationContext.handleUnexpectedToken(DeserializationContext.java:1075)     at com.fasterxml.jackson.databind.deser.std.CollectionDeserializer.handleNonArray(CollectionDeserializer.java:338)     at com.fasterxml.jackson.databind.deser.std.CollectionDeserializer.deserialize(CollectionDeserializer.java:269)     at com.fasterxml.jackson.databind.deser.std.CollectionDeserializer.deserialize(CollectionDeserializer.java:259)     at com.fasterxml.jackson.databind.deser.std.CollectionDeserializer.deserialize(CollectionDeserializer.java:26)     at com.fasterxml.jackson.databind.ObjectMapper._readMapAndClose(ObjectMapper.java:3798)     at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:2922)     at org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter.readJavaType(AbstractJackson2HttpMessageConverter.java:231)     ... 38 more 

Obviously the security annotations at the top are not working. So what exactly is the issue and how do I fix it?

Edit 1: I tried doing:

Object response = template.exchange("/get", HttpMethod.GET, request, Object.class); 

And got:

<401 Unauthorized,{status=401, message=Authentication failed, errorCode=10, timestamp=1497654855545},{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], X-Frame-Options=[DENY], Content-Type=[application/json;charset=ISO-8859-1], Content-Length=[89], Date=[Fri, 16 Jun 2017 23:14:15 GMT]}> 

For our security we're using org.springframework.security.authentication.AuthenticationProvider and org.springframework.security.authentication.AuthenticationManager

Edit 2: Per skadya's suggestion I created a new class like so:

@Configuration public class AnonymousConfig extends WebSecurityConfigurerAdapter {     @Override     public void configure(HttpSecurity web) throws Exception {         web.antMatcher("**/*").anonymous();     } } 

But now when I run my integration test I get the following error:

java.lang.IllegalStateException: Failed to load ApplicationContext      at org.springframework.test.context.cache.DefaultCacheAwareContextLoaderDelegate.loadContext(DefaultCacheAwareContextLoaderDelegate.java:124)     at org.springframework.test.context.support.DefaultTestContext.getApplicationContext(DefaultTestContext.java:83)     at org.springframework.boot.test.autoconfigure.SpringBootDependencyInjectionTestExecutionListener.prepareTestInstance(SpringBootDependencyInjectionTestExecutionListener.java:47)     at org.springframework.test.context.TestContextManager.prepareTestInstance(TestContextManager.java:230)     at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.createTest(SpringJUnit4ClassRunner.java:228)     at org.springframework.test.context.junit4.SpringJUnit4ClassRunner$1.runReflectiveCall(SpringJUnit4ClassRunner.java:287)     at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)     at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.methodBlock(SpringJUnit4ClassRunner.java:289)     at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:247)     at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:94)     at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)     at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)     at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)     at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)     at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)     at org.springframework.test.context.junit4.statements.RunBeforeTestClassCallbacks.evaluate(RunBeforeTestClassCallbacks.java:61)     at org.springframework.test.context.junit4.statements.RunAfterTestClassCallbacks.evaluate(RunAfterTestClassCallbacks.java:70)     at org.junit.runners.ParentRunner.run(ParentRunner.java:363)     at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.run(SpringJUnit4ClassRunner.java:191)     at org.junit.runner.JUnitCore.run(JUnitCore.java:137)     at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:68)     at com.intellij.rt.execution.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:51)     at com.intellij.rt.execution.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:242)     at com.intellij.rt.execution.junit.JUnitStarter.main(JUnitStarter.java:70) Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'org.springframework.security.config.annotation.web.configuration.WebSecurityConfiguration': Injection of autowired dependencies failed; nested exception is java.lang.IllegalStateException: @Order on WebSecurityConfigurers must be unique. Order of 100 was already used on config.AnonymousConfig$$EnhancerBySpringCGLIB$$ba18b8d7@6291f725, so it cannot be used on security.WebSecurityConfig$$EnhancerBySpringCGLIB$$9d88e7e@1bfaaae1 too.     at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor.postProcessPropertyValues(AutowiredAnnotationBeanPostProcessor.java:372)     at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.populateBean(AbstractAutowireCapableBeanFactory.java:1264)     at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:553)     at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:483)     at org.springframework.beans.factory.support.AbstractBeanFactory$1.getObject(AbstractBeanFactory.java:306)     at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:230)     at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:302)     at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:197)     at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:761)     at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:866)     at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:542)     at org.springframework.boot.context.embedded.EmbeddedWebApplicationContext.refresh(EmbeddedWebApplicationContext.java:122)     at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:737)     at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:370)     at org.springframework.boot.SpringApplication.run(SpringApplication.java:314)     at org.springframework.boot.test.context.SpringBootContextLoader.loadContext(SpringBootContextLoader.java:120)     at org.springframework.test.context.cache.DefaultCacheAwareContextLoaderDelegate.loadContextInternal(DefaultCacheAwareContextLoaderDelegate.java:98)     at org.springframework.test.context.cache.DefaultCacheAwareContextLoaderDelegate.loadContext(DefaultCacheAwareContextLoaderDelegate.java:116)     ... 23 more Caused by: java.lang.IllegalStateException: @Order on WebSecurityConfigurers must be unique. Order of 100 was already used on config.AnonymousConfig$$EnhancerBySpringCGLIB$$ba18b8d7@6291f725, so it cannot be used on security.WebSecurityConfig$$EnhancerBySpringCGLIB$$9d88e7e@1bfaaae1 too.     at org.springframework.security.config.annotation.web.configuration.WebSecurityConfiguration.setFilterChainProxySecurityConfigurer(WebSecurityConfiguration.java:148)     at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)     at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)     at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)     at java.lang.reflect.Method.invoke(Method.java:498)     at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor$AutowiredMethodElement.inject(AutowiredAnnotationBeanPostProcessor.java:701)     at org.springframework.beans.factory.annotation.InjectionMetadata.inject(InjectionMetadata.java:88)     at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor.postProcessPropertyValues(AutowiredAnnotationBeanPostProcessor.java:366)     ... 40 more 

Looks like it's clashing with the websecurity config we have in the normal project. Here's that file:

@EnableWebSecurity @EnableWebMvc @EnableGlobalMethodSecurity(prePostEnabled = true) public class WebSecurityConfig extends WebSecurityConfigurerAdapter { //configuration } 

I tried adding @Order(1000) which fixed the above issue but still ended up in a 401 Unauthorized

like image 965
Richard Avatar asked Jun 09 '17 21:06

Richard


2 Answers

You can try excluding few more auto configurations:

@EnableAutoConfiguration(exclude = { org.springframework.boot.autoconfigure.security.SecurityAutoConfiguration.class, org.springframework.boot.autoconfigure.security.SecurityFilterAutoConfiguration.class, org.springframework.boot.autoconfigure.security.FallbackWebSecurityAutoConfiguration.class, org.springframework.boot.autoconfigure.security.oauth2.OAuth2AutoConfiguration.class }) 

Btw, more elegant way of excluding stuff is by defining application-test.properties in your test sources and marking your test with @Profile("test"). Then just add this to your config:

spring.autoconfigure.exclude=org.springframework.boot.autoconfigure.security.SecurityAutoConfiguration,org.springframework.boot.autoconfigure.security.SecurityFilterAutoConfiguration,org.springframework.boot.autoconfigure.security.FallbackWebSecurityAutoConfiguration,org.springframework.boot.autoconfigure.security.oauth2.OAuth2AutoConfiguration 

All the possible configurations that can be excluded you can find here: spring.factories

like image 191
Danylo Zatorsky Avatar answered Oct 04 '22 04:10

Danylo Zatorsky


You have couple of options to provide authentication in the spring boot integration test. You may need to adjust a few things to make it all work at your end.

Mock based approach

This uses test WebApplicationContext injected into MockMvc with @WithMockUser annotation to provide authentication user and WithMockUserSecurityContextFactory creating the security context for the mock user.

SecurityMockMvcConfigurers registers the security filter springSecurityFilterChain with MockMvc.

import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.http.MediaType; import org.springframework.security.test.context.support.WithMockUser; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.web.context.WebApplicationContext;  import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.notNullValue; import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;  @RunWith(SpringRunner.class) @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK) public class HelloControllerIT  {      @Autowired     private WebApplicationContext context;      private  MockMvc mvc;      @Before     public void setup() {         mvc = MockMvcBuilders                 .webAppContextSetup(context)                 .apply(springSecurity()) // enable security for the mock set up                 .build();     }      @WithMockUser(value = "test", password = "pass")     @Test     public void test() throws Exception {         String contentType = MediaType.APPLICATION_JSON + ";charset=UTF-8";          String authzToken = mvc                 .perform(                         post("/authenticate")                                 .contentType(                                         MediaType.APPLICATION_JSON).                             content("")).                  andExpect(status().isOk())                 .andExpect(content().contentType(contentType))                 .andExpect(jsonPath("$.token", is(notNullValue())))                 .andReturn().getResponse().getContentAsString();          System.out.print(authzToken);//{"token":"1a3434a"}      }  } 

In-memory auth provider based approach

This uses in-memory auth provider with basic authentication user.

Register in-memory auth provider and enable basic auth, disable anonymous access in HttpSecurity in the WebSecurityConfigurerAdapter.

When in-memory provider is registered, DefaultInMemoryUserDetailsManagerConfigurer creates the basic auth user in the memory.

When basic authentication is enabled, HttpBasicConfigurer configures BasicAuthenticationFilter. Authenticates the test user and creates the security context.

Security Configuration

@EnableWebSecurity @EnableWebMvc @EnableGlobalMethodSecurity(prePostEnabled = true) public class WebSecurityConfig extends WebSecurityConfigurerAdapter {      @Override     public void configure(AuthenticationManagerBuilder auth) throws Exception {                     // register test user with in memory authentication provider          auth.inMemoryAuthentication().withUser("test").password("pass").roles("ROLES");     }                  @Override     public void configure(HttpSecurity http) throws Exception {                     // enable basic authentication & disable anoymous access         http.authorizeRequests().anyRequest().authenticated().and().httpBasic().and().anonymous().disable();         }  } 

Authentication Endpoint

@Controller @RequestMapping("/authenticate") public class AuthenticationController {      @RequestMapping(method = RequestMethod.POST)     @ResponseBody     public TokenClass getToken() {         TokenClass tokenClass = new TokenClass();         tokenClass.setToken("1a3434a");         return tokenClass;     }  } 

Pojo

public class TokenClass {      private String token;      public String getToken() {         return token;     }      public void setToken(String token) {         this.token = token;     } } 

Test Controller

import com.fasterxml.jackson.databind.JsonNode; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.web.client.TestRestTemplate; import org.springframework.http.*; import org.springframework.test.context.junit4.SpringRunner;  import java.util.Arrays; import java.util.Base64;  @RunWith(SpringRunner.class) @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) public class HelloControllerIT  {      @Autowired     private TestRestTemplate template;      @Test     public void test() throws Exception {         HttpHeaders authHeaders = new HttpHeaders();         String token = new String(Base64.getEncoder().encode(                 ("test" + ":" + "pass").getBytes()));         authHeaders.set("Authorization", "Basic " + token);         JsonNode loginResponse = template.postForObject("/authenticate", new HttpEntity<>(null, authHeaders), JsonNode.class);          HttpHeaders authzHeaders = new HttpHeaders();         authzHeaders.setAccept(Arrays.asList(MediaType.APPLICATION_JSON));         authzHeaders.add("X-Authorization", "Bearer " + loginResponse.get("token").textValue());         authzHeaders.add("Content-Type", "application/json");          ResponseEntity response = template.exchange("/secure",                 HttpMethod.GET,                 new HttpEntity<>(null, authzHeaders),                 String.class         );     } } 
like image 28
s7vr Avatar answered Oct 04 '22 05:10

s7vr