mockmvc post request without csrf or invalid csrf is not failing - spring-boot

I have a spring boot-2.5.1 application with spring security 5.
WebSecurityConfig looks as below:
#Slf4j
#Configuration
#EnableWebSecurity
#RequiredArgsConstructor
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
private final CustomProperties customProperties;
#Override
protected void configure(HttpSecurity http) throws Exception {
if (customProperties.isEnabled()) {
log.info("authentication enabled.");
http.authorizeRequests()
.anyRequest()
.authenticated()
.and()
.oauth2Login()
.redirectionEndpoint()
.baseUri(customProperties.getAuthorizationResponseUri())
.and()
.userInfoEndpoint(userInfo -> userInfo.userService(new CustomOAuth2UserService()::loadUser));
} else {
log.info("authentication disabled.");
http.authorizeRequests()
.anyRequest()
.permitAll();
}
}
public void configure(WebSecurity webSecurity) {
webSecurity.ignoring()
.antMatchers("/actuator/info", "/actuator/health/*");
}
}
}
Controller looks like as below:
#Controller
#RequiredArgsConstructor
public class UserController {
private final UserService userService;
#PostMapping("/users")
public #ResponseBody Mono<Map<String, String>> saveUsers(#RequestBody List<UserDto> userDtos) {
return userService.saveUser(userDtos);
}
}
JUnit corresponding to the above controller:
#AutoConfigureMockMvc
#WebMvcTest(UserController.class)
class UserControllerTest {
#Autowired
private MockMvc mockMvc;
#MockBean
private UserService userService;
#SneakyThrows
#Test
#WithMockTestUser
void shouldSaveUsers() {
var userDtos = "...";
mockMvc.perform(post("/users")
.contentType(APPLICATION_JSON)
.content(userDtos))
.andDo(print())
.andExpect(status().isOk());
}
}
Above JUnit without CSRF is giving status as OK instead of forbidden request.
If I debug, I could see, if csrf() is not included then no token will be generated & assigned to request. However, still, the request is passing. Ideally, it should throw forbidden request access.
Even with csrf(), I can see token is generated & assigned to parameter.
In both cases, I do not see anywhere POST request is being validated whether it contains CSRF token or not with mockmvc.
Does mockmvc need any extra configuration to validate whether POST request contains CSRF or not?

Rather than auto-configuring MockMvc, you need to build MockMvc using SecurityMockMvcConfigurers.springSecurity().
This adds the Spring Bean named "springSecurityFilterChain" as a Filter and ensures that the TestSecurityContextHolder is leveraged for each request.
#Autowired
private WebApplicationContext context;
private MockMvc mockMvc;
#Before
public void setup() {
this.mockMvc = MockMvcBuilders
.webAppContextSetup(this.context)
.apply(springSecurity())
.build();
}

Related

Mocking JWT token in #SpringBootTest with WebTestClient

