Integration test for rest controller with #WebMvcTest or #SpringBootTest results in 404. (Spring boot) - spring-boot

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.
Another oprinon:
#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?

Related

Spring Boot TestRestTemplate return null body

Havin a quite very simple test class followed by a video tut:
#SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
public class TestClassTest {
#LocalServerPort
private int port;
#Autowired
private TestRestTemplate testRestTemmplate;
#Test
public void test_one() {
String url = "http://localhost:" + port + "/v1/read";
UriComponents builder = UriComponentsBuilder.fromHttpUrl(url).build();
HttpHeaders headers = new HttpHeaders();
headers.set("accept", "application/json");
HttpEntity<String> requestEntity = new HttpEntity<>(null, headers);
ResponseEntity<String> response = testRestTemmplate.exchange(builder.toString(), HttpMethod.GET, requestEntity, String.class);
System.out.println(response.getBody());
assertEquals(HttpStatus.OK, response.getStatusCode());
}
}
The underlying controller class for test looks like:
#RestController
public class TestController {
#RequestMapping(value = "/v1/read", method = RequestMethod.GET, produces = "application/json")
public ResponseEntity<String> data() {
return ResponseEntity.status(HttpStatus.OK).header("status", "completed").body("fertig!");
}
}
Some issues i wondering with:
Running the test will evaluate to green thats fine but if i change the url in the test it will be also OK
The body from response is null and
the header i set within the ResponseEntity is no part of the response from testResttemplate.
Where are my misunderstandings?
Edit: here are my imports:
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.boot.test.web.server.LocalServerPort;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.web.util.UriComponents;
import org.springframework.web.util.UriComponentsBuilder;
As seen this are only the "standard" import coming from spring dependencies. Nothing exotics.

Getting 500 error while testing webflux code with wiremock

I have spring webflux app with user controller class with end point "/user and user service class.The user service class making call to external api. I am trying to test the service class using wiremock and junit 5 to mock out external api.. However I am getting below error ->
021-07-30 18:22:52.511 ERROR 16974 --- [o-auto-1-exec-2] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] threw exception
java.net.ConnectException: Connection refused
Code is uploaded at path : https://github.com/neeleshsethi/wiremockdemp/tree/master
It seems it cannot find controller as adding a print statement in controller is not printing anything. Below is the code ->
#Service
public class UserService {
#Autowired
WebClient webClient;
public Mono<User> createuser(User user) {
return webClient.post()
.uri("/usercreate")
.contentType(MediaType.APPLICATION_JSON)
.accept(MediaType.APPLICATION_JSON)
.body(BodyInserters.fromValue(user))
.retrieve()
.bodyToMono(User.class);
}
}
WireMock Inti class:
package com.example.wiremockdemo;
import com.github.tomakehurst.wiremock.WireMockServer;
import com.github.tomakehurst.wiremock.core.WireMockConfiguration;
import org.springframework.boot.test.util.TestPropertyValues;
import org.springframework.context.ApplicationContextInitializer;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.event.ContextClosedEvent;
import java.util.Map;
public class WireMockInit implements ApplicationContextInitializer<ConfigurableApplicationContext> {
#Override
public void initialize(ConfigurableApplicationContext applicationContext) {
WireMockServer wireMockServer = new WireMockServer(new WireMockConfiguration().dynamicPort());
wireMockServer.start();
applicationContext.addApplicationListener( applicationEvent ->
{
if(applicationEvent instanceof ContextClosedEvent)
{
wireMockServer.stop();
}
}
);
applicationContext.getBeanFactory().registerSingleton("wireMockServer", wireMockServer);
TestPropertyValues.of("externalBaseUrl",wireMockServer.baseUrl())
.applyTo(applicationContext);
}
}
test class:
package com.example.wiremockdemo;
import com.example.wiremockdemo.model.User;
import com.github.tomakehurst.wiremock.WireMockServer;
import com.github.tomakehurst.wiremock.client.WireMock;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.web.server.LocalServerPort;
import org.springframework.http.MediaType;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.web.reactive.server.WebTestClient;
import org.springframework.web.reactive.function.BodyInserters;
import reactor.core.publisher.Mono;
import java.awt.*;
import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
#SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
#ContextConfiguration(initializers = {WireMockInit.class})
class WiremockdemoApplicationTests {
#Autowired
WebTestClient webTestClient;
#Autowired
private WireMockServer wireMockServer;
#LocalServerPort
private Integer port;
#Test
void createUsertest() {
System.out.println("Creating stub");
wireMockServer.stubFor(
WireMock.post("/usercreate")
.willReturn(
aResponse()
.withHeader("Content-Type", MediaType.APPLICATION_JSON_VALUE)
.withBodyFile("response.json"))
);
byte[] temp = webTestClient.post()
.uri("http://localhost:" + port + "/user")
// .uri("/user")
.contentType(MediaType.APPLICATION_JSON)
//.accept(MediaType.APPLICATION_JSON)
.body(BodyInserters.fromValue(createUser()))
.exchange()
.expectBody()
.returnResult()
.getResponseBody();
String s = new String(temp);
System.out.println("Response :" +s);
}
public User createUser()
{
return User.builder()
.firstName("neel")
.age(32)
.id(1234)
.build();
}
}
Change this line to:
TestPropertyValues.of(Map.of("externalBaseUrl", wireMockServer.baseUrl()));
This is the correct way to set test properties.

