SEC-2859: Add CsrfTokenArgumentResolver

This commit is contained in:
Rob Winch 2015-02-18 10:51:30 -06:00
parent c7718a1286
commit a27c33754c
5 changed files with 335 additions and 1 deletions

View File

@ -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<HandlerMethodArgumentResolver> argumentResolvers) {
argumentResolvers.add(new AuthenticationPrincipalArgumentResolver());
argumentResolvers.add(new org.springframework.security.web.bind.support.AuthenticationPrincipalArgumentResolver());
argumentResolvers.add(new CsrfTokenArgumentResolver());
}
@ConditionalOnMissingBean(RequestDataValueProcessor.class)

View File

@ -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();
}
}
}

View File

@ -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 <<csrf-include-csrf-token,include the CSRF Token>> 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 <<mvc-enablewebsecurity>> 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]]

View File

@ -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}:
*
* <pre>
* @RestController
* public class MyController {
* @MessageMapping("/im")
* public CsrfToken csrf(CsrfToken token) {
* return token;
* }
* </pre>
*
*
* @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;
}
}

View File

@ -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) {}
}
}