I have the following controller class in a Spring Boot project:
#GetMapping
public ResponseEntity<UserResponse> getUser(#AuthenticationPrincipal CustomUserDetails userDetails) {
try {
final UserResponse userData = userService.getUser(userDetails.getId());
return ResponseEntity.ok(userData);
} catch (UserNotFoundException e) {
log.error("User with id {} not found", userDetails.getId());
return ResponseEntity.notFound().build();
}
}
This resource is only accessible if the client sends a JWT token with Authorization: Bearer <token>. The CustomUserDetails are provided by a CustomUserDetailsService after having parsed the JWT token via a JwtRequestFilter.
Now I'd like to write a #SpringBootTest which uses a real HTTP client calling this resource. My first idea was to use a MockMvc object but then I read about the WebTestClient provided by Spring.
However I don't yet understand how I would be able to mock the JWT token. This is my initial attempt:
#SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
#AutoConfigureWebTestClient
public class UserControllerIT {
#Autowired
private ApplicationContext context;
private WebTestClient webTestClient;
#MockBean
private UserRepo userRepo;
#BeforeEach
public void setup() {
webTestClient = WebTestClient
.bindToApplicationContext(context)
.apply(springSecurity())
.configureClient()
.build();
}
#Test
#WithMockUser
public void someTest() {
final User user = createUser("foo#bar.com", "my-password");
when(userRepo.findById(anyLong())).thenReturn(Optional.of(user));
webTestClient
.mutateWith(mockJwt())
.get()
.uri("/user")
.header(ACCEPT, APPLICATION_JSON_VALUE)
.exchange()
.expectStatus().is2xxSuccessful();
}
This test fails with the following exception:
org.springframework.beans.factory.NoSuchBeanDefinitionException: No bean named 'webHandler' available
However I'm not sure if my approach makes sense. Is WebTestClient the "correct" way? Or do I need to use a WebClient?
My security configuration:
#Configuration
#EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
private final JwtRequestFilter jwtRequestFilter;
public SecurityConfiguration(JwtRequestFilter jwtRequestFilter) {
this.jwtRequestFilter = jwtRequestFilter;
}
#Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
#Bean
#Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
#Override
protected void configure(final HttpSecurity httpSecurity) throws Exception {
httpSecurity
.cors().and()
.addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class)
.mvcMatcher("/services/**").authorizeRequests()
.mvcMatchers(PUBLIC_RESOURCES).permitAll()
.anyRequest().authenticated()
.and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
}
WebTestClient is the recommended replacement for TestRestTemplate.
A deep-dive into the Spring Security source at https://github.com/spring-projects/spring-security, shows some examples of use of WebTestClient with JWT. E.g.: ServerOAuth2ResourceServerApplicationITests
Given that you have a service JwtTokenProvider that is responsible for generating JWT-tokens, a test may look like below. Or, if possible, you may use constant tokens like in ServerOAuth2ResourceServerApplicationITests.
package no.yourcompany.yourapp.yourpackage;
import no.yourcompany.yourapp.configuration.JwtTokenProvider;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.reactive.AutoConfigureWebTestClient;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.test.web.reactive.server.WebTestClient;
#SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
#AutoConfigureWebTestClient
public class YourIntegrationTest {
#Autowired
WebTestClient webTestClient;
#Autowired
JwtTokenProvider jwtTokenProvider;
#Test
public void postTest_withValidToken_receiveOk() {
var tokenString = jwtTokenProvider.createToken(
new UsernamePasswordAuthenticationToken("test-user", "P4ssword"));
webTestClient
.post().uri("/test")
.headers(http -> http.setBearerAuth(tokenString))
.exchange()
.expectStatus().isOk();
}
}
For WebTestClient, add this to POM
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
<scope>test</scope>
</dependency>

Sping Boot DefaultSecurityFilterChain configuration is different in test than in production

We'd like to secure the HTTP endpoint metrics that is a built-in endpoint of Spring Boot. Therefore, we write own WebSecurityConfigurerAdapter, see below
#Configuration
#Order(95)
public class MetricsWebSecurityConfigurerAdapter extends WebSecurityConfigurerAdapter {
#Value("${metrics.username}")
private String metricsUsername;
#Value("${metrics.password}")
private String metricsPassword;
#Autowired
private MyAuthenticationEntryPoint authenticationEntryPoint;
#Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser(metricsUsername).password("{noop}" + metricsPassword)
.roles("METRICS");
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable(); // disable csrf for our requests.
http
.antMatcher("/metrics") //
.httpBasic() //
.authenticationEntryPoint(authenticationEntryPoint) //
.and() //
.authorizeRequests() //
.antMatchers("/metrics").authenticated(); //
}
}
My Spring Boot test for testing the authentication is
#ExtendWith(SpringExtension.class)
#SpringBootTest
#TestPropertySource(locations = "classpath:test.properties")
#AutoConfigureMockMvc
public class MetricsEndpointTest {
#Autowired
private MockMvc mockMvc;
#Test
public void testGetMetricsForbidden() throws Exception {
mockMvc.perform(get("/metrics"))
.andExpect(status().isForbidden());
}
#Test
public void testGetMetrics() throws Exception {
mockMvc.perform(get("/metrics")
.header("Authorization", "Basic " + getBasicAuthentication("testmetricsuser", "testmetricspass")))
.andExpect(status().isOk());
}
private static String getBasicAuthentication(String user, String password) {
String token = user + ":" + password;
try {
return DatatypeConverter.printBase64Binary(token.getBytes("UTF-8"));
} catch (UnsupportedEncodingException ex) {
throw new IllegalStateException("Cannot encode with UTF-8", ex);
}
}
}
The test testGetMetricsForbidden fails.
When I debugged the class WebSecurity during the test run and in production, I can find following difference in the variable securityFilterChains :
In production:
In test:
The difference is that in production we have five security filter chains and in test six security filter chains. The sixth filter chain is responsible that the test fails because it matches at first. I think the #Order is the reason for the order in the test. My question is how can I disabled that sixth filter in my test.

Spring Boot Security UsernamePasswordAuthenticationFilter does not limit the login url to only POST