JWT Authorization Kotlin Coroutines Spring Security and Spring WebFlux

I'm trying to implement Authentication & Authorization with Spring Boot (2.3.1.RELEASE) WebFlux + Kotlin + Coroutines, I think that I reach a point where I cannot find any useful information about Authorization even in the Spring Security github source code.
If I understand correctly, for the Authentication flow I did the following:
#Bean
fun authenticationWebFilter(reactiveAuthenticationManager: ReactiveAuthenticationManager,
jwtConverter: JWTConverter,
serverAuthenticationSuccessHandler: ServerAuthenticationSuccessHandler): AuthenticationWebFilter {
val authenticationWebFilter = AuthenticationWebFilter(reactiveAuthenticationManager)
authenticationWebFilter.setRequiresAuthenticationMatcher { ServerWebExchangeMatchers.pathMatchers(HttpMethod.POST, "/login").matches(it) }
authenticationWebFilter.setServerAuthenticationConverter(jwtConverter)
authenticationWebFilter.setAuthenticationSuccessHandler(serverAuthenticationSuccessHandler)
return authenticationWebFilter
}
The flow is:
ServerWebExchangeMatcher is executed and check if the client is requesting the login endpoint with the correct HTTP verb.
If everything is ok, the ServerAuthenticationConverter gets the username and password from the body and creates a unauthenticated UsernamePasswordAuthenticationToken.
ReactiveAuthenticationManager is called and performs the authentication (looks in db and check passwords).
If everything goes well the ServerAuthenticationSuccessHandler is called.
My ServerAuthenticationConverter:
import com.kemenu.dark.admin.application.HttpExceptionFactory.badRequest
import com.kemenu.dark.admin.application.login.LoginRequest
import kotlinx.coroutines.reactive.awaitFirstOrNull
import kotlinx.coroutines.reactor.mono
import org.springframework.core.ResolvableType
import org.springframework.http.MediaType
import org.springframework.http.codec.json.AbstractJackson2Decoder
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken
import org.springframework.security.core.Authentication
import org.springframework.security.web.server.authentication.ServerAuthenticationConverter
import org.springframework.stereotype.Component
import org.springframework.web.server.ServerWebExchange
import reactor.core.publisher.Mono
import javax.validation.Validator
#Component
class JWTConverter(private val jacksonDecoder: AbstractJackson2Decoder,
private val validator: Validator) : ServerAuthenticationConverter {
override fun convert(exchange: ServerWebExchange?): Mono<Authentication> = mono {
val loginRequest = getUsernameAndPassword(exchange!!) ?: throw badRequest()
if (validator.validate(loginRequest).isNotEmpty()) {
throw badRequest()
}
return#mono UsernamePasswordAuthenticationToken(loginRequest.username, loginRequest.password)
}
private suspend fun getUsernameAndPassword(exchange: ServerWebExchange): LoginRequest? {
val dataBuffer = exchange.request.body
val type = ResolvableType.forClass(LoginRequest::class.java)
return jacksonDecoder
.decodeToMono(dataBuffer, type, MediaType.APPLICATION_JSON, mapOf())
.onErrorResume { Mono.empty<LoginRequest>() }
.cast(LoginRequest::class.java)
.awaitFirstOrNull()
}
}
My ReactiveAuthenticationManager:
#Bean
fun reactiveAuthenticationManager(reactiveUserDetailsService: AdminReactiveUserDetailsService,
passwordEncoder: PasswordEncoder): ReactiveAuthenticationManager {
val manager = UserDetailsRepositoryReactiveAuthenticationManager(reactiveUserDetailsService)
manager.setPasswordEncoder(passwordEncoder)
return manager
}
My ServerAuthenticationSuccessHandler:
import com.kemenu.dark.admin.application.HttpExceptionFactory.unauthorized
import com.kemenu.dark.admin.application.security.JWTService
import kotlinx.coroutines.reactor.mono
import org.springframework.security.core.Authentication
import org.springframework.security.core.userdetails.User
import org.springframework.security.web.server.WebFilterExchange
import org.springframework.security.web.server.authentication.ServerAuthenticationSuccessHandler
import org.springframework.stereotype.Component
import reactor.core.publisher.Mono
#Component
class JWTServerAuthenticationSuccessHandler(private val jwtService: JWTService) : ServerAuthenticationSuccessHandler {
private val FIFTEEN_MIN = 1000 * 60 * 15
private val FOUR_HOURS = 1000 * 60 * 60 * 4
override fun onAuthenticationSuccess(webFilterExchange: WebFilterExchange?, authentication: Authentication?): Mono<Void> = mono {
val principal = authentication?.principal ?: throw unauthorized()
when(principal) {
is User -> {
val accessToken = jwtService.accessToken(principal.username, FIFTEEN_MIN)
val refreshToken = jwtService.refreshToken(principal.username, FOUR_HOURS)
webFilterExchange?.exchange?.response?.headers?.set("Authorization", accessToken)
webFilterExchange?.exchange?.response?.headers?.set("JWT-Refresh-Token", refreshToken)
}
}
return#mono null
}
}
My Security config:
import com.kemenu.dark.admin.application.security.authentication.AdminReactiveUserDetailsService
import com.kemenu.dark.admin.application.security.authentication.JWTConverter
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.core.io.ClassPathResource
import org.springframework.http.HttpMethod
import org.springframework.http.MediaType
import org.springframework.http.codec.json.AbstractJackson2Decoder
import org.springframework.http.codec.json.Jackson2JsonDecoder
import org.springframework.security.authentication.ReactiveAuthenticationManager
import org.springframework.security.authentication.UserDetailsRepositoryReactiveAuthenticationManager
import org.springframework.security.authorization.ReactiveAuthorizationManager
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity
import org.springframework.security.config.web.server.SecurityWebFiltersOrder
import org.springframework.security.config.web.server.ServerHttpSecurity
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder
import org.springframework.security.crypto.password.PasswordEncoder
import org.springframework.security.web.server.SecurityWebFilterChain
import org.springframework.security.web.server.authentication.AuthenticationWebFilter
import org.springframework.security.web.server.authentication.ServerAuthenticationSuccessHandler
import org.springframework.security.web.server.authorization.AuthorizationWebFilter
import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatchers
import org.springframework.web.reactive.config.EnableWebFlux
import org.springframework.web.reactive.config.WebFluxConfigurer
import org.springframework.web.reactive.function.server.router
import org.springframework.web.server.ServerWebExchange
import java.net.URI
#Configuration
#EnableWebFlux
#EnableWebFluxSecurity
class WebConfig : WebFluxConfigurer {
#Bean
fun configureSecurity(http: ServerHttpSecurity,
jwtAuthenticationFilter: AuthenticationWebFilter,
jwtAuthorizationWebFilter: AuthorizationWebFilter): SecurityWebFilterChain {
return http
.cors().disable()
.csrf().disable()
.httpBasic().disable()
.formLogin().disable()
.logout().disable()
.authorizeExchange()
.pathMatchers("/login").permitAll()
.anyExchange().authenticated()
.and()
.addFilterAt(jwtAuthenticationFilter, SecurityWebFiltersOrder.AUTHENTICATION)
.addFilterAt(jwtAuthorizationWebFilter, SecurityWebFiltersOrder.AUTHORIZATION)
.build()
}
#Bean
fun mainRouter() = router {
accept(MediaType.TEXT_HTML).nest {
GET("/") { temporaryRedirect(URI("/index.html")).build() }
}
resources("/**", ClassPathResource("public/"))
}
#Bean
fun passwordEncoder(): PasswordEncoder = BCryptPasswordEncoder()
#Bean
fun authenticationWebFilter(reactiveAuthenticationManager: ReactiveAuthenticationManager,
jwtConverter: JWTConverter,
serverAuthenticationSuccessHandler: ServerAuthenticationSuccessHandler): AuthenticationWebFilter {
val authenticationWebFilter = AuthenticationWebFilter(reactiveAuthenticationManager)
authenticationWebFilter.setRequiresAuthenticationMatcher { ServerWebExchangeMatchers.pathMatchers(HttpMethod.POST, "/login").matches(it) }
authenticationWebFilter.setServerAuthenticationConverter(jwtConverter)
authenticationWebFilter.setAuthenticationSuccessHandler(serverAuthenticationSuccessHandler)
return authenticationWebFilter
}
#Bean
fun authorizationWebFilter(jwtReactiveAuthorizationManager: ReactiveAuthorizationManager<ServerWebExchange>): AuthorizationWebFilter = AuthorizationWebFilter(jwtReactiveAuthorizationManager)
#Bean
fun jacksonDecoder(): AbstractJackson2Decoder = Jackson2JsonDecoder()
#Bean
fun reactiveAuthenticationManager(reactiveUserDetailsService: AdminReactiveUserDetailsService,
passwordEncoder: PasswordEncoder): ReactiveAuthenticationManager {
val manager = UserDetailsRepositoryReactiveAuthenticationManager(reactiveUserDetailsService)
manager.setPasswordEncoder(passwordEncoder)
return manager
}
}
Now for the Authorization I created a ReactiveAuthorizationManager:
import com.kemenu.dark.admin.application.security.JWTService
import kotlinx.coroutines.reactor.mono
import org.springframework.http.HttpHeaders
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken
import org.springframework.security.authorization.AuthorizationDecision
import org.springframework.security.authorization.ReactiveAuthorizationManager
import org.springframework.security.core.Authentication
import org.springframework.security.core.context.SecurityContextHolder
import org.springframework.stereotype.Component
import org.springframework.web.server.ServerWebExchange
import reactor.core.publisher.Mono
#Component
class JWTReactiveAuthorizationManager(private val jwtService: JWTService) : ReactiveAuthorizationManager<ServerWebExchange> {
override fun check(authentication: Mono<Authentication>?, exchange: ServerWebExchange?): Mono<AuthorizationDecision> = mono {
val authHeader = exchange?.request?.headers?.getFirst(HttpHeaders.AUTHORIZATION) ?: return#mono AuthorizationDecision(false)
if (!authHeader.startsWith("Bearer ")) {
return#mono AuthorizationDecision(false)
}
val decodedJWT = jwtService.decodeAccessToken(authHeader)
if (decodedJWT.subject.isNullOrBlank()) {
return#mono AuthorizationDecision(false)
}
SecurityContextHolder.getContext().authentication = UsernamePasswordAuthenticationToken(decodedJWT.subject, null, listOf())
return#mono AuthorizationDecision(true)
}
}
And I added it to the configuration as a WebFilter for Authorization.
So far so good, my problem comes when I run this test:
#Test
fun `Given an admin when tries to fetch data from customers API with AUTHORIZATION then receives the data`() {
val customer = CustomerHelper.random()
runBlocking {
customerRepository.save(customer)
}
webTestClient
.get().uri("/v1/customers")
.header(HttpHeaders.AUTHORIZATION, accessToken())
.exchange()
.expectStatus().isOk
.expectBodyList<Customer>()
.contains(customer)
}
Then I receive an unauthorized 401 http error, why?

