ソースを参照

SEC-2422: Session timeout detection with CSRF protection

Rob Winch 11 年 前
コミット
7f714ebb23

+ 78 - 6
config/src/main/java/org/springframework/security/config/annotation/web/configurers/CsrfConfigurer.java

@@ -15,14 +15,22 @@
  */
 package org.springframework.security.config.annotation.web.configurers;
 
+import java.util.LinkedHashMap;
+
+import org.springframework.security.access.AccessDeniedException;
 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.access.AccessDeniedHandlerImpl;
+import org.springframework.security.web.access.DelegatingAccessDeniedHandler;
 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.csrf.MissingCsrfTokenException;
+import org.springframework.security.web.session.InvalidSessionAccessDeniedHandler;
+import org.springframework.security.web.session.InvalidSessionStrategy;
 import org.springframework.security.web.util.matcher.RequestMatcher;
 import org.springframework.util.Assert;
 
@@ -50,6 +58,7 @@ import org.springframework.util.Assert;
  * <li>
  * {@link ExceptionHandlingConfigurer#accessDeniedHandler(AccessDeniedHandler)}
  * is used to determine how to handle CSRF attempts</li>
+ * <li>{@link InvalidSessionStrategy}</li>
  * </ul>
  *
  * @author Rob Winch
@@ -100,12 +109,9 @@ public final class CsrfConfigurer<H extends HttpSecurityBuilder<H>> extends Abst
         if(requireCsrfProtectionMatcher != null) {
             filter.setRequireCsrfProtectionMatcher(requireCsrfProtectionMatcher);
         }
-        ExceptionHandlingConfigurer<H> exceptionConfig = http.getConfigurer(ExceptionHandlingConfigurer.class);
-        if(exceptionConfig != null) {
-            AccessDeniedHandler accessDeniedHandler = exceptionConfig.getAccessDeniedHandler();
-            if(accessDeniedHandler != null) {
-                filter.setAccessDeniedHandler(accessDeniedHandler);
-            }
+        AccessDeniedHandler accessDeniedHandler = createAccessDeniedHandler(http);
+        if(accessDeniedHandler != null) {
+            filter.setAccessDeniedHandler(accessDeniedHandler);
         }
         LogoutConfigurer<H> logoutConfigurer = http.getConfigurer(LogoutConfigurer.class);
         if(logoutConfigurer != null) {
@@ -118,4 +124,70 @@ public final class CsrfConfigurer<H extends HttpSecurityBuilder<H>> extends Abst
         filter = postProcess(filter);
         http.addFilter(filter);
     }
+
+    /**
+     * Gets the default {@link AccessDeniedHandler} from the
+     * {@link ExceptionHandlingConfigurer#getAccessDeniedHandler()} or create a
+     * {@link AccessDeniedHandlerImpl} if not available.
+     *
+     * @param http the {@link HttpSecurityBuilder}
+     * @return the {@link AccessDeniedHandler}
+     */
+    @SuppressWarnings("unchecked")
+    private AccessDeniedHandler getDefaultAccessDeniedHandler(H http) {
+        ExceptionHandlingConfigurer<H> exceptionConfig = http.getConfigurer(ExceptionHandlingConfigurer.class);
+        AccessDeniedHandler handler = null;
+        if(exceptionConfig != null) {
+            handler = exceptionConfig.getAccessDeniedHandler();
+        }
+        if(handler == null) {
+            handler = new AccessDeniedHandlerImpl();
+        }
+        return handler;
+    }
+
+    /**
+     * Gets the default {@link InvalidSessionStrategy} from the
+     * {@link SessionManagementConfigurer#getInvalidSessionStrategy()} or null
+     * if not available.
+     *
+     * @param http
+     *            the {@link HttpSecurityBuilder}
+     * @return the {@link InvalidSessionStrategy}
+     */
+    @SuppressWarnings("unchecked")
+    private InvalidSessionStrategy getInvalidSessionStrategy(H http) {
+        SessionManagementConfigurer<H> sessionManagement = http.getConfigurer(SessionManagementConfigurer.class);
+        if(sessionManagement == null) {
+            return null;
+        }
+        return sessionManagement.getInvalidSessionStrategy();
+    }
+
+    /**
+     * Creates the {@link AccessDeniedHandler} from the result of
+     * {@link #getDefaultAccessDeniedHandler(HttpSecurityBuilder)} and
+     * {@link #getInvalidSessionStrategy(HttpSecurityBuilder)}. If
+     * {@link #getInvalidSessionStrategy(HttpSecurityBuilder)} is non-null, then
+     * a {@link DelegatingAccessDeniedHandler} is used in combination with
+     * {@link InvalidSessionAccessDeniedHandler} and the
+     * {@link #getDefaultAccessDeniedHandler(HttpSecurityBuilder)}. Otherwise,
+     * only {@link #getDefaultAccessDeniedHandler(HttpSecurityBuilder)} is used.
+     *
+     * @param http the {@link HttpSecurityBuilder}
+     * @return the {@link AccessDeniedHandler}
+     */
+    private AccessDeniedHandler createAccessDeniedHandler(H http) {
+        InvalidSessionStrategy invalidSessionStrategy = getInvalidSessionStrategy(http);
+        AccessDeniedHandler defaultAccessDeniedHandler = getDefaultAccessDeniedHandler(http);
+        if(invalidSessionStrategy == null) {
+            return defaultAccessDeniedHandler;
+        }
+
+        InvalidSessionAccessDeniedHandler invalidSessionDeniedHandler = new InvalidSessionAccessDeniedHandler(invalidSessionStrategy);
+        LinkedHashMap<Class<? extends AccessDeniedException>, AccessDeniedHandler> handlers =
+                new LinkedHashMap<Class<? extends AccessDeniedException>, AccessDeniedHandler>();
+        handlers.put(MissingCsrfTokenException.class, invalidSessionDeniedHandler);
+        return new DelegatingAccessDeniedHandler(handlers, defaultAccessDeniedHandler);
+    }
 }

+ 22 - 1
config/src/main/java/org/springframework/security/config/annotation/web/configurers/SessionManagementConfigurer.java

@@ -42,6 +42,7 @@ import org.springframework.security.web.context.SecurityContextRepository;
 import org.springframework.security.web.savedrequest.NullRequestCache;
 import org.springframework.security.web.savedrequest.RequestCache;
 import org.springframework.security.web.session.ConcurrentSessionFilter;
+import org.springframework.security.web.session.InvalidSessionStrategy;
 import org.springframework.security.web.session.SessionManagementFilter;
 import org.springframework.security.web.session.SimpleRedirectInvalidSessionStrategy;
 import org.springframework.util.Assert;
@@ -66,6 +67,7 @@ import org.springframework.util.Assert;
  * <li>{@link RequestCache}</li>
  * <li>{@link SecurityContextRepository}</li>
  * <li>{@link SessionManagementConfigurer}</li>
+ * <li>{@link InvalidSessionStrategy}</li>
  * </ul>
  *
  * <h2>Shared Objects Used</h2>
@@ -83,6 +85,7 @@ import org.springframework.util.Assert;
 public final class SessionManagementConfigurer<H extends HttpSecurityBuilder<H>> extends AbstractHttpConfigurer<SessionManagementConfigurer<H>,H> {
     private SessionAuthenticationStrategy sessionFixationAuthenticationStrategy = createDefaultSessionFixationProtectionStrategy();
     private SessionAuthenticationStrategy sessionAuthenticationStrategy;
+    private InvalidSessionStrategy invalidSessionStrategy;
     private List<SessionAuthenticationStrategy> sessionAuthenticationStrategies = new ArrayList<SessionAuthenticationStrategy>();
     private SessionRegistry sessionRegistry = new SessionRegistryImpl();
     private Integer maximumSessions;
@@ -365,6 +368,7 @@ public final class SessionManagementConfigurer<H extends HttpSecurityBuilder<H>>
             }
         }
         http.setSharedObject(SessionAuthenticationStrategy.class, getSessionAuthenticationStrategy());