I looked all throughout and nobody else is having this issue that I can find. The authentication works correctly but the login url works on any HTTP method (GET, PUT, etc) vs. only working on POST. I tried manually setting filter.setPostOnly(true); on the custom JWTAuthenticationFilter I made, but it still allows on all methods.
#Configuration
#EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
private static final String FILTER_PROCESS_URL = "/authentication";
private static final String HEALTH_RESOURCE_URL = "/health/**";
private CustomUserDetailService userDetailsService;
private BCryptPasswordEncoder bCryptPasswordEncoder;
public WebSecurityConfig(CowCalfUserDetailService userDetailsService, BCryptPasswordEncoder bCryptPasswordEncoder) {
this.userDetailsService = userDetailsService;
this.bCryptPasswordEncoder = bCryptPasswordEncoder;
}
#Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity
.cors().and().csrf().disable().authorizeRequests()
.antMatchers(HttpMethod.POST, FILTER_PROCESS_URL).permitAll()
.antMatchers(HttpMethod.GET, HEALTH_RESOURCE_URL).permitAll()
.anyRequest().authenticated()
.and()
.addFilterBefore(getJWTAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class)
.addFilter(new JWTAuthorizationFilter(authenticationManagerBean()))
// this disables session creation on Spring Security
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
#Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService)
.passwordEncoder(bCryptPasswordEncoder);
}
public JWTAuthenticationFilter getJWTAuthenticationFilter() throws Exception {
final JWTAuthenticationFilter filter = new JWTAuthenticationFilter(authenticationManagerBean());
filter.setPostOnly(true);
filter.setFilterProcessesUrl(FILTER_PROCESS_URL);
return filter;
}
}
If i understand you correctly, you want to limit the /authentication only to the POST Http-Method. If yes, you could achieve it by adding the following code snippet to your method in the "RestController" for the Authentication:
#RequestMapping(value = "/authentication", method = RequestMethod.POST)
RequestMapping Docs
I found out how setPostOnly() works. UsernamePasswordAuthenticationFilter.attemptAuthentication() checks for POST before attempting the authentication and throws an exception. This method I have overridden with my custom JWTAuthenticationFilter. I just did the same and added a check in my overridden method too. Thank you for the suggestions!

Confused about Spring boot test specific #TestConfiguration

According to Spring Boot Docs, a nested #TestConfiguration should be detected by tests automatically.
But in my test codes it is problematic when I ran the whole test class, it was not detected even I added it explicitly by #Import. The test code structure is like the following:
#SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
#RunWith(SpringRunner.class)
//#Import(IntegrationTests.TestSecurityConfig.class)
public class IntegrationTests {
// test methods
// test configuration
#TestConfiguration
static class TestSecurityConfig {}
}
When I ran single test cases(test methods) individually, all tests are passed as expected, but when I ran the test class directly, some tests are failed, the #TestConfiguration was not applied to test.
The complete codes of this IntegrationTests is here.
UPDATE:
A workaround added in my codes to make the tests passed.
#TestComponent
#Slf4j
static class TestUserDetailsService implements UserDetailsService {
private final PasswordEncoder passwordEncoder;
TestUserDetailsService(PasswordEncoder passwordEncoder) {
this.passwordEncoder = passwordEncoder;
}
#Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
UserDetails user = User.withUsername("user")
.password(passwordEncoder.encode("password"))
.roles("USER")
.accountExpired(false)
.accountLocked(false)
.credentialsExpired(false)
.disabled(false)
.build();
UserDetails admin = User.withUsername("admin")
.password(passwordEncoder.encode("password"))
.roles("ADMIN")
.accountExpired(false)
.accountLocked(false)
.credentialsExpired(false)
.disabled(false)
.build();
log.debug("dummy user:" + user);
log.debug("dummy admin:" + admin);
if ("user".equals(username)) {
return user;
} else {
return admin;
}
}
}
#TestConfiguration
#Slf4j
#Import(TestUserDetailsService.class)
#Order(-1)
static class TestSecurityConfig extends WebSecurityConfigurerAdapter {
#Inject
PasswordEncoder passwordEncoder;
#Inject
UserDetailsService userDetailsService;
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.httpBasic()
.and()
.authorizeRequests()
.antMatchers(HttpMethod.GET, "/posts/**").permitAll()
.antMatchers(HttpMethod.DELETE, "/posts/**").hasRole("ADMIN")
.anyRequest().authenticated()
.and()
.csrf().disable();
}
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder);
}
#Override
#Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}
}
There are still some things confused me.
In the test class, why the #TestConfiguration can not detect #TestComponent located in the same test, I have to add #Import to fix it.
As described in the security section of Spring Boot Docs, I was thinking defining a UserDetailsService bean is enough, it will serve the users in security, but it did not work in tests. I have to configure a WebSecurityConfigurerAdapter and expose AuthenticationManager for test, why? And more confused me is as described before , running the tests one by one is ok if there is no WebSecurityConfigurerAdapter defined for test.
The #TestConfiguration annotated WebSecurityConfigurerAdapter does not get a higer order, I have to add #Order on it. I was thinking a #TestConfiguration bean should get Primary automatically and replace the bean in my application config, right?
Simply adding #Order(Ordered.HIGHEST_PRECEDENCE) solved it in my case:
#Order(Ordered.HIGHEST_PRECEDENCE)
#TestConfiguration
I'm not quite sure why that isn't the default. My #ConditionalOnBean was evaluated before that #TestConfiguration was actually initializing those beans.

Authentication should not be null in unit tests with spring-session

I have a spring boot (version 1.5.9.RELEASE) application which uses spring-session to store sessions on Redis. It also uses spring-security to authenticate users. When running the application, after a successful login, the security context contains the Authentication object. But when running unit tests I get this error message Authentication should not be null. Code to reproduce is the following:
#SpringBootApplication
public class DemoRedisDataSessionApplication {
#Configuration
#EnableWebSecurity
#EnableRedisHttpSession(redisNamespace = "demo-redis-spring-session")
public static class AppConfiguration extends WebSecurityConfigurerAdapter {
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication().withUser("user").password("0000").roles("USER");
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin().and()
.authorizeRequests().anyRequest().fullyAuthenticated();
}
}
#RestController
public static class AppController {
#GetMapping("/secured")
public String secured() {
return "secured";
}
}
public static void main(String[] args) {
SpringApplication.run(DemoRedisDataSessionApplication.class, args);
}
}
Here is application.properties
spring.session.store-type=redis
Here is the failing test
#RunWith(SpringRunner.class)
#SpringBootTest
#AutoConfigureMockMvc
public class DemoRedisDataSessionApplicationTests {
#Autowired
private MockMvc mockMvc;
#Test
public void testUserShouldBeAuthenticated() throws Exception {
mockMvc.perform(formLogin().user("user").password("0000"))
.andExpect(status().is3xxRedirection())
.andExpect(authenticated());
}
}
Error message for the failed test:
java.lang.AssertionError: Authentication should not be null
at org.springframework.test.util.AssertionErrors.fail(AssertionErrors.java:35)
at org.springframework.test.util.AssertionErrors.assertTrue(AssertionErrors.java:65)
at org.springframework.security.test.web.servlet.response.SecurityMockMvcResultMatchers$AuthenticatedMatcher.match(SecurityMockMvcResultMatchers.java:98)
In particular it seems that the session is null in the class HttpSessionSecurityContextRepository line 110, but I don't understand why.
I expect the user to be authenticated and the SecurityContext populated after a successful login. Do you have any idea on how to solve this?
Updated:
Firstly, you need to instruct your authentication provider (in your case, it is the default DaoAuthenticationProvider) to return what kind of Authentication object. For instance, you can add httpBasic() into your configure(HttpSecurity http) method in your customized WebSecurityConfigurerAdapter. Essentially, httpBasic() will convert your username and password to a UsernamePasswordAuthenticationToken object such that your DaoAuthenticationProvider can use it to do authentication.
In addition, you need to permitAll for you login url.
#Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin().and()
.authorizeRequests()
.antMatchers("/login/**").permitAll()
.anyRequest().fullyAuthenticated()
.and().httpBasic();
}
With regards to the unit test, the issue was due to the fact that you didn't wire-in spring security into your mockMvc object. As you are actually spring-boot, I would give you a sample solution with spring-boot-test:
#RunWith(SpringRunner.class)
#SpringBootTest
#WebAppConfiguration
public class DemoRedisDataSessionApplicationTests {
#Autowired
WebApplicationContext wac;
private MockMvc mockMvc;
#Before
public void setUp() {
mockMvc = MockMvcBuilders.webAppContextSetup(wac)
.apply(springSecurity())
.build();
}
#Test
public void testUserShouldBeAuthenticated() throws Exception {
mockMvc.perform(formLogin().user("user").password("0000"))
.andExpect(status().is3xxRedirection())
.andExpect(authenticated());
}
}
Key Note: springSecurity() in the code above is from import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity.

Resources