Spring Boot RestController DELETE request fails without .csrf().disable()

I have a Spring Boot Rest Service proof of concept.
I have this for my security: (obviously a poor real implmentation).
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
#Configuration
public class MySecurityConfiguration extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
/* not production grade quality */
httpSecurity.authorizeRequests().anyRequest().permitAll();
}
// #Override
// public void configure(WebSecurity web) throws Exception {
// web.debug(true);
// }
}
Using Postman:
All of my GETs were working fine. Then I added a DELETE request. and got
{
"timestamp": "blah blah blah",
"status": 403,
"error": "Forbidden",
"message": "Forbidden",
"path": "/v1/mything/1"
}
Postman setup: (not rocket science)
DELETE
http://localhost:8080/v1/mythings/1
So I added the ".csrf().disable()", and my DELETE works.
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
#Configuration
public class MySecurityConfiguration extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
/* not production grade quality */
httpSecurity.csrf().disable(); /* had to add this "Cross Site Request Forgery" disable for DELETE operations */
httpSecurity.authorizeRequests().anyRequest().permitAll();
}
// #Override
// public void configure(WebSecurity web) throws Exception {
// web.debug(true);
// }
}
But my question is WHY does .csrf().disable() .. allow DELETE requests? Seems somewhat unrelated.
Thanks.
My full rest controller below:
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import javax.inject.Inject;
import java.time.OffsetDateTime;
import java.util.Collection;
import java.util.Optional;
import java.util.Set;
#RestController
#RequestMapping("/v1")
public class MyThingController {
private final Logger logger;
private final IMyThingManager mythingManager;
/* The Inject annotation is the signal for which constructor to use for IoC when there are multiple constructors. Not needed in single constructor scenarios */
#Inject
public MyThingController(IMyThingManager mythingManager) {
this(LoggerFactory.getLogger(MyThingController.class), mythingManager);
}
public MyThingController(Logger lgr, IMyThingManager mythingManager) {
if (null == lgr) {
throw new IllegalArgumentException("Logger is null");
}
if (null == mythingManager) {
throw new IllegalArgumentException("IMyThingManager is null");
}
this.logger = lgr;
this.mythingManager = mythingManager;
}
#RequestMapping(value = "/mythings", method = RequestMethod.GET)
Collection<MyThingDto> getAllMyThings() {
Collection<MyThingDto> returnItems = this.mythingManager.getAll();
return returnItems;
}
#RequestMapping(method = RequestMethod.GET, value = "mythings/{mythingKey}")
ResponseEntity<MyThingDto> getMyThingById(#PathVariable Long mythingKey) {
this.logger.info(String.format("Method getMyThingById called. (mythingKey=\"%1s\")", mythingKey));
Optional<MyThingDto> foundItem = this.mythingManager.getSingle(mythingKey);
ResponseEntity<MyThingDto> responseEntity = new ResponseEntity<>(HttpStatus.NOT_FOUND);
if (foundItem.isPresent()) {
responseEntity = new ResponseEntity<>(foundItem.get(), HttpStatus.OK);
}
return responseEntity;
}
#RequestMapping(value = "mythings/{mythingKey}", method = RequestMethod.DELETE, produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<Integer> deleteUser(#PathVariable("mythingKey") Long mythingKey) {
this.logger.info(String.format("Method deleteUser called. (mythingKey=\"%1s\")", mythingKey));
int rowCount = this.mythingManager.deleteByKey(mythingKey);
int rowCount = 1; /* use this to "fake it" */
ResponseEntity<Integer> responseEntity = new ResponseEntity<>(HttpStatus.NOT_FOUND);
if (rowCount > 0) {
responseEntity = new ResponseEntity<>(rowCount, HttpStatus.OK);
}
return responseEntity;
}
}
CSRF protection checks for a CSRF token on changing methods like POST, PUT, DELETE. And as a REST API is stateless you don't have a token in a cookie. That's why you have to disable it for REST APIs.
References
https://security.stackexchange.com/questions/166724/should-i-use-csrf-protection-on-rest-api-endpoints
Spring Security Reference: https://docs.spring.io/spring-security/site/docs/current/reference/html5/#csrf
Guide to CSRF Protection in Spring https://www.baeldung.com/spring-security-csrf
The guide to CSRF Protection says: "However, if our stateless API uses a session cookie authentication, we need to enable CSRF protection as we'll see next."
For this case the solution is not to disable csrf. Is there another possibility to use CSRF Protection with DELETE