+        http.setSharedObject(InvalidSessionStrategy.class, getInvalidSessionStrategy());
     }
 
     @Override
@@ -375,7 +379,7 @@ public final class SessionManagementConfigurer<H extends HttpSecurityBuilder<H>>
             sessionManagementFilter.setAuthenticationFailureHandler(new SimpleUrlAuthenticationFailureHandler(sessionAuthenticationErrorUrl));
         }
         if(invalidSessionUrl != null) {
-            sessionManagementFilter.setInvalidSessionStrategy(new SimpleRedirectInvalidSessionStrategy(invalidSessionUrl));
+            sessionManagementFilter.setInvalidSessionStrategy(getInvalidSessionStrategy());
         }
         AuthenticationTrustResolver trustResolver = http.getSharedObject(AuthenticationTrustResolver.class);
         if(trustResolver != null) {
@@ -391,6 +395,23 @@ public final class SessionManagementConfigurer<H extends HttpSecurityBuilder<H>>
         }
     }
 
+    /**
+     * Gets the {@link InvalidSessionStrategy} to use. If
+     * {@link #invalidSessionUrl} is null, returns null otherwise
+     * {@link SimpleRedirectInvalidSessionStrategy} is used.
+     *
+     * @return the {@link InvalidSessionStrategy} to use
+     */
+    InvalidSessionStrategy getInvalidSessionStrategy() {
+        if(invalidSessionUrl == null) {
+            return null;
+        }
+        if(invalidSessionStrategy == null) {
+            invalidSessionStrategy = new SimpleRedirectInvalidSessionStrategy(invalidSessionUrl);
+        }
+        return invalidSessionStrategy;
+    }
+
     /**
      * Gets the {@link SessionCreationPolicy}. Can not be null.
      * @return the {@link SessionCreationPolicy}

+ 56 - 3
config/src/main/java/org/springframework/security/config/http/CsrfBeanDefinitionParser.java

@@ -15,17 +15,26 @@
  */
 package org.springframework.security.config.http;
 
