diff --git a/config/config.gradle b/config/config.gradle index 86123f1525..57fe44bb78 100644 --- a/config/config.gradle +++ b/config/config.gradle @@ -19,6 +19,7 @@ dependencies { project(':spring-security-ldap'), project(':spring-security-openid'), "org.springframework:spring-web:$springVersion", + "org.springframework:spring-webmvc:$springVersion", "org.aspectj:aspectjweaver:$aspectjVersion" provided "org.apache.tomcat:tomcat-servlet-api:$servletApiVersion" diff --git a/config/pom.xml b/config/pom.xml index 85569dc3b2..fb9a5c4570 100644 --- a/config/pom.xml +++ b/config/pom.xml @@ -133,6 +133,13 @@ compile true + + org.springframework + spring-webmvc + 3.2.3.RELEASE + compile + true + org.apache.tomcat tomcat-servlet-api diff --git a/config/src/main/java/org/springframework/security/config/Elements.java b/config/src/main/java/org/springframework/security/config/Elements.java index a324f592e7..aef2d8f223 100644 --- a/config/src/main/java/org/springframework/security/config/Elements.java +++ b/config/src/main/java/org/springframework/security/config/Elements.java @@ -55,4 +55,5 @@ public abstract class Elements { public static final String DEBUG = "debug"; public static final String HTTP_FIREWALL = "http-firewall"; public static final String HEADERS = "headers"; + public static final String CSRF = "csrf"; } diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/builders/FilterComparator.java b/config/src/main/java/org/springframework/security/config/annotation/web/builders/FilterComparator.java index 6b500111a3..16108001eb 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/builders/FilterComparator.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/builders/FilterComparator.java @@ -37,6 +37,7 @@ import org.springframework.security.web.authentication.www.BasicAuthenticationFi import org.springframework.security.web.authentication.www.DigestAuthenticationFilter; import org.springframework.security.web.context.SecurityContextPersistenceFilter; import org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter; +import org.springframework.security.web.csrf.CsrfFilter; import org.springframework.security.web.header.HeaderWriterFilter; import org.springframework.security.web.jaasapi.JaasApiIntegrationFilter; import org.springframework.security.web.savedrequest.RequestCacheAwareFilter; @@ -69,6 +70,8 @@ final class FilterComparator implements Comparator, Serializable { order += STEP; put(HeaderWriterFilter.class, order); order += STEP; + put(CsrfFilter.class, order); + order += STEP; put(LogoutFilter.class, order); order += STEP; put(X509AuthenticationFilter.class, order); diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/builders/HttpSecurity.java b/config/src/main/java/org/springframework/security/config/annotation/web/builders/HttpSecurity.java index 8a295a464c..1a3407db7c 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/builders/HttpSecurity.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/builders/HttpSecurity.java @@ -38,6 +38,7 @@ import org.springframework.security.config.annotation.web.configuration.WebSecur import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.config.annotation.web.configurers.AnonymousConfigurer; import org.springframework.security.config.annotation.web.configurers.ChannelSecurityConfigurer; +import org.springframework.security.config.annotation.web.configurers.CsrfConfigurer; import org.springframework.security.config.annotation.web.configurers.ExceptionHandlingConfigurer; import org.springframework.security.config.annotation.web.configurers.ExpressionUrlAuthorizationConfigurer; import org.springframework.security.config.annotation.web.configurers.FormLoginConfigurer; @@ -663,6 +664,17 @@ public final class HttpSecurity extends AbstractConfiguredSecurityBuilder()); } + + /** + * Adds CSRF support + * + * @return the {@link ServletApiConfigurer} for further customizations + * @throws Exception + */ + public CsrfConfigurer csrf() throws Exception { + return getOrApply(new CsrfConfigurer()); + } + /** * Provides logout support. This is automatically applied when using * {@link WebSecurityConfigurerAdapter}. The default is that accessing diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configuration/CsrfWebMvcConfiguration.java b/config/src/main/java/org/springframework/security/config/annotation/web/configuration/CsrfWebMvcConfiguration.java new file mode 100644 index 0000000000..bc7775c86a --- /dev/null +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configuration/CsrfWebMvcConfiguration.java @@ -0,0 +1,40 @@ +/* + * 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.config.annotation.web.configuration; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.web.servlet.support.csrf.CsrfRequestDataValueProcessor; +import org.springframework.web.servlet.config.annotation.EnableWebMvc; +import org.springframework.web.servlet.support.RequestDataValueProcessor; + +/** + * Used to add a {@link RequestDataValueProcessor} for Spring MVC and Spring + * Security CSRF integration. This configuration is added whenever + * {@link EnableWebMvc} is added by {@link SpringWebMvcImportSelector} and the + * DispatcherServlet is present on the classpath. + * + * @author Rob Winch + * @since 3.2 + */ +@Configuration +class CsrfWebMvcConfiguration { + + @Bean + public RequestDataValueProcessor requestDataValueProcessor() { + return new CsrfRequestDataValueProcessor(); + } +} diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configuration/EnableWebSecurity.java b/config/src/main/java/org/springframework/security/config/annotation/web/configuration/EnableWebSecurity.java index 39b8fab879..3fdc5e9ae7 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configuration/EnableWebSecurity.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configuration/EnableWebSecurity.java @@ -77,7 +77,7 @@ import org.springframework.security.config.annotation.web.WebSecurityConfigurer; @Retention(value=java.lang.annotation.RetentionPolicy.RUNTIME) @Target(value={java.lang.annotation.ElementType.TYPE}) @Documented -@Import({WebSecurityConfiguration.class,ObjectPostProcessorConfiguration.class,AuthenticationConfiguration.class}) +@Import({WebSecurityConfiguration.class,ObjectPostProcessorConfiguration.class,AuthenticationConfiguration.class, SpringWebMvcImportSelector.class}) public @interface EnableWebSecurity { /** diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configuration/SpringWebMvcImportSelector.java b/config/src/main/java/org/springframework/security/config/annotation/web/configuration/SpringWebMvcImportSelector.java new file mode 100644 index 0000000000..5cc4073bc0 --- /dev/null +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configuration/SpringWebMvcImportSelector.java @@ -0,0 +1,39 @@ +/* + * 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.config.annotation.web.configuration; + +import org.springframework.context.annotation.ImportSelector; +import org.springframework.core.type.AnnotationMetadata; +import org.springframework.util.ClassUtils; + +/** + * Used by {@link EnableWebSecurity} to conditionaly import + * {@link CsrfWebMvcConfiguration} when the DispatcherServlet is present on the + * classpath. + * + * @author Rob Winch + * @since 3.2 + */ +class SpringWebMvcImportSelector implements ImportSelector { + + /* (non-Javadoc) + * @see org.springframework.context.annotation.ImportSelector#selectImports(org.springframework.core.type.AnnotationMetadata) + */ + public String[] selectImports(AnnotationMetadata importingClassMetadata) { + boolean webmvcPresent = ClassUtils.isPresent("org.springframework.web.servlet.DispatcherServlet", getClass().getClassLoader()); + return webmvcPresent ? new String[] {"org.springframework.security.config.annotation.web.configuration.CsrfWebMvcConfiguration"} : new String[] {}; + } +} diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configuration/WebSecurityConfigurerAdapter.java b/config/src/main/java/org/springframework/security/config/annotation/web/configuration/WebSecurityConfigurerAdapter.java index 398b76db0c..23751949c1 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configuration/WebSecurityConfigurerAdapter.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configuration/WebSecurityConfigurerAdapter.java @@ -154,6 +154,7 @@ public abstract class WebSecurityConfigurerAdapter implements SecurityConfigurer http.setSharedObject(ContentNegotiationStrategy.class, contentNegotiationStrategy); if(!disableDefaults) { http + .csrf().and() .addFilter(new WebAsyncManagerIntegrationFilter()) .exceptionHandling().and() .headers().and() diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/AbstractHttpConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/AbstractHttpConfigurer.java index 5c6dd82f0b..bb7b21136d 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/AbstractHttpConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/AbstractHttpConfigurer.java @@ -30,4 +30,15 @@ import org.springframework.security.web.DefaultSecurityFilterChain; */ abstract class AbstractHttpConfigurer> extends SecurityConfigurerAdapter { + /** + * Disables the {@link AbstractHttpConfigurer} by removing it. After doing + * so a fresh version of the configuration can be applied. + * + * @return the {@link HttpSecurityBuilder} for additional customizations + */ + @SuppressWarnings("unchecked") + public B disable() { + getBuilder().removeConfigurer(getClass()); + return getBuilder(); + } } diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/AnonymousConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/AnonymousConfigurer.java index c227c1b558..4a4dc01f57 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/AnonymousConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/AnonymousConfigurer.java @@ -39,7 +39,7 @@ import org.springframework.security.web.authentication.AnonymousAuthenticationFi * @author Rob Winch * @since 3.2 */ -public final class AnonymousConfigurer> extends SecurityConfigurerAdapter { +public final class AnonymousConfigurer> extends AbstractHttpConfigurer { private String key; private AuthenticationProvider authenticationProvider; private AnonymousAuthenticationFilter authenticationFilter; @@ -53,18 +53,6 @@ public final class AnonymousConfigurer> extends public AnonymousConfigurer() { } - /** - * Disables anonymous authentication. - * - * @return the {@link HttpSecurity} since no further customization of anonymous authentication would be - * meaningful. - */ - @SuppressWarnings("unchecked") - public H disable() { - getBuilder().removeConfigurer(getClass()); - return getBuilder(); - } - /** * Sets the key to identify tokens created for anonymous authentication. Default is a secure randomly generated * key. diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/CsrfConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/CsrfConfigurer.java new file mode 100644 index 0000000000..7879cf7440 --- /dev/null +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/CsrfConfigurer.java @@ -0,0 +1,121 @@ +/* + * 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.config.annotation.web.configurers; + +import org.springframework.security.config.annotation.web.HttpSecurityBuilder; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.web.access.AccessDeniedHandler; +import org.springframework.security.web.csrf.CsrfAuthenticationStrategy; +import org.springframework.security.web.csrf.CsrfFilter; +import org.springframework.security.web.csrf.CsrfLogoutHandler; +import org.springframework.security.web.csrf.CsrfTokenRepository; +import org.springframework.security.web.csrf.HttpSessionCsrfTokenRepository; +import org.springframework.security.web.util.RequestMatcher; +import org.springframework.util.Assert; + +/** + * Adds CSRF protection for the methods as specified by + * {@link #requireCsrfProtectionMatcher(RequestMatcher)}. + * + *

Security Filters

+ * + * The following Filters are populated + * + *
    + *
  • {@link CsrfFilter}
  • + *
+ * + *

Shared Objects Created

+ * + * No shared objects are created. + * + *

Shared Objects Used

+ * + *
    + *
  • + * {@link ExceptionHandlingConfigurer#accessDeniedHandler(AccessDeniedHandler)} + * is used to determine how to handle CSRF attempts
  • + *
+ * + * @author Rob Winch + * @since 3.2 + */ +public final class CsrfConfigurer> extends AbstractHttpConfigurer { + private CsrfTokenRepository csrfTokenRepository = new HttpSessionCsrfTokenRepository(); + private RequestMatcher requireCsrfProtectionMatcher; + + /** + * Creates a new instance + * @see HttpSecurity#csrf() + */ + public CsrfConfigurer() { + } + + /** + * Specify the {@link CsrfTokenRepository} to use. The default is an {@link HttpSessionCsrfTokenRepository}. + * + * @param csrfTokenRepository the {@link CsrfTokenRepository} to use + * @return the {@link CsrfConfigurer} for further customizations + */ + public CsrfConfigurer csrfTokenRepository(CsrfTokenRepository csrfTokenRepository) { + Assert.notNull(csrfTokenRepository, "csrfTokenRepository cannot be null"); + this.csrfTokenRepository = csrfTokenRepository; + return this; + } + + /** + * Specify the {@link RequestMatcher} to use for determining when CSRF + * should be applied. The default is to ignore GET, HEAD, TRACE, OPTIONS and + * process all other requests. + * + * @param requireCsrfProtectionMatcher + * the {@link RequestMatcher} to use + * @return the {@link CsrfConfigurer} for further customizations + */ + public CsrfConfigurer requireCsrfProtectionMatcher(RequestMatcher requireCsrfProtectionMatcher) { + Assert.notNull(csrfTokenRepository, "requireCsrfProtectionMatcher cannot be null"); + this.requireCsrfProtectionMatcher = requireCsrfProtectionMatcher; + return this; + } + + @SuppressWarnings("unchecked") + @Override + public void configure(H http) throws Exception { + CsrfFilter filter = new CsrfFilter(csrfTokenRepository); + if(requireCsrfProtectionMatcher != null) { + filter.setRequireCsrfProtectionMatcher(requireCsrfProtectionMatcher); + } + ExceptionHandlingConfigurer exceptionConfig = http.getConfigurer(ExceptionHandlingConfigurer.class); + if(exceptionConfig != null) { + AccessDeniedHandler accessDeniedHandler = exceptionConfig.getAccessDeniedHandler(); + if(accessDeniedHandler != null) { + filter.setAccessDeniedHandler(accessDeniedHandler); + } + } + LogoutConfigurer logoutConfigurer = http.getConfigurer(LogoutConfigurer.class); + if(logoutConfigurer != null) { + logoutConfigurer.addLogoutHandler(new CsrfLogoutHandler(csrfTokenRepository)); + } + SessionManagementConfigurer sessionConfigurer = http.getConfigurer(SessionManagementConfigurer.class); + if(sessionConfigurer != null) { + sessionConfigurer.addSessionAuthenticationStrategy(new CsrfAuthenticationStrategy(csrfTokenRepository)); + } + filter = postProcess(filter); + http.addFilter(filter); + } +} \ No newline at end of file diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/ExceptionHandlingConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/ExceptionHandlingConfigurer.java index 0bc492d876..cfaf8e1eb2 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/ExceptionHandlingConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/ExceptionHandlingConfigurer.java @@ -153,6 +153,15 @@ public final class ExceptionHandlingConfigurer> return this.authenticationEntryPoint; } + /** + * Gets the {@link AccessDeniedHandler} that is configured. + * + * @return the {@link AccessDeniedHandler} + */ + AccessDeniedHandler getAccessDeniedHandler() { + return this.accessDeniedHandler; + } + @Override public void configure(H http) throws Exception { AuthenticationEntryPoint entryPoint = getAuthenticationEntryPoint(http); diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/LogoutConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/LogoutConfigurer.java index 4f36b8f102..8c7616d0eb 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/LogoutConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/LogoutConfigurer.java @@ -16,7 +16,6 @@ package org.springframework.security.config.annotation.web.configurers; import java.util.ArrayList; -import java.util.Collections; import java.util.List; import javax.servlet.http.HttpSession; @@ -31,6 +30,8 @@ import org.springframework.security.web.authentication.logout.LogoutSuccessHandl import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler; import org.springframework.security.web.authentication.logout.SimpleUrlLogoutSuccessHandler; import org.springframework.security.web.authentication.ui.DefaultLoginPageViewFilter; +import org.springframework.security.web.util.AntPathRequestMatcher; +import org.springframework.security.web.util.RequestMatcher; /** * Adds logout support. Other {@link SecurityConfigurer} instances may invoke @@ -63,7 +64,7 @@ public final class LogoutConfigurer> extends Ab private SecurityContextLogoutHandler contextLogoutHandler = new SecurityContextLogoutHandler(); private String logoutSuccessUrl = "/login?logout"; private LogoutSuccessHandler logoutSuccessHandler; - private String logoutUrl = "/logout"; + private RequestMatcher logoutRequestMatcher = new AntPathRequestMatcher("/logout", "POST"); private boolean permitAll; private boolean customLogoutSuccess; @@ -97,12 +98,22 @@ public final class LogoutConfigurer> extends Ab } /** - * The URL that triggers logout to occur. The default is "/logout" + * The URL that triggers logout to occur on HTTP POST. The default is "/logout" * @param logoutUrl the URL that will invoke logout. * @return the {@link LogoutConfigurer} for further customization */ public LogoutConfigurer logoutUrl(String logoutUrl) { - this.logoutUrl = logoutUrl; + return logoutRequestMatcher(new AntPathRequestMatcher(logoutUrl, "POST")); + } + + + /** + * The RequestMatcher that triggers logout to occur on HTTP POST. The default is "/logout" + * @param logoutRequestMatcher the RequestMatcher used to determine if logout should occur. + * @return the {@link LogoutConfigurer} for further customization + */ + public LogoutConfigurer logoutRequestMatcher(RequestMatcher logoutRequestMatcher) { + this.logoutRequestMatcher = logoutRequestMatcher; return this; } @@ -189,7 +200,8 @@ public final class LogoutConfigurer> extends Ab @Override public void init(H http) throws Exception { if(permitAll) { - PermitAllSupport.permitAll(http, this.logoutUrl, this.logoutSuccessUrl); + PermitAllSupport.permitAll(http, this.logoutSuccessUrl); + PermitAllSupport.permitAll(http, this.logoutRequestMatcher); } DefaultLoginPageViewFilter loginPageGeneratingFilter = http.getSharedObject(DefaultLoginPageViewFilter.class); @@ -245,7 +257,7 @@ public final class LogoutConfigurer> extends Ab logoutHandlers.add(contextLogoutHandler); LogoutHandler[] handlers = logoutHandlers.toArray(new LogoutHandler[logoutHandlers.size()]); LogoutFilter result = new LogoutFilter(getLogoutSuccessHandler(), handlers); - result.setFilterProcessesUrl(logoutUrl); + result.setLogoutRequestMatcher(logoutRequestMatcher); result = postProcess(result); return result; } diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/PermitAllSupport.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/PermitAllSupport.java index 2291a2d71f..28ada4568e 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/PermitAllSupport.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/PermitAllSupport.java @@ -31,17 +31,25 @@ import org.springframework.security.web.util.RequestMatcher; */ final class PermitAllSupport { - @SuppressWarnings("unchecked") public static void permitAll(HttpSecurityBuilder> http, String... urls) { + for(String url : urls) { + if(url != null) { + permitAll(http, new ExactUrlRequestMatcher(url)); + } + } + } + + @SuppressWarnings("unchecked") + public static void permitAll(HttpSecurityBuilder> http, RequestMatcher... requestMatchers) { ExpressionUrlAuthorizationConfigurer configurer = http.getConfigurer(ExpressionUrlAuthorizationConfigurer.class); if(configurer == null) { throw new IllegalStateException("permitAll only works with HttpSecurity.authorizeRequests()"); } - for(String url : urls) { - if(url != null) { - configurer.addMapping(0, new UrlMapping(new ExactUrlRequestMatcher(url), SecurityConfig.createList(ExpressionUrlAuthorizationConfigurer.permitAll))); + for(RequestMatcher matcher : requestMatchers) { + if(matcher != null) { + configurer.addMapping(0, new UrlMapping(matcher, SecurityConfig.createList(ExpressionUrlAuthorizationConfigurer.permitAll))); } } } diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/RequestCacheConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/RequestCacheConfigurer.java index 1d57e5664d..7d5747b6d9 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/RequestCacheConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/RequestCacheConfigurer.java @@ -20,6 +20,7 @@ import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.web.savedrequest.HttpSessionRequestCache; import org.springframework.security.web.savedrequest.RequestCache; import org.springframework.security.web.savedrequest.RequestCacheAwareFilter; +import org.springframework.security.web.util.AntPathRequestMatcher; /** * Adds request cache for Spring Security. Specifically this ensures that @@ -71,6 +72,11 @@ public final class RequestCacheConfigurer> exte return this; } + @Override + public void init(H http) throws Exception { + http.setSharedObject(RequestCache.class, getRequestCache(http)); + } + @Override public void configure(H http) throws Exception { RequestCache requestCache = getRequestCache(http); @@ -93,6 +99,8 @@ public final class RequestCacheConfigurer> exte if(result != null) { return result; } - return new HttpSessionRequestCache(); + HttpSessionRequestCache defaultCache = new HttpSessionRequestCache(); + defaultCache.setRequestMatcher(new AntPathRequestMatcher("/**", "GET")); + return defaultCache; } } \ No newline at end of file diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/ServletApiConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/ServletApiConfigurer.java index 2971fbd951..b343e4f3a5 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/ServletApiConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/ServletApiConfigurer.java @@ -64,12 +64,6 @@ public final class ServletApiConfigurer> extend return this; } - @SuppressWarnings("unchecked") - public H disable() { - getBuilder().removeConfigurer(getClass()); - return getBuilder(); - } - @Override @SuppressWarnings("unchecked") public void configure(H http) throws Exception { diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/SessionManagementConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/SessionManagementConfigurer.java index 9763231f6e..de865a5d87 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/SessionManagementConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/SessionManagementConfigurer.java @@ -15,6 +15,7 @@ */ package org.springframework.security.config.annotation.web.configurers; +import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -80,6 +81,7 @@ import org.springframework.util.Assert; public final class SessionManagementConfigurer> extends AbstractHttpConfigurer { private SessionAuthenticationStrategy sessionFixationAuthenticationStrategy = createDefaultSessionFixationProtectionStrategy(); private SessionAuthenticationStrategy sessionAuthenticationStrategy; + private List sessionAuthenticationStrategies = new ArrayList(); private SessionRegistry sessionRegistry = new SessionRegistryImpl(); private Integer maximumSessions; private String expiredUrl; @@ -173,6 +175,18 @@ public final class SessionManagementConfigurer> return this; } + /** + * Adds an additional {@link SessionAuthenticationStrategy} to be used within the {@link CompositeSessionAuthenticationStrategy}. + * + * @param sessionAuthenticationStrategy + * @return the {@link SessionManagementConfigurer} for further + * customizations + */ + SessionManagementConfigurer addSessionAuthenticationStrategy(SessionAuthenticationStrategy sessionAuthenticationStrategy) { + this.sessionAuthenticationStrategies.add(sessionAuthenticationStrategy); + return this; + } + public SessionFixationConfigurer sessionFixation() { return new SessionFixationConfigurer(); } @@ -400,6 +414,7 @@ public final class SessionManagementConfigurer> if(sessionAuthenticationStrategy != null) { return sessionAuthenticationStrategy; } + List delegateStrategies = sessionAuthenticationStrategies; if(isConcurrentSessionControlEnabled()) { ConcurrentSessionControlAuthenticationStrategy concurrentSessionControlStrategy = new ConcurrentSessionControlAuthenticationStrategy(sessionRegistry); concurrentSessionControlStrategy.setMaximumSessions(maximumSessions); @@ -409,11 +424,11 @@ public final class SessionManagementConfigurer> RegisterSessionAuthenticationStrategy registerSessionStrategy = new RegisterSessionAuthenticationStrategy(sessionRegistry); registerSessionStrategy = postProcess(registerSessionStrategy); - List delegateStrategies = Arrays.asList(concurrentSessionControlStrategy, sessionFixationAuthenticationStrategy, registerSessionStrategy); - sessionAuthenticationStrategy = postProcess(new CompositeSessionAuthenticationStrategy(delegateStrategies)); + delegateStrategies.addAll(Arrays.asList(concurrentSessionControlStrategy, sessionFixationAuthenticationStrategy, registerSessionStrategy)); } else { - sessionAuthenticationStrategy = sessionFixationAuthenticationStrategy; + delegateStrategies.add(sessionFixationAuthenticationStrategy); } + sessionAuthenticationStrategy = postProcess(new CompositeSessionAuthenticationStrategy(delegateStrategies)); return sessionAuthenticationStrategy; } diff --git a/config/src/main/java/org/springframework/security/config/http/AuthenticationConfigBuilder.java b/config/src/main/java/org/springframework/security/config/http/AuthenticationConfigBuilder.java index 4bcf8369ae..0e432cee21 100644 --- a/config/src/main/java/org/springframework/security/config/http/AuthenticationConfigBuilder.java +++ b/config/src/main/java/org/springframework/security/config/http/AuthenticationConfigBuilder.java @@ -116,6 +116,7 @@ final class AuthenticationConfigBuilder { private BeanReference jeeProviderRef; private RootBeanDefinition preAuthEntryPoint; private BeanMetadataElement mainEntryPoint; + private BeanMetadataElement accessDeniedHandler; private BeanDefinition logoutFilter; @SuppressWarnings("rawtypes") @@ -125,9 +126,10 @@ final class AuthenticationConfigBuilder { private final BeanReference requestCache; private final BeanReference portMapper; private final BeanReference portResolver; + private final BeanMetadataElement csrfLogoutHandler; public AuthenticationConfigBuilder(Element element, ParserContext pc, SessionCreationPolicy sessionPolicy, - BeanReference requestCache, BeanReference authenticationManager, BeanReference sessionStrategy, BeanReference portMapper, BeanReference portResolver) { + BeanReference requestCache, BeanReference authenticationManager, BeanReference sessionStrategy, BeanReference portMapper, BeanReference portResolver, BeanMetadataElement csrfLogoutHandler) { this.httpElt = element; this.pc = pc; this.requestCache = requestCache; @@ -136,6 +138,7 @@ final class AuthenticationConfigBuilder { && sessionPolicy != SessionCreationPolicy.STATELESS; this.portMapper = portMapper; this.portResolver = portResolver; + this.csrfLogoutHandler = csrfLogoutHandler; createAnonymousFilter(); createRememberMeFilter(authenticationManager); @@ -483,7 +486,7 @@ final class AuthenticationConfigBuilder { void createLogoutFilter() { Element logoutElt = DomUtils.getChildElementByTagName(httpElt, Elements.LOGOUT); if (logoutElt != null || autoConfig) { - LogoutBeanDefinitionParser logoutParser = new LogoutBeanDefinitionParser(rememberMeServicesId); + LogoutBeanDefinitionParser logoutParser = new LogoutBeanDefinitionParser(rememberMeServicesId, csrfLogoutHandler); logoutFilter = logoutParser.parse(logoutElt, pc); logoutHandlers = logoutParser.getLogoutHandlers(); } @@ -493,6 +496,9 @@ final class AuthenticationConfigBuilder { ManagedList getLogoutHandlers() { if(logoutHandlers == null && rememberMeProviderRef != null) { logoutHandlers = new ManagedList(); + if(csrfLogoutHandler != null) { + logoutHandlers.add(csrfLogoutHandler); + } logoutHandlers.add(new RuntimeBeanReference(rememberMeServicesId)); logoutHandlers.add(new RootBeanDefinition(SecurityContextLogoutHandler.class)); } @@ -504,6 +510,10 @@ final class AuthenticationConfigBuilder { return mainEntryPoint; } + BeanMetadataElement getAccessDeniedHandlerBean() { + return accessDeniedHandler; + } + void createAnonymousFilter() { Element anonymousElt = DomUtils.getChildElementByTagName(httpElt, Elements.ANONYMOUS); @@ -559,7 +569,8 @@ final class AuthenticationConfigBuilder { void createExceptionTranslationFilter() { BeanDefinitionBuilder etfBuilder = BeanDefinitionBuilder.rootBeanDefinition(ExceptionTranslationFilter.class); - etfBuilder.addPropertyValue("accessDeniedHandler", createAccessDeniedHandler(httpElt, pc)); + accessDeniedHandler = createAccessDeniedHandler(httpElt, pc); + etfBuilder.addPropertyValue("accessDeniedHandler", accessDeniedHandler); assert requestCache != null; mainEntryPoint = selectEntryPoint(); etfBuilder.addConstructorArgValue(mainEntryPoint); diff --git a/config/src/main/java/org/springframework/security/config/http/CsrfBeanDefinitionParser.java b/config/src/main/java/org/springframework/security/config/http/CsrfBeanDefinitionParser.java new file mode 100644 index 0000000000..1cdeb8bad7 --- /dev/null +++ b/config/src/main/java/org/springframework/security/config/http/CsrfBeanDefinitionParser.java @@ -0,0 +1,88 @@ +/* + * 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 org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.parsing.BeanComponentDefinition; +import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.beans.factory.xml.BeanDefinitionParser; +import org.springframework.beans.factory.xml.ParserContext; +import org.springframework.security.web.csrf.CsrfAuthenticationStrategy; +import org.springframework.security.web.csrf.CsrfFilter; +import org.springframework.security.web.csrf.CsrfLogoutHandler; +import org.springframework.security.web.csrf.HttpSessionCsrfTokenRepository; +import org.springframework.security.web.servlet.support.csrf.CsrfRequestDataValueProcessor; +import org.springframework.util.ClassUtils; +import org.springframework.util.StringUtils; +import org.w3c.dom.Element; + +/** + * Parser for the {@code CsrfFilter}. + * + * @author Rob Winch + * @since 3.2 + */ +public class CsrfBeanDefinitionParser implements BeanDefinitionParser { + + private static final String REQUEST_DATA_VALUE_PROCESSOR = "requestDataValueProcessor"; + private static final String DISPATCHER_SERVLET_CLASS_NAME = "org.springframework.web.servlet.DispatcherServlet"; + private static final String ATT_MATCHER = "request-matcher-ref"; + private static final String ATT_REPOSITORY = "token-repository-ref"; + + private String csrfRepositoryRef; + + public BeanDefinition parse(Element element, ParserContext pc) { + boolean webmvcPresent = ClassUtils.isPresent(DISPATCHER_SERVLET_CLASS_NAME, getClass().getClassLoader()); + if(webmvcPresent) { + RootBeanDefinition beanDefinition = new RootBeanDefinition(CsrfRequestDataValueProcessor.class); + BeanComponentDefinition componentDefinition = + new BeanComponentDefinition(beanDefinition, REQUEST_DATA_VALUE_PROCESSOR); + pc.registerBeanComponent(componentDefinition); + } + + csrfRepositoryRef = element.getAttribute(ATT_REPOSITORY); + String matcherRef = element.getAttribute(ATT_MATCHER); + + BeanDefinitionBuilder builder = BeanDefinitionBuilder.rootBeanDefinition(CsrfFilter.class); + + if(!StringUtils.hasText(csrfRepositoryRef)) { + RootBeanDefinition csrfTokenRepository = new RootBeanDefinition(HttpSessionCsrfTokenRepository.class); + csrfRepositoryRef = pc.getReaderContext().generateBeanName(csrfTokenRepository); + pc.registerBeanComponent(new BeanComponentDefinition(csrfTokenRepository, csrfRepositoryRef)); + } + + builder.addConstructorArgReference(csrfRepositoryRef); + + if(StringUtils.hasText(matcherRef)) { + builder.addPropertyReference("requireCsrfProtectionMatcher", matcherRef); + } + + return builder.getBeanDefinition(); + } + + BeanDefinition getCsrfAuthenticationStrategy() { + BeanDefinitionBuilder csrfAuthenticationStrategy = BeanDefinitionBuilder.rootBeanDefinition(CsrfAuthenticationStrategy.class); + csrfAuthenticationStrategy.addConstructorArgReference(csrfRepositoryRef); + return csrfAuthenticationStrategy.getBeanDefinition(); + } + + BeanDefinition getCsrfLogoutHandler() { + BeanDefinitionBuilder csrfAuthenticationStrategy = BeanDefinitionBuilder.rootBeanDefinition(CsrfLogoutHandler.class); + csrfAuthenticationStrategy.addConstructorArgReference(csrfRepositoryRef); + return csrfAuthenticationStrategy.getBeanDefinition(); + } +} diff --git a/config/src/main/java/org/springframework/security/config/http/HttpConfigurationBuilder.java b/config/src/main/java/org/springframework/security/config/http/HttpConfigurationBuilder.java index 6bfe96b928..51272bf1ff 100644 --- a/config/src/main/java/org/springframework/security/config/http/HttpConfigurationBuilder.java +++ b/config/src/main/java/org/springframework/security/config/http/HttpConfigurationBuilder.java @@ -28,7 +28,6 @@ import javax.servlet.http.HttpServletRequest; import org.springframework.beans.BeanMetadataElement; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.config.BeanReference; -import org.springframework.beans.factory.config.BeanReferenceFactoryBean; import org.springframework.beans.factory.config.RuntimeBeanReference; import org.springframework.beans.factory.parsing.BeanComponentDefinition; import org.springframework.beans.factory.parsing.CompositeComponentDefinition; @@ -71,6 +70,7 @@ import org.springframework.security.web.servletapi.SecurityContextHolderAwareReq import org.springframework.security.web.session.ConcurrentSessionFilter; import org.springframework.security.web.session.SessionManagementFilter; import org.springframework.security.web.session.SimpleRedirectInvalidSessionStrategy; +import org.springframework.security.web.util.AntPathRequestMatcher; import org.springframework.util.ClassUtils; import org.springframework.util.ReflectionUtils; import org.springframework.util.StringUtils; @@ -126,6 +126,9 @@ class HttpConfigurationBuilder { private BeanReference fsi; private BeanReference requestCache; private BeanDefinition addHeadersFilter; + private BeanDefinition csrfFilter; + private BeanMetadataElement csrfLogoutHandler; + private BeanMetadataElement csrfAuthStrategy; public HttpConfigurationBuilder(Element element, ParserContext pc, BeanReference portMapper, BeanReference portResolver, BeanReference authenticationManager) { @@ -152,6 +155,7 @@ class HttpConfigurationBuilder { sessionPolicy = SessionCreationPolicy.IF_REQUIRED; } + createCsrfFilter(); createSecurityContextPersistenceFilter(); createSessionManagementFilters(); createWebAsyncManagerFilter(); @@ -195,6 +199,12 @@ class HttpConfigurationBuilder { } } + void setAccessDeniedHandler(BeanMetadataElement accessDeniedHandler) { + if(csrfFilter != null) { + csrfFilter.getPropertyValues().add("accessDeniedHandler", accessDeniedHandler); + } + } + // Needed to account for placeholders static String createPath(String path, boolean lowerCase) { return lowerCase ? path.toLowerCase() : path; @@ -298,6 +308,10 @@ class HttpConfigurationBuilder { BeanDefinitionBuilder sessionFixationStrategy = null; BeanDefinitionBuilder registerSessionStrategy; + if(csrfAuthStrategy != null) { + delegateSessionStrategies.add(csrfAuthStrategy); + } + if (sessionControlEnabled) { assert sessionRegistryRef != null; concurrentSessionStrategy = BeanDefinitionBuilder.rootBeanDefinition(ConcurrentSessionControlAuthenticationStrategy.class); @@ -541,6 +555,12 @@ class HttpConfigurationBuilder { requestCacheBldr = BeanDefinitionBuilder.rootBeanDefinition(HttpSessionRequestCache.class); requestCacheBldr.addPropertyValue("createSessionAllowed", sessionPolicy == SessionCreationPolicy.IF_REQUIRED); requestCacheBldr.addPropertyValue("portResolver", portResolver); + if(csrfFilter != null) { + BeanDefinitionBuilder requestCacheMatcherBldr = BeanDefinitionBuilder.rootBeanDefinition(AntPathRequestMatcher.class); + requestCacheMatcherBldr.addConstructorArgValue("/**"); + requestCacheMatcherBldr.addConstructorArgValue("GET"); + requestCacheBldr.addPropertyValue("requestMatcher", requestCacheMatcherBldr.getBeanDefinition()); + } } BeanDefinition bean = requestCacheBldr.getBeanDefinition(); @@ -617,6 +637,20 @@ class HttpConfigurationBuilder { } + private void createCsrfFilter() { + Element elmt = DomUtils.getChildElementByTagName(httpElt, Elements.CSRF); + if (elmt != null) { + CsrfBeanDefinitionParser csrfParser = new CsrfBeanDefinitionParser(); + this.csrfFilter = csrfParser.parse(elmt, pc); + this.csrfAuthStrategy = csrfParser.getCsrfAuthenticationStrategy(); + this.csrfLogoutHandler = csrfParser.getCsrfLogoutHandler(); + } + } + + BeanMetadataElement getCsrfLogoutHandler() { + return this.csrfLogoutHandler; + } + BeanReference getSessionStrategy() { return sessionStrategyRef; } @@ -668,6 +702,10 @@ class HttpConfigurationBuilder { filters.add(new OrderDecorator(addHeadersFilter, HEADERS_FILTER)); } + if (csrfFilter != null) { + filters.add(new OrderDecorator(csrfFilter, CSRF_FILTER)); + } + return filters; } } diff --git a/config/src/main/java/org/springframework/security/config/http/HttpSecurityBeanDefinitionParser.java b/config/src/main/java/org/springframework/security/config/http/HttpSecurityBeanDefinitionParser.java index 3d3fda7436..5043db0e74 100644 --- a/config/src/main/java/org/springframework/security/config/http/HttpSecurityBeanDefinitionParser.java +++ b/config/src/main/java/org/springframework/security/config/http/HttpSecurityBeanDefinitionParser.java @@ -137,10 +137,11 @@ public class HttpSecurityBeanDefinitionParser implements BeanDefinitionParser { AuthenticationConfigBuilder authBldr = new AuthenticationConfigBuilder(element, pc, httpBldr.getSessionCreationPolicy(), httpBldr.getRequestCache(), authenticationManager, - httpBldr.getSessionStrategy(), portMapper, portResolver); + httpBldr.getSessionStrategy(), portMapper, portResolver, httpBldr.getCsrfLogoutHandler()); httpBldr.setLogoutHandlers(authBldr.getLogoutHandlers()); httpBldr.setEntryPoint(authBldr.getEntryPointBean()); + httpBldr.setAccessDeniedHandler(authBldr.getAccessDeniedHandlerBean()); authenticationProviders.addAll(authBldr.getProviders()); diff --git a/config/src/main/java/org/springframework/security/config/http/LogoutBeanDefinitionParser.java b/config/src/main/java/org/springframework/security/config/http/LogoutBeanDefinitionParser.java index ace6458eef..57a0d46be8 100644 --- a/config/src/main/java/org/springframework/security/config/http/LogoutBeanDefinitionParser.java +++ b/config/src/main/java/org/springframework/security/config/http/LogoutBeanDefinitionParser.java @@ -15,6 +15,7 @@ */ package org.springframework.security.config.http; +import org.springframework.beans.BeanMetadataElement; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.config.RuntimeBeanReference; import org.springframework.beans.factory.support.BeanDefinitionBuilder; @@ -44,13 +45,15 @@ class LogoutBeanDefinitionParser implements BeanDefinitionParser { static final String ATT_DELETE_COOKIES = "delete-cookies"; final String rememberMeServices; - private ManagedList logoutHandlers = new ManagedList(); + private ManagedList logoutHandlers = new ManagedList(); - public LogoutBeanDefinitionParser(String rememberMeServices) { + public LogoutBeanDefinitionParser(String rememberMeServices, BeanMetadataElement csrfLogoutHandler) { this.rememberMeServices = rememberMeServices; + if(csrfLogoutHandler != null) { + logoutHandlers.add(csrfLogoutHandler); + } } - @SuppressWarnings("unchecked") public BeanDefinition parse(Element element, ParserContext pc) { String logoutUrl = null; String successHandlerRef = null; @@ -111,7 +114,7 @@ class LogoutBeanDefinitionParser implements BeanDefinitionParser { return builder.getBeanDefinition(); } - ManagedList getLogoutHandlers() { + ManagedList getLogoutHandlers() { return logoutHandlers; } } diff --git a/config/src/main/java/org/springframework/security/config/http/SecurityFilters.java b/config/src/main/java/org/springframework/security/config/http/SecurityFilters.java index 20194ecedb..d88c23ff25 100644 --- a/config/src/main/java/org/springframework/security/config/http/SecurityFilters.java +++ b/config/src/main/java/org/springframework/security/config/http/SecurityFilters.java @@ -30,6 +30,7 @@ enum SecurityFilters { /** {@link WebAsyncManagerIntegrationFilter} */ WEB_ASYNC_MANAGER_FILTER, HEADERS_FILTER, + CSRF_FILTER, LOGOUT_FILTER, X509_FILTER, PRE_AUTH_FILTER, diff --git a/config/src/main/resources/org/springframework/security/config/spring-security-3.2.rnc b/config/src/main/resources/org/springframework/security/config/spring-security-3.2.rnc index 028241f14e..e67163eb67 100644 --- a/config/src/main/resources/org/springframework/security/config/spring-security-3.2.rnc +++ b/config/src/main/resources/org/springframework/security/config/spring-security-3.2.rnc @@ -281,7 +281,7 @@ http-firewall = http = ## Container element for HTTP security configuration. Multiple elements can now be defined, each with a specific pattern to which the enclosed security configuration applies. A pattern can also be configured to bypass Spring Security's filters completely by setting the "secured" attribute to "false". - element http {http.attlist, (intercept-url* & access-denied-handler? & form-login? & openid-login? & x509? & jee? & http-basic? & logout? & session-management & remember-me? & anonymous? & port-mappings & custom-filter* & request-cache? & expression-handler? & headers?) } + element http {http.attlist, (intercept-url* & access-denied-handler? & form-login? & openid-login? & x509? & jee? & http-basic? & logout? & session-management & remember-me? & anonymous? & port-mappings & custom-filter* & request-cache? & expression-handler? & headers? & csrf?) } http.attlist &= ## The request URL pattern which will be mapped to the filter chain created by this element. If omitted, the filter chain will match all requests. attribute pattern {xsd:token}? @@ -718,8 +718,18 @@ jdbc-user-service.attlist &= jdbc-user-service.attlist &= role-prefix? +csrf = + ## Element for configuration of the CsrfFilter for protection against CSRF. It also updates the default RequestCache to only replay "GET" requests. + element csrf {csrf-options.attlist} +csrf-options.attlist &= + ## The RequestMatcher instance to be used to determine if CSRF should be applied. Default is any HTTP method except "GET", "TRACE", "HEAD", "OPTIONS" + attribute request-matcher-ref { xsd:token }? +csrf-options.attlist &= + ## The CsrfTokenRepository to use. The default is HttpSessionCsrfTokenRepository + attribute token-repository-ref { xsd:token }? + headers = - ## Element for configuration of the AddHeadersFilter. Enables easy setting for the X-Frame-Options, X-XSS-Protection and X-Content-Type-Options headers. + ## Element for configuration of the HeaderWritersFilter. Enables easy setting for the X-Frame-Options, X-XSS-Protection and X-Content-Type-Options headers. element headers {cache-control? & xss-protection? & hsts? & frame-options? & content-type-options? & header*} hsts = @@ -783,7 +793,7 @@ header.attlist &= ## The value for the header. attribute value {xsd:token}? header.attlist &= - ## Reference to a custom HeaderFactory implementation. + ## Reference to a custom HeaderWriter implementation. ref? any-user-service = user-service | jdbc-user-service | ldap-user-service @@ -808,4 +818,4 @@ position = ## The explicit position at which the custom-filter should be placed in the chain. Use if you are replacing a standard filter. attribute position {named-security-filter} -named-security-filter = "FIRST" | "CHANNEL_FILTER" | "CONCURRENT_SESSION_FILTER" | "SECURITY_CONTEXT_FILTER" | "LOGOUT_FILTER" | "X509_FILTER" | "PRE_AUTH_FILTER" | "CAS_FILTER" | "FORM_LOGIN_FILTER" | "OPENID_FILTER" |"BASIC_AUTH_FILTER" | "SERVLET_API_SUPPORT_FILTER" | "REMEMBER_ME_FILTER" | "ANONYMOUS_FILTER" | "EXCEPTION_TRANSLATION_FILTER" | "SESSION_MANAGEMENT_FILTER" | "FILTER_SECURITY_INTERCEPTOR" | "SWITCH_USER_FILTER" | "LAST" +named-security-filter = "FIRST" | "CHANNEL_FILTER" | "CONCURRENT_SESSION_FILTER" | "HEADERS_FILTER" | "CSRF_FILTER" | "SECURITY_CONTEXT_FILTER" | "LOGOUT_FILTER" | "X509_FILTER" | "PRE_AUTH_FILTER" | "CAS_FILTER" | "FORM_LOGIN_FILTER" | "OPENID_FILTER" |"BASIC_AUTH_FILTER" | "SERVLET_API_SUPPORT_FILTER" | "REMEMBER_ME_FILTER" | "ANONYMOUS_FILTER" | "EXCEPTION_TRANSLATION_FILTER" | "SESSION_MANAGEMENT_FILTER" | "FILTER_SECURITY_INTERCEPTOR" | "SWITCH_USER_FILTER" | "LAST" diff --git a/config/src/main/resources/org/springframework/security/config/spring-security-3.2.xsd b/config/src/main/resources/org/springframework/security/config/spring-security-3.2.xsd index bbc1055b6a..366c6e8f68 100644 --- a/config/src/main/resources/org/springframework/security/config/spring-security-3.2.xsd +++ b/config/src/main/resources/org/springframework/security/config/spring-security-3.2.xsd @@ -1025,6 +1025,7 @@ + @@ -2238,9 +2239,34 @@ + + + Element for configuration of the CsrfFilter for protection against CSRF. It also updates + the default RequestCache to only replay "GET" requests. + + + + + + + + + + The RequestMatcher instance to be used to determine if CSRF should be applied. Default is + any HTTP method except "GET", "TRACE", "HEAD", "OPTIONS" + + + + + + The CsrfTokenRepository to use. The default is HttpSessionCsrfTokenRepository + + + + - Element for configuration of the AddHeadersFilter. Enables easy setting for the + Element for configuration of the HeaderWritersFilter. Enables easy setting for the X-Frame-Options, X-XSS-Protection and X-Content-Type-Options headers. @@ -2479,6 +2505,8 @@ + + diff --git a/config/src/test/groovy/org/springframework/security/config/AbstractXmlConfigTests.groovy b/config/src/test/groovy/org/springframework/security/config/AbstractXmlConfigTests.groovy index a7d6590182..420034e517 100644 --- a/config/src/test/groovy/org/springframework/security/config/AbstractXmlConfigTests.groovy +++ b/config/src/test/groovy/org/springframework/security/config/AbstractXmlConfigTests.groovy @@ -1,6 +1,8 @@ package org.springframework.security.config import groovy.xml.MarkupBuilder + +import org.mockito.Mockito; import org.springframework.context.support.AbstractXmlApplicationContext import org.springframework.security.config.util.InMemoryXmlApplicationContext import org.springframework.security.core.context.SecurityContextHolder @@ -37,6 +39,12 @@ abstract class AbstractXmlConfigTests extends Specification { SecurityContextHolder.clearContext(); } + def mockBean(Class clazz, String id = clazz.simpleName) { + xml.'b:bean'(id: id, 'class': Mockito.class.name, 'factory-method':'mock') { + 'b:constructor-arg'(value : clazz.name) + } + } + def bean(String name, Class clazz) { xml.'b:bean'(id: name, 'class': clazz.name) } diff --git a/config/src/test/groovy/org/springframework/security/config/annotation/BaseSpringSpec.groovy b/config/src/test/groovy/org/springframework/security/config/annotation/BaseSpringSpec.groovy index 1978d67631..a29320da2b 100644 --- a/config/src/test/groovy/org/springframework/security/config/annotation/BaseSpringSpec.groovy +++ b/config/src/test/groovy/org/springframework/security/config/annotation/BaseSpringSpec.groovy @@ -25,16 +25,18 @@ import org.springframework.mock.web.MockHttpServletRequest import org.springframework.mock.web.MockHttpServletResponse import org.springframework.security.authentication.AuthenticationManager import org.springframework.security.authentication.AuthenticationProvider -import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder import org.springframework.security.core.Authentication -import org.springframework.security.core.authority.AuthorityUtils; +import org.springframework.security.core.authority.AuthorityUtils import org.springframework.security.core.context.SecurityContextHolder import org.springframework.security.core.context.SecurityContextImpl import org.springframework.security.web.FilterChainProxy import org.springframework.security.web.access.intercept.FilterSecurityInterceptor import org.springframework.security.web.context.HttpRequestResponseHolder import org.springframework.security.web.context.HttpSessionSecurityContextRepository +import org.springframework.security.web.csrf.CsrfToken +import org.springframework.security.web.csrf.HttpSessionCsrfTokenRepository import spock.lang.AutoCleanup import spock.lang.Specification @@ -50,11 +52,26 @@ abstract class BaseSpringSpec extends Specification { MockHttpServletRequest request MockHttpServletResponse response MockFilterChain chain + CsrfToken csrfToken def setup() { + setupWeb(null) + } + + def setupWeb(httpSession = null) { request = new MockHttpServletRequest(method:"GET") + if(httpSession) { + request.session = httpSession + } response = new MockHttpServletResponse() chain = new MockFilterChain() + setupCsrf() + } + + def setupCsrf(csrfTokenValue="BaseSpringSpec_CSRFTOKEN") { + csrfToken = new CsrfToken("X-CSRF-TOKEN","_csrf",csrfTokenValue) + new HttpSessionCsrfTokenRepository().saveToken(csrfToken, request,response) + request.setParameter(csrfToken.parameterName, csrfToken.token) } AuthenticationManagerBuilder authenticationBldr = new AuthenticationManagerBuilder(ObjectPostProcessor.QUIESCENT_POSTPROCESSOR).inMemoryAuthentication().and() @@ -117,6 +134,10 @@ abstract class BaseSpringSpec extends Specification { authenticationProviders().find { provider.isAssignableFrom(it.class) } } + def getCurrentAuthentication() { + new HttpSessionSecurityContextRepository().loadContext(new HttpRequestResponseHolder(request, response)).authentication + } + def login(String username="user", String role="ROLE_USER") { login(new UsernamePasswordAuthenticationToken(username, null, AuthorityUtils.createAuthorityList(role))) } diff --git a/config/src/test/groovy/org/springframework/security/config/annotation/BaseWebSpecuritySpec.groovy b/config/src/test/groovy/org/springframework/security/config/annotation/BaseWebSpecuritySpec.groovy deleted file mode 100644 index 29f1c9d6ab..0000000000 --- a/config/src/test/groovy/org/springframework/security/config/annotation/BaseWebSpecuritySpec.groovy +++ /dev/null @@ -1,52 +0,0 @@ -/* - * 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.config.annotation; - -import org.springframework.context.ConfigurableApplicationContext -import org.springframework.context.annotation.AnnotationConfigApplicationContext; -import org.springframework.mock.web.MockFilterChain; -import org.springframework.mock.web.MockHttpServletRequest; -import org.springframework.mock.web.MockHttpServletResponse; -import org.springframework.security.core.context.SecurityContextHolder -import org.springframework.security.web.FilterChainProxy; - -import spock.lang.AutoCleanup -import spock.lang.Specification - -/** - * - * @author Rob Winch - */ -abstract class BaseWebSpecuritySpec extends BaseSpringSpec { - FilterChainProxy springSecurityFilterChain - MockHttpServletRequest request - MockHttpServletResponse response - MockFilterChain chain - - def setup() { - request = new MockHttpServletRequest(method:"GET") - response = new MockHttpServletResponse() - chain = new MockFilterChain() - } - - - def loadConfig(Class... configs) { - super.loadConfig(configs) - springSecurityFilterChain = context.getBean(FilterChainProxy) - } - - -} diff --git a/config/src/test/groovy/org/springframework/security/config/annotation/web/SampleWebSecurityConfigurerAdapterTests.groovy b/config/src/test/groovy/org/springframework/security/config/annotation/web/SampleWebSecurityConfigurerAdapterTests.groovy index 5ef38b733d..8a2f65af32 100644 --- a/config/src/test/groovy/org/springframework/security/config/annotation/web/SampleWebSecurityConfigurerAdapterTests.groovy +++ b/config/src/test/groovy/org/springframework/security/config/annotation/web/SampleWebSecurityConfigurerAdapterTests.groovy @@ -20,8 +20,7 @@ import javax.servlet.http.HttpServletResponse import org.springframework.beans.factory.annotation.Autowired import org.springframework.context.annotation.Configuration import org.springframework.core.annotation.Order -import org.springframework.security.authentication.AuthenticationManager -import org.springframework.security.config.annotation.BaseWebSpecuritySpec +import org.springframework.security.config.annotation.BaseSpringSpec import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder import org.springframework.security.config.annotation.web.builders.HttpSecurity import org.springframework.security.config.annotation.web.builders.WebSecurity @@ -34,7 +33,7 @@ import org.springframework.security.config.annotation.web.configuration.WebSecur * @author Rob Winch * */ -public class SampleWebSecurityConfigurerAdapterTests extends BaseWebSpecuritySpec { +public class SampleWebSecurityConfigurerAdapterTests extends BaseSpringSpec { def "README HelloWorld Sample works"() { setup: "Sample Config is loaded" loadConfig(HelloWorldWebSecurityConfigurerAdapter) diff --git a/config/src/test/groovy/org/springframework/security/config/annotation/web/WebSecurityConfigurerAdapterTests.groovy b/config/src/test/groovy/org/springframework/security/config/annotation/web/WebSecurityConfigurerAdapterTests.groovy index 333e2bad81..206b72fa28 100644 --- a/config/src/test/groovy/org/springframework/security/config/annotation/web/WebSecurityConfigurerAdapterTests.groovy +++ b/config/src/test/groovy/org/springframework/security/config/annotation/web/WebSecurityConfigurerAdapterTests.groovy @@ -79,7 +79,8 @@ class WebSecurityConfigurerAdapterTests extends BaseSpringSpec { 'Strict-Transport-Security': 'max-age=31536000 ; includeSubDomains', 'Cache-Control': 'no-cache,no-store,max-age=0,must-revalidate', 'Pragma':'no-cache', - 'X-XSS-Protection' : '1; mode=block'] + 'X-XSS-Protection' : '1; mode=block', + 'X-CSRF-TOKEN' : csrfToken.token] } @EnableWebSecurity diff --git a/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/CsrfConfigurerTests.groovy b/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/CsrfConfigurerTests.groovy new file mode 100644 index 0000000000..d82c72d81a --- /dev/null +++ b/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/CsrfConfigurerTests.groovy @@ -0,0 +1,355 @@ +/* + * 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.config.annotation.web.configurers + +import javax.servlet.http.HttpServletResponse + +import org.springframework.context.annotation.Configuration +import org.springframework.security.config.annotation.BaseSpringSpec +import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder +import org.springframework.security.config.annotation.web.builders.HttpSecurity +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter +import org.springframework.security.web.access.AccessDeniedHandler +import org.springframework.security.web.csrf.CsrfFilter; +import org.springframework.security.web.csrf.CsrfTokenRepository; +import org.springframework.security.web.servlet.support.csrf.CsrfRequestDataValueProcessor; +import org.springframework.security.web.util.RequestMatcher; + +import spock.lang.Unroll; + +/** + * + * @author Rob Winch + */ +class CsrfConfigurerTests extends BaseSpringSpec { + + @Unroll + def "csrf applied by default"() { + setup: + loadConfig(CsrfAppliedDefaultConfig) + request.method = httpMethod + clearCsrfToken() + when: + 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: + loadConfig(CsrfAppliedDefaultConfig) + then: + context.getBean(CsrfRequestDataValueProcessor) + } + + @Configuration + @EnableWebSecurity + static class CsrfAppliedDefaultConfig extends WebSecurityConfigurerAdapter { + + @Override + protected void configure(HttpSecurity http) throws Exception { + } + } + + def "csrf disable"() { + setup: + loadConfig(DisableCsrfConfig) + request.method = "POST" + clearCsrfToken() + when: + springSecurityFilterChain.doFilter(request,response,chain) + then: + !findFilter(CsrfFilter) + response.status == HttpServletResponse.SC_OK + } + + @Configuration + @EnableWebSecurity + static class DisableCsrfConfig extends WebSecurityConfigurerAdapter { + + @Override + protected void configure(HttpSecurity http) throws Exception { + http + .csrf().disable() + } + } + + def "csrf requireCsrfProtectionMatcher"() { + setup: + RequireCsrfProtectionMatcherConfig.matcher = Mock(RequestMatcher) + RequireCsrfProtectionMatcherConfig.matcher.matches(_) >>> [false,true] + loadConfig(RequireCsrfProtectionMatcherConfig) + clearCsrfToken() + when: + springSecurityFilterChain.doFilter(request,response,chain) + then: + response.status == HttpServletResponse.SC_OK + when: + springSecurityFilterChain.doFilter(request,response,chain) + then: + response.status == HttpServletResponse.SC_FORBIDDEN + } + + @Configuration + @EnableWebSecurity + static class RequireCsrfProtectionMatcherConfig extends WebSecurityConfigurerAdapter { + static RequestMatcher matcher + + @Override + protected void configure(HttpSecurity http) throws Exception { + http + .csrf() + .requireCsrfProtectionMatcher(matcher) + } + } + + def "csrf csrfTokenRepository"() { + setup: + CsrfTokenRepositoryConfig.repo = Mock(CsrfTokenRepository) + loadConfig(CsrfTokenRepositoryConfig) + clearCsrfToken() + when: + springSecurityFilterChain.doFilter(request,response,chain) + then: + 1 * CsrfTokenRepositoryConfig.repo.loadToken(_) >> csrfToken + response.status == HttpServletResponse.SC_OK + } + + def "csrf clears on logout"() { + setup: + CsrfTokenRepositoryConfig.repo = Mock(CsrfTokenRepository) + 1 * CsrfTokenRepositoryConfig.repo.loadToken(_) >> csrfToken + loadConfig(CsrfTokenRepositoryConfig) + login() + request.method = "POST" + request.servletPath = "/logout" + when: + springSecurityFilterChain.doFilter(request,response,chain) + then: + 1 * CsrfTokenRepositoryConfig.repo.saveToken(null, _, _) + } + + def "csrf clears on login"() { + setup: + CsrfTokenRepositoryConfig.repo = Mock(CsrfTokenRepository) + 1 * CsrfTokenRepositoryConfig.repo.loadToken(_) >> csrfToken + loadConfig(CsrfTokenRepositoryConfig) + request.method = "POST" + request.getSession() + request.servletPath = "/login" + request.setParameter("username", "user") + request.setParameter("password", "password") + when: + springSecurityFilterChain.doFilter(request,response,chain) + then: + response.redirectedUrl == "/" + 1 * CsrfTokenRepositoryConfig.repo.saveToken(null, _, _) + } + + @Configuration + @EnableWebSecurity + static class CsrfTokenRepositoryConfig extends WebSecurityConfigurerAdapter { + static CsrfTokenRepository repo + + @Override + protected void configure(HttpSecurity http) throws Exception { + http + .formLogin() + .and() + .csrf() + .csrfTokenRepository(repo) + } + @Override + protected void registerAuthentication(AuthenticationManagerBuilder auth) + throws Exception { + auth + .inMemoryAuthentication() + .withUser("user").password("password").roles("USER") + } + } + + def "csrf access denied handler"() { + setup: + AccessDeniedHandlerConfig.deniedHandler = Mock(AccessDeniedHandler) + 1 * AccessDeniedHandlerConfig.deniedHandler.handle(_, _, _) + loadConfig(AccessDeniedHandlerConfig) + clearCsrfToken() + request.method = "POST" + when: + springSecurityFilterChain.doFilter(request,response,chain) + then: + response.status == HttpServletResponse.SC_OK + } + + @Configuration + @EnableWebSecurity + static class AccessDeniedHandlerConfig extends WebSecurityConfigurerAdapter { + static AccessDeniedHandler deniedHandler + + @Override + protected void configure(HttpSecurity http) throws Exception { + http + .exceptionHandling() + .accessDeniedHandler(deniedHandler) + } + } + + def "formLogin requires CSRF token"() { + setup: + loadConfig(FormLoginConfig) + clearCsrfToken() + request.setParameter("username", "user") + request.setParameter("password", "password") + request.servletPath = "/login" + request.method = "POST" + when: + springSecurityFilterChain.doFilter(request,response,chain) + then: + response.status == HttpServletResponse.SC_FORBIDDEN + currentAuthentication == null + } + + @Configuration + @EnableWebSecurity + static class FormLoginConfig extends WebSecurityConfigurerAdapter { + static AccessDeniedHandler deniedHandler + + @Override + protected void configure(HttpSecurity http) throws Exception { + http + .formLogin() + } + } + + def "logout requires CSRF token"() { + setup: + loadConfig(LogoutConfig) + clearCsrfToken() + login() + request.servletPath = "/logout" + request.method = "POST" + when: + springSecurityFilterChain.doFilter(request,response,chain) + then: "logout is not allowed and user is still authenticated" + response.status == HttpServletResponse.SC_FORBIDDEN + currentAuthentication != null + } + + @Configuration + @EnableWebSecurity + static class LogoutConfig extends WebSecurityConfigurerAdapter { + static AccessDeniedHandler deniedHandler + + @Override + protected void configure(HttpSecurity http) throws Exception { + http + .formLogin() + } + } + + def "csrf disables POST requests from RequestCache"() { + setup: + CsrfDisablesPostRequestFromRequestCacheConfig.repo = Mock(CsrfTokenRepository) + loadConfig(CsrfDisablesPostRequestFromRequestCacheConfig) + 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" + 1 * CsrfDisablesPostRequestFromRequestCacheConfig.repo.loadToken(_) >> csrfToken + response.status == HttpServletResponse.SC_MOVED_TEMPORARILY + response.redirectedUrl == "http://localhost/login" + when: "authenticate successfully" + super.setupWeb(request.session) + request.servletPath = "/login" + 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" + 1 * CsrfDisablesPostRequestFromRequestCacheConfig.repo.loadToken(_) >> csrfToken + response.status == HttpServletResponse.SC_MOVED_TEMPORARILY + response.redirectedUrl == "/" + } + + def "csrf enables GET requests with RequestCache"() { + setup: + CsrfDisablesPostRequestFromRequestCacheConfig.repo = Mock(CsrfTokenRepository) + loadConfig(CsrfDisablesPostRequestFromRequestCacheConfig) + 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" + 1 * CsrfDisablesPostRequestFromRequestCacheConfig.repo.loadToken(_) >> csrfToken + response.status == HttpServletResponse.SC_MOVED_TEMPORARILY + response.redirectedUrl == "http://localhost/login" + when: "authenticate successfully" + super.setupWeb(request.session) + request.servletPath = "/login" + 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" + 1 * CsrfDisablesPostRequestFromRequestCacheConfig.repo.loadToken(_) >> csrfToken + response.status == HttpServletResponse.SC_MOVED_TEMPORARILY + response.redirectedUrl == "http://localhost/some-url" + } + + @Configuration + @EnableWebSecurity + static class CsrfDisablesPostRequestFromRequestCacheConfig extends WebSecurityConfigurerAdapter { + static CsrfTokenRepository repo + + @Override + protected void configure(HttpSecurity http) throws Exception { + http + .authorizeRequests() + .anyRequest().authenticated() + .and() + .formLogin() + .and() + .csrf() + .csrfTokenRepository(repo) + } + @Override + protected void registerAuthentication(AuthenticationManagerBuilder auth) + throws Exception { + auth + .inMemoryAuthentication() + .withUser("user").password("password").roles("USER") + } + } + + def clearCsrfToken() { + request.removeAllParameters() + } +} diff --git a/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/DefaultFiltersTests.groovy b/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/DefaultFiltersTests.groovy index 9ca1dbf854..5c51ebc448 100644 --- a/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/DefaultFiltersTests.groovy +++ b/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/DefaultFiltersTests.groovy @@ -37,7 +37,8 @@ import org.springframework.security.web.authentication.AnonymousAuthenticationFi import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter import org.springframework.security.web.authentication.logout.LogoutFilter import org.springframework.security.web.context.SecurityContextPersistenceFilter -import org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter; +import org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter +import org.springframework.security.web.csrf.CsrfFilter import org.springframework.security.web.header.HeaderWriterFilter import org.springframework.security.web.savedrequest.RequestCacheAwareFilter import org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter @@ -107,17 +108,17 @@ class DefaultFiltersTests extends BaseSpringSpec { def "FilterChainProxyBuilder ignoring resources"() { when: - context = new AnnotationConfigApplicationContext(FilterChainProxyBuilderIgnoringConfig) + loadConfig(FilterChainProxyBuilderIgnoringConfig) then: - List filterChains = context.getBean(FilterChainProxy).filterChains - filterChains.size() == 2 - filterChains[0].requestMatcher.pattern == '/resources/**' - filterChains[0].filters.empty - filterChains[1].requestMatcher instanceof AnyRequestMatcher - filterChains[1].filters.collect { it.class } == - [WebAsyncManagerIntegrationFilter, SecurityContextPersistenceFilter, HeaderWriterFilter, LogoutFilter, RequestCacheAwareFilter, - SecurityContextHolderAwareRequestFilter, AnonymousAuthenticationFilter, SessionManagementFilter, - ExceptionTranslationFilter, FilterSecurityInterceptor ] + List filterChains = context.getBean(FilterChainProxy).filterChains + filterChains.size() == 2 + filterChains[0].requestMatcher.pattern == '/resources/**' + filterChains[0].filters.empty + filterChains[1].requestMatcher instanceof AnyRequestMatcher + filterChains[1].filters.collect { it.class } == + [WebAsyncManagerIntegrationFilter, SecurityContextPersistenceFilter, HeaderWriterFilter, CsrfFilter, LogoutFilter, RequestCacheAwareFilter, + SecurityContextHolderAwareRequestFilter, AnonymousAuthenticationFilter, SessionManagementFilter, + ExceptionTranslationFilter, FilterSecurityInterceptor ] } @Configuration @@ -139,17 +140,16 @@ class DefaultFiltersTests extends BaseSpringSpec { def "DefaultFilters.permitAll()"() { when: - context = new AnnotationConfigApplicationContext(DefaultFiltersConfigPermitAll) + loadConfig(DefaultFiltersConfigPermitAll) + MockHttpServletResponse response = new MockHttpServletResponse() + request = new MockHttpServletRequest(servletPath : uri, queryString: query, method:"POST") + setupCsrf() + springSecurityFilterChain.doFilter(request, response, new MockFilterChain()) then: - FilterChainProxy filterChain = context.getBean(FilterChainProxy) - - expect: - MockHttpServletResponse response = new MockHttpServletResponse() - filterChain.doFilter(new MockHttpServletRequest(servletPath : uri, queryString: query), response, new MockFilterChain()) - response.redirectedUrl == null + response.redirectedUrl == "/login?logout" where: - uri | query - "/logout" | null + uri | query + "/logout" | null } @Configuration diff --git a/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/DefaultLoginPageConfigurerTests.groovy b/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/DefaultLoginPageConfigurerTests.groovy index 44f1889354..c05ad41fcf 100644 --- a/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/DefaultLoginPageConfigurerTests.groovy +++ b/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/DefaultLoginPageConfigurerTests.groovy @@ -42,28 +42,16 @@ import org.springframework.security.web.authentication.ui.DefaultLoginPageViewFi * */ public class DefaultLoginPageConfigurerTests extends BaseSpringSpec { - FilterChainProxy springSecurityFilterChain - MockHttpServletRequest request - MockHttpServletResponse response - MockFilterChain chain - - def setup() { - request = new MockHttpServletRequest(method:"GET") - response = new MockHttpServletResponse() - chain = new MockFilterChain() - } - def "http/form-login default login generating page"() { setup: loadConfig(DefaultLoginPageConfig) - springSecurityFilterChain = context.getBean(FilterChainProxy) when: springSecurityFilterChain.doFilter(request,response,chain) then: findFilter(DefaultLoginPageViewFilter) response.getRedirectedUrl() == "http://localhost/login" when: "request the login page" - setup() + super.setup() request.requestURI = "/login" springSecurityFilterChain.doFilter(request,response,chain) then: @@ -73,10 +61,11 @@ public class DefaultLoginPageConfigurerTests extends BaseSpringSpec { User: Password: + """ when: "fail to log in" - setup() + super.setup() request.servletPath = "/login" request.method = "POST" springSecurityFilterChain.doFilter(request,response,chain) @@ -84,7 +73,7 @@ public class DefaultLoginPageConfigurerTests extends BaseSpringSpec { response.getRedirectedUrl() == "/login?error" when: "request the error page" HttpSession session = request.session - setup() + super.setup() request.session = session request.requestURI = "/login" request.queryString = "error" @@ -96,10 +85,11 @@ public class DefaultLoginPageConfigurerTests extends BaseSpringSpec { User: Password: + """ when: "login success" - setup() + super.setup() request.servletPath = "/login" request.method = "POST" request.parameters.username = ["user"] as String[] @@ -112,7 +102,6 @@ public class DefaultLoginPageConfigurerTests extends BaseSpringSpec { def "logout success renders"() { setup: loadConfig(DefaultLoginPageConfig) - springSecurityFilterChain = context.getBean(FilterChainProxy) when: "logout success" request.requestURI = "/login" request.queryString = "logout" @@ -125,6 +114,7 @@ public class DefaultLoginPageConfigurerTests extends BaseSpringSpec { User: Password: + """ } @@ -144,7 +134,6 @@ public class DefaultLoginPageConfigurerTests extends BaseSpringSpec { def "custom logout success handler prevents rendering"() { setup: loadConfig(DefaultLoginPageCustomLogoutSuccessHandlerConfig) - springSecurityFilterChain = context.getBean(FilterChainProxy) when: "logout success" request.requestURI = "/login" request.queryString = "logout" @@ -172,7 +161,6 @@ public class DefaultLoginPageConfigurerTests extends BaseSpringSpec { def "custom logout success url prevents rendering"() { setup: loadConfig(DefaultLoginPageCustomLogoutConfig) - springSecurityFilterChain = context.getBean(FilterChainProxy) when: "logout success" request.requestURI = "/login" request.queryString = "logout" @@ -200,9 +188,8 @@ public class DefaultLoginPageConfigurerTests extends BaseSpringSpec { def "http/form-login default login with remember me"() { setup: loadConfig(DefaultLoginPageWithRememberMeConfig) - springSecurityFilterChain = context.getBean(FilterChainProxy) when: "request the login page" - setup() + super.setup() request.requestURI = "/login" springSecurityFilterChain.doFilter(request,response,chain) then: @@ -213,6 +200,7 @@ public class DefaultLoginPageConfigurerTests extends BaseSpringSpec { Password: Remember me on this computer. + """ } @@ -234,7 +222,6 @@ public class DefaultLoginPageConfigurerTests extends BaseSpringSpec { def "http/form-login default login with openid"() { setup: loadConfig(DefaultLoginPageWithOpenIDConfig) - springSecurityFilterChain = context.getBean(FilterChainProxy) when: "request the login page" request.requestURI = "/login" springSecurityFilterChain.doFilter(request,response,chain) @@ -244,6 +231,7 @@ public class DefaultLoginPageConfigurerTests extends BaseSpringSpec { Identity: + """ } @@ -262,7 +250,6 @@ public class DefaultLoginPageConfigurerTests extends BaseSpringSpec { def "http/form-login default login with openid, form login, and rememberme"() { setup: loadConfig(DefaultLoginPageWithFormLoginOpenIDRememberMeConfig) - springSecurityFilterChain = context.getBean(FilterChainProxy) when: "request the login page" request.requestURI = "/login" springSecurityFilterChain.doFilter(request,response,chain) @@ -274,6 +261,7 @@ public class DefaultLoginPageConfigurerTests extends BaseSpringSpec { Password: Remember me on this computer. +

Login with OpenID Identity

@@ -281,6 +269,7 @@ public class DefaultLoginPageConfigurerTests extends BaseSpringSpec {
Remember me on this computer.
+
""" } diff --git a/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/FormLoginConfigurerTests.groovy b/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/FormLoginConfigurerTests.groovy index 0aee9ddaa1..cbf4f23a98 100644 --- a/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/FormLoginConfigurerTests.groovy +++ b/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/FormLoginConfigurerTests.groovy @@ -41,6 +41,7 @@ import org.springframework.security.web.authentication.logout.LogoutFilter import org.springframework.security.web.authentication.session.SessionFixationProtectionStrategy import org.springframework.security.web.context.SecurityContextPersistenceFilter import org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter +import org.springframework.security.web.csrf.CsrfFilter; import org.springframework.security.web.header.HeaderWriterFilter import org.springframework.security.web.savedrequest.RequestCacheAwareFilter import org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter @@ -64,7 +65,7 @@ class FormLoginConfigurerTests extends BaseSpringSpec { filterChains[0].filters.empty filterChains[1].requestMatcher instanceof AnyRequestMatcher filterChains[1].filters.collect { it.class.name.contains('$') ? it.class.superclass : it.class } == - [WebAsyncManagerIntegrationFilter, SecurityContextPersistenceFilter, HeaderWriterFilter, LogoutFilter, UsernamePasswordAuthenticationFilter, + [WebAsyncManagerIntegrationFilter, SecurityContextPersistenceFilter, HeaderWriterFilter, CsrfFilter, LogoutFilter, UsernamePasswordAuthenticationFilter, RequestCacheAwareFilter, SecurityContextHolderAwareRequestFilter, AnonymousAuthenticationFilter, SessionManagementFilter, ExceptionTranslationFilter, FilterSecurityInterceptor ] @@ -78,7 +79,7 @@ class FormLoginConfigurerTests extends BaseSpringSpec { !authFilter.requiresAuthentication(new MockHttpServletRequest(servletPath : "/login", method: "GET"), new MockHttpServletResponse()) and: "SessionFixationProtectionStrategy is configured correctly" - SessionFixationProtectionStrategy sessionStrategy = ReflectionTestUtils.getField(authFilter,"sessionStrategy") + SessionFixationProtectionStrategy sessionStrategy = ReflectionTestUtils.getField(authFilter,"sessionStrategy").delegateStrategies.find { SessionFixationProtectionStrategy } sessionStrategy.migrateSessionAttributes and: "Exception handling is configured correctly" @@ -112,11 +113,13 @@ class FormLoginConfigurerTests extends BaseSpringSpec { def "FormLogin.permitAll()"() { when: "load formLogin() with permitAll" context = new AnnotationConfigApplicationContext(FormLoginConfigPermitAll) - - then: "the formLogin URLs are granted access" FilterChainProxy filterChain = context.getBean(FilterChainProxy) MockHttpServletResponse response = new MockHttpServletResponse() - filterChain.doFilter(new MockHttpServletRequest(servletPath : servletPath, requestURI: servletPath, queryString: query, method: method), response, new MockFilterChain()) + request = new MockHttpServletRequest(servletPath : servletPath, requestURI: servletPath, queryString: query, method: method) + setupCsrf() + + then: "the formLogin URLs are granted access" + filterChain.doFilter(request, response, new MockFilterChain()) response.redirectedUrl == redirectUrl where: diff --git a/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/LogoutConfigurerTests.groovy b/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/LogoutConfigurerTests.groovy index 974e0e7fee..2eb84d5252 100644 --- a/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/LogoutConfigurerTests.groovy +++ b/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/LogoutConfigurerTests.groovy @@ -47,8 +47,11 @@ class LogoutConfigurerTests extends BaseSpringSpec { def "invoke logout twice does not override"() { when: loadConfig(InvokeTwiceDoesNotOverride) + request.method = "POST" + request.servletPath = "/custom/logout" + findFilter(LogoutFilter).doFilter(request,response,chain) then: - findFilter(LogoutFilter).filterProcessesUrl == "/custom/logout" + response.redirectedUrl == "/login?logout" } @Configuration diff --git a/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/NamespaceHttpBasicTests.groovy b/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/NamespaceHttpBasicTests.groovy index c226354261..c274f5d7c5 100644 --- a/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/NamespaceHttpBasicTests.groovy +++ b/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/NamespaceHttpBasicTests.groovy @@ -39,35 +39,24 @@ import org.springframework.security.web.authentication.www.BasicAuthenticationFi * */ public class NamespaceHttpBasicTests extends BaseSpringSpec { - FilterChainProxy springSecurityFilterChain - MockHttpServletRequest request - MockHttpServletResponse response - MockFilterChain chain - - def setup() { - request = new MockHttpServletRequest() - response = new MockHttpServletResponse() - chain = new MockFilterChain() - } def "http/http-basic"() { setup: loadConfig(HttpBasicConfig) - springSecurityFilterChain = context.getBean(FilterChainProxy) when: springSecurityFilterChain.doFilter(request,response,chain) then: response.status == HttpServletResponse.SC_UNAUTHORIZED when: "fail to log in" - setup() - login("user","invalid") + super.setup() + basicLogin("user","invalid") springSecurityFilterChain.doFilter(request,response,chain) then: "unauthorized" response.status == HttpServletResponse.SC_UNAUTHORIZED response.getHeader("WWW-Authenticate") == 'Basic realm="Spring Security Application"' when: "login success" - setup() - login() + super.setup() + basicLogin() then: "sent to default succes page" !response.committed } @@ -86,9 +75,8 @@ public class NamespaceHttpBasicTests extends BaseSpringSpec { def "http@realm"() { setup: loadConfig(CustomHttpBasicConfig) - springSecurityFilterChain = context.getBean(FilterChainProxy) when: - login("user","invalid") + basicLogin("user","invalid") springSecurityFilterChain.doFilter(request,response,chain) then: "unauthorized" response.status == HttpServletResponse.SC_UNAUTHORIZED @@ -109,7 +97,6 @@ public class NamespaceHttpBasicTests extends BaseSpringSpec { def "http-basic@authentication-details-source-ref"() { when: loadConfig(AuthenticationDetailsSourceHttpBasicConfig) - springSecurityFilterChain = context.getBean(FilterChainProxy) then: findFilter(BasicAuthenticationFilter).authenticationDetailsSource.class == CustomAuthenticationDetailsSource } @@ -128,20 +115,20 @@ public class NamespaceHttpBasicTests extends BaseSpringSpec { def "http-basic@entry-point-ref"() { setup: loadConfig(EntryPointRefHttpBasicConfig) - springSecurityFilterChain = context.getBean(FilterChainProxy) when: springSecurityFilterChain.doFilter(request,response,chain) then: response.status == HttpServletResponse.SC_INTERNAL_SERVER_ERROR when: "fail to log in" - setup() - login("user","invalid") + super.setup() + basicLogin("user","invalid") springSecurityFilterChain.doFilter(request,response,chain) then: "custom" response.status == HttpServletResponse.SC_INTERNAL_SERVER_ERROR when: "login success" - setup() - login() + super.setup() + basicLogin() + springSecurityFilterChain.doFilter(request,response,chain) then: "sent to default succes page" !response.committed } @@ -162,7 +149,7 @@ public class NamespaceHttpBasicTests extends BaseSpringSpec { } } - def login(String username="user",String password="password") { + def basicLogin(String username="user",String password="password") { def credentials = username + ":" + password request.addHeader("Authorization", "Basic " + credentials.bytes.encodeBase64()) } diff --git a/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/NamespaceHttpHeadersTests.groovy b/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/NamespaceHttpHeadersTests.groovy index 91eeffa3bd..e5d4a37258 100644 --- a/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/NamespaceHttpHeadersTests.groovy +++ b/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/NamespaceHttpHeadersTests.groovy @@ -36,6 +36,7 @@ import org.springframework.security.web.util.AnyRequestMatcher * */ public class NamespaceHttpHeadersTests extends BaseSpringSpec { + def "http/headers"() { setup: loadConfig(HeadersDefaultConfig) @@ -48,7 +49,8 @@ public class NamespaceHttpHeadersTests extends BaseSpringSpec { 'Strict-Transport-Security': 'max-age=31536000 ; includeSubDomains', 'Cache-Control': 'no-cache,no-store,max-age=0,must-revalidate', 'Pragma':'no-cache', - 'X-XSS-Protection' : '1; mode=block'] + 'X-XSS-Protection' : '1; mode=block', + 'X-CSRF-TOKEN' : csrfToken.token] } @Configuration @@ -68,7 +70,8 @@ public class NamespaceHttpHeadersTests extends BaseSpringSpec { springSecurityFilterChain.doFilter(request,response,chain) then: responseHeaders == ['Cache-Control': 'no-cache,no-store,max-age=0,must-revalidate', - 'Pragma':'no-cache'] + 'Pragma':'no-cache', + 'X-CSRF-TOKEN' : csrfToken.token] } @Configuration @@ -88,7 +91,8 @@ public class NamespaceHttpHeadersTests extends BaseSpringSpec { when: springSecurityFilterChain.doFilter(request,response,chain) then: - responseHeaders == ['Strict-Transport-Security': 'max-age=31536000 ; includeSubDomains'] + responseHeaders == ['Strict-Transport-Security': 'max-age=31536000 ; includeSubDomains', + 'X-CSRF-TOKEN' : csrfToken.token] } @Configuration @@ -107,7 +111,8 @@ public class NamespaceHttpHeadersTests extends BaseSpringSpec { when: springSecurityFilterChain.doFilter(request,response,chain) then: - responseHeaders == ['Strict-Transport-Security': 'max-age=15768000'] + responseHeaders == ['Strict-Transport-Security': 'max-age=15768000', + 'X-CSRF-TOKEN' : csrfToken.token] } @Configuration @@ -128,7 +133,8 @@ public class NamespaceHttpHeadersTests extends BaseSpringSpec { when: springSecurityFilterChain.doFilter(request,response,chain) then: - responseHeaders == ['X-Frame-Options': 'SAMEORIGIN'] + responseHeaders == ['X-Frame-Options': 'SAMEORIGIN', + 'X-CSRF-TOKEN' : csrfToken.token] } @Configuration @@ -150,7 +156,8 @@ public class NamespaceHttpHeadersTests extends BaseSpringSpec { when: springSecurityFilterChain.doFilter(request,response,chain) then: - responseHeaders == ['X-Frame-Options': 'ALLOW-FROM https://example.com'] + responseHeaders == ['X-Frame-Options': 'ALLOW-FROM https://example.com', + 'X-CSRF-TOKEN' : csrfToken.token] } @@ -171,7 +178,8 @@ public class NamespaceHttpHeadersTests extends BaseSpringSpec { when: springSecurityFilterChain.doFilter(request,response,chain) then: - responseHeaders == ['X-XSS-Protection': '1; mode=block'] + responseHeaders == ['X-XSS-Protection': '1; mode=block', + 'X-CSRF-TOKEN' : csrfToken.token] } @Configuration @@ -191,7 +199,8 @@ public class NamespaceHttpHeadersTests extends BaseSpringSpec { when: springSecurityFilterChain.doFilter(request,response,chain) then: - responseHeaders == ['X-XSS-Protection': '1'] + responseHeaders == ['X-XSS-Protection': '1', + 'X-CSRF-TOKEN' : csrfToken.token] } @Configuration @@ -211,7 +220,8 @@ public class NamespaceHttpHeadersTests extends BaseSpringSpec { when: springSecurityFilterChain.doFilter(request,response,chain) then: - responseHeaders == ['X-Content-Type-Options': 'nosniff'] + responseHeaders == ['X-Content-Type-Options': 'nosniff', + 'X-CSRF-TOKEN' : csrfToken.token] } @Configuration @@ -233,7 +243,8 @@ public class NamespaceHttpHeadersTests extends BaseSpringSpec { when: springSecurityFilterChain.doFilter(request,response,chain) then: - responseHeaders == ['customHeaderName': 'customHeaderValue'] + responseHeaders == ['customHeaderName': 'customHeaderValue', + 'X-CSRF-TOKEN' : csrfToken.token] } @Configuration @@ -245,4 +256,5 @@ public class NamespaceHttpHeadersTests extends BaseSpringSpec { .addHeaderWriter(new StaticHeadersWriter("customHeaderName", "customHeaderValue")) } } + } diff --git a/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/NamespaceHttpLogoutTests.groovy b/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/NamespaceHttpLogoutTests.groovy index 42525a2900..4578cbb2af 100644 --- a/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/NamespaceHttpLogoutTests.groovy +++ b/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/NamespaceHttpLogoutTests.groovy @@ -71,23 +71,13 @@ import org.springframework.security.web.util.RequestMatcher * */ public class NamespaceHttpLogoutTests extends BaseSpringSpec { - FilterChainProxy springSecurityFilterChain - MockHttpServletRequest request - MockHttpServletResponse response - MockFilterChain chain - - def setup() { - request = new MockHttpServletRequest() - response = new MockHttpServletResponse() - chain = new MockFilterChain() - } def "http/logout"() { setup: loadConfig(HttpLogoutConfig) - springSecurityFilterChain = context.getBean(FilterChainProxy) login() - request.setRequestURI("/logout") + request.servletPath = "/logout" + request.method = "POST" when: springSecurityFilterChain.doFilter(request,response,chain) then: @@ -106,9 +96,9 @@ public class NamespaceHttpLogoutTests extends BaseSpringSpec { def "http/logout custom"() { setup: loadConfig(CustomHttpLogoutConfig) - springSecurityFilterChain = context.getBean(FilterChainProxy) login() - request.setRequestURI("/custom-logout") + request.servletPath = "/custom-logout" + request.method = "POST" when: springSecurityFilterChain.doFilter(request,response,chain) then: @@ -135,9 +125,9 @@ public class NamespaceHttpLogoutTests extends BaseSpringSpec { def "http/logout@success-handler-ref"() { setup: loadConfig(SuccessHandlerRefHttpLogoutConfig) - springSecurityFilterChain = context.getBean(FilterChainProxy) login() - request.setRequestURI("/logout") + request.servletPath = "/logout" + request.method = "POST" when: springSecurityFilterChain.doFilter(request,response,chain) then: diff --git a/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/NamespaceHttpOpenIDLoginTests.groovy b/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/NamespaceHttpOpenIDLoginTests.groovy index 44c745f7b7..86e22b0bf6 100644 --- a/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/NamespaceHttpOpenIDLoginTests.groovy +++ b/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/NamespaceHttpOpenIDLoginTests.groovy @@ -44,21 +44,9 @@ import org.springframework.security.web.authentication.WebAuthenticationDetailsS * */ public class NamespaceHttpOpenIDLoginTests extends BaseSpringSpec { - FilterChainProxy springSecurityFilterChain - MockHttpServletRequest request - MockHttpServletResponse response - MockFilterChain chain - - def setup() { - request = new MockHttpServletRequest() - response = new MockHttpServletResponse() - chain = new MockFilterChain() - } - def "http/openid-login"() { when: loadConfig(OpenIDLoginConfig) - springSecurityFilterChain = context.getBean(FilterChainProxy) then: findFilter(OpenIDAuthenticationFilter).consumer.class == OpenID4JavaConsumer when: @@ -66,7 +54,7 @@ public class NamespaceHttpOpenIDLoginTests extends BaseSpringSpec { then: response.getRedirectedUrl() == "http://localhost/login" when: "fail to log in" - setup() + super.setup() request.servletPath = "/login/openid" request.method = "POST" springSecurityFilterChain.doFilter(request,response,chain) @@ -89,7 +77,6 @@ public class NamespaceHttpOpenIDLoginTests extends BaseSpringSpec { def "http/openid-login/attribute-exchange"() { when: loadConfig(OpenIDLoginAttributeExchangeConfig) - springSecurityFilterChain = context.getBean(FilterChainProxy) OpenID4JavaConsumer consumer = findFilter(OpenIDAuthenticationFilter).consumer then: consumer.class == OpenID4JavaConsumer @@ -117,7 +104,7 @@ public class NamespaceHttpOpenIDLoginTests extends BaseSpringSpec { then: response.getRedirectedUrl() == "http://localhost/login" when: "fail to log in" - setup() + super.setup() request.servletPath = "/login/openid" request.method = "POST" springSecurityFilterChain.doFilter(request,response,chain) @@ -165,13 +152,12 @@ public class NamespaceHttpOpenIDLoginTests extends BaseSpringSpec { def "http/openid-login custom"() { setup: loadConfig(OpenIDLoginCustomConfig) - springSecurityFilterChain = context.getBean(FilterChainProxy) when: springSecurityFilterChain.doFilter(request,response,chain) then: response.getRedirectedUrl() == "http://localhost/authentication/login" when: "fail to log in" - setup() + super.setup() request.servletPath = "/authentication/login/process" request.method = "POST" springSecurityFilterChain.doFilter(request,response,chain) @@ -200,7 +186,6 @@ public class NamespaceHttpOpenIDLoginTests extends BaseSpringSpec { when: OpenIDLoginCustomRefsConfig.AUDS = Mock(AuthenticationUserDetailsService) loadConfig(OpenIDLoginCustomRefsConfig) - springSecurityFilterChain = context.getBean(FilterChainProxy) then: "CustomWebAuthenticationDetailsSource is used" findFilter(OpenIDAuthenticationFilter).authenticationDetailsSource.class == CustomWebAuthenticationDetailsSource findAuthenticationProvider(OpenIDAuthenticationProvider).userDetailsService == OpenIDLoginCustomRefsConfig.AUDS diff --git a/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/NamespaceHttpX509Tests.groovy b/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/NamespaceHttpX509Tests.groovy index e3407563ca..0bcd29f109 100644 --- a/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/NamespaceHttpX509Tests.groovy +++ b/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/NamespaceHttpX509Tests.groovy @@ -55,22 +55,10 @@ import org.springframework.test.util.ReflectionTestUtils * */ public class NamespaceHttpX509Tests extends BaseSpringSpec { - FilterChainProxy springSecurityFilterChain - MockHttpServletRequest request - MockHttpServletResponse response - MockFilterChain chain - - def setup() { - request = new MockHttpServletRequest() - response = new MockHttpServletResponse() - chain = new MockFilterChain() - } - def "http/x509 can authenticate"() { setup: X509Certificate certificate = loadCert("rod.cer") loadConfig(X509Config) - springSecurityFilterChain = context.getBean(FilterChainProxy) when: request.setAttribute("javax.servlet.request.X509Certificate", [certificate] as X509Certificate[] ) springSecurityFilterChain.doFilter(request, response, chain); @@ -148,7 +136,6 @@ public class NamespaceHttpX509Tests extends BaseSpringSpec { setup: X509Certificate certificate = loadCert("rodatexampledotcom.cer") loadConfig(SubjectPrincipalRegexConfig) - springSecurityFilterChain = context.getBean(FilterChainProxy) when: request.setAttribute("javax.servlet.request.X509Certificate", [certificate] as X509Certificate[] ) springSecurityFilterChain.doFilter(request, response, chain); @@ -182,7 +169,6 @@ public class NamespaceHttpX509Tests extends BaseSpringSpec { setup: X509Certificate certificate = loadCert("rodatexampledotcom.cer") loadConfig(UserDetailsServiceRefConfig) - springSecurityFilterChain = context.getBean(FilterChainProxy) when: request.setAttribute("javax.servlet.request.X509Certificate", [certificate] as X509Certificate[] ) springSecurityFilterChain.doFilter(request, response, chain); @@ -216,7 +202,6 @@ public class NamespaceHttpX509Tests extends BaseSpringSpec { setup: X509Certificate certificate = loadCert("rodatexampledotcom.cer") loadConfig(AuthenticationUserDetailsServiceConfig) - springSecurityFilterChain = context.getBean(FilterChainProxy) when: request.setAttribute("javax.servlet.request.X509Certificate", [certificate] as X509Certificate[] ) springSecurityFilterChain.doFilter(request, response, chain); diff --git a/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/NamespaceRememberMeTests.groovy b/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/NamespaceRememberMeTests.groovy index 9a4f116b94..0d49679eba 100644 --- a/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/NamespaceRememberMeTests.groovy +++ b/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/NamespaceRememberMeTests.groovy @@ -81,8 +81,10 @@ public class NamespaceRememberMeTests extends BaseSpringSpec { when: "logout" super.setup() request.setSession(session) + super.setupCsrf() request.setCookies(rememberMeCookie) - request.requestURI = "/logout" + request.servletPath = "/logout" + request.method = "POST" springSecurityFilterChain.doFilter(request,response,chain) rememberMeCookie = getRememberMeCookie() then: "logout cookie expired" diff --git a/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/NamespaceSessionManagementTests.groovy b/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/NamespaceSessionManagementTests.groovy index 08234aa4fd..7d232f501f 100644 --- a/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/NamespaceSessionManagementTests.groovy +++ b/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/NamespaceSessionManagementTests.groovy @@ -41,7 +41,7 @@ class NamespaceSessionManagementTests extends BaseSpringSpec { when: loadConfig(SessionManagementConfig) then: - findFilter(SessionManagementFilter).sessionAuthenticationStrategy instanceof SessionFixationProtectionStrategy + findSessionAuthenticationStrategy(SessionFixationProtectionStrategy) } @EnableWebSecurity @@ -91,7 +91,7 @@ class NamespaceSessionManagementTests extends BaseSpringSpec { when: loadConfig(RefsSessionManagementConfig) then: - findFilter(SessionManagementFilter).sessionAuthenticationStrategy == RefsSessionManagementConfig.SAS + findFilter(SessionManagementFilter).sessionAuthenticationStrategy.delegateStrategies.find { it == RefsSessionManagementConfig.SAS } } @EnableWebSecurity @@ -110,7 +110,7 @@ class NamespaceSessionManagementTests extends BaseSpringSpec { when: loadConfig(SFPNoneSessionManagementConfig) then: - findFilter(SessionManagementFilter).sessionAuthenticationStrategy.class == NullAuthenticatedSessionStrategy + findFilter(SessionManagementFilter).sessionAuthenticationStrategy.delegateStrategies.find { it instanceof NullAuthenticatedSessionStrategy } } @EnableWebSecurity @@ -128,7 +128,7 @@ class NamespaceSessionManagementTests extends BaseSpringSpec { when: loadConfig(SFPMigrateSessionManagementConfig) then: - findFilter(SessionManagementFilter).sessionAuthenticationStrategy.migrateSessionAttributes + findSessionAuthenticationStrategy(SessionFixationProtectionStrategy).migrateSessionAttributes } @EnableWebSecurity @@ -145,7 +145,11 @@ class NamespaceSessionManagementTests extends BaseSpringSpec { when: loadConfig(SFPNewSessionSessionManagementConfig) then: - !findFilter(SessionManagementFilter).sessionAuthenticationStrategy.migrateSessionAttributes + !findSessionAuthenticationStrategy(SessionFixationProtectionStrategy).migrateSessionAttributes + } + + def findSessionAuthenticationStrategy(def c) { + findFilter(SessionManagementFilter).sessionAuthenticationStrategy.delegateStrategies.find { it.class.isAssignableFrom(c) } } @EnableWebSecurity diff --git a/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/ServletApiConfigurerTests.groovy b/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/ServletApiConfigurerTests.groovy index b402e1f521..b3f4a5ecfa 100644 --- a/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/ServletApiConfigurerTests.groovy +++ b/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/ServletApiConfigurerTests.groovy @@ -26,6 +26,7 @@ import org.springframework.security.config.annotation.web.configuration.EnableWe import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter import org.springframework.security.web.AuthenticationEntryPoint import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler; +import org.springframework.security.web.csrf.CsrfLogoutHandler; import org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter /** @@ -59,7 +60,7 @@ class ServletApiConfigurerTests extends BaseSpringSpec { and: "requestFactory != null" filter.requestFactory != null and: "logoutHandlers populated" - filter.logoutHandlers.collect { it.class } == [SecurityContextLogoutHandler] + filter.logoutHandlers.collect { it.class } == [CsrfLogoutHandler, SecurityContextLogoutHandler] } @CompileStatic diff --git a/config/src/test/groovy/org/springframework/security/config/http/AbstractHttpConfigTests.groovy b/config/src/test/groovy/org/springframework/security/config/http/AbstractHttpConfigTests.groovy index 416c17ecd5..c23444bcaa 100644 --- a/config/src/test/groovy/org/springframework/security/config/http/AbstractHttpConfigTests.groovy +++ b/config/src/test/groovy/org/springframework/security/config/http/AbstractHttpConfigTests.groovy @@ -57,8 +57,11 @@ abstract class AbstractHttpConfigTests extends AbstractXmlConfigTests { } List getFilters(String url) { - def fcp = appContext.getBean(BeanIds.FILTER_CHAIN_PROXY); - return fcp.getFilters(url) + springSecurityFilterChain.getFilters(url) + } + + Filter getSpringSecurityFilterChain() { + appContext.getBean(BeanIds.FILTER_CHAIN_PROXY) } FilterInvocation createFilterinvocation(String path, String method) { 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 new file mode 100644 index 0000000000..49d5c6c80c --- /dev/null +++ b/config/src/test/groovy/org/springframework/security/config/http/CsrfConfigTests.groovy @@ -0,0 +1,257 @@ +/* + * 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 static org.mockito.Mockito.* +import static org.mockito.Matchers.* + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse + +import org.spockframework.compiler.model.WhenBlock; +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.config.annotation.web.configurers.CsrfConfigurerTests.CsrfTokenRepositoryConfig; +import org.springframework.security.config.annotation.web.configurers.CsrfConfigurerTests.RequireCsrfProtectionMatcherConfig +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.csrf.CsrfTokenRepository; +import org.springframework.security.web.servlet.support.csrf.CsrfRequestDataValueProcessor +import org.springframework.security.web.util.RequestMatcher + +import spock.lang.Unroll + +/** + * + * @author Rob Winch + */ +class CsrfConfigTests extends AbstractHttpConfigTests { + MockHttpServletRequest request = new MockHttpServletRequest() + MockHttpServletResponse response = new MockHttpServletResponse() + MockFilterChain chain = new MockFilterChain() + + def 'no http csrf filter by default'() { + when: + httpAutoConfig { + } + 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",CsrfRequestDataValueProcessor) + } + + 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 CsrfToken("X-CSRF-TOKEN","_csrf", "abc") + when(repo.loadToken(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/spring_security_login" + when: "authenticate successfully" + response = new MockHttpServletResponse() + request = new MockHttpServletRequest(session: request.session) + request.requestURI = "/j_spring_security_check" + request.setParameter(token.parameterName,token.token) + request.setParameter("j_username","user") + request.setParameter("j_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 CsrfToken("X-CSRF-TOKEN","_csrf", "abc") + when(repo.loadToken(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/spring_security_login" + when: "authenticate successfully" + response = new MockHttpServletResponse() + request = new MockHttpServletRequest(session: request.session) + request.requestURI = "/j_spring_security_check" + request.setParameter(token.parameterName,token.token) + request.setParameter("j_username","user") + request.setParameter("j_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 "csrf requireCsrfProtectionMatcher"() { + setup: + httpAutoConfig { + 'csrf'('request-matcher-ref':'matcher') + } + mockBean(RequestMatcher,'matcher') + createAppContext() + 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"() { + setup: + httpAutoConfig { + 'csrf'('token-repository-ref':'repo') + } + mockBean(CsrfTokenRepository,'repo') + createAppContext() + CsrfTokenRepository repo = appContext.getBean("repo",CsrfTokenRepository) + CsrfToken token = new CsrfToken("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 CsrfToken("X-CSRF-TOKEN","_csrf", "abc") + when(repo.loadToken(any(HttpServletRequest))).thenReturn(token) + request.setParameter(token.parameterName,token.token) + request.method = "POST" + request.setParameter("j_username","user") + request.setParameter("j_password","password") + request.requestURI = "/j_spring_security_check" + when: + springSecurityFilterChain.doFilter(request,response,chain) + then: + verify(repo).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 CsrfToken("X-CSRF-TOKEN","_csrf", "abc") + when(repo.loadToken(any(HttpServletRequest))).thenReturn(token) + request.setParameter(token.parameterName,token.token) + request.method = "POST" + request.requestURI = "/j_spring_security_logout" + when: + springSecurityFilterChain.doFilter(request,response,chain) + then: + verify(repo).saveToken(eq(null),any(HttpServletRequest), any(HttpServletResponse)) + } +} diff --git a/config/src/test/groovy/org/springframework/security/config/http/SessionManagementConfigTests.groovy b/config/src/test/groovy/org/springframework/security/config/http/SessionManagementConfigTests.groovy index 8731384229..db71fa47e1 100644 --- a/config/src/test/groovy/org/springframework/security/config/http/SessionManagementConfigTests.groovy +++ b/config/src/test/groovy/org/springframework/security/config/http/SessionManagementConfigTests.groovy @@ -275,9 +275,7 @@ class SessionManagementConfigTests extends AbstractHttpConfigTests { httpAutoConfig { 'session-management'('session-authentication-strategy-ref':'ss') } - xml.'b:bean'(id: 'ss', 'class': Mockito.class.name, 'factory-method':'mock') { - 'b:constructor-arg'(value : SessionAuthenticationStrategy.class.name) - } + mockBean(SessionAuthenticationStrategy,'ss') createAppContext() MockHttpServletRequest request = new MockHttpServletRequest(); diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/CsrfConfigurerNoWebMvcTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/CsrfConfigurerNoWebMvcTests.java new file mode 100644 index 0000000000..e00de90ad7 --- /dev/null +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/CsrfConfigurerNoWebMvcTests.java @@ -0,0 +1,85 @@ +/* + * 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.config.annotation.web.configurers; + +import static org.fest.assertions.Assertions.assertThat; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.eq; +import static org.powermock.api.mockito.PowerMockito.spy; +import static org.powermock.api.mockito.PowerMockito.when; + +import org.junit.After; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.powermock.core.classloader.annotations.PrepareForTest; +import org.powermock.modules.junit4.PowerMockRunner; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.util.ClassUtils; + +/** + * @author Rob Winch + * + */ +@RunWith(PowerMockRunner.class) +@PrepareForTest({ClassUtils.class}) +public class CsrfConfigurerNoWebMvcTests { + ConfigurableApplicationContext context; + + @After + public void teardown() { + if(context != null) { + context.close(); + } + } + + @Test + public void missingDispatcherServletPreventsCsrfRequestDataValueProcessor() { + spy(ClassUtils.class); + when(ClassUtils.isPresent(eq("org.springframework.web.servlet.DispatcherServlet"), any(ClassLoader.class))).thenReturn(false); + + loadContext(CsrfDefaultsConfig.class); + + assertThat(context.containsBeanDefinition("requestDataValueProcessor")).isFalse(); + } + + @Test + public void findDispatcherServletPreventsCsrfRequestDataValueProcessor() { + loadContext(CsrfDefaultsConfig.class); + + assertThat(context.containsBeanDefinition("requestDataValueProcessor")).isTrue(); + } + + @EnableWebSecurity + @Configuration + static class CsrfDefaultsConfig extends WebSecurityConfigurerAdapter { + + @Override + protected void configure(HttpSecurity http) throws Exception { + } + } + + private void loadContext(Class configs) { + AnnotationConfigApplicationContext annotationConfigApplicationContext = new AnnotationConfigApplicationContext(); + annotationConfigApplicationContext.register(configs); + annotationConfigApplicationContext.refresh(); + this.context = annotationConfigApplicationContext; + } +} diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/SessionManagementConfigurerServlet31Tests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/SessionManagementConfigurerServlet31Tests.java index efe74d6b82..6f834abd76 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/SessionManagementConfigurerServlet31Tests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/SessionManagementConfigurerServlet31Tests.java @@ -49,6 +49,8 @@ import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextImpl; import org.springframework.security.web.context.HttpRequestResponseHolder; import org.springframework.security.web.context.HttpSessionSecurityContextRepository; +import org.springframework.security.web.csrf.CsrfToken; +import org.springframework.security.web.csrf.HttpSessionCsrfTokenRepository; import org.springframework.util.ReflectionUtils; @@ -94,6 +96,8 @@ public class SessionManagementConfigurerServlet31Tests { request.setMethod("POST"); request.setParameter("username", "user"); request.setParameter("password", "password"); + CsrfToken token = new HttpSessionCsrfTokenRepository().generateAndSaveToken(request, response); + request.setParameter(token.getParameterName(),token.getToken()); when(ReflectionUtils.findMethod(HttpServletRequest.class, "changeSessionId")).thenReturn(method); loadConfig(SessionManagementDefaultSessionFixationServlet31Config.class); diff --git a/docs/guides/src/asciidoc/hello-includes/secure-the-application.asc b/docs/guides/src/asciidoc/hello-includes/secure-the-application.asc index 364f509bed..399fb51ed4 100644 --- a/docs/guides/src/asciidoc/hello-includes/secure-the-application.asc +++ b/docs/guides/src/asciidoc/hello-includes/secure-the-application.asc @@ -89,6 +89,14 @@ The <> will: * Allow the user with the *Username* _user_ and the *Password* _password_ to authenticate with form based authentication * Allow the user with the *Username* _user_ and the *Password* _password_ to authenticate with HTTP basic authentication * Allow the user to logout +* http://en.wikipedia.org/wiki/Cross-site_request_forgery[CSRF attack] prevention +* http://en.wikipedia.org/wiki/Session_fixation[Session Fixation] protection +* Security Header integration +** http://en.wikipedia.org/wiki/HTTP_Strict_Transport_Security[HTTP Strict Transport Security] for secure requests +** http://msdn.microsoft.com/en-us/library/ie/gg622941(v=vs.85).aspx[X-Content-Type-Options] integration +** Cache Control (can be overridden later by your application to allow caching of your static resources) +** http://msdn.microsoft.com/en-us/library/dd565647(v=vs.85).aspx[X-XSS-Protection] integration +** X-Frame-Options integration to help prevent http://en.wikipedia.org/wiki/Clickjacking[Clickjacking] * Integrate with the following Servlet API methods ** http://docs.oracle.com/javaee/6/api/javax/servlet/http/HttpServletRequest.html#getRemoteUser()[HttpServletRequest#getRemoteUser()] ** http://docs.oracle.com/javaee/6/api/javax/servlet/http/HttpServletRequest.html#getUserPrincipal()[HttpServletRequest.html#getUserPrincipal()] diff --git a/docs/guides/src/asciidoc/hellomvc.asc b/docs/guides/src/asciidoc/hellomvc.asc index 1aa0c004ca..79425d343e 100644 --- a/docs/guides/src/asciidoc/hellomvc.asc +++ b/docs/guides/src/asciidoc/hellomvc.asc @@ -112,10 +112,12 @@ Now that we can view the user name, let's update the application to allow loggin [subs="verbatim,quotes"] ---- ---- +In order to help protect against http://en.wikipedia.org/wiki/Cross-site_request_forgery[CSRF attacks], by default, Spring Security Java Configuration log out requires: -Refresh the page at http://localhost:8080/sample/ and you will see the log out link. Click the link and see that the application logs you out successfully. +* the HTTP method must be a POST +* the CSRF token must be added to the request. Since we are using Spring MVC, the CSRF token is automatically added as a hidden input for you (view the source to see it). If you were not using Spring MVC, you can access the CsrfToken on the ServletRequest using the attribute _csrf + +Refresh the page at http://localhost:8080/sample/ and you will see the log out button. Click the button and see that the application logs you out successfully. include::hello-includes/basic-authentication.asc[] diff --git a/docs/guides/src/asciidoc/helloworld.asc b/docs/guides/src/asciidoc/helloworld.asc index 8260ed7146..1448442014 100644 --- a/docs/guides/src/asciidoc/helloworld.asc +++ b/docs/guides/src/asciidoc/helloworld.asc @@ -1,6 +1,6 @@ = Hello Spring Security Java Config :author: Rob Winch -:starter-appname: insecure +:starter-appname: insecure :completed-appname: helloworld-jc :verify-starter-app-include: hello-includes/verify-insecure-app.asc @@ -77,18 +77,24 @@ Now that we can view the user name, let's update the application to allow loggin

This is secured!

+

+ Hello +

-

- Hello -

-

- Click here to log out. -

+
+ + +
---- -Refresh the page at http://localhost:8080/sample/ and you will see the log out link. Click the link and see that the application logs you out successfully. +In order to help protect against http://en.wikipedia.org/wiki/Cross-site_request_forgery[CSRF attacks], by default, Spring Security Java Configuration log out requires: + +* the HTTP method must be a POST +* the CSRF token must be added to the request You can access it on the ServletRequest using the attribute _csrf as illustrated above. If you were using Spring MVC, the CSRF token is automatically added as a hidden input for you. + +Refresh the page at http://localhost:8080/sample/ and you will see the log out button. Click the logout button and see that the application logs you out successfully. include::hello-includes/basic-authentication.asc[] diff --git a/docs/manual/src/docbook/appendix-namespace.xml b/docs/manual/src/docbook/appendix-namespace.xml index 757bf78dae..e7789b11be 100644 --- a/docs/manual/src/docbook/appendix-namespace.xml +++ b/docs/manual/src/docbook/appendix-namespace.xml @@ -211,6 +211,7 @@ access-denied-handler anonymous + csrf custom-filter expression-handler form-login @@ -518,6 +519,30 @@ +
+ <literal><csrf></literal> + This element will add CSRF to the application. It also updates the default RequestCache + to only replay "GET" requests upon successful authentication. +
+ Parent Elements of <literal><csrf></literal> + + http + +
+
+ <literal><csrf></literal> Attributes +
+ <literal>token-repository-ref</literal> + The CsrfTokenRepository to use. The default is + HttpSessionCsrfTokenRepository. +
+
+ <literal>request-matcher-ref</literal> + The RequestMatcher instance to be used to determine if CSRF should be applied. Default is any + HTTP method except "GET", "TRACE", "HEAD", "OPTIONS". +
+
+
<literal><custom-filter></literal> This element is used to add a filter to the filter chain. It doesn't create any diff --git a/docs/manual/src/docbook/namespace-config.xml b/docs/manual/src/docbook/namespace-config.xml index a5db36e094..92e35cf443 100644 --- a/docs/manual/src/docbook/namespace-config.xml +++ b/docs/manual/src/docbook/namespace-config.xml @@ -716,9 +716,14 @@ List<OpenIDAttribute> attributes = token.getAttributes();The HEADERS_FILTER - HeadersFilter + HeaderWriterFilter http/headers + + CSRF_FILTER + CsrfFilter + http/csrf + LOGOUT_FILTER LogoutFilter diff --git a/samples/concurrency-jc/src/main/webapp/WEB-INF/decorators/main.jsp b/samples/concurrency-jc/src/main/webapp/WEB-INF/decorators/main.jsp index 7a1b4c800b..d35ae29086 100644 --- a/samples/concurrency-jc/src/main/webapp/WEB-INF/decorators/main.jsp +++ b/samples/concurrency-jc/src/main/webapp/WEB-INF/decorators/main.jsp @@ -75,7 +75,7 @@ a { color: green; } - .navbar-text a { + .navbar-form { margin-left: 1em; } @@ -103,13 +103,16 @@ Spring Security Sample diff --git a/samples/contacts-xml/contacts.gradle b/samples/contacts-xml/contacts.gradle index a3c7dc4201..c8f6029068 100644 --- a/samples/contacts-xml/contacts.gradle +++ b/samples/contacts-xml/contacts.gradle @@ -22,8 +22,7 @@ dependencies { "javax.servlet:jstl:$jstlVersion", "hsqldb:hsqldb:$hsqlVersion", "org.slf4j:jcl-over-slf4j:$slf4jVersion", - "ch.qos.logback:logback-classic:$logbackVersion" - - optional "net.sf.ehcache:ehcache:$ehcacheVersion" + "ch.qos.logback:logback-classic:$logbackVersion", + "net.sf.ehcache:ehcache:$ehcacheVersion" } \ No newline at end of file diff --git a/samples/contacts-xml/pom.xml b/samples/contacts-xml/pom.xml index 21906a1d8c..4b52aa2644 100644 --- a/samples/contacts-xml/pom.xml +++ b/samples/contacts-xml/pom.xml @@ -123,13 +123,6 @@ compile true - - net.sf.ehcache - ehcache - 1.6.2 - compile - true - org.apache.tomcat tomcat-servlet-api @@ -154,6 +147,12 @@ 1.2 runtime + + net.sf.ehcache + ehcache + 1.6.2 + runtime + org.slf4j jcl-over-slf4j diff --git a/samples/contacts-xml/src/main/resources/applicationContext-security.xml b/samples/contacts-xml/src/main/resources/applicationContext-security.xml index d6ae0f13a3..ee87c40660 100644 --- a/samples/contacts-xml/src/main/resources/applicationContext-security.xml +++ b/samples/contacts-xml/src/main/resources/applicationContext-security.xml @@ -31,6 +31,7 @@ + diff --git a/samples/contacts-xml/src/main/webapp/WEB-INF/jsp/add.jsp b/samples/contacts-xml/src/main/webapp/WEB-INF/jsp/add.jsp index 5b14a52275..02630f9858 100644 --- a/samples/contacts-xml/src/main/webapp/WEB-INF/jsp/add.jsp +++ b/samples/contacts-xml/src/main/webapp/WEB-INF/jsp/add.jsp @@ -33,6 +33,8 @@ Please fix all errors!

+ + " value=""/> ">Home diff --git a/samples/contacts-xml/src/main/webapp/WEB-INF/jsp/addPermission.jsp b/samples/contacts-xml/src/main/webapp/WEB-INF/jsp/addPermission.jsp index 52c05365ec..dfd2476c64 100644 --- a/samples/contacts-xml/src/main/webapp/WEB-INF/jsp/addPermission.jsp +++ b/samples/contacts-xml/src/main/webapp/WEB-INF/jsp/addPermission.jsp @@ -13,12 +13,12 @@ Recipient: - + @@ -29,12 +29,12 @@ Permission: - + @@ -47,6 +47,7 @@ Please fix all errors!

+ " value=""/>

diff --git a/samples/contacts-xml/src/main/webapp/exitUser.jsp b/samples/contacts-xml/src/main/webapp/exitUser.jsp index be70f3240c..b0792e5091 100644 --- a/samples/contacts-xml/src/main/webapp/exitUser.jsp +++ b/samples/contacts-xml/src/main/webapp/exitUser.jsp @@ -33,6 +33,7 @@ + " value=""/> diff --git a/samples/contacts-xml/src/main/webapp/login.jsp b/samples/contacts-xml/src/main/webapp/login.jsp index f82a307d61..1b3fb58470 100644 --- a/samples/contacts-xml/src/main/webapp/login.jsp +++ b/samples/contacts-xml/src/main/webapp/login.jsp @@ -40,7 +40,7 @@ - + " value=""/> diff --git a/samples/contacts-xml/src/main/webapp/switchUser.jsp b/samples/contacts-xml/src/main/webapp/switchUser.jsp index 09ec096d4e..f052fe262c 100644 --- a/samples/contacts-xml/src/main/webapp/switchUser.jsp +++ b/samples/contacts-xml/src/main/webapp/switchUser.jsp @@ -36,7 +36,7 @@ User: - + " value=""/> diff --git a/samples/hellomvc-jc/src/main/webapp/WEB-INF/decorators/main.jsp b/samples/hellomvc-jc/src/main/webapp/WEB-INF/decorators/main.jsp index 3695fd8c1c..d35ae29086 100644 --- a/samples/hellomvc-jc/src/main/webapp/WEB-INF/decorators/main.jsp +++ b/samples/hellomvc-jc/src/main/webapp/WEB-INF/decorators/main.jsp @@ -75,7 +75,7 @@ a { color: green; } - .navbar-text a { + .navbar-form { margin-left: 1em; } @@ -103,10 +103,10 @@ Spring Security Sample

diff --git a/samples/insecuremvc/src/main/webapp/WEB-INF/decorators/main.jsp b/samples/insecuremvc/src/main/webapp/WEB-INF/decorators/main.jsp index c06ace4581..d35ae29086 100644 --- a/samples/insecuremvc/src/main/webapp/WEB-INF/decorators/main.jsp +++ b/samples/insecuremvc/src/main/webapp/WEB-INF/decorators/main.jsp @@ -75,7 +75,7 @@ a { color: green; } - .navbar-text a { + .navbar-form { margin-left: 1em; } @@ -103,6 +103,11 @@ Spring Security Sample diff --git a/samples/ldap-jc/src/main/webapp/WEB-INF/decorators/main.jsp b/samples/ldap-jc/src/main/webapp/WEB-INF/decorators/main.jsp index 7a1b4c800b..d35ae29086 100644 --- a/samples/ldap-jc/src/main/webapp/WEB-INF/decorators/main.jsp +++ b/samples/ldap-jc/src/main/webapp/WEB-INF/decorators/main.jsp @@ -75,7 +75,7 @@ a { color: green; } - .navbar-text a { + .navbar-form { margin-left: 1em; } @@ -103,13 +103,16 @@ Spring Security Sample diff --git a/samples/messages-jc/pom.xml b/samples/messages-jc/pom.xml index 91509d22e8..fc50f2b1e9 100644 --- a/samples/messages-jc/pom.xml +++ b/samples/messages-jc/pom.xml @@ -127,12 +127,6 @@ 3.2.3.RELEASE compile
- - org.springframework - spring-core - 3.2.3.RELEASE - compile - org.springframework spring-core @@ -145,6 +139,12 @@ + + org.springframework + spring-core + 3.2.3.RELEASE + compile + org.springframework spring-instrument diff --git a/samples/openid-jc/src/main/webapp/WEB-INF/decorators/main.jsp b/samples/openid-jc/src/main/webapp/WEB-INF/decorators/main.jsp index 7e2efd0414..8d6469da2b 100644 --- a/samples/openid-jc/src/main/webapp/WEB-INF/decorators/main.jsp +++ b/samples/openid-jc/src/main/webapp/WEB-INF/decorators/main.jsp @@ -75,7 +75,7 @@ a { color: green; } - .navbar-text a { + .navbar-form { margin-left: 1em; } @@ -104,6 +104,11 @@ Spring Security Sample diff --git a/samples/preauth-jc/src/main/webapp/WEB-INF/decorators/main.jsp b/samples/preauth-jc/src/main/webapp/WEB-INF/decorators/main.jsp index 7a1b4c800b..d35ae29086 100644 --- a/samples/preauth-jc/src/main/webapp/WEB-INF/decorators/main.jsp +++ b/samples/preauth-jc/src/main/webapp/WEB-INF/decorators/main.jsp @@ -75,7 +75,7 @@ a { color: green; } - .navbar-text a { + .navbar-form { margin-left: 1em; } @@ -103,13 +103,16 @@ Spring Security Sample diff --git a/samples/rememberme-jc/src/main/webapp/WEB-INF/decorators/main.jsp b/samples/rememberme-jc/src/main/webapp/WEB-INF/decorators/main.jsp index 7a1b4c800b..d35ae29086 100644 --- a/samples/rememberme-jc/src/main/webapp/WEB-INF/decorators/main.jsp +++ b/samples/rememberme-jc/src/main/webapp/WEB-INF/decorators/main.jsp @@ -75,7 +75,7 @@ a { color: green; } - .navbar-text a { + .navbar-form { margin-left: 1em; } @@ -103,13 +103,16 @@ Spring Security Sample diff --git a/samples/x509-jc/src/main/webapp/WEB-INF/decorators/main.jsp b/samples/x509-jc/src/main/webapp/WEB-INF/decorators/main.jsp index 7a1b4c800b..d35ae29086 100644 --- a/samples/x509-jc/src/main/webapp/WEB-INF/decorators/main.jsp +++ b/samples/x509-jc/src/main/webapp/WEB-INF/decorators/main.jsp @@ -75,7 +75,7 @@ a { color: green; } - .navbar-text a { + .navbar-form { margin-left: 1em; } @@ -103,13 +103,16 @@ Spring Security Sample diff --git a/web/pom.xml b/web/pom.xml index 9edc861889..7259c06230 100644 --- a/web/pom.xml +++ b/web/pom.xml @@ -122,6 +122,13 @@ compile true + + org.springframework + spring-webmvc + 3.2.3.RELEASE + compile + true + org.apache.tomcat tomcat-servlet-api diff --git a/web/src/main/java/org/springframework/security/web/authentication/logout/LogoutFilter.java b/web/src/main/java/org/springframework/security/web/authentication/logout/LogoutFilter.java index 2854c4648e..d4f583aaec 100644 --- a/web/src/main/java/org/springframework/security/web/authentication/logout/LogoutFilter.java +++ b/web/src/main/java/org/springframework/security/web/authentication/logout/LogoutFilter.java @@ -28,6 +28,7 @@ import javax.servlet.http.HttpServletResponse; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.web.util.RequestMatcher; import org.springframework.security.web.util.UrlUtils; import org.springframework.util.Assert; import org.springframework.util.StringUtils; @@ -49,7 +50,9 @@ public class LogoutFilter extends GenericFilterBean { //~ Instance fields ================================================================================================ - private String filterProcessesUrl = "/j_spring_security_logout"; + private String filterProcessesUrl; + private RequestMatcher logoutRequestMatcher; + private final List handlers; private final LogoutSuccessHandler logoutSuccessHandler; @@ -65,6 +68,7 @@ public class LogoutFilter extends GenericFilterBean { this.handlers = Arrays.asList(handlers); Assert.notNull(logoutSuccessHandler, "logoutSuccessHandler cannot be null"); this.logoutSuccessHandler = logoutSuccessHandler; + setFilterProcessesUrl("/j_spring_security_logout"); } public LogoutFilter(String logoutSuccessUrl, LogoutHandler... handlers) { @@ -77,6 +81,7 @@ public class LogoutFilter extends GenericFilterBean { urlLogoutSuccessHandler.setDefaultTargetUrl(logoutSuccessUrl); } logoutSuccessHandler = urlLogoutSuccessHandler; + setFilterProcessesUrl("/j_spring_security_logout"); } //~ Methods ======================================================================================================== @@ -114,35 +119,55 @@ public class LogoutFilter extends GenericFilterBean { * @return true if logout should occur, false otherwise */ protected boolean requiresLogout(HttpServletRequest request, HttpServletResponse response) { - String uri = request.getRequestURI(); - int pathParamIndex = uri.indexOf(';'); - - if (pathParamIndex > 0) { - // strip everything from the first semi-colon - uri = uri.substring(0, pathParamIndex); - } - - int queryParamIndex = uri.indexOf('?'); - - if (queryParamIndex > 0) { - // strip everything from the first question mark - uri = uri.substring(0, queryParamIndex); - } - - if ("".equals(request.getContextPath())) { - return uri.endsWith(filterProcessesUrl); - } - - return uri.endsWith(request.getContextPath() + filterProcessesUrl); + return logoutRequestMatcher.matches(request); } + public void setLogoutRequestMatcher(RequestMatcher logoutRequestMatcher) { + Assert.notNull(logoutRequestMatcher, "logoutRequestMatcher cannot be null"); + this.logoutRequestMatcher = logoutRequestMatcher; + } + + @Deprecated public void setFilterProcessesUrl(String filterProcessesUrl) { - Assert.isTrue(UrlUtils.isValidRedirectUrl(filterProcessesUrl), filterProcessesUrl + " isn't a valid value for" + - " 'filterProcessesUrl'"); + this.logoutRequestMatcher = new FilterProcessUrlRequestMatcher(filterProcessesUrl); this.filterProcessesUrl = filterProcessesUrl; } + @Deprecated protected String getFilterProcessesUrl() { return filterProcessesUrl; } + + private static final class FilterProcessUrlRequestMatcher implements RequestMatcher { + private final String filterProcessesUrl; + + private FilterProcessUrlRequestMatcher(String filterProcessesUrl) { + Assert.hasLength(filterProcessesUrl, "filterProcessesUrl must be specified"); + Assert.isTrue(UrlUtils.isValidRedirectUrl(filterProcessesUrl), filterProcessesUrl + " isn't a valid redirect URL"); + this.filterProcessesUrl = filterProcessesUrl; + } + + public boolean matches(HttpServletRequest request) { + String uri = request.getRequestURI(); + int pathParamIndex = uri.indexOf(';'); + + if (pathParamIndex > 0) { + // strip everything from the first semi-colon + uri = uri.substring(0, pathParamIndex); + } + + int queryParamIndex = uri.indexOf('?'); + + if (queryParamIndex > 0) { + // strip everything from the first question mark + uri = uri.substring(0, queryParamIndex); + } + + if ("".equals(request.getContextPath())) { + return uri.endsWith(filterProcessesUrl); + } + + return uri.endsWith(request.getContextPath() + filterProcessesUrl); + } + } } diff --git a/web/src/main/java/org/springframework/security/web/authentication/session/CompositeSessionAuthenticationStrategy.java b/web/src/main/java/org/springframework/security/web/authentication/session/CompositeSessionAuthenticationStrategy.java index 21769f7195..37ef7ffeaf 100644 --- a/web/src/main/java/org/springframework/security/web/authentication/session/CompositeSessionAuthenticationStrategy.java +++ b/web/src/main/java/org/springframework/security/web/authentication/session/CompositeSessionAuthenticationStrategy.java @@ -67,7 +67,7 @@ public class CompositeSessionAuthenticationStrategy implements SessionAuthentica throw new IllegalArgumentException("delegateStrategies cannot contain null entires. Got " + delegateStrategies); } } - this.delegateStrategies = new ArrayList(delegateStrategies); + this.delegateStrategies = delegateStrategies; } /* (non-Javadoc) diff --git a/web/src/main/java/org/springframework/security/web/authentication/ui/DefaultLoginPageViewFilter.java b/web/src/main/java/org/springframework/security/web/authentication/ui/DefaultLoginPageViewFilter.java index bb945f6ad5..d1188e626c 100644 --- a/web/src/main/java/org/springframework/security/web/authentication/ui/DefaultLoginPageViewFilter.java +++ b/web/src/main/java/org/springframework/security/web/authentication/ui/DefaultLoginPageViewFilter.java @@ -27,6 +27,7 @@ import javax.servlet.http.HttpSession; import org.springframework.security.core.AuthenticationException; import org.springframework.security.web.WebAttributes; +import org.springframework.security.web.csrf.CsrfToken; import org.springframework.web.filter.GenericFilterBean; /** @@ -164,6 +165,7 @@ public class DefaultLoginPageViewFilter extends GenericFilterBean { } sb.append(" \n"); + renderHiddenInputs(sb, request); sb.append(" \n"); sb.append(""); } @@ -181,6 +183,7 @@ public class DefaultLoginPageViewFilter extends GenericFilterBean { sb.append(" \n"); sb.append(" \n"); + renderHiddenInputs(sb, request); sb.append(""); } @@ -189,6 +192,14 @@ public class DefaultLoginPageViewFilter extends GenericFilterBean { return sb.toString(); } + private void renderHiddenInputs(StringBuilder sb, HttpServletRequest request) { + CsrfToken token = (CsrfToken) request.getAttribute(CsrfToken.class.getName()); + + if(token != null) { + sb.append(" \n"); + } + } + private boolean isLogoutSuccess(HttpServletRequest request) { return logoutSuccessUrl != null && matches(request, logoutSuccessUrl); } diff --git a/web/src/main/java/org/springframework/security/web/csrf/CsrfAuthenticationStrategy.java b/web/src/main/java/org/springframework/security/web/csrf/CsrfAuthenticationStrategy.java new file mode 100644 index 0000000000..f508e51599 --- /dev/null +++ b/web/src/main/java/org/springframework/security/web/csrf/CsrfAuthenticationStrategy.java @@ -0,0 +1,56 @@ +/* + * 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.csrf; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.security.core.Authentication; +import org.springframework.security.web.authentication.session.SessionAuthenticationException; +import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy; +import org.springframework.util.Assert; + +/** + * {@link CsrfAuthenticationStrategy} is in charge of removing the {@link CsrfToken} upon + * authenticating. A new {@link CsrfToken} will then be generated by the framework upon + * the next request. + * + * @author Rob Winch + * @since 3.2 + */ +public final class CsrfAuthenticationStrategy implements + SessionAuthenticationStrategy { + + private final CsrfTokenRepository csrfTokenRepository; + + /** + * Creates a new instance + * @param csrfTokenRepository the {@link CsrfTokenRepository} to use + */ + public CsrfAuthenticationStrategy(CsrfTokenRepository csrfTokenRepository) { + Assert.notNull(csrfTokenRepository,"csrfTokenRepository cannot be null"); + this.csrfTokenRepository = csrfTokenRepository; + } + + /* (non-Javadoc) + * @see org.springframework.security.web.authentication.session.SessionAuthenticationStrategy#onAuthentication(org.springframework.security.core.Authentication, javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse) + */ + public void onAuthentication(Authentication authentication, + HttpServletRequest request, HttpServletResponse response) + throws SessionAuthenticationException { + this.csrfTokenRepository.saveToken(null, request, response); + } +} \ No newline at end of file diff --git a/web/src/main/java/org/springframework/security/web/csrf/CsrfFilter.java b/web/src/main/java/org/springframework/security/web/csrf/CsrfFilter.java new file mode 100644 index 0000000000..5851dc7c13 --- /dev/null +++ b/web/src/main/java/org/springframework/security/web/csrf/CsrfFilter.java @@ -0,0 +1,141 @@ +/* + * 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.csrf; + +import java.io.IOException; +import java.util.regex.Pattern; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; + +import org.springframework.security.web.access.AccessDeniedHandler; +import org.springframework.security.web.access.AccessDeniedHandlerImpl; +import org.springframework.security.web.util.RequestMatcher; +import org.springframework.util.Assert; +import org.springframework.web.filter.OncePerRequestFilter; + +/** + *

+ * Applies CSRF protection using a synchronizer token pattern. Developers are + * required to ensure that {@link CsrfFilter} is invoked for any request that + * allows state to change. Typically this just means that they should ensure + * their web application follows proper REST semantics (i.e. do not change state + * with the HTTP methods GET, HEAD, TRACE, OPTIONS). + *

+ * + *

+ * Typically the {@link CsrfTokenRepository} implementation chooses to store the + * {@link CsrfToken} in {@link HttpSession} with + * {@link HttpSessionCsrfTokenRepository}. This is preferred to storing the + * token in a cookie which. + *

+ * + * @author Rob Winch + * @since 3.2 + */ +public final class CsrfFilter extends OncePerRequestFilter { + private final CsrfTokenRepository tokenRepository; + private RequestMatcher requireCsrfProtectionMatcher = new DefaultRequiresCsrfMatcher(); + private AccessDeniedHandler accessDeniedHandler = new AccessDeniedHandlerImpl(); + + public CsrfFilter(CsrfTokenRepository csrfTokenRepository) { + Assert.notNull(csrfTokenRepository, "csrfTokenRepository cannot be null"); + this.tokenRepository = csrfTokenRepository; + } + + /* (non-Javadoc) + * @see org.springframework.web.filter.OncePerRequestFilter#doFilterInternal(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse, javax.servlet.FilterChain) + */ + @Override + protected void doFilterInternal(HttpServletRequest request, + HttpServletResponse response, FilterChain filterChain) + throws ServletException, IOException { + CsrfToken csrfToken = tokenRepository.loadToken(request); + if(csrfToken == null) { + csrfToken = tokenRepository.generateAndSaveToken(request, response); + } + request.setAttribute(CsrfToken.class.getName(), csrfToken); + request.setAttribute(csrfToken.getParameterName(), csrfToken); + response.addHeader(csrfToken.getHeaderName(), csrfToken.getToken()); + + if(!requireCsrfProtectionMatcher.matches(request)) { + filterChain.doFilter(request, response); + return; + } + + String actualToken = request.getHeader(csrfToken.getHeaderName()); + if(actualToken == null) { + actualToken = request.getParameter(csrfToken.getParameterName()); + } + if(!csrfToken.getToken().equals(actualToken)) { + accessDeniedHandler.handle(request, response, new InvalidCsrfTokenException(csrfToken, actualToken)); + return; + } + + filterChain.doFilter(request, response); + } + + /** + * Specifies a {@link RequestMatcher} that is used to determine if CSRF + * protection should be applied. If the {@link RequestMatcher} returns true + * for a given request, then CSRF protection is applied. + * + *

+ * The default is to apply CSRF protection for any HTTP method other than + * GET, HEAD, TRACE, OPTIONS. + *

+ * + * @param requireCsrfProtectionMatcher + * the {@link RequestMatcher} used to determine if CSRF + * protection should be applied. + */ + public void setRequireCsrfProtectionMatcher(RequestMatcher requireCsrfProtectionMatcher) { + Assert.notNull(requireCsrfProtectionMatcher, "requireCsrfProtectionMatcher cannot be null"); + this.requireCsrfProtectionMatcher = requireCsrfProtectionMatcher; + } + + + /** + * Specifies a {@link AccessDeniedHandler} that should be used when CSRF protection fails. + * + *

+ * The default is to use AccessDeniedHandlerImpl with no arguments. + *

+ * + * @param accessDeniedHandler + * the {@link AccessDeniedHandler} to use + */ + public void setAccessDeniedHandler(AccessDeniedHandler accessDeniedHandler) { + Assert.notNull(accessDeniedHandler, "accessDeniedHandler cannot be null"); + this.accessDeniedHandler = accessDeniedHandler; + } + + private static class DefaultRequiresCsrfMatcher implements RequestMatcher { + private Pattern allowedMethods = Pattern.compile("^(GET|HEAD|TRACE|OPTIONS)$"); + + /* (non-Javadoc) + * @see org.springframework.security.web.util.RequestMatcher#matches(javax.servlet.http.HttpServletRequest) + */ + public boolean matches(HttpServletRequest request) { + return !allowedMethods.matcher(request.getMethod()).matches(); + } + } +} diff --git a/web/src/main/java/org/springframework/security/web/csrf/CsrfLogoutHandler.java b/web/src/main/java/org/springframework/security/web/csrf/CsrfLogoutHandler.java new file mode 100644 index 0000000000..35b07df109 --- /dev/null +++ b/web/src/main/java/org/springframework/security/web/csrf/CsrfLogoutHandler.java @@ -0,0 +1,54 @@ +/* + * 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.csrf; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.security.core.Authentication; +import org.springframework.security.web.authentication.logout.LogoutHandler; +import org.springframework.util.Assert; + +/** + * {@link CsrfLogoutHandler} is in charge of removing the {@link CsrfToken} upon + * logout. A new {@link CsrfToken} will then be generated by the framework upon + * the next request. + * + * @author Rob Winch + * @since 3.2 + */ +public final class CsrfLogoutHandler implements LogoutHandler { + private final CsrfTokenRepository csrfTokenRepository; + + /** + * Creates a new instance + * @param csrfTokenRepository the {@link CsrfTokenRepository} to use + */ + public CsrfLogoutHandler(CsrfTokenRepository csrfTokenRepository) { + Assert.notNull(csrfTokenRepository,"csrfTokenRepository cannot be null"); + this.csrfTokenRepository = csrfTokenRepository; + } + + /** + * Clears the {@link CsrfToken} + * + * @see org.springframework.security.web.authentication.logout.LogoutHandler#logout(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse, org.springframework.security.core.Authentication) + */ + public void logout(HttpServletRequest request, + HttpServletResponse response, Authentication authentication) { + this.csrfTokenRepository.saveToken(null, request, response); + } +} \ No newline at end of file diff --git a/web/src/main/java/org/springframework/security/web/csrf/CsrfToken.java b/web/src/main/java/org/springframework/security/web/csrf/CsrfToken.java new file mode 100644 index 0000000000..46f680d5b7 --- /dev/null +++ b/web/src/main/java/org/springframework/security/web/csrf/CsrfToken.java @@ -0,0 +1,78 @@ +/* + * 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.csrf; + +import java.io.Serializable; + +import org.springframework.util.Assert; + +/** + * A CSRF token that is used to protect against CSRF attacks. + * + * @author Rob Winch + * @since 3.2 + */ +@SuppressWarnings("serial") +public final class CsrfToken implements Serializable { + + private final String token; + + private final String parameterName; + + private final String headerName; + + /** + * Creates a new instance + * @param headerName the HTTP header name to use + * @param parameterName the HTTP parameter name to use + * @param token the value of the token (i.e. expected value of the HTTP parameter of parametername). + */ + public CsrfToken(String headerName, String parameterName, String token) { + Assert.hasLength(headerName, "headerName cannot be null or empty"); + Assert.hasLength(parameterName, "parameterName cannot be null or empty"); + Assert.hasLength(token, "token cannot be null or empty"); + this.headerName = headerName; + this.parameterName = parameterName; + this.token = token; + } + + /** + * Gets the HTTP header that the CSRF is populated on the response and can + * be placed on requests instead of the parameter. Cannot be null. + * + * @return the HTTP header that the CSRF is populated on the response and + * can be placed on requests instead of the parameter + */ + public String getHeaderName() { + return headerName; + } + + /** + * Gets the HTTP parameter name that should contain the token. Cannot be null. + * @return the HTTP parameter name that should contain the token. + */ + public String getParameterName() { + return parameterName; + } + + /** + * Gets the token value. Cannot be null. + * @return the token value + */ + public String getToken() { + return token; + } +} diff --git a/web/src/main/java/org/springframework/security/web/csrf/CsrfTokenRepository.java b/web/src/main/java/org/springframework/security/web/csrf/CsrfTokenRepository.java new file mode 100644 index 0000000000..4b705fd27c --- /dev/null +++ b/web/src/main/java/org/springframework/security/web/csrf/CsrfTokenRepository.java @@ -0,0 +1,71 @@ +/* + * 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.csrf; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; + +/** + * An API to allow changing the method in which the expected {@link CsrfToken} + * is associated to the {@link HttpServletRequest}. For example, it may be + * stored in {@link HttpSession}. + * + * @see HttpSessionCsrfTokenRepository + * + * @author Rob Winch + * @since 3.2 + * + */ +public interface CsrfTokenRepository { + + /** + * Generates and saves the expected {@link CsrfToken} + * + * @param request + * the {@link HttpServletRequest} to use + * @param response + * the {@link HttpServletResponse} to use + * @return the {@link CsrfToken} that was generated and saved. Cannot be + * null. + */ + CsrfToken generateAndSaveToken(HttpServletRequest request, + HttpServletResponse response); + + /** + * Saves the {@link CsrfToken} using the {@link HttpServletRequest} and + * {@link HttpServletResponse}. If the {@link CsrfToken} is null, it is the + * same as deleting it. + * + * @param token + * the {@link CsrfToken} to save or null to delete + * @param request + * the {@link HttpServletRequest} to use + * @param response + * the {@link HttpServletResponse} to use + */ + void saveToken(CsrfToken token, HttpServletRequest request, + HttpServletResponse response); + + /** + * Loads the expected {@link CsrfToken} from the {@link HttpServletRequest} + * + * @param request + * the {@link HttpServletRequest} to use + * @return the {@link CsrfToken} or null if none exists + */ + CsrfToken loadToken(HttpServletRequest request); +} diff --git a/web/src/main/java/org/springframework/security/web/csrf/HttpSessionCsrfTokenRepository.java b/web/src/main/java/org/springframework/security/web/csrf/HttpSessionCsrfTokenRepository.java new file mode 100644 index 0000000000..6eecd01b63 --- /dev/null +++ b/web/src/main/java/org/springframework/security/web/csrf/HttpSessionCsrfTokenRepository.java @@ -0,0 +1,109 @@ +/* + * 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.csrf; + +import java.util.UUID; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; + +import org.springframework.util.Assert; + +/** + * A {@link CsrfTokenRepository} that stores the {@link CsrfToken} in the {@link HttpSession}. + * + * @author Rob Winch + * @since 3.2 + */ +public final class HttpSessionCsrfTokenRepository implements CsrfTokenRepository { + private static final String DEFAULT_CSRF_PARAMETER_NAME = "_csrf"; + + private static final String DEFAULT_CSRF_HEADER_NAME = "X-CSRF-TOKEN"; + + private static final String DEFAULT_CSRF_TOKEN_ATTR_NAME = HttpSessionCsrfTokenRepository.class.getName().concat(".CSRF_TOKEN"); + + private String parameterName = DEFAULT_CSRF_PARAMETER_NAME; + + private String headerName = DEFAULT_CSRF_HEADER_NAME; + + private String sessionAttributeName = DEFAULT_CSRF_TOKEN_ATTR_NAME; + + /* + * (non-Javadoc) + * @see org.springframework.security.web.csrf.CsrfTokenRepository#saveToken(org.springframework.security.web.csrf.CsrfToken, javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse) + */ + public void saveToken(CsrfToken token, HttpServletRequest request, + HttpServletResponse response) { + HttpSession session = request.getSession(); + if(token == null) { + session.removeAttribute(sessionAttributeName); + } else { + session.setAttribute(sessionAttributeName, token); + } + } + + /* (non-Javadoc) + * @see org.springframework.security.web.csrf.CsrfTokenRepository#loadToken(javax.servlet.http.HttpServletRequest) + */ + public CsrfToken loadToken(HttpServletRequest request) { + return (CsrfToken) request.getSession().getAttribute(sessionAttributeName); + } + + /* (non-Javadoc) + * @see org.springframework.security.web.csrf.CsrfTokenRepository#generateNewToken(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse) + */ + public CsrfToken generateAndSaveToken(HttpServletRequest request, + HttpServletResponse response) { + CsrfToken token = new CsrfToken(headerName, parameterName, createNewToken()); + saveToken(token, request, response); + return token; + } + + /** + * Sets the {@link HttpServletRequest} parameter name that the {@link CsrfToken} is expected to appear on + * @param parameterName the new parameter name to use + */ + public void setParameterName(String parameterName) { + Assert.hasLength(parameterName, "parameterName cannot be null or empty"); + this.parameterName = parameterName; + } + + /** + * Sets the header name that the {@link CsrfToken} is expected to appear on + * and the header that the response will contain the {@link CsrfToken}. + * + * @param parameterName + * the new parameter name to use + */ + public void setHeaderName(String parameterName) { + Assert.hasLength(parameterName, "parameterName cannot be null or empty"); + this.parameterName = parameterName; + } + + /** + * Sets the {@link HttpSession} attribute name that the {@link CsrfToken} is stored in + * @param sessionAttributeName the new attribute name to use + */ + public void setSessionAttributeName(String sessionAttributeName) { + Assert.hasLength(sessionAttributeName, "sessionAttributename cannot be null or empty"); + this.sessionAttributeName = sessionAttributeName; + } + + private String createNewToken() { + return UUID.randomUUID().toString(); + } +} diff --git a/web/src/main/java/org/springframework/security/web/csrf/InvalidCsrfTokenException.java b/web/src/main/java/org/springframework/security/web/csrf/InvalidCsrfTokenException.java new file mode 100644 index 0000000000..38faff7e35 --- /dev/null +++ b/web/src/main/java/org/springframework/security/web/csrf/InvalidCsrfTokenException.java @@ -0,0 +1,40 @@ +/* + * 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.csrf; + +import org.springframework.security.access.AccessDeniedException; + + +/** + * Thrown when an invalid or missing {@link CsrfToken} is found in the HttpServletRequest + * + * @author Rob Winch + * @since 3.2 + */ +@SuppressWarnings("serial") +public class InvalidCsrfTokenException extends AccessDeniedException { + + /** + * @param msg + */ + public InvalidCsrfTokenException(CsrfToken expectedAccessToken, String actualAccessToken) { + super("Invalid CSRF Token '" + actualAccessToken + + "' was found on the request parameter '" + + expectedAccessToken.getParameterName() + "' or header '" + + expectedAccessToken.getHeaderName() + "'."); + } + +} diff --git a/web/src/main/java/org/springframework/security/web/servlet/support/csrf/CsrfRequestDataValueProcessor.java b/web/src/main/java/org/springframework/security/web/servlet/support/csrf/CsrfRequestDataValueProcessor.java new file mode 100644 index 0000000000..c11cb3cf3e --- /dev/null +++ b/web/src/main/java/org/springframework/security/web/servlet/support/csrf/CsrfRequestDataValueProcessor.java @@ -0,0 +1,86 @@ +/* + * 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.servlet.support.csrf; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import javax.servlet.http.HttpServletRequest; + +import org.springframework.security.web.csrf.CsrfToken; +import org.springframework.web.servlet.support.RequestDataValueProcessor; + +/** + * Integration with Spring Web MVC that automatically adds the {@link CsrfToken} + * into forms with hidden inputs when using Spring tag libraries. + * + * @author Rob Winch + * @since 3.2 + */ +public final class CsrfRequestDataValueProcessor implements + RequestDataValueProcessor { + + /* + * (non-Javadoc) + * + * @see org.springframework.web.servlet.support.RequestDataValueProcessor# + * processAction(javax.servlet.http.HttpServletRequest, java.lang.String) + */ + public String processAction(HttpServletRequest request, String action) { + return action; + } + + /* + * (non-Javadoc) + * + * @see org.springframework.web.servlet.support.RequestDataValueProcessor# + * processFormFieldValue(javax.servlet.http.HttpServletRequest, + * java.lang.String, java.lang.String, java.lang.String) + */ + public String processFormFieldValue(HttpServletRequest request, + String name, String value, String type) { + return value; + } + + /* + * (non-Javadoc) + * + * @see org.springframework.web.servlet.support.RequestDataValueProcessor# + * getExtraHiddenFields(javax.servlet.http.HttpServletRequest) + */ + public Map getExtraHiddenFields(HttpServletRequest request) { + CsrfToken token = (CsrfToken) request.getAttribute(CsrfToken.class + .getName()); + if (token == null) { + return Collections.emptyMap(); + } + Map hiddenFields = new HashMap(1); + hiddenFields.put(token.getParameterName(), token.getToken()); + return hiddenFields; + } + + /* + * (non-Javadoc) + * + * @see + * org.springframework.web.servlet.support.RequestDataValueProcessor#processUrl + * (javax.servlet.http.HttpServletRequest, java.lang.String) + */ + public String processUrl(HttpServletRequest request, String url) { + return url; + } +} diff --git a/web/src/test/java/org/springframework/security/web/csrf/CsrfAuthenticationStrategyTests.java b/web/src/test/java/org/springframework/security/web/csrf/CsrfAuthenticationStrategyTests.java new file mode 100644 index 0000000000..77aad4ccdc --- /dev/null +++ b/web/src/test/java/org/springframework/security/web/csrf/CsrfAuthenticationStrategyTests.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.csrf; + +import static org.mockito.Mockito.verify; + +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.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.security.authentication.TestingAuthenticationToken; + +/** + * @author Rob Winch + * + */ +@RunWith(MockitoJUnitRunner.class) +public class CsrfAuthenticationStrategyTests { + @Mock + private CsrfTokenRepository csrfTokenRepository; + + private MockHttpServletRequest request; + + private MockHttpServletResponse response; + + private CsrfAuthenticationStrategy strategy; + + @Before + public void setup() { + request = new MockHttpServletRequest(); + response = new MockHttpServletResponse(); + strategy = new CsrfAuthenticationStrategy(csrfTokenRepository); + } + + @Test(expected = IllegalArgumentException.class) + public void constructorNullCsrfTokenRepository() { + new CsrfAuthenticationStrategy(null); + } + + @Test + public void logoutRemovesCsrfToken() { + strategy.onAuthentication(new TestingAuthenticationToken("user", "password", "ROLE_USER"),request, response); + + verify(csrfTokenRepository).saveToken(null, request, response); + } + +} + diff --git a/web/src/test/java/org/springframework/security/web/csrf/CsrfFilterTests.java b/web/src/test/java/org/springframework/security/web/csrf/CsrfFilterTests.java new file mode 100644 index 0000000000..fc1986608f --- /dev/null +++ b/web/src/test/java/org/springframework/security/web/csrf/CsrfFilterTests.java @@ -0,0 +1,303 @@ +/* + * 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.csrf; + +import static org.fest.assertions.Assertions.assertThat; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyZeroInteractions; +import static org.mockito.Mockito.when; + +import java.io.IOException; +import java.util.Arrays; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletResponse; + +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.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.security.web.access.AccessDeniedHandler; +import org.springframework.security.web.util.RequestMatcher; + +/** + * @author Rob Winch + * + */ +@RunWith(MockitoJUnitRunner.class) +public class CsrfFilterTests { + + @Mock + private RequestMatcher requestMatcher; + @Mock + private CsrfTokenRepository tokenRepository; + @Mock + private FilterChain filterChain; + @Mock + private AccessDeniedHandler deniedHandler; + + private MockHttpServletRequest request; + private MockHttpServletResponse response; + private CsrfToken token; + + + private CsrfFilter filter; + + @Before + public void setup() { + token = new CsrfToken("headerName","paramName", "csrfTokenValue"); + resetRequestResponse(); + filter = new CsrfFilter(tokenRepository); + filter.setRequireCsrfProtectionMatcher(requestMatcher); + filter.setAccessDeniedHandler(deniedHandler); + } + + private void resetRequestResponse() { + request = new MockHttpServletRequest(); + response = new MockHttpServletResponse(); + } + + @Test(expected = IllegalArgumentException.class) + public void constructorNullRepository() { + new CsrfFilter(null); + } + + @Test + public void doFilterAccessDeniedNoTokenPresent() throws ServletException, IOException { + when(requestMatcher.matches(request)).thenReturn(true); + when(tokenRepository.loadToken(request)).thenReturn(token); + + filter.doFilter(request, response, filterChain); + + assertThat(response.getHeader(token.getHeaderName())).isEqualTo(token.getToken()); + assertThat(request.getAttribute(token.getParameterName())).isEqualTo(token); + assertThat(request.getAttribute(CsrfToken.class.getName())).isEqualTo(token); + + verify(deniedHandler).handle(eq(request), eq(response), any(InvalidCsrfTokenException.class)); + verifyZeroInteractions(filterChain); + } + + @Test + public void doFilterAccessDeniedIncorrectTokenPresent() throws ServletException, IOException { + when(requestMatcher.matches(request)).thenReturn(true); + when(tokenRepository.loadToken(request)).thenReturn(token); + request.setParameter(token.getParameterName(), token.getToken()+ " INVALID"); + + filter.doFilter(request, response, filterChain); + + assertThat(response.getHeader(token.getHeaderName())).isEqualTo(token.getToken()); + assertThat(request.getAttribute(token.getParameterName())).isEqualTo(token); + assertThat(request.getAttribute(CsrfToken.class.getName())).isEqualTo(token); + + verify(deniedHandler).handle(eq(request), eq(response), any(InvalidCsrfTokenException.class)); + verifyZeroInteractions(filterChain); + } + + @Test + public void doFilterAccessDeniedIncorrectTokenPresentHeader() throws ServletException, IOException { + when(requestMatcher.matches(request)).thenReturn(true); + when(tokenRepository.loadToken(request)).thenReturn(token); + request.addHeader(token.getHeaderName(), token.getToken()+ " INVALID"); + + filter.doFilter(request, response, filterChain); + + assertThat(response.getHeader(token.getHeaderName())).isEqualTo(token.getToken()); + assertThat(request.getAttribute(token.getParameterName())).isEqualTo(token); + assertThat(request.getAttribute(CsrfToken.class.getName())).isEqualTo(token); + + verify(deniedHandler).handle(eq(request), eq(response), any(InvalidCsrfTokenException.class)); + verifyZeroInteractions(filterChain); + } + + @Test + public void doFilterAccessDeniedIncorrectTokenPresentHeaderPreferredOverParameter() throws ServletException, IOException { + when(requestMatcher.matches(request)).thenReturn(true); + when(tokenRepository.loadToken(request)).thenReturn(token); + request.setParameter(token.getParameterName(), token.getToken()); + request.addHeader(token.getHeaderName(), token.getToken()+ " INVALID"); + + filter.doFilter(request, response, filterChain); + + assertThat(response.getHeader(token.getHeaderName())).isEqualTo(token.getToken()); + assertThat(request.getAttribute(token.getParameterName())).isEqualTo(token); + assertThat(request.getAttribute(CsrfToken.class.getName())).isEqualTo(token); + + verify(deniedHandler).handle(eq(request), eq(response), any(InvalidCsrfTokenException.class)); + verifyZeroInteractions(filterChain); + } + + @Test + public void doFilterNotCsrfRequestExistingToken() throws ServletException, IOException { + when(requestMatcher.matches(request)).thenReturn(false); + when(tokenRepository.loadToken(request)).thenReturn(token); + + filter.doFilter(request, response, filterChain); + + assertThat(response.getHeader(token.getHeaderName())).isEqualTo(token.getToken()); + assertThat(request.getAttribute(token.getParameterName())).isEqualTo(token); + assertThat(request.getAttribute(CsrfToken.class.getName())).isEqualTo(token); + + verify(filterChain).doFilter(request, response); + verifyZeroInteractions(deniedHandler); + } + + @Test + public void doFilterNotCsrfRequestGenerateToken() throws ServletException, IOException { + when(requestMatcher.matches(request)).thenReturn(false); + when(tokenRepository.generateAndSaveToken(request, response)).thenReturn(token); + + filter.doFilter(request, response, filterChain); + + assertThat(response.getHeader(token.getHeaderName())).isEqualTo(token.getToken()); + assertThat(request.getAttribute(token.getParameterName())).isEqualTo(token); + assertThat(request.getAttribute(CsrfToken.class.getName())).isEqualTo(token); + + verify(filterChain).doFilter(request, response); + verifyZeroInteractions(deniedHandler); + } + + @Test + public void doFilterIsCsrfRequestExistingTokenHeader() throws ServletException, IOException { + when(requestMatcher.matches(request)).thenReturn(true); + when(tokenRepository.loadToken(request)).thenReturn(token); + request.addHeader(token.getHeaderName(), token.getToken()); + + filter.doFilter(request, response, filterChain); + + assertThat(response.getHeader(token.getHeaderName())).isEqualTo(token.getToken()); + assertThat(request.getAttribute(token.getParameterName())).isEqualTo(token); + assertThat(request.getAttribute(CsrfToken.class.getName())).isEqualTo(token); + + verify(filterChain).doFilter(request, response); + verifyZeroInteractions(deniedHandler); + } + + @Test + public void doFilterIsCsrfRequestExistingTokenHeaderPreferredOverInvalidParam() throws ServletException, IOException { + when(requestMatcher.matches(request)).thenReturn(true); + when(tokenRepository.loadToken(request)).thenReturn(token); + request.setParameter(token.getParameterName(), token.getToken()+ " INVALID"); + request.addHeader(token.getHeaderName(), token.getToken()); + + filter.doFilter(request, response, filterChain); + + assertThat(response.getHeader(token.getHeaderName())).isEqualTo(token.getToken()); + assertThat(request.getAttribute(token.getParameterName())).isEqualTo(token); + assertThat(request.getAttribute(CsrfToken.class.getName())).isEqualTo(token); + + verify(filterChain).doFilter(request, response); + verifyZeroInteractions(deniedHandler); + } + + @Test + public void doFilterIsCsrfRequestExistingToken() throws ServletException, IOException { + when(requestMatcher.matches(request)).thenReturn(true); + when(tokenRepository.loadToken(request)).thenReturn(token); + request.setParameter(token.getParameterName(), token.getToken()); + + filter.doFilter(request, response, filterChain); + + assertThat(response.getHeader(token.getHeaderName())).isEqualTo(token.getToken()); + assertThat(request.getAttribute(token.getParameterName())).isEqualTo(token); + assertThat(request.getAttribute(CsrfToken.class.getName())).isEqualTo(token); + + verify(filterChain).doFilter(request, response); + verifyZeroInteractions(deniedHandler); + } + + @Test + public void doFilterIsCsrfRequestGenerateToken() throws ServletException, IOException { + when(requestMatcher.matches(request)).thenReturn(true); + when(tokenRepository.generateAndSaveToken(request, response)).thenReturn(token); + request.setParameter(token.getParameterName(), token.getToken()); + + filter.doFilter(request, response, filterChain); + + assertThat(response.getHeader(token.getHeaderName())).isEqualTo(token.getToken()); + assertThat(request.getAttribute(token.getParameterName())).isEqualTo(token); + assertThat(request.getAttribute(CsrfToken.class.getName())).isEqualTo(token); + + verify(filterChain).doFilter(request, response); + verifyZeroInteractions(deniedHandler); + } + + @Test + public void doFilterDefaultRequireCsrfProtectionMatcherAllowedMethods() throws ServletException, IOException { + filter = new CsrfFilter(tokenRepository); + filter.setAccessDeniedHandler(deniedHandler); + + for(String method : Arrays.asList("GET","TRACE", "OPTIONS", "HEAD")) { + resetRequestResponse(); + when(tokenRepository.loadToken(request)).thenReturn(token); + request.setMethod(method); + + filter.doFilter(request, response, filterChain); + + verify(filterChain).doFilter(request, response); + verifyZeroInteractions(deniedHandler); + } + } + + @Test + public void doFilterDefaultRequireCsrfProtectionMatcherDeniedMethods() throws ServletException, IOException { + filter = new CsrfFilter(tokenRepository); + filter.setAccessDeniedHandler(deniedHandler); + + for(String method : Arrays.asList("POST","PUT", "PATCH", "DELETE", "INVALID")) { + resetRequestResponse(); + when(tokenRepository.loadToken(request)).thenReturn(token); + request.setMethod(method); + + filter.doFilter(request, response, filterChain); + + verify(deniedHandler).handle(eq(request), eq(response), any(InvalidCsrfTokenException.class)); + verifyZeroInteractions(filterChain); + } + } + + @Test + public void doFilterDefaultAccessDenied() throws ServletException, IOException { + filter = new CsrfFilter(tokenRepository); + filter.setRequireCsrfProtectionMatcher(requestMatcher); + when(requestMatcher.matches(request)).thenReturn(true); + when(tokenRepository.loadToken(request)).thenReturn(token); + + filter.doFilter(request, response, filterChain); + + assertThat(response.getHeader(token.getHeaderName())).isEqualTo(token.getToken()); + assertThat(request.getAttribute(token.getParameterName())).isEqualTo(token); + assertThat(request.getAttribute(CsrfToken.class.getName())).isEqualTo(token); + + assertThat(response.getStatus()).isEqualTo(HttpServletResponse.SC_FORBIDDEN); + verifyZeroInteractions(filterChain); + } + + @Test(expected = IllegalArgumentException.class) + public void setRequireCsrfProtectionMatcherNull() { + filter.setRequireCsrfProtectionMatcher(null); + } + + @Test(expected = IllegalArgumentException.class) + public void setAccessDeniedHandlerNull() { + filter.setAccessDeniedHandler(null); + } +} diff --git a/web/src/test/java/org/springframework/security/web/csrf/CsrfLogoutHandlerTests.java b/web/src/test/java/org/springframework/security/web/csrf/CsrfLogoutHandlerTests.java new file mode 100644 index 0000000000..665d7ea4eb --- /dev/null +++ b/web/src/test/java/org/springframework/security/web/csrf/CsrfLogoutHandlerTests.java @@ -0,0 +1,63 @@ +/* + * 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.csrf; + +import static org.mockito.Mockito.verify; + +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.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.security.authentication.TestingAuthenticationToken; + +/** + * @author Rob Winch + * @since 3.2 + */ +@RunWith(MockitoJUnitRunner.class) +public class CsrfLogoutHandlerTests { + @Mock + private CsrfTokenRepository csrfTokenRepository; + + private MockHttpServletRequest request; + + private MockHttpServletResponse response; + + private CsrfLogoutHandler handler; + + @Before + public void setup() { + request = new MockHttpServletRequest(); + response = new MockHttpServletResponse(); + handler = new CsrfLogoutHandler(csrfTokenRepository); + } + + @Test(expected = IllegalArgumentException.class) + public void constructorNullCsrfTokenRepository() { + new CsrfLogoutHandler(null); + } + + @Test + public void logoutRemovesCsrfToken() { + handler.logout(request, response, new TestingAuthenticationToken("user", "password", "ROLE_USER")); + + verify(csrfTokenRepository).saveToken(null, request, response); + } + +} diff --git a/web/src/test/java/org/springframework/security/web/csrf/CsrfTokenTests.java b/web/src/test/java/org/springframework/security/web/csrf/CsrfTokenTests.java new file mode 100644 index 0000000000..f3997b5f28 --- /dev/null +++ b/web/src/test/java/org/springframework/security/web/csrf/CsrfTokenTests.java @@ -0,0 +1,58 @@ +/* + * 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.csrf; + +import org.junit.Test; + +/** + * @author Rob Winch + * + */ +public class CsrfTokenTests { + private final String headerName = "headerName"; + private final String parameterName = "parameterName"; + private final String tokenValue = "tokenValue"; + + @Test(expected = IllegalArgumentException.class) + public void constructorNullHeaderName() { + new CsrfToken(null,parameterName, tokenValue); + } + + @Test(expected = IllegalArgumentException.class) + public void constructorEmptyHeaderName() { + new CsrfToken("",parameterName, tokenValue); + } + + @Test(expected = IllegalArgumentException.class) + public void constructorNullParameterName() { + new CsrfToken(headerName,null, tokenValue); + } + + @Test(expected = IllegalArgumentException.class) + public void constructorEmptyParameterName() { + new CsrfToken(headerName,"", tokenValue); + } + + @Test(expected = IllegalArgumentException.class) + public void constructorNullTokenValue() { + new CsrfToken(headerName,parameterName, null); + } + + @Test(expected = IllegalArgumentException.class) + public void constructorEmptyTokenValue() { + new CsrfToken(headerName,parameterName, ""); + } +} diff --git a/web/src/test/java/org/springframework/security/web/csrf/HttpSessionCsrfTokenRepositoryTests.java b/web/src/test/java/org/springframework/security/web/csrf/HttpSessionCsrfTokenRepositoryTests.java new file mode 100644 index 0000000000..d450452fe3 --- /dev/null +++ b/web/src/test/java/org/springframework/security/web/csrf/HttpSessionCsrfTokenRepositoryTests.java @@ -0,0 +1,127 @@ +/* + * 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.csrf; + +import static org.fest.assertions.Assertions.assertThat; + +import org.junit.Before; +import org.junit.Test; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; + +/** + * @author Rob Winch + * + */ +public class HttpSessionCsrfTokenRepositoryTests { + private MockHttpServletRequest request; + + private MockHttpServletResponse response; + + private CsrfToken token; + private HttpSessionCsrfTokenRepository repo; + + @Before + public void setup() { + request = new MockHttpServletRequest(); + response = new MockHttpServletResponse(); + repo = new HttpSessionCsrfTokenRepository(); + } + + @Test + public void generateAndSaveToken() { + token = repo.generateAndSaveToken(request, response); + + assertThat(token.getParameterName()).isEqualTo("_csrf"); + assertThat(token.getToken()).isNotEmpty(); + + CsrfToken loadedToken = repo.loadToken(request); + + assertThat(loadedToken).isEqualTo(token); + } + + @Test + public void generateAndSaveTokenCustomParameter() { + String paramName = "_csrf"; + repo.setParameterName(paramName); + + token = repo.generateAndSaveToken(request, response); + + assertThat(token.getParameterName()).isEqualTo(paramName); + assertThat(token.getToken()).isNotEmpty(); + } + + @Test + public void loadTokenNull() { + assertThat(repo.loadToken(request)).isNull(); + } + + @Test + public void saveToken() { + CsrfToken tokenToSave = new CsrfToken("123", "abc", "def"); + repo.saveToken(tokenToSave, request, response); + + String attrName = request.getSession().getAttributeNames() + .nextElement(); + CsrfToken loadedToken = (CsrfToken) request.getSession().getAttribute( + attrName); + + assertThat(loadedToken).isEqualTo(tokenToSave); + } + + @Test + public void saveTokenCustomSessionAttribute() { + CsrfToken tokenToSave = new CsrfToken("123", "abc", "def"); + String sessionAttributeName = "custom"; + repo.setSessionAttributeName(sessionAttributeName); + repo.saveToken(tokenToSave, request, response); + + CsrfToken loadedToken = (CsrfToken) request.getSession().getAttribute( + sessionAttributeName); + + assertThat(loadedToken).isEqualTo(tokenToSave); + } + + @Test + public void saveTokenNullToken() { + saveToken(); + + repo.saveToken(null, request, response); + + assertThat(request.getSession().getAttributeNames().hasMoreElements()) + .isFalse(); + } + + @Test(expected = IllegalArgumentException.class) + public void setSessionAttributeNameEmpty() { + repo.setSessionAttributeName(""); + } + + @Test(expected = IllegalArgumentException.class) + public void setSessionAttributeNameNull() { + repo.setSessionAttributeName(null); + } + + @Test(expected = IllegalArgumentException.class) + public void setParameterNameEmpty() { + repo.setParameterName(""); + } + + @Test(expected = IllegalArgumentException.class) + public void setParameterNameNull() { + repo.setParameterName(null); + } +} diff --git a/web/src/test/java/org/springframework/security/web/servlet/support/csrf/CsrfRequestDataValueProcessorTests.java b/web/src/test/java/org/springframework/security/web/servlet/support/csrf/CsrfRequestDataValueProcessorTests.java new file mode 100644 index 0000000000..98ced2d165 --- /dev/null +++ b/web/src/test/java/org/springframework/security/web/servlet/support/csrf/CsrfRequestDataValueProcessorTests.java @@ -0,0 +1,79 @@ +/* + * 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.servlet.support.csrf; + +import static org.fest.assertions.Assertions.assertThat; + +import java.util.HashMap; +import java.util.Map; + +import org.junit.Before; +import org.junit.Test; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.security.web.csrf.CsrfToken; + +/** + * @author Rob Winch + * + */ +public class CsrfRequestDataValueProcessorTests { + private MockHttpServletRequest request; + + private MockHttpServletResponse response; + + private CsrfRequestDataValueProcessor processor; + + @Before + public void setup() { + request = new MockHttpServletRequest(); + response = new MockHttpServletResponse(); + processor = new CsrfRequestDataValueProcessor(); + } + + @Test + public void getExtraHiddenFieldsNoCsrfToken() { + assertThat(processor.getExtraHiddenFields(request)).isEmpty(); + } + + @Test + public void getExtraHiddenFieldsHasCsrfToken() { + CsrfToken token = new CsrfToken("1", "a", "b"); + request.setAttribute(CsrfToken.class.getName(), token); + Map expected = new HashMap(); + expected.put(token.getParameterName(),token.getToken()); + + assertThat(processor.getExtraHiddenFields(request)).isEqualTo(expected); + } + + @Test + public void processAction() { + String action = "action"; + assertThat(processor.processAction(request, action)).isEqualTo(action); + } + + @Test + public void processFormFieldValue() { + String value = "action"; + assertThat(processor.processFormFieldValue(request, "name", value, "hidden")).isEqualTo(value); + } + + @Test + public void processUrl() { + String url = "url"; + assertThat(processor.processUrl(request, url)).isEqualTo(url); + } +} diff --git a/web/web.gradle b/web/web.gradle index bd70712124..afed22fa61 100644 --- a/web/web.gradle +++ b/web/web.gradle @@ -11,6 +11,8 @@ dependencies { "org.springframework:spring-tx:$springVersion", "org.springframework:spring-web:$springVersion" + optional "org.springframework:spring-webmvc:$springVersion" + provided "org.apache.tomcat:tomcat-servlet-api:$servletApiVersion" testCompile project(':spring-security-core').sourceSets.test.output,