diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configuration/WebMvcSecurityConfiguration.java b/config/src/main/java/org/springframework/security/config/annotation/web/configuration/WebMvcSecurityConfiguration.java index 2928697ae3..1bdf1156b8 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configuration/WebMvcSecurityConfiguration.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configuration/WebMvcSecurityConfiguration.java @@ -17,6 +17,7 @@ package org.springframework.security.config.annotation.web.configuration; import org.springframework.context.annotation.Bean; import org.springframework.security.web.method.annotation.AuthenticationPrincipalArgumentResolver; +import org.springframework.security.web.method.annotation.CsrfTokenArgumentResolver; import org.springframework.security.web.servlet.support.csrf.CsrfRequestDataValueProcessor; import org.springframework.web.method.support.HandlerMethodArgumentResolver; import org.springframework.web.servlet.config.annotation.EnableWebMvc; @@ -44,6 +45,7 @@ class WebMvcSecurityConfiguration extends WebMvcConfigurerAdapter { List argumentResolvers) { argumentResolvers.add(new AuthenticationPrincipalArgumentResolver()); argumentResolvers.add(new org.springframework.security.web.bind.support.AuthenticationPrincipalArgumentResolver()); + argumentResolvers.add(new CsrfTokenArgumentResolver()); } @ConditionalOnMissingBean(RequestDataValueProcessor.class) diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configuration/WebMvcSecurityConfigurationTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configuration/WebMvcSecurityConfigurationTests.java new file mode 100644 index 0000000000..ca5899556a --- /dev/null +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configuration/WebMvcSecurityConfigurationTests.java @@ -0,0 +1,138 @@ +/* + * Copyright 2002-2015 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.annotation.web.configuration; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.authentication.TestingAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.security.core.authority.AuthorityUtils; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.web.csrf.CsrfToken; +import org.springframework.security.web.csrf.DefaultCsrfToken; +import org.springframework.stereotype.Controller; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.context.web.WebAppConfiguration; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.ResultMatcher; +import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder; +import org.springframework.test.web.servlet.setup.DefaultMockMvcBuilder; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.context.WebApplicationContext; +import org.springframework.web.servlet.ModelAndView; +import org.springframework.web.servlet.config.annotation.EnableWebMvc; + +/** + * @author Rob Winch + */ +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration +@WebAppConfiguration +public class WebMvcSecurityConfigurationTests { + + @Autowired + WebApplicationContext context; + + MockMvc mockMvc; + + Authentication authentication; + + @Before + public void setup() { + mockMvc = MockMvcBuilders.webAppContextSetup(context).build(); + authentication = new TestingAuthenticationToken("user","password", AuthorityUtils.createAuthorityList("ROLE_USER")); + SecurityContextHolder.getContext().setAuthentication(authentication); + } + + @After + public void cleanup() { + SecurityContextHolder.clearContext(); + } + + @Test + public void authenticationPrincipalResolved() throws Exception { + mockMvc + .perform(get("/authentication-principal")) + .andExpect(assertResult(authentication.getPrincipal())) + .andExpect(view().name("authentication-principal-view")); + } + + @Test + public void deprecatedAuthenticationPrincipalResolved() throws Exception { + mockMvc + .perform(get("/deprecated-authentication-principal")) + .andExpect(assertResult(authentication.getPrincipal())) + .andExpect(view().name("deprecated-authentication-principal-view")); + } + + @Test + public void csrfToken() throws Exception { + CsrfToken csrfToken = new DefaultCsrfToken("headerName", "paramName", "token"); + MockHttpServletRequestBuilder request = + get("/csrf") + .requestAttr(CsrfToken.class.getName(), csrfToken); + + mockMvc + .perform(request) + .andExpect(assertResult(csrfToken)); + } + + private ResultMatcher assertResult(Object expected) { + return model().attribute("result", expected); + } + + @Controller + static class TestController { + + @RequestMapping("/authentication-principal") + public ModelAndView authenticationPrincipal(@AuthenticationPrincipal String principal) { + return new ModelAndView("authentication-principal-view", "result", principal); + } + + @RequestMapping("/deprecated-authentication-principal") + public ModelAndView deprecatedAuthenticationPrincipal(@org.springframework.security.web.bind.annotation.AuthenticationPrincipal String principal) { + return new ModelAndView("deprecated-authentication-principal-view", "result", principal); + } + + @RequestMapping("/csrf") + public ModelAndView csrf(CsrfToken token) { + return new ModelAndView("view", "result", token); + } + } + + @Configuration + @EnableWebMvc + @EnableWebSecurity + static class Config { + @Bean + public TestController testController() { + return new TestController(); + } + } + +} \ No newline at end of file diff --git a/docs/manual/src/docs/asciidoc/index.adoc b/docs/manual/src/docs/asciidoc/index.adoc index a65d93d2c8..6e0f003430 100644 --- a/docs/manual/src/docs/asciidoc/index.adoc +++ b/docs/manual/src/docs/asciidoc/index.adoc @@ -6040,7 +6040,7 @@ Spring Security provides a number of optional integrations with Spring MVC. This WARN: As of Spring Security 4.0, `@EnableWebMvcSecurity` is deprecated. The replacement is `@EnableWebSecurity` which will determine adding the Spring MVC features based upon the classpath. -To enable Spring Security integration with Spring MVC add the `@EnableWebSecurity` annotation to your configuration. A typical example will look something like this: +To enable Spring Security integration with Spring MVC add the `@EnableWebSecurity` annotation to your configuration. [[mvc-authentication-principal]] @@ -6134,6 +6134,8 @@ There is no automatic integration with a `DeferredResult` that is returned by co [[mvc-csrf]] === Spring MVC and CSRF Integration +==== Automatic Token Inclusion + Spring Security will automatically <> within forms that use the http://docs.spring.io/spring/docs/3.2.x/spring-framework-reference/html/view.html#view-jsp-formtaglib-formtag[Spring MVC form tag]. For example, the following JSP: [source,xml] @@ -6174,6 +6176,30 @@ Will output HTML that is similar to the following: ---- +[[mvc-csrf-resolver]] +==== Resolving the CsrfToken + +Spring Security provides `CsrfTokenResolver` which can automatically resolve the current `CsrfToken` for Spring MVC arguments. +By using <> you will automatically have this added to your Spring MVC configuration. +If you use XML based configuraiton, you must add this yourself. + +Once `CsrfTokenResolver` is properly configured, you can expose the `CsrfToken` to your static HTML based application. + +[source,java] +---- +@RestController +public class CsrfController { + + @RequestMapping("/csrf") + public CsrfToken csrf(CsrfToken token) { + return token; + } +} +---- + +It is important to keep the `CsrfToken` a secret from other domains. +This means if you are using https://developer.mozilla.org/en-US/docs/Web/HTTP/Access_control_CORS[Cross Origin Sharing (CORS)], you should **NOT** expose the `CsrfToken` to any external domains. + = Appendix [[appendix-schema]] diff --git a/web/src/main/java/org/springframework/security/web/method/annotation/CsrfTokenArgumentResolver.java b/web/src/main/java/org/springframework/security/web/method/annotation/CsrfTokenArgumentResolver.java new file mode 100644 index 0000000000..0bcf4027a1 --- /dev/null +++ b/web/src/main/java/org/springframework/security/web/method/annotation/CsrfTokenArgumentResolver.java @@ -0,0 +1,64 @@ +/* + * Copyright 2002-2013 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.web.method.annotation; + +import org.springframework.core.MethodParameter; +import org.springframework.security.web.csrf.CsrfToken; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.support.WebDataBinderFactory; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.method.support.ModelAndViewContainer; + + +/** + * Allows resolving the current {@link CsrfToken}. For example, the following + * {@link RestController} will resolve the current {@link CsrfToken}: + * + *
+ * @RestController
+ * public class MyController {
+ *     @MessageMapping("/im")
+ *     public CsrfToken csrf(CsrfToken token) {
+ *         return token;
+ *     }
+ * 
+ * + * + * @author Rob Winch + * @since 4.0 + */ +public final class CsrfTokenArgumentResolver implements + HandlerMethodArgumentResolver { + + + /* (non-Javadoc) + * @see org.springframework.web.method.support.HandlerMethodArgumentResolver#supportsParameter(org.springframework.core.MethodParameter) + */ + public boolean supportsParameter(MethodParameter parameter) { + return CsrfToken.class.equals(parameter.getParameterType()); + } + + /* (non-Javadoc) + * @see org.springframework.web.method.support.HandlerMethodArgumentResolver#resolveArgument(org.springframework.core.MethodParameter, org.springframework.web.method.support.ModelAndViewContainer, org.springframework.web.context.request.NativeWebRequest, org.springframework.web.bind.support.WebDataBinderFactory) + */ + public Object resolveArgument(MethodParameter parameter, + ModelAndViewContainer mavContainer, NativeWebRequest webRequest, + WebDataBinderFactory binderFactory) throws Exception { + CsrfToken token = (CsrfToken) webRequest.getAttribute(CsrfToken.class.getName(), NativeWebRequest.SCOPE_REQUEST); + return token; + } +} \ No newline at end of file diff --git a/web/src/test/java/org/springframework/security/web/method/annotation/CsrfTokenArgumentResolverTests.java b/web/src/test/java/org/springframework/security/web/method/annotation/CsrfTokenArgumentResolverTests.java new file mode 100644 index 0000000000..b4fb6497b3 --- /dev/null +++ b/web/src/test/java/org/springframework/security/web/method/annotation/CsrfTokenArgumentResolverTests.java @@ -0,0 +1,104 @@ +/* + * Copyright 2002-2015 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.web.method.annotation; + +import static org.fest.assertions.Assertions.assertThat; + +import java.lang.reflect.Method; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; +import org.springframework.core.MethodParameter; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.security.web.csrf.CsrfToken; +import org.springframework.security.web.csrf.DefaultCsrfToken; +import org.springframework.util.ReflectionUtils; +import org.springframework.web.bind.support.WebDataBinderFactory; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.context.request.ServletWebRequest; +import org.springframework.web.method.support.ModelAndViewContainer; + +/** + * + * @author Rob Winch + * + */ +@RunWith(MockitoJUnitRunner.class) +public class CsrfTokenArgumentResolverTests { + @Mock + private ModelAndViewContainer mavContainer; + @Mock + private WebDataBinderFactory binderFactory; + + private MockHttpServletRequest request; + private NativeWebRequest webRequest; + + private CsrfToken token; + + private CsrfTokenArgumentResolver resolver; + + @Before + public void setup() { + token = new DefaultCsrfToken("X-CSRF-TOKEN", "_csrf", "secret"); + resolver = new CsrfTokenArgumentResolver(); + request = new MockHttpServletRequest(); + webRequest = new ServletWebRequest(request); + } + + @Test + public void supportsParameterFalse() { + assertThat(resolver.supportsParameter(noToken())).isFalse(); + } + + @Test + public void supportsParameterTrue() { + assertThat(resolver.supportsParameter(token())).isTrue(); + } + + @Test + public void resolveArgumentNotFound() throws Exception { + assertThat(resolver.resolveArgument(token(), mavContainer, webRequest, binderFactory)).isNull(); + } + + @Test + public void resolveArgumentFound() throws Exception { + request.setAttribute(CsrfToken.class.getName(), token); + + assertThat(resolver.resolveArgument(token(), mavContainer, webRequest, binderFactory)).isSameAs(token); + } + + private MethodParameter noToken() { + return getMethodParameter("noToken", String.class); + } + + private MethodParameter token() { + return getMethodParameter("token", CsrfToken.class); + } + + private MethodParameter getMethodParameter(String methodName, Class... paramTypes) { + Method method = ReflectionUtils.findMethod(TestController.class, methodName,paramTypes); + return new MethodParameter(method,0); + } + + public static class TestController { + public void noToken(String user) {} + public void token(CsrfToken token) {} + } + +}