+import org.springframework.beans.BeanMetadataElement;
 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.ManagedMap;
 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.access.AccessDeniedException;
+import org.springframework.security.config.annotation.web.HttpSecurityBuilder;
+import org.springframework.security.web.access.AccessDeniedHandler;
+import org.springframework.security.web.access.DelegatingAccessDeniedHandler;
 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.csrf.MissingCsrfTokenException;
 import org.springframework.security.web.servlet.support.csrf.CsrfRequestDataValueProcessor;
+import org.springframework.security.web.session.InvalidSessionAccessDeniedHandler;
+import org.springframework.security.web.session.InvalidSessionStrategy;
 import org.springframework.util.ClassUtils;
 import org.springframework.util.StringUtils;
 import org.w3c.dom.Element;
@@ -44,6 +53,7 @@ public class CsrfBeanDefinitionParser implements BeanDefinitionParser {
     private static final String ATT_REPOSITORY = "token-repository-ref";
 
     private String csrfRepositoryRef;
+    private BeanDefinition csrfFilter;
 
     public BeanDefinition parse(Element element, ParserContext pc) {
         boolean webmvcPresent = ClassUtils.isPresent(DISPATCHER_SERVLET_CLASS_NAME, getClass().getClassLoader());
@@ -58,21 +68,64 @@ public class CsrfBeanDefinitionParser implements BeanDefinitionParser {
         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));
         }
 
+        BeanDefinitionBuilder builder = BeanDefinitionBuilder.rootBeanDefinition(CsrfFilter.class);
         builder.addConstructorArgReference(csrfRepositoryRef);
 
         if(StringUtils.hasText(matcherRef)) {
             builder.addPropertyReference("requireCsrfProtectionMatcher", matcherRef);
         }
 