Unable to Mock RestTemplate.exchange

As part of TDD i want to be able to test every portion of my SpringBoot rest application. However i am unable to mock external calls.
Application structure
1. Few rest endpoints which internally call external rest endpoints.
2. All calls to external endpoints are orchestrated through a local http client which utilizes RestTemplate as httpClient.
#RunWith(SpringRunner.class)
#SpringBootTest(webEnvironment = WebEnvironment.MOCK, classes = TestDrivenDevelopmentWithJavaApplication.class)
public class TestDrivenDevelopmentWithJavaApplicationTests {
#Autowired
private WebApplicationContext webApplicationContext;
private MockMvc mockMvc;
#MockBean
private RestTemplate client;
#Before
public void setup() throws Exception {
mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext).build();
Structure1Root category = new Structure1Root();
Category cat = new Category();
cat.setCategoryName("Test1");
cat.setDescription("Test");
category.setD(cat);
Mockito.when(client.exchange(
ArgumentMatchers.eq("https://services.odata.org/V2/Northwind/Northwind.svc/Products(1)?$format=json"),
ArgumentMatchers.eq(HttpMethod.GET), ArgumentMatchers.eq(null),
ArgumentMatchers.eq(Structure1Root.class)))
.thenReturn(new ResponseEntity<Structure1Root>(category, HttpStatus.OK));
}
#Test
public void testendpoint1() throws Exception {
mockMvc.perform(MockMvcRequestBuilders.get("/api/endpoint1?token=1").contentType(MediaType.APPLICATION_JSON))
.andExpect(content().string(org.hamcrest.Matchers.containsString("Test1")));
}
}
Even though i have setup the mock on client.exchange(RestTemplate.exchange), i see response returned by client.exchange is null and not the response specified in thenReturn
Controller Code
#RestController
#RequestMapping(path = Endpoint.base)
public class Endpoint {
public static final String base = "/api";
#Autowired
MyHttpClient<Structure2Root> client;
#Autowired
MyHttpClient<Structure1Root> Cclient;
#GetMapping(path = "/endpoint1")
public ResponseEntity<Structure2Root> callEndpt1(#RequestParam String token) {
Response<Structure2Root> resp = client
.execute("https://services.odata.org/V2/Northwind/Northwind.svc/Products(1)?$format=json", Structure2Root.class);
return new ResponseEntity<Structure2Root>(resp.getResponse(), HttpStatus.OK);
}
#GetMapping(path = "/endpoint2")
public ResponseEntity<Structure1Root> callEndpt2(#RequestParam String token) {
Response<Structure1Root> resp = Cclient.execute(
"https://services.odata.org/V2/Northwind/Northwind.svc/Categories(1)?$format=json", Structure1Root.class);
return new ResponseEntity<Structure1Root>(resp.getResponse(),HttpStatus.OK);
}
}
And finally, local http client code
#Service
public class MyHttpClient<O> {
#Autowired
RestTemplate client;
public MyHttpClient() {
// TODO Auto-generated constructor stub
}
public Response<O> execute(String url, Class<O> generic) {
ResponseEntity<O> resp = client.exchange(url, HttpMethod.GET, null, generic);
return new Response<O>(resp.getStatusCode(), resp.getBody());
}
}
this client.execute is what i intend to intercept in the first code block
However never seems to work and always returns a null.
The Git Repo
Regards,
Veera
You have used the wrong object while mocking. You should be using Structure2Root rather then Structure1Root
The correct test class is below which is working perfectly fine.
package com.demo.samples.tdd;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import com.demo.samples.tdd.responses.Product;
import com.demo.samples.tdd.responses.Structure2Root;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentMatchers;
import org.mockito.Mockito;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.context.WebApplicationContext;
import com.demo.samples.tdd.responses.Category;
import com.demo.samples.tdd.responses.Structure1Root;
#RunWith(SpringRunner.class)
#SpringBootTest(webEnvironment = WebEnvironment.MOCK, classes = TestDrivenDevelopmentWithJavaApplication.class)
public class TestDrivenDevelopmentWithJavaApplicationTests {
#Autowired
private WebApplicationContext webApplicationContext;
private MockMvc mockMvc;
#MockBean
private RestTemplate client;
#Before
public void setup() throws Exception {
mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext).build();
// Structure1Root category = new Structure1Root();
// Category cat = new Category();
// cat.setCategoryName("Test1");
// cat.setDescription("Test");
// category.setD(cat);
//
// Mockito.when(client.exchange(
// ArgumentMatchers.eq("https://services.odata.org/V2/Northwind/Northwind.svc/Products(1)?$format=json"),
// ArgumentMatchers.eq(HttpMethod.GET), ArgumentMatchers.eq(null),
// ArgumentMatchers.eq(Structure1Root.class)))
// .thenReturn(new ResponseEntity<Structure1Root>(category, HttpStatus.OK));
Structure2Root category2 = new Structure2Root();
Product product = new Product();
product.setProductName("Test1");
product.setUnitPrice("1");
category2.setD(product);
Mockito.when(client.exchange(
ArgumentMatchers.eq("https://services.odata.org/V2/Northwind/Northwind.svc/Products(1)?$format=json"),
ArgumentMatchers.eq(HttpMethod.GET), ArgumentMatchers.eq(null),
ArgumentMatchers.eq(Structure2Root.class)))
.thenReturn(new ResponseEntity<Structure2Root>(category2, HttpStatus.OK));
}
#Test
public void testendpoint1() throws Exception {
mockMvc.perform(MockMvcRequestBuilders.get("/api/endpoint1?token=1").contentType(MediaType.APPLICATION_JSON))
.andExpect(content().string(org.hamcrest.Matchers.containsString("Test1")));
}
}

Resources