diff --git a/config/src/test/groovy/org/springframework/security/config/http/CsrfConfigTests.groovy b/config/src/test/groovy/org/springframework/security/config/http/CsrfConfigTests.groovy deleted file mode 100644 index 9d8277018c..0000000000 --- a/config/src/test/groovy/org/springframework/security/config/http/CsrfConfigTests.groovy +++ /dev/null @@ -1,342 +0,0 @@ -/* - * Copyright 2002-2012 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on - * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the - * specific language governing permissions and limitations under the License. - */ -package org.springframework.security.config.http - -import javax.servlet.http.HttpServletRequest -import javax.servlet.http.HttpServletResponse - -import spock.lang.Unroll - -import org.springframework.mock.web.MockFilterChain -import org.springframework.mock.web.MockHttpServletRequest -import org.springframework.mock.web.MockHttpServletResponse -import org.springframework.security.access.AccessDeniedException -import org.springframework.security.authentication.UsernamePasswordAuthenticationToken -import org.springframework.security.core.Authentication -import org.springframework.security.core.authority.AuthorityUtils -import org.springframework.security.core.context.SecurityContextImpl -import org.springframework.security.web.access.AccessDeniedHandler -import org.springframework.security.web.context.HttpRequestResponseHolder -import org.springframework.security.web.context.HttpSessionSecurityContextRepository -import org.springframework.security.web.csrf.CsrfFilter -import org.springframework.security.web.csrf.CsrfToken -import org.springframework.security.web.csrf.CsrfTokenRepository -import org.springframework.security.web.csrf.DefaultCsrfToken -import org.springframework.security.web.util.matcher.RequestMatcher -import org.springframework.web.servlet.support.RequestDataValueProcessor - -import static org.mockito.Matchers.* -import static org.mockito.Mockito.* - -/** - * - * @author Rob Winch - */ -class CsrfConfigTests extends AbstractHttpConfigTests { - MockHttpServletRequest request = new MockHttpServletRequest() - MockHttpServletResponse response = new MockHttpServletResponse() - MockFilterChain chain = new MockFilterChain() - - @Unroll - def 'csrf is enabled by default'() { - setup: - httpAutoConfig { - } - createAppContext() - when: - request.method = httpMethod - springSecurityFilterChain.doFilter(request,response,chain) - then: - response.status == httpStatus - where: - httpMethod | httpStatus - 'POST' | HttpServletResponse.SC_FORBIDDEN - 'PUT' | HttpServletResponse.SC_FORBIDDEN - 'PATCH' | HttpServletResponse.SC_FORBIDDEN - 'DELETE' | HttpServletResponse.SC_FORBIDDEN - 'INVALID' | HttpServletResponse.SC_FORBIDDEN - 'GET' | HttpServletResponse.SC_OK - 'HEAD' | HttpServletResponse.SC_OK - 'TRACE' | HttpServletResponse.SC_OK - 'OPTIONS' | HttpServletResponse.SC_OK - } - - def 'csrf disabled'() { - when: - httpAutoConfig { csrf(disabled:true) } - createAppContext() - then: - !getFilter(CsrfFilter) - } - - @Unroll - def 'csrf defaults'() { - setup: - httpAutoConfig { 'csrf'() } - createAppContext() - when: - request.method = httpMethod - springSecurityFilterChain.doFilter(request,response,chain) - then: - response.status == httpStatus - where: - httpMethod | httpStatus - 'POST' | HttpServletResponse.SC_FORBIDDEN - 'PUT' | HttpServletResponse.SC_FORBIDDEN - 'PATCH' | HttpServletResponse.SC_FORBIDDEN - 'DELETE' | HttpServletResponse.SC_FORBIDDEN - 'INVALID' | HttpServletResponse.SC_FORBIDDEN - 'GET' | HttpServletResponse.SC_OK - 'HEAD' | HttpServletResponse.SC_OK - 'TRACE' | HttpServletResponse.SC_OK - 'OPTIONS' | HttpServletResponse.SC_OK - } - - def 'csrf default creates CsrfRequestDataValueProcessor'() { - when: - httpAutoConfig { 'csrf'() } - createAppContext() - then: - appContext.getBean("requestDataValueProcessor",RequestDataValueProcessor) - } - - def 'csrf custom AccessDeniedHandler'() { - setup: - httpAutoConfig { - 'access-denied-handler'(ref:'adh') - 'csrf'() - } - mockBean(AccessDeniedHandler,'adh') - createAppContext() - AccessDeniedHandler adh = appContext.getBean(AccessDeniedHandler) - request.method = "POST" - when: - springSecurityFilterChain.doFilter(request,response,chain) - then: - verify(adh).handle(any(HttpServletRequest),any(HttpServletResponse),any(AccessDeniedException)) - response.status == HttpServletResponse.SC_OK // our mock doesn't do anything - } - - def "csrf disables posts for RequestCache"() { - setup: - httpAutoConfig { - 'csrf'('token-repository-ref':'repo') - 'intercept-url'(pattern:"/**",access:'ROLE_USER') - } - mockBean(CsrfTokenRepository,'repo') - createAppContext() - CsrfTokenRepository repo = appContext.getBean("repo",CsrfTokenRepository) - CsrfToken token = new DefaultCsrfToken("X-CSRF-TOKEN","_csrf", "abc") - when(repo.loadToken(any(HttpServletRequest))).thenReturn(token) - when(repo.generateToken(any(HttpServletRequest))).thenReturn(token) - request.setParameter(token.parameterName,token.token) - request.servletPath = "/some-url" - request.requestURI = "/some-url" - request.method = "POST" - when: "CSRF passes and our session times out" - springSecurityFilterChain.doFilter(request,response,chain) - then: "sent to the login page" - response.status == HttpServletResponse.SC_MOVED_TEMPORARILY - response.redirectedUrl == "http://localhost/login" - when: "authenticate successfully" - response = new MockHttpServletResponse() - request = new MockHttpServletRequest(session: request.session) - request.servletPath = "/login" - request.setParameter(token.parameterName,token.token) - request.setParameter("username","user") - request.setParameter("password","password") - request.method = "POST" - springSecurityFilterChain.doFilter(request,response,chain) - then: "sent to default success because we don't want csrf attempts made prior to authentication to pass" - response.status == HttpServletResponse.SC_MOVED_TEMPORARILY - response.redirectedUrl == "/" - } - - def "csrf enables gets for RequestCache"() { - setup: - httpAutoConfig { - 'csrf'('token-repository-ref':'repo') - 'intercept-url'(pattern:"/**",access:'ROLE_USER') - } - mockBean(CsrfTokenRepository,'repo') - createAppContext() - CsrfTokenRepository repo = appContext.getBean("repo",CsrfTokenRepository) - CsrfToken token = new DefaultCsrfToken("X-CSRF-TOKEN","_csrf", "abc") - when(repo.loadToken(any(HttpServletRequest))).thenReturn(token) - when(repo.generateToken(any(HttpServletRequest))).thenReturn(token) - request.setParameter(token.parameterName,token.token) - request.servletPath = "/some-url" - request.requestURI = "/some-url" - request.method = "GET" - when: "CSRF passes and our session times out" - springSecurityFilterChain.doFilter(request,response,chain) - then: "sent to the login page" - response.status == HttpServletResponse.SC_MOVED_TEMPORARILY - response.redirectedUrl == "http://localhost/login" - when: "authenticate successfully" - response = new MockHttpServletResponse() - request = new MockHttpServletRequest(session: request.session) - request.servletPath = "/login" - request.setParameter(token.parameterName,token.token) - request.setParameter("username","user") - request.setParameter("password","password") - request.method = "POST" - springSecurityFilterChain.doFilter(request,response,chain) - then: "sent to original URL since it was a GET" - response.status == HttpServletResponse.SC_MOVED_TEMPORARILY - response.redirectedUrl == "http://localhost/some-url" - } - - def "SEC-2422: csrf expire CSRF token and session-management invalid-session-url"() { - setup: - httpAutoConfig { - 'csrf'() - 'session-management'('invalid-session-url': '/error/sessionError') - } - createAppContext() - request.setParameter("_csrf","abc") - request.method = "POST" - when: "No existing expected CsrfToken (session times out) and a POST" - springSecurityFilterChain.doFilter(request,response,chain) - then: "sent to the session timeout page page" - response.status == HttpServletResponse.SC_MOVED_TEMPORARILY - response.redirectedUrl == "/error/sessionError" - when: "Existing expected CsrfToken and a POST (invalid token provided)" - response = new MockHttpServletResponse() - request = new MockHttpServletRequest(session: request.session, method:'POST') - springSecurityFilterChain.doFilter(request,response,chain) - then: "Access Denied occurs" - response.status == HttpServletResponse.SC_FORBIDDEN - } - - def "csrf requireCsrfProtectionMatcher"() { - setup: - httpAutoConfig { 'csrf'('request-matcher-ref':'matcher') } - mockBean(RequestMatcher,'matcher') - createAppContext() - request.method = 'POST' - RequestMatcher matcher = appContext.getBean("matcher",RequestMatcher) - when: - when(matcher.matches(any(HttpServletRequest))).thenReturn(false) - springSecurityFilterChain.doFilter(request,response,chain) - then: - response.status == HttpServletResponse.SC_OK - when: - when(matcher.matches(any(HttpServletRequest))).thenReturn(true) - springSecurityFilterChain.doFilter(request,response,chain) - then: - response.status == HttpServletResponse.SC_FORBIDDEN - } - - def "csrf csrfTokenRepository default delays save"() { - setup: - httpAutoConfig { - } - createAppContext() - request.method = "GET" - when: - springSecurityFilterChain.doFilter(request,response,chain) - then: - response.status == HttpServletResponse.SC_OK - request.getSession(false) == null - } - - def "csrf csrfTokenRepository"() { - setup: - httpAutoConfig { 'csrf'('token-repository-ref':'repo') } - mockBean(CsrfTokenRepository,'repo') - createAppContext() - CsrfTokenRepository repo = appContext.getBean("repo",CsrfTokenRepository) - CsrfToken token = new DefaultCsrfToken("X-CSRF-TOKEN","_csrf", "abc") - when(repo.loadToken(any(HttpServletRequest))).thenReturn(token) - request.setParameter(token.parameterName,token.token) - request.method = "POST" - when: - springSecurityFilterChain.doFilter(request,response,chain) - then: - response.status == HttpServletResponse.SC_OK - when: - request.setParameter(token.parameterName,token.token+"INVALID") - springSecurityFilterChain.doFilter(request,response,chain) - then: - response.status == HttpServletResponse.SC_FORBIDDEN - } - - def "csrf clears on login"() { - setup: - httpAutoConfig { 'csrf'('token-repository-ref':'repo') } - mockBean(CsrfTokenRepository,'repo') - createAppContext() - CsrfTokenRepository repo = appContext.getBean("repo",CsrfTokenRepository) - CsrfToken token = new DefaultCsrfToken("X-CSRF-TOKEN","_csrf", "abc") - when(repo.loadToken(any(HttpServletRequest))).thenReturn(token) - when(repo.generateToken(any(HttpServletRequest))).thenReturn(token) - request.setParameter(token.parameterName,token.token) - request.method = "POST" - request.setParameter("username","user") - request.setParameter("password","password") - request.servletPath = "/login" - when: - springSecurityFilterChain.doFilter(request,response,chain) - then: - verify(repo, atLeastOnce()).saveToken(eq(null),any(HttpServletRequest), any(HttpServletResponse)) - } - - def "csrf clears on logout"() { - setup: - httpAutoConfig { 'csrf'('token-repository-ref':'repo') } - mockBean(CsrfTokenRepository,'repo') - createAppContext() - CsrfTokenRepository repo = appContext.getBean("repo",CsrfTokenRepository) - CsrfToken token = new DefaultCsrfToken("X-CSRF-TOKEN","_csrf", "abc") - when(repo.loadToken(any(HttpServletRequest))).thenReturn(token) - request.setParameter(token.parameterName,token.token) - request.method = "POST" - request.servletPath = "/logout" - when: - springSecurityFilterChain.doFilter(request,response,chain) - then: - verify(repo).saveToken(eq(null),any(HttpServletRequest), any(HttpServletResponse)) - } - - def "SEC-2495: csrf disables logout on GET"() { - setup: - httpAutoConfig { 'csrf'() } - createAppContext() - login() - request.method = "GET" - request.requestURI = "/logout" - when: - springSecurityFilterChain.doFilter(request,response,chain) - then: - getAuthentication(request) != null - } - - - def login(String username="user", String role="ROLE_USER") { - login(new UsernamePasswordAuthenticationToken(username, null, AuthorityUtils.createAuthorityList(role))) - } - - def login(Authentication auth) { - HttpSessionSecurityContextRepository repo = new HttpSessionSecurityContextRepository() - HttpRequestResponseHolder requestResponseHolder = new HttpRequestResponseHolder(request, response) - repo.loadContext(requestResponseHolder) - repo.saveContext(new SecurityContextImpl(authentication:auth), requestResponseHolder.request, requestResponseHolder.response) - } - - def getAuthentication(HttpServletRequest request) { - HttpSessionSecurityContextRepository repo = new HttpSessionSecurityContextRepository() - HttpRequestResponseHolder requestResponseHolder = new HttpRequestResponseHolder(request, response) - repo.loadContext(requestResponseHolder)?.authentication - } -} diff --git a/config/src/test/java/org/springframework/security/config/http/CsrfConfigTests.java b/config/src/test/java/org/springframework/security/config/http/CsrfConfigTests.java new file mode 100644 index 0000000000..31d166252e --- /dev/null +++ b/config/src/test/java/org/springframework/security/config/http/CsrfConfigTests.java @@ -0,0 +1,646 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.security.config.http; + +import org.eclipse.jetty.http.HttpStatus; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpMethod; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpSession; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.config.test.SpringTestContext; +import org.springframework.security.config.test.SpringTestRule; +import org.springframework.security.test.context.annotation.SecurityTestExecutionListeners; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.security.test.web.support.WebTestUtils; +import org.springframework.security.web.FilterChainProxy; +import org.springframework.security.web.access.AccessDeniedHandler; +import org.springframework.security.web.csrf.CsrfFilter; +import org.springframework.security.web.csrf.CsrfToken; +import org.springframework.security.web.util.matcher.RequestMatcher; +import org.springframework.stereotype.Controller; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.MvcResult; +import org.springframework.test.web.servlet.ResultMatcher; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.context.WebApplicationContext; +import org.springframework.web.servlet.support.RequestDataValueProcessor; + +import javax.servlet.Filter; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.net.URI; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; +import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.head; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.options; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.patch; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.request; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrl; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import static org.springframework.web.bind.annotation.RequestMethod.DELETE; +import static org.springframework.web.bind.annotation.RequestMethod.GET; +import static org.springframework.web.bind.annotation.RequestMethod.HEAD; +import static org.springframework.web.bind.annotation.RequestMethod.OPTIONS; +import static org.springframework.web.bind.annotation.RequestMethod.PATCH; +import static org.springframework.web.bind.annotation.RequestMethod.POST; +import static org.springframework.web.bind.annotation.RequestMethod.PUT; +import static org.springframework.web.bind.annotation.RequestMethod.TRACE; + +/** + * + * @author Rob Winch + * @author Josh Cummings + */ +@RunWith(SpringJUnit4ClassRunner.class) +@SecurityTestExecutionListeners +public class CsrfConfigTests { + private static final String CONFIG_LOCATION_PREFIX = + "classpath:org/springframework/security/config/http/CsrfConfigTests"; + + @Rule + public final SpringTestRule spring = new SpringTestRule(); + + @Autowired + MockMvc mvc; + + @Test + public void postWhenDefaultConfigurationThenForbiddenSinceCsrfIsEnabled() throws Exception { + this.spring.configLocations( + this.xml("AutoConfig") + ).autowire(); + + this.mvc.perform(post("/csrf")) + .andExpect(status().isForbidden()) + .andExpect(csrfCreated()); + } + + @Test + public void putWhenDefaultConfigurationThenForbiddenSinceCsrfIsEnabled() throws Exception { + this.spring.configLocations( + this.xml("AutoConfig") + ).autowire(); + + this.mvc.perform(put("/csrf")) + .andExpect(status().isForbidden()) + .andExpect(csrfCreated()); + } + + @Test + public void patchWhenDefaultConfigurationThenForbiddenSinceCsrfIsEnabled() throws Exception { + this.spring.configLocations( + this.xml("AutoConfig") + ).autowire(); + + this.mvc.perform(patch("/csrf")) + .andExpect(status().isForbidden()) + .andExpect(csrfCreated()); + } + + @Test + public void deleteWhenDefaultConfigurationThenForbiddenSinceCsrfIsEnabled() throws Exception { + this.spring.configLocations( + this.xml("AutoConfig") + ).autowire(); + + this.mvc.perform(delete("/csrf")) + .andExpect(status().isForbidden()) + .andExpect(csrfCreated()); + } + + @Test + public void invalidWhenDefaultConfigurationThenForbiddenSinceCsrfIsEnabled() throws Exception { + this.spring.configLocations( + this.xml("AutoConfig") + ).autowire(); + + this.mvc.perform(request("INVALID", new URI("/csrf"))) + .andExpect(status().isForbidden()) + .andExpect(csrfCreated()); + } + + @Test + public void getWhenDefaultConfigurationThenCsrfIsEnabled() throws Exception { + this.spring.configLocations( + this.xml("shared-controllers"), + this.xml("AutoConfig") + ).autowire(); + + this.mvc.perform(get("/csrf")) + .andExpect(csrfInBody()); + } + + + @Test + public void headWhenDefaultConfigurationThenCsrfIsEnabled() throws Exception { + this.spring.configLocations( + this.xml("shared-controllers"), + this.xml("AutoConfig") + ).autowire(); + + this.mvc.perform(head("/csrf-in-header")) + .andExpect(csrfInHeader()); + } + + @Test + public void traceWhenDefaultConfigurationThenCsrfIsEnabled() throws Exception { + this.spring.configLocations( + this.xml("shared-controllers"), + this.xml("AutoConfig") + ).autowire(); + + MockMvc traceEnabled = MockMvcBuilders + .webAppContextSetup((WebApplicationContext) this.spring.getContext()) + .apply(springSecurity()) + .addDispatcherServletCustomizer(dispatcherServlet -> dispatcherServlet.setDispatchTraceRequest(true)) + .build(); + + traceEnabled.perform(request(HttpMethod.TRACE, "/csrf-in-header")) + .andExpect(csrfInHeader()); + } + + @Test + public void optionsWhenDefaultConfigurationThenCsrfIsEnabled() throws Exception { + this.spring.configLocations( + this.xml("shared-controllers"), + this.xml("AutoConfig") + ).autowire(); + + this.mvc.perform(options("/csrf-in-header")) + .andExpect(csrfInHeader()); + } + + @Test + public void postWhenCsrfDisabledThenRequestAllowed() throws Exception { + this.spring.configLocations( + this.xml("shared-controllers"), + this.xml("CsrfDisabled") + ).autowire(); + + this.mvc.perform(post("/ok")) + .andExpect(status().isOk()); + + assertThat(getFilter(this.spring, CsrfFilter.class)).isNull(); + } + + @Test + public void postWhenCsrfElementEnabledThenForbidden() throws Exception { + this.spring.configLocations( + this.xml("CsrfEnabled") + ).autowire(); + + this.mvc.perform(post("/csrf")) + .andExpect(status().isForbidden()) + .andExpect(csrfCreated()); + } + + @Test + public void putWhenCsrfElementEnabledThenForbidden() throws Exception { + this.spring.configLocations( + this.xml("CsrfEnabled") + ).autowire(); + + this.mvc.perform(put("/csrf")) + .andExpect(status().isForbidden()) + .andExpect(csrfCreated()); + } + + @Test + public void patchWhenCsrfElementEnabledThenForbidden() throws Exception { + this.spring.configLocations( + this.xml("CsrfEnabled") + ).autowire(); + + this.mvc.perform(patch("/csrf")) + .andExpect(status().isForbidden()) + .andExpect(csrfCreated()); + } + + @Test + public void deleteWhenCsrfElementEnabledThenForbidden() throws Exception { + this.spring.configLocations( + this.xml("CsrfEnabled") + ).autowire(); + + this.mvc.perform(delete("/csrf")) + .andExpect(status().isForbidden()) + .andExpect(csrfCreated()); + } + + @Test + public void invalidWhenCsrfElementEnabledThenForbidden() throws Exception { + this.spring.configLocations( + this.xml("CsrfEnabled") + ).autowire(); + + this.mvc.perform(request("INVALID", new URI("/csrf"))) + .andExpect(status().isForbidden()) + .andExpect(csrfCreated()); + } + + @Test + public void getWhenCsrfElementEnabledThenOk() throws Exception { + this.spring.configLocations( + this.xml("shared-controllers"), + this.xml("CsrfEnabled") + ).autowire(); + + this.mvc.perform(get("/csrf")) + .andExpect(csrfInBody()); + } + + @Test + public void headWhenCsrfElementEnabledThenOk() throws Exception { + this.spring.configLocations( + this.xml("shared-controllers"), + this.xml("CsrfEnabled") + ).autowire(); + + this.mvc.perform(head("/csrf-in-header")) + .andExpect(csrfInHeader()); + } + + @Test + public void traceWhenCsrfElementEnabledThenOk() throws Exception { + this.spring.configLocations( + this.xml("shared-controllers"), + this.xml("CsrfEnabled") + ).autowire(); + + MockMvc traceEnabled = MockMvcBuilders + .webAppContextSetup((WebApplicationContext) this.spring.getContext()) + .apply(springSecurity()) + .addDispatcherServletCustomizer(dispatcherServlet -> dispatcherServlet.setDispatchTraceRequest(true)) + .build(); + + traceEnabled.perform(request(HttpMethod.TRACE, "/csrf-in-header")) + .andExpect(csrfInHeader()); + } + + @Test + public void optionsWhenCsrfElementEnabledThenOk() throws Exception { + this.spring.configLocations( + this.xml("shared-controllers"), + this.xml("CsrfEnabled") + ).autowire(); + + this.mvc.perform(options("/csrf-in-header")) + .andExpect(csrfInHeader()); + } + + @Test + public void autowireWhenCsrfElementEnabledThenCreatesCsrfRequestDataValueProcessor() { + this.spring.configLocations( + this.xml("CsrfEnabled") + ).autowire(); + + assertThat(this.spring.getContext().getBean(RequestDataValueProcessor.class)).isNotNull(); + } + + @Test + public void postWhenUsingCsrfAndCustomAccessDeniedHandlerThenTheHandlerIsAppropriatelyEngaged() + throws Exception { + + this.spring.configLocations( + this.xml("WithAccessDeniedHandler"), + this.xml("shared-access-denied-handler") + ).autowire(); + + this.mvc.perform(post("/ok")) + .andExpect(status().isIAmATeapot()); + } + + @Test + public void postWhenHasCsrfTokenButSessionExpiresThenRequestIsCancelledAfterSuccessfulAuthentication() + throws Exception { + + this.spring.configLocations( + this.xml("CsrfEnabled") + ).autowire(); + + // simulates a request that has no authentication (e.g. session time-out) + MvcResult result = this.mvc.perform(post("/authenticated") + .with(csrf())) + .andExpect(redirectedUrl("http://localhost/login")) + .andReturn(); + + MockHttpSession session = (MockHttpSession) result.getRequest().getSession(); + + // if the request cache is consulted, then it will redirect back to /some-url, which we don't want + this.mvc.perform(post("/login") + .param("username", "user") + .param("password", "password") + .session(session) + .with(csrf())) + .andExpect(redirectedUrl("/")); + } + + @Test + public void getWhenHasCsrfTokenButSessionExpiresThenRequestIsRememeberedAfterSuccessfulAuthentication() + throws Exception { + + this.spring.configLocations( + this.xml("CsrfEnabled") + ).autowire(); + + // simulates a request that has no authentication (e.g. session time-out) + MvcResult result = + this.mvc.perform(get("/authenticated")) + .andExpect(redirectedUrl("http://localhost/login")) + .andReturn(); + + MockHttpSession session = (MockHttpSession) result.getRequest().getSession(); + + // if the request cache is consulted, then it will redirect back to /some-url, which we do want + this.mvc.perform(post("/login") + .param("username", "user") + .param("password", "password") + .session(session) + .with(csrf())) + .andExpect(redirectedUrl("http://localhost/authenticated")); + } + + /** + * SEC-2422: csrf expire CSRF token and session-management invalid-session-url + */ + @Test + public void postWhenUsingCsrfAndCustomSessionManagementAndNoSessionThenStillRedirectsToInvalidSessionUrl() + throws Exception { + + this.spring.configLocations( + this.xml("WithSessionManagement") + ).autowire(); + + MvcResult result = this.mvc.perform(post("/ok").param("_csrf", "abc")) + .andExpect(redirectedUrl("/error/sessionError")) + .andReturn(); + + MockHttpSession session = (MockHttpSession) result.getRequest().getSession(); + + this.mvc.perform(post("/csrf") + .session(session)) + .andExpect(status().isForbidden()); + } + + @Test + public void requestWhenUsingCustomRequestMatcherConfiguredThenAppliesAccordingly() + throws Exception { + + SpringTestContext context = + this.spring.configLocations( + this.xml("shared-controllers"), + this.xml("WithRequestMatcher"), + this.xml("mock-request-matcher") + ); + + context.autowire(); + + RequestMatcher matcher = context.getContext().getBean(RequestMatcher.class); + when(matcher.matches(any(HttpServletRequest.class))).thenReturn(false); + + this.mvc.perform(post("/ok")).andExpect(status().isOk()); + + when(matcher.matches(any(HttpServletRequest.class))).thenReturn(true); + + this.mvc.perform(get("/ok")).andExpect(status().isForbidden()); + } + + @Test + public void getWhenDefaultConfigurationThenSessionNotImmediatelyCreated() + throws Exception { + + this.spring.configLocations( + this.xml("shared-controllers"), + this.xml("AutoConfig") + ).autowire(); + + MvcResult result = this.mvc.perform(get("/ok")) + .andExpect(status().isOk()) + .andReturn(); + + assertThat(result.getRequest().getSession(false)).isNull(); + } + + @Test + @WithMockUser + public void postWhenCsrfMismatchesThenForbidden() + throws Exception { + + this.spring.configLocations( + this.xml("shared-controllers"), + this.xml("AutoConfig") + ).autowire(); + + MvcResult result = this.mvc.perform(get("/ok")).andReturn(); + + MockHttpSession session = (MockHttpSession) result.getRequest().getSession(); + + this.mvc.perform(post("/ok") + .session(session) + .with(csrf().useInvalidToken())) + .andExpect(status().isForbidden()); + } + + @Test + public void loginWhenDefaultConfigurationThenCsrfCleared() + throws Exception { + + this.spring.configLocations( + this.xml("shared-controllers"), + this.xml("AutoConfig") + ).autowire(); + + MvcResult result = this.mvc.perform(get("/csrf")).andReturn(); + + MockHttpSession session = (MockHttpSession) result.getRequest().getSession(); + + this.mvc.perform(post("/login") + .param("username", "user") + .param("password", "password") + .session(session) + .with(csrf())) + .andExpect(status().isFound()); + + this.mvc.perform(get("/csrf").session(session)) + .andExpect(csrfChanged(result)); + } + + @Test + public void logoutWhenDefaultConfigurationThenCsrfCleared() + throws Exception { + + this.spring.configLocations( + this.xml("shared-controllers"), + this.xml("AutoConfig") + ).autowire(); + + MvcResult result = this.mvc.perform(get("/csrf")).andReturn(); + + MockHttpSession session = (MockHttpSession) result.getRequest().getSession(); + + this.mvc.perform(post("/logout").session(session) + .with(csrf())) + .andExpect(status().isFound()); + + this.mvc.perform(get("/csrf").session(session)) + .andExpect(csrfChanged(result)); + } + + /** + * SEC-2495: csrf disables logout on GET + */ + @Test + @WithMockUser + public void logoutWhenDefaultConfigurationThenDisabled() + throws Exception { + + this.spring.configLocations( + this.xml("shared-controllers"), + this.xml("CsrfEnabled") + ).autowire(); + + this.mvc.perform(get("/logout")).andExpect(status().isNotFound()); + + // still logged in + this.mvc.perform(get("/authenticated")).andExpect(status().isOk()); + } + + private T getFilter(SpringTestContext context, Class type) { + FilterChainProxy chain = context.getContext().getBean(FilterChainProxy.class); + + List filters = chain.getFilters("/any"); + + for ( Filter filter : filters ) { + if ( type.isAssignableFrom(filter.getClass()) ) { + return (T) filter; + } + } + + return null; + } + + private String xml(String configName) { + return CONFIG_LOCATION_PREFIX + "-" + configName + ".xml"; + } + + @Controller + public static class RootController { + @RequestMapping(value = "/csrf-in-header", method = { HEAD, TRACE, OPTIONS }) + @ResponseBody + String csrfInHeaderAndBody(CsrfToken token, HttpServletResponse response) { + response.setHeader(token.getHeaderName(), token.getToken()); + return csrfInBody(token); + } + + @RequestMapping(value = "/csrf", method = { POST, PUT, PATCH, DELETE, GET }) + @ResponseBody + String csrfInBody(CsrfToken token) { + return token.getToken(); + } + + @RequestMapping(value = "/ok", method = { POST, GET }) + @ResponseBody + String ok() { + return "ok"; + } + + @GetMapping("/authenticated") + @ResponseBody + String authenticated() { + return "authenticated"; + } + } + + private static class TeapotAccessDeniedHandler implements AccessDeniedHandler { + + @Override + public void handle( + HttpServletRequest request, + HttpServletResponse response, + AccessDeniedException accessDeniedException) { + + response.setStatus(HttpStatus.IM_A_TEAPOT_418); + } + } + + ResultMatcher csrfChanged(MvcResult first) { + return (second) -> { + assertThat(first).isNotNull(); + assertThat(second).isNotNull(); + assertThat(first.getResponse().getContentAsString()) + .isNotEqualTo(second.getResponse().getContentAsString()); + }; + } + + ResultMatcher csrfCreated() { + return new CsrfCreatedResultMatcher(); + } + + ResultMatcher csrfInHeader() { + return new CsrfReturnedResultMatcher(result -> result.getResponse().getHeader("X-CSRF-TOKEN")); + } + + ResultMatcher csrfInBody() { + return new CsrfReturnedResultMatcher(result -> result.getResponse().getContentAsString()); + } + + @FunctionalInterface + interface ExceptionalFunction { + OUT apply(IN in) throws Exception; + } + + static class CsrfCreatedResultMatcher implements ResultMatcher { + @Override + public void match(MvcResult result) throws Exception { + MockHttpServletRequest request = result.getRequest(); + CsrfToken token = WebTestUtils.getCsrfTokenRepository(request).loadToken(request); + assertThat(token).isNotNull(); + } + } + + static class CsrfReturnedResultMatcher implements ResultMatcher { + ExceptionalFunction token; + + public CsrfReturnedResultMatcher(ExceptionalFunction token) { + this.token = token; + } + + @Override + public void match(MvcResult result) throws Exception { + MockHttpServletRequest request = result.getRequest(); + CsrfToken token = WebTestUtils.getCsrfTokenRepository(request).loadToken(request); + assertThat(token).isNotNull(); + assertThat(token.getToken()).isEqualTo(this.token.apply(result)); + } + } + +} diff --git a/config/src/test/resources/org/springframework/security/config/http/CsrfConfigTests-AutoConfig.xml b/config/src/test/resources/org/springframework/security/config/http/CsrfConfigTests-AutoConfig.xml new file mode 100644 index 0000000000..44959cf8d9 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/CsrfConfigTests-AutoConfig.xml @@ -0,0 +1,27 @@ + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/CsrfConfigTests-CsrfDisabled.xml b/config/src/test/resources/org/springframework/security/config/http/CsrfConfigTests-CsrfDisabled.xml new file mode 100644 index 0000000000..7c7dc7dcde --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/CsrfConfigTests-CsrfDisabled.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/CsrfConfigTests-CsrfEnabled.xml b/config/src/test/resources/org/springframework/security/config/http/CsrfConfigTests-CsrfEnabled.xml new file mode 100644 index 0000000000..3a09d6e370 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/CsrfConfigTests-CsrfEnabled.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/CsrfConfigTests-WithAccessDeniedHandler.xml b/config/src/test/resources/org/springframework/security/config/http/CsrfConfigTests-WithAccessDeniedHandler.xml new file mode 100644 index 0000000000..299c8d6780 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/CsrfConfigTests-WithAccessDeniedHandler.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/CsrfConfigTests-WithRequestMatcher.xml b/config/src/test/resources/org/springframework/security/config/http/CsrfConfigTests-WithRequestMatcher.xml new file mode 100644 index 0000000000..fff5265b3e --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/CsrfConfigTests-WithRequestMatcher.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/CsrfConfigTests-WithSessionManagement.xml b/config/src/test/resources/org/springframework/security/config/http/CsrfConfigTests-WithSessionManagement.xml new file mode 100644 index 0000000000..aa9903414e --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/CsrfConfigTests-WithSessionManagement.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/CsrfConfigTests-mock-csrf-token-repository.xml b/config/src/test/resources/org/springframework/security/config/http/CsrfConfigTests-mock-csrf-token-repository.xml new file mode 100644 index 0000000000..679057095e --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/CsrfConfigTests-mock-csrf-token-repository.xml @@ -0,0 +1,28 @@ + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/CsrfConfigTests-mock-request-matcher.xml b/config/src/test/resources/org/springframework/security/config/http/CsrfConfigTests-mock-request-matcher.xml new file mode 100644 index 0000000000..5ef112ef23 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/CsrfConfigTests-mock-request-matcher.xml @@ -0,0 +1,27 @@ + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/CsrfConfigTests-shared-access-denied-handler.xml b/config/src/test/resources/org/springframework/security/config/http/CsrfConfigTests-shared-access-denied-handler.xml new file mode 100644 index 0000000000..48bc445349 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/CsrfConfigTests-shared-access-denied-handler.xml @@ -0,0 +1,25 @@ + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/CsrfConfigTests-shared-controllers.xml b/config/src/test/resources/org/springframework/security/config/http/CsrfConfigTests-shared-controllers.xml new file mode 100644 index 0000000000..414a8092a2 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/CsrfConfigTests-shared-controllers.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/CsrfConfigTests-shared-csrf-token-repository.xml b/config/src/test/resources/org/springframework/security/config/http/CsrfConfigTests-shared-csrf-token-repository.xml new file mode 100644 index 0000000000..b081722838 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/CsrfConfigTests-shared-csrf-token-repository.xml @@ -0,0 +1,25 @@ + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/CsrfConfigTests-shared-userservice.xml b/config/src/test/resources/org/springframework/security/config/http/CsrfConfigTests-shared-userservice.xml new file mode 100644 index 0000000000..a3ca9b70ac --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/CsrfConfigTests-shared-userservice.xml @@ -0,0 +1,26 @@ + + + + + + + +