-        return builder.getBeanDefinition();
+        csrfFilter = builder.getBeanDefinition();
+        return csrfFilter;
+    }
+
+    /**
+     * Populate the AccessDeniedHandler on the {@link CsrfFilter}
+     *
+     * @param invalidSessionStrategy the {@link InvalidSessionStrategy} to use
+     * @param defaultDeniedHandler the {@link AccessDeniedHandler} to use
+     */
+    void initAccessDeniedHandler(BeanDefinition invalidSessionStrategy, BeanMetadataElement defaultDeniedHandler) {
+        BeanMetadataElement accessDeniedHandler = createAccessDeniedHandler(invalidSessionStrategy, defaultDeniedHandler);
+        csrfFilter.getPropertyValues().addPropertyValue("accessDeniedHandler", accessDeniedHandler);
+    }
+
+    /**
+     * Creates the {@link AccessDeniedHandler} from the result of
+     * {@link #getDefaultAccessDeniedHandler(HttpSecurityBuilder)} and
+     * {@link #getInvalidSessionStrategy(HttpSecurityBuilder)}. If
+     * {@link #getInvalidSessionStrategy(HttpSecurityBuilder)} is non-null, then
+     * a {@link DelegatingAccessDeniedHandler} is used in combination with
+     * {@link InvalidSessionAccessDeniedHandler} and the
+     * {@link #getDefaultAccessDeniedHandler(HttpSecurityBuilder)}. Otherwise,
+     * only {@link #getDefaultAccessDeniedHandler(HttpSecurityBuilder)} is used.
+     *
+     * @param invalidSessionStrategy the {@link InvalidSessionStrategy} to use
+     * @param defaultDeniedHandler the {@link AccessDeniedHandler} to use
+     *
+     * @return the {@link BeanMetadataElement} that is the {@link AccessDeniedHandler} to populate on the {@link CsrfFilter}
+     */
+    private BeanMetadataElement createAccessDeniedHandler(BeanDefinition invalidSessionStrategy, BeanMetadataElement defaultDeniedHandler) {
+        if(invalidSessionStrategy == null) {
+            return defaultDeniedHandler;
+        }
+        ManagedMap<Class<? extends AccessDeniedException>,BeanDefinition> handlers =
+                new ManagedMap<Class<? extends AccessDeniedException>, BeanDefinition>();
+        BeanDefinitionBuilder invalidSessionHandlerBldr = BeanDefinitionBuilder.rootBeanDefinition(InvalidSessionAccessDeniedHandler.class);
+        invalidSessionHandlerBldr.addConstructorArgValue(invalidSessionStrategy);
+        handlers.put(MissingCsrfTokenException.class, invalidSessionHandlerBldr.getBeanDefinition());
+
+        BeanDefinitionBuilder deniedBldr = BeanDefinitionBuilder.rootBeanDefinition(DelegatingAccessDeniedHandler.class);
+        deniedBldr.addConstructorArgValue(handlers);
+        deniedBldr.addConstructorArgValue(defaultDeniedHandler);
+
+        return deniedBldr.getBeanDefinition();
     }
 
     BeanDefinition getCsrfAuthenticationStrategy() {

+ 15 - 6
config/src/main/java/org/springframework/security/config/http/HttpConfigurationBuilder.java

@@ -130,6 +130,10 @@ class HttpConfigurationBuilder {
     private BeanMetadataElement csrfLogoutHandler;
     private BeanMetadataElement csrfAuthStrategy;
 
+    private CsrfBeanDefinitionParser csrfParser;
+
+    private BeanDefinition invalidSession;
+
     public HttpConfigurationBuilder(Element element, ParserContext pc,
             BeanReference portMapper, BeanReference portResolver, BeanReference authenticationManager) {
         this.httpElt = element;
@@ -200,8 +204,8 @@ class HttpConfigurationBuilder {
     }
 
     void setAccessDeniedHandler(BeanMetadataElement accessDeniedHandler) {
-        if(csrfFilter != null) {
-            csrfFilter.getPropertyValues().add("accessDeniedHandler", accessDeniedHandler);
+        if(csrfParser != null ) {
+            csrfParser.initAccessDeniedHandler(this.invalidSession, accessDeniedHandler);
         }
     }
 
@@ -381,7 +385,10 @@ class HttpConfigurationBuilder {
         }
 
         if (StringUtils.hasText(invalidSessionUrl)) {
-            sessionMgmtFilter.addPropertyValue("invalidSessionStrategy", new SimpleRedirectInvalidSessionStrategy(invalidSessionUrl));
+            BeanDefinitionBuilder invalidSessionBldr = BeanDefinitionBuilder.rootBeanDefinition(SimpleRedirectInvalidSessionStrategy.class);
+            invalidSessionBldr.addConstructorArgValue(invalidSessionUrl);
+            invalidSession = invalidSessionBldr.getBeanDefinition();
+            sessionMgmtFilter.addPropertyValue("invalidSessionStrategy", invalidSession);
         }
 
         sessionMgmtFilter.addConstructorArgReference(sessionAuthStratRef);
@@ -637,14 +644,16 @@ class HttpConfigurationBuilder {
 
     }
 
-    private void createCsrfFilter() {
+    private CsrfBeanDefinitionParser createCsrfFilter() {
         Element elmt = DomUtils.getChildElementByTagName(httpElt, Elements.CSRF);
         if (elmt != null) {
-            CsrfBeanDefinitionParser csrfParser = new CsrfBeanDefinitionParser();
-            this.csrfFilter = csrfParser.parse(elmt, pc);
+            csrfParser = new CsrfBeanDefinitionParser();
+            csrfFilter = csrfParser.parse(elmt, pc);
             this.csrfAuthStrategy = csrfParser.getCsrfAuthenticationStrategy();
             this.csrfLogoutHandler = csrfParser.getCsrfLogoutHandler();
+            return csrfParser;
         }
+        return null;
     }
 
     BeanMetadataElement getCsrfLogoutHandler() {

+ 38 - 6
config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/CsrfConfigurerTests.groovy

@@ -18,19 +18,20 @@ package org.springframework.security.config.annotation.web.configurers
 import javax.servlet.http.HttpServletResponse
 
 import org.springframework.context.annotation.Configuration
+import org.springframework.mock.web.MockHttpServletRequest
+import org.springframework.mock.web.MockHttpServletResponse
 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.matcher.RequestMatcher;
-import org.springframework.web.servlet.support.RequestDataValueProcessor;
+import org.springframework.security.web.csrf.CsrfFilter
+import org.springframework.security.web.csrf.CsrfTokenRepository
+import org.springframework.security.web.util.matcher.RequestMatcher
+import org.springframework.web.servlet.support.RequestDataValueProcessor
 
-import spock.lang.Unroll;
+import spock.lang.Unroll
 
 /**
  *
@@ -100,6 +101,37 @@ class CsrfConfigurerTests extends BaseSpringSpec {
         }
     }
 
+    def "SEC-2422: csrf expire CSRF token and session-management invalid-session-url"() {
+        setup:
+            loadConfig(InvalidSessionUrlConfig)
+            request.session.clearAttributes()
+            request.setParameter("_csrf","abc")
+            request.method = "POST"
+        when: "No existing expected CsrfToken (session times out) and a POST"
+            springSecurityFilterChain.doFilter(request,response,chain)
+        then: "sent to the session timeout page page"
+            response.status == HttpServletResponse.SC_MOVED_TEMPORARILY
+            response.redirectedUrl == "/error/sessionError"
+        when: "Existing expected CsrfToken and a POST (invalid token provided)"
+            response = new MockHttpServletResponse()
+            request = new MockHttpServletRequest(session: request.session, method:'POST')
+            springSecurityFilterChain.doFilter(request,response,chain)
+        then: "Access Denied occurs"
+            response.status == HttpServletResponse.SC_FORBIDDEN
+    }
+
+    @Configuration
+    @EnableWebSecurity
+    static class InvalidSessionUrlConfig extends WebSecurityConfigurerAdapter {
+        @Override
+        protected void configure(HttpSecurity http) throws Exception {
+            http
+                .csrf().and()
+                .sessionManagement()
+                    .invalidSessionUrl("/error/sessionError")
+        }
+    }
+
     def "csrf requireCsrfProtectionMatcher"() {
         setup:
             RequireCsrfProtectionMatcherConfig.matcher = Mock(RequestMatcher)

+ 22 - 0
config/src/test/groovy/org/springframework/security/config/http/CsrfConfigTests.groovy

@@ -174,6 +174,28 @@ class CsrfConfigTests extends AbstractHttpConfigTests {
             response.redirectedUrl == "http://localhost/some-url"
     }
 
+    def "SEC-2422: csrf expire CSRF token and session-management invalid-session-url"() {
+        setup:
+            httpAutoConfig {
+                'csrf'()
+                'session-management'('invalid-session-url': '/error/sessionError')
+            }
+            createAppContext()
+            request.setParameter("_csrf","abc")
+            request.method = "POST"
+        when: "No existing expected CsrfToken (session times out) and a POST"
+            springSecurityFilterChain.doFilter(request,response,chain)
+        then: "sent to the session timeout page page"
+            response.status == HttpServletResponse.SC_MOVED_TEMPORARILY
+            response.redirectedUrl == "/error/sessionError"
+        when: "Existing expected CsrfToken and a POST (invalid token provided)"
+            response = new MockHttpServletResponse()
+            request = new MockHttpServletRequest(session: request.session, method:'POST')
+            springSecurityFilterChain.doFilter(request,response,chain)
+        then: "Access Denied occurs"
+            response.status == HttpServletResponse.SC_FORBIDDEN
+    }
+
     def "csrf requireCsrfProtectionMatcher"() {
         setup:
             httpAutoConfig {

+ 81 - 0
web/src/main/java/org/springframework/security/web/access/DelegatingAccessDeniedHandler.java

@@ -0,0 +1,81 @@
+/*
+ * 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.access;
+
+import java.io.IOException;
+import java.util.LinkedHashMap;
+import java.util.Map.Entry;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.springframework.security.access.AccessDeniedException;
+import org.springframework.util.Assert;
+
+/**
+ * An {@link AccessDeniedHandler} that delegates to other
+ * {@link AccessDeniedHandler} instances based upon the type of
+ * {@link AccessDeniedException} passed into
+ * {@link #handle(HttpServletRequest, HttpServletResponse, AccessDeniedException)}.
+ *
+ * @author Rob Winch
+ * @since 3.2
+ *
+ */
+public final class DelegatingAccessDeniedHandler implements AccessDeniedHandler {
+    private final LinkedHashMap<Class<? extends AccessDeniedException>, AccessDeniedHandler> handlers;
+
+    private final AccessDeniedHandler defaultHander;
+
+    /**
+     * Creates a new instance
+     *
+     * @param handlers
+     *            a map of the {@link AccessDeniedException} class to the
+     *            {@link AccessDeniedHandler} that should be used. Each is
+     *            considered in the order they are specified and only the first
+     *            {@link AccessDeniedHandler} is ued.
+     * @param defaultHander
+     *            the default {@link AccessDeniedHandler} that should be used if
+     *            none of the handlers matches.
+     */
+    public DelegatingAccessDeniedHandler(
+            LinkedHashMap<Class<? extends AccessDeniedException>, AccessDeniedHandler> handlers,
+            AccessDeniedHandler defaultHander) {
+        Assert.notEmpty(handlers, "handlers cannot be null or empty");
+        Assert.notNull(defaultHander, "defaultHandler cannot be null");
+        this.handlers = handlers;
+        this.defaultHander = defaultHander;
+    }
+
+
+    public void handle(HttpServletRequest request,
+            HttpServletResponse response,
+            AccessDeniedException accessDeniedException) throws IOException,
+            ServletException {
+        for(Entry<Class<? extends AccessDeniedException>, AccessDeniedHandler>  entry : handlers.entrySet()) {
+            Class<? extends AccessDeniedException> handlerClass = entry.getKey();
+            if(handlerClass.isAssignableFrom(accessDeniedException.getClass())) {
+                AccessDeniedHandler handler = entry.getValue();
+                handler.handle(request, response, accessDeniedException);
+                return;
+            }
+        }
+        defaultHander.handle(request, response, accessDeniedException);
+    }
+
+}

+ 32 - 0
web/src/main/java/org/springframework/security/web/csrf/CsrfException.java

@@ -0,0 +1,32 @@
+/*
+ * 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 CsrfException extends AccessDeniedException {
+
+    public CsrfException(String message) {
+        super(message);
+    }
+}

+ 7 - 2
web/src/main/java/org/springframework/security/web/csrf/CsrfFilter.java

@@ -73,7 +73,8 @@ public final class CsrfFilter extends OncePerRequestFilter {
             HttpServletResponse response, FilterChain filterChain)
             throws ServletException, IOException {
         CsrfToken csrfToken = tokenRepository.loadToken(request);
-        if(csrfToken == null) {
+        final boolean missingToken = csrfToken == null;
+        if(missingToken) {
             CsrfToken generatedToken = tokenRepository.generateToken(request);
             csrfToken = new SaveOnAccessCsrfToken(tokenRepository, request, response, generatedToken);
         }
@@ -93,7 +94,11 @@ public final class CsrfFilter extends OncePerRequestFilter {
             if(logger.isDebugEnabled()) {
                 logger.debug("Invalid CSRF token found for " + UrlUtils.buildFullRequestUrl(request));
             }
-            accessDeniedHandler.handle(request, response, new InvalidCsrfTokenException(csrfToken, actualToken));
+            if(missingToken) {
+                accessDeniedHandler.handle(request, response, new MissingCsrfTokenException(actualToken));
+            } else {
+                accessDeniedHandler.handle(request, response, new InvalidCsrfTokenException(csrfToken, actualToken));
+            }
             return;
         }
 

+ 5 - 6
web/src/main/java/org/springframework/security/web/csrf/InvalidCsrfTokenException.java

@@ -15,17 +15,17 @@
  */
 package org.springframework.security.web.csrf;
 
-import org.springframework.security.access.AccessDeniedException;
-
+import javax.servlet.http.HttpServletRequest;
 
 /**
- * Thrown when an invalid or missing {@link CsrfToken} is found in the HttpServletRequest
+ * Thrown when an expected {@link CsrfToken} exists, but it does not match the
+ * value present on the {@link HttpServletRequest}
  *
  * @author Rob Winch
  * @since 3.2
  */
 @SuppressWarnings("serial")
-public class InvalidCsrfTokenException extends AccessDeniedException {
+public class InvalidCsrfTokenException extends CsrfException {
 
     /**
      * @param msg
@@ -36,5 +36,4 @@ public class InvalidCsrfTokenException extends AccessDeniedException {
                 + expectedAccessToken.getParameterName() + "' or header '"
                 + expectedAccessToken.getHeaderName() + "'.");
     }
-
-}
+}

+ 30 - 0
web/src/main/java/org/springframework/security/web/csrf/MissingCsrfTokenException.java

@@ -0,0 +1,30 @@
+/*
+ * 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;
+
+/**
+ * Thrown when no expected {@link CsrfToken} is found but is required.
+ *
+ * @author Rob Winch
+ * @since 3.2
+ */
+@SuppressWarnings("serial")
+public class MissingCsrfTokenException extends CsrfException {
+
+    public MissingCsrfTokenException(String actualToken) {
+        super("Expected CSRF token not found. Has your session expired?");
+    }
+}

+ 53 - 0
web/src/main/java/org/springframework/security/web/session/InvalidSessionAccessDeniedHandler.java

@@ -0,0 +1,53 @@
+/*
+ * 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.session;
+
+import java.io.IOException;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.springframework.security.access.AccessDeniedException;
+import org.springframework.security.web.access.AccessDeniedHandler;
+import org.springframework.util.Assert;
+
+/**
+ * An adapter of {@link InvalidSessionStrategy} to {@link AccessDeniedHandler}
+ *
+ * @author Rob Winch
+ * @since 3.2
+ */
+public final class InvalidSessionAccessDeniedHandler implements AccessDeniedHandler {
+    private final InvalidSessionStrategy invalidSessionStrategy;
+
+    /**
+     * Creates a new instance
+     * @param invalidSessionStrategy the {@link InvalidSessionStrategy} to delegate to
+     */
+    public InvalidSessionAccessDeniedHandler(
+            InvalidSessionStrategy invalidSessionStrategy) {
+        Assert.notNull(invalidSessionStrategy, "invalidSessionStrategy cannot be null");
+        this.invalidSessionStrategy = invalidSessionStrategy;
+    }
+
+    public void handle(HttpServletRequest request,
+            HttpServletResponse response,
+            AccessDeniedException accessDeniedException) throws IOException,
+            ServletException {
+        invalidSessionStrategy.onInvalidSessionDetected(request, response);
+    }
+}

+ 84 - 0
web/src/test/java/org/springframework/security/web/access/DelegatingAccessDeniedHandlerTests.java

@@ -0,0 +1,84 @@
+/*
+ * 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.access;
+
+import static org.mockito.Matchers.any;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+
+import java.util.LinkedHashMap;
+
+import javax.servlet.http.HttpServletRequest;
+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.security.access.AccessDeniedException;
+import org.springframework.security.web.csrf.CsrfException;
+import org.springframework.security.web.csrf.InvalidCsrfTokenException;
+import org.springframework.security.web.csrf.MissingCsrfTokenException;
+
+@RunWith(MockitoJUnitRunner.class)
+public class DelegatingAccessDeniedHandlerTests {
+    @Mock
+    private AccessDeniedHandler handler1;
+    @Mock
+    private AccessDeniedHandler handler2;
+    @Mock
+    private AccessDeniedHandler handler3;
+    @Mock
+    private HttpServletRequest request;
+    @Mock
+    private HttpServletResponse response;
+
+    private LinkedHashMap<Class<? extends AccessDeniedException>,AccessDeniedHandler> handlers;
+
+    private DelegatingAccessDeniedHandler handler;
+
+    @Before
+    public void setup() {
+            handlers = new LinkedHashMap<Class<? extends AccessDeniedException>, AccessDeniedHandler>();
+    }
+
+    @Test
+    public void moreSpecificDoesNotInvokeLessSpecific() throws Exception {
+        handlers.put(CsrfException.class, handler1);
+        handler = new DelegatingAccessDeniedHandler(handlers, handler3);
+
+        AccessDeniedException accessDeniedException = new AccessDeniedException("");
+        handler.handle(request, response, accessDeniedException);
+
+        verify(handler1,never()).handle(any(HttpServletRequest.class), any(HttpServletResponse.class), any(AccessDeniedException.class));
+        verify(handler3).handle(request, response, accessDeniedException);
+    }
+
+    @Test
+    public void matchesDoesNotInvokeDefault() throws Exception {
+        handlers.put(InvalidCsrfTokenException.class, handler1);
+        handlers.put(MissingCsrfTokenException.class, handler2);
+        handler = new DelegatingAccessDeniedHandler(handlers, handler3);
+
+        AccessDeniedException accessDeniedException = new MissingCsrfTokenException("123");
+        handler.handle(request, response, accessDeniedException);
+
+        verify(handler1,never()).handle(any(HttpServletRequest.class), any(HttpServletResponse.class), any(AccessDeniedException.class));
+        verify(handler2).handle(request, response, accessDeniedException);
+        verify(handler3,never()).handle(any(HttpServletRequest.class), any(HttpServletResponse.class), any(AccessDeniedException.class));
+    }
+}