简体   繁体   中英

Why is Spring Security CSRF check failing when using MockMVC MockMvc

Sorry for the information overload, I would think that a lot more information is here than needed.

So I have this test code

package com.myapp.ui.controller.user;

import static com.myapp.ui.controller.user.PasswordResetController.PASSWORD_RESET_PATH;
import static com.myapp.ui.controller.user.PasswordResetController.ResetPasswordAuthenticatedDto;
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.NullAndEmptySource;
import org.junit.jupiter.params.provider.ValueSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.http.MediaType;
import org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers;
import org.springframework.test.context.junit.jupiter.web.SpringJUnitWebConfig;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.RequestBuilder;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.result.MockMvcResultMatchers;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.myapp.db.liquibase.LiquibaseService;
import com.myapp.sample.ModelBuilderFactory;

@ComponentScan( "com.myapp.ui")
@SpringJUnitWebConfig(  classes = { LiquibaseService.class, ControllerTestBaseConfig.class }  )
public class PasswordResetControllerTest  {

    @Autowired
    private ModelBuilderFactory mbf;

    private final ObjectMapper mapper = new ObjectMapper();
    private MockMvc mockMvc;
    private String username;

    @BeforeEach
    void setup( WebApplicationContext wac) throws Exception {
        username = mbf.siteUser().get().getUsername();
        this.mockMvc = MockMvcBuilders.webAppContextSetup( wac )
            .apply( SecurityMockMvcConfigurers.springSecurity() )
            .defaultRequest( get( "/" ).with( user(username) ) )
            .alwaysDo( print() )
            .build();
    }

    @ParameterizedTest
    @NullAndEmptySource
    @ValueSource(strings = "abcd")
    void testPasswordResetInvalidate( String password ) throws Exception {
        ResetPasswordAuthenticatedDto dto = new ResetPasswordAuthenticatedDto( username, password );

        RequestBuilder builder = MockMvcRequestBuilders.patch( PASSWORD_RESET_PATH )
            .contentType( MediaType.APPLICATION_JSON )
            .content( mapper.writer().writeValueAsString( dto ) );

        mockMvc.perform( builder )
            .andExpect( MockMvcResultMatchers.status().isNoContent() );
    }

}

This is the relevant debug output

[INFO ] PATCH /ui/authn/user/password-reset for user null (session 1, csrf f47a7f39-c310-4e5d-a4be-cc9bd120229f) with session cookie (null) will be validated against CSRF [main] com.myapp.config.MyCsrfRequestMatcher.matches(MyCsrfRequestMatcher.java:76) 
[DEBUG] Invalid CSRF token found for http://localhost/ui/authn/user/password-reset            [main] org.springframework.security.web.csrf.CsrfFilter.doFilterInternal(CsrfFilter.java:127) 
[DEBUG] Starting new session (if required) and redirecting to '/login?error=timeout'          [main] org.springframework.security.web.session.SimpleRedirectInvalidSessionStrategy.onInvalidSessionDetected(SimpleRedirectInvalidSessionStrategy.java:49) 
[DEBUG] Redirecting to '/login?error=timeout'                                                 [main] org.springframework.security.web.DefaultRedirectStrategy.sendRedirect(DefaultRedirectStrategy.java:54) 
[DEBUG] Not injecting HSTS header since it did not match the requestMatcher org.springframework.security.web.header.writers.HstsHeaderWriter$SecureRequestMatcher@4b367665 [main] org.springframework.security.web.header.writers.HstsHeaderWriter.writeHeaders(HstsHeaderWriter.java:169) 
[DEBUG] SecurityContextHolder now cleared, as request processing completed                    [main] org.springframework.security.web.context.SecurityContextPersistenceFilter.doFilter(SecurityContextPersistenceFilter.java:119) 

MockHttpServletRequest:
      HTTP Method = PATCH
      Request URI = /ui/authn/user/password-reset
       Parameters = {}
          Headers = [Content-Type:"application/json", Content-Length:"68"]
             Body = <no character encoding set>
    Session Attrs = {org.springframework.security.web.csrf.HttpSessionCsrfTokenRepository.CSRF_TOKEN=org.springframework.security.web.csrf.DefaultCsrfToken@55b1156b, SPRING_SECURITY_CONTEXT=org.springframework.security.core.context.SecurityContextImpl@12919b87: Authentication: org.springframework.security.authentication.UsernamePasswordAuthenticationToken@12919b87: Principal: org.springframework.security.core.userdetails.User@1e05232c: Username: lab_admin4oyc8tkytxu4zllguk-1qg; Password: [PROTECTED]; Enabled: true; AccountNonExpired: true; credentialsNonExpired: true; AccountNonLocked: true; Granted Authorities: ROLE_USER; Credentials: [PROTECTED]; Authenticated: true; Details: null; Granted Authorities: ROLE_USER}

Handler:
             Type = null

Async:
    Async started = false
     Async result = null

Resolved Exception:
             Type = null

ModelAndView:
        View name = null
             View = null
            Model = null

FlashMap:
       Attributes = null

MockHttpServletResponse:
           Status = 302
    Error message = null
          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", X-Frame-Options:"SAMEORIGIN", Location:"/login?error=timeout"]
     Content type = null
             Body = 
    Forwarded URL = null
   Redirected URL = /login?error=timeout
          Cookies = []

java.lang.AssertionError: Status expected:<204> but was:<302>
Expected :204
Actual   :302

This is our relevant SecurityConfig

    @Override
    protected void configure( HttpSecurity http ) throws Exception {

        ApplicationContext ctx = getApplicationContext();
        http
            .addFilterAfter( ctx.getBean( TimeoutFilter.class ), SecurityContextPersistenceFilter.class )
            .addFilterAt( ctx.getBean( "myLogoutFilter", LogoutFilter.class ), LogoutFilter.class )
            .addFilterBefore( ctx.getBean( IpAddressAuditFilter.class ), FilterSecurityInterceptor.class )
            .addFilterAt( ctx.getBean( MyConcurrentSessionFilter.class ), ConcurrentSessionFilter.class )
            .addFilterAfter( ctx.getBean( RequestLogFilter.class ), FilterSecurityInterceptor.class )
            .headers().xssProtection().and().frameOptions().sameOrigin().and()
            .authorizeRequests()
            .antMatchers(
                HttpMethod.GET,
                "/styles/**.css",
                "/fonts/**",
                "/scripts/**.js",
                "/VAADIN/**",
                "/jsp/**",
                "/*.css",
                "/help/**",
                "/public/error/**"
            ).permitAll()
            .antMatchers( "/app/**", "/downloads/**", "/ui/authn/**" ).fullyAuthenticated()
            .antMatchers(
                "/login**",
                "/register/**",
                "/public/**",
                "/sso_logout",
                "/sso_auth_failure",
                "/sso_concurrent_session",
                "/sso_timeout"
            ).anonymous()
            .anyRequest().denyAll()
            .and()
            .formLogin()
            .loginPage( "/login" )
            .loginProcessingUrl( SecurityConstants.loginPath() )
            .defaultSuccessUrl( "/app", true )
            .failureUrl( "/login?error=badCredentials" )
            .usernameParameter( SecurityConstants.usernameField() )
            .passwordParameter( SecurityConstants.passwordField())
            .permitAll()
            .and()
            .exceptionHandling()
            .accessDeniedHandler( new MyAccessDeniedHandler() )
            .authenticationEntryPoint( new LoginUrlAuthenticationEntryPoint( "/login" ) )
            .and()
            .csrf().requireCsrfProtectionMatcher( csrfRequestMatcher )
            .and()
            .sessionManagement().sessionFixation().newSession().invalidSessionUrl( "/login?error=timeout" )
            .maximumSessions( 1 ).sessionRegistry( sessionRegistry )
            .expiredUrl( "/login?error=concurrentSession"  );
    }

The test is redirecting to timeout because it see's the session as invalid.

we are using the the spring boot parent BOM only for dependency management, we have not yet converted to spring boot (WIP). Information included so you know what versions we're on.

  <parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.2.8.RELEASE</version>
    <relativePath />
  </parent>

Given that I'm using the spring security user() and springSecurity() for setting up the MockMvc, I'm not certain why the CSRF isn't right. I added my path to the paths that we exclude when checking for CSRF, and this problem went away. What would I need to do to get have the CSRF check pass?

It doesn't look like you're generating a CSRF token for your request anywhere in your test. Logs do show you have some CSRF token in there, but if you're not providing such token in a request, CsrfFilter will create you one and will compare to that one, which I would expect to fail in a manner you reported.

You can create a csrf token for mockMvc request by giving SecurityMockMvcRequestPostProcessors.csrf() as a parameter for mockMvc.perform(builder).with(... ) .

To examine if you have a csrf token in your request, I'd run this through a debugger and place a breakpoint around the lines 120-123 of CsrfFilter .

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM