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.