ソースを参照

SEC-965: Added support for CAS proxy ticket authentication on any URL

Rob Winch 14 年 前
コミット
a76a947b12

+ 20 - 1
cas/src/main/java/org/springframework/security/cas/ServiceProperties.java

@@ -38,6 +38,8 @@ public class ServiceProperties implements InitializingBean {
 
     private String service;
 
+    private boolean authenticateAllArtifacts;
+
     private boolean sendRenew = false;
 
     private String artifactParameter = DEFAULT_CAS_ARTIFACT_PARAMETER;
@@ -47,7 +49,9 @@ public class ServiceProperties implements InitializingBean {
     //~ Methods ========================================================================================================
 
     public void afterPropertiesSet() throws Exception {
-        Assert.hasLength(this.service, "service must be specified.");
+        if(!authenticateAllArtifacts) {
+            Assert.hasLength(this.service, "service must be specified unless authenticateAllArtifacts is true.");
+        }
         Assert.hasLength(this.artifactParameter, "artifactParameter cannot be empty.");
         Assert.hasLength(this.serviceParameter, "serviceParameter cannot be empty.");
     }
@@ -115,4 +119,19 @@ public class ServiceProperties implements InitializingBean {
     public final void setServiceParameter(final String serviceParameter) {
         this.serviceParameter = serviceParameter;
     }
+
+    public final boolean isAuthenticateAllArtifacts() {
+        return authenticateAllArtifacts;
+    }
+
+    /**
+     * If true, then any non-null artifact (ticket) should be authenticated.
+     * Additionally, the service will be determined dynamically in order to
+     * ensure the service matches the expected value for this artifact.
+     *
+     * @param authenticateAllArtifacts
+     */
+    public final void setAuthenticateAllArtifacts(final boolean authenticateAllArtifacts) {
+        this.authenticateAllArtifacts = authenticateAllArtifacts;
+    }
 }

+ 33 - 2
cas/src/main/java/org/springframework/security/cas/authentication/CasAuthenticationProvider.java

@@ -15,6 +15,8 @@
 
 package org.springframework.security.cas.authentication;
 
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
 import org.jasig.cas.client.validation.Assertion;
 import org.jasig.cas.client.validation.TicketValidationException;
 import org.jasig.cas.client.validation.TicketValidator;
@@ -28,6 +30,7 @@ import org.springframework.security.authentication.BadCredentialsException;
 import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
 import org.springframework.security.cas.ServiceProperties;
 import org.springframework.security.cas.web.CasAuthenticationFilter;
+import org.springframework.security.cas.web.authentication.ServiceAuthenticationDetails;
 import org.springframework.security.core.Authentication;
 import org.springframework.security.core.AuthenticationException;
 import org.springframework.security.core.SpringSecurityMessageSource;
@@ -50,6 +53,9 @@ import org.springframework.util.Assert;
  * @author Scott Battaglia
  */
 public class CasAuthenticationProvider implements AuthenticationProvider, InitializingBean, MessageSourceAware {
+    //~ Static fields/initializers =====================================================================================
+
+    private static final Log logger = LogFactory.getLog(CasAuthenticationProvider.class);
 
     //~ Instance fields ================================================================================================
 
@@ -72,7 +78,6 @@ public class CasAuthenticationProvider implements AuthenticationProvider, Initia
         Assert.notNull(this.statelessTicketCache, "A statelessTicketCache must be set");
         Assert.hasText(this.key, "A Key is required so CasAuthenticationProvider can identify tokens it previously authenticated");
         Assert.notNull(this.messages, "A message source must be set");
-        Assert.notNull(this.serviceProperties, "serviceProperties is a required field.");
     }
 
     public Authentication authenticate(Authentication authentication) throws AuthenticationException {
@@ -132,7 +137,7 @@ public class CasAuthenticationProvider implements AuthenticationProvider, Initia
 
     private CasAuthenticationToken authenticateNow(final Authentication authentication) throws AuthenticationException {
         try {
-            final Assertion assertion = this.ticketValidator.validate(authentication.getCredentials().toString(), serviceProperties.getService());
+            final Assertion assertion = this.ticketValidator.validate(authentication.getCredentials().toString(), getServiceUrl(authentication));
             final UserDetails userDetails = loadUserByAssertion(assertion);
             userDetailsChecker.check(userDetails);
             return new CasAuthenticationToken(this.key, userDetails, authentication.getCredentials(),
@@ -142,6 +147,32 @@ public class CasAuthenticationProvider implements AuthenticationProvider, Initia
         }
     }
 
+    /**
+     * Gets the serviceUrl. If the {@link Authentication#getDetails()} is an
+     * instance of {@link ServiceAuthenticationDetails}, then
+     * {@link ServiceAuthenticationDetails#getServiceUrl()} is used. Otherwise,
+     * the {@link ServiceProperties#getService()} is used.
+     *
+     * @param authentication
+     * @return
+     */
+    private String getServiceUrl(Authentication authentication) {
+        String serviceUrl;
+        if(authentication.getDetails() instanceof ServiceAuthenticationDetails) {
+            serviceUrl = ((ServiceAuthenticationDetails)authentication.getDetails()).getServiceUrl();
+        }else if(serviceProperties == null){
+            throw new IllegalStateException("serviceProperties cannot be null unless Authentication.getDetails() implements ServiceAuthenticationDetails.");
+        }else if(serviceProperties.getService() == null){
+            throw new IllegalStateException("serviceProperties.getService() cannot be null unless Authentication.getDetails() implements ServiceAuthenticationDetails.");
+        }else {
+            serviceUrl = serviceProperties.getService();
+        }
+        if(logger.isDebugEnabled()) {
+            logger.debug("serviceUrl = "+serviceUrl);
+        }
+        return serviceUrl;
+    }
+
     /**
      * Template method for retrieving the UserDetails based on the assertion.  Default is to call configured userDetailsService and pass the username.  Deployers
      * can override this method and retrieve the user based on any criteria they desire.

+ 1 - 0
cas/src/main/java/org/springframework/security/cas/web/CasAuthenticationEntryPoint.java

@@ -65,6 +65,7 @@ public class CasAuthenticationEntryPoint implements AuthenticationEntryPoint, In
     public void afterPropertiesSet() throws Exception {
         Assert.hasLength(this.loginUrl, "loginUrl must be specified");
         Assert.notNull(this.serviceProperties, "serviceProperties must be specified");
+        Assert.notNull(this.serviceProperties.getService(),"serviceProperties.getService() cannot be null.");
     }
 
     public final void commence(final HttpServletRequest servletRequest, final HttpServletResponse response,

+ 268 - 19
cas/src/main/java/org/springframework/security/cas/web/CasAuthenticationFilter.java

@@ -17,41 +17,139 @@ package org.springframework.security.cas.web;
 
 import java.io.IOException;
 
+import javax.servlet.FilterChain;
+import javax.servlet.ServletException;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 
 import org.jasig.cas.client.proxy.ProxyGrantingTicketStorage;
 import org.jasig.cas.client.util.CommonUtils;
 import org.jasig.cas.client.validation.TicketValidator;
+import org.springframework.security.authentication.AnonymousAuthenticationToken;
+import org.springframework.security.authentication.AuthenticationDetailsSource;
 import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.authentication.event.InteractiveAuthenticationSuccessEvent;
 import org.springframework.security.cas.ServiceProperties;
+import org.springframework.security.cas.web.authentication.ServiceAuthenticationDetails;
+import org.springframework.security.cas.web.authentication.ServiceAuthenticationDetailsSource;
 import org.springframework.security.core.Authentication;
 import org.springframework.security.core.AuthenticationException;
+import org.springframework.security.core.context.SecurityContextHolder;
 import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
-
+import org.springframework.security.web.authentication.AuthenticationFailureHandler;
+import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
+import org.springframework.util.Assert;
 
 /**
- * Processes a CAS service ticket.
+ * Processes a CAS service ticket, obtains proxy granting tickets, and processes proxy tickets.
+ * <h2>Service Tickets</h2>
  * <p>
  * A service ticket consists of an opaque ticket string. It arrives at this filter by the user's browser successfully
  * authenticating using CAS, and then receiving a HTTP redirect to a <code>service</code>. The opaque ticket string is
- * presented in the <code>ticket</code> request parameter. This filter monitors the <code>service</code> URL so it can
- * receive the service ticket and process it. The CAS server knows which <code>service</code> URL to use via the
- * {@link ServiceProperties#getService()} method.
+ * presented in the <code>ticket</code> request parameter.
+ * <p>
+ * This filter monitors the <code>service</code> URL so it can
+ * receive the service ticket and process it. By default this filter processes the URL <tt>/j_spring_cas_security_check</tt>.
+ * When processing this URL, the value of {@link ServiceProperties#getService()} is used as the <tt>service</tt> when validating
+ * the <code>ticket</code>. This means that it is important that {@link ServiceProperties#getService()} specifies the same value
+ * as the <tt>filterProcessesUrl</tt>.
  * <p>
  * Processing the service ticket involves creating a <code>UsernamePasswordAuthenticationToken</code> which
  * uses {@link #CAS_STATEFUL_IDENTIFIER} for the <code>principal</code> and the opaque ticket string as the
  * <code>credentials</code>.
+ * <h2>Obtaining Proxy Granting Tickets</h2>
+ * <p>
+ * If specified, the filter can also monitor the <code>proxyReceptorUrl</code>. The filter will respond to requests matching
+ * this url so that the CAS Server can provide a PGT to the filter. Note that in addition to the <code>proxyReceptorUrl</code> a non-null
+ * <code>proxyGrantingTicketStorage</code> must be provided in order for the filter to respond to proxy receptor requests. By configuring
+ * a shared {@link ProxyGrantingTicketStorage} between the {@link TicketValidator} and the CasAuthenticationFilter one can have the
+ * CasAuthenticationFilter handle the proxying requirements for CAS.
+ * <h2>Proxy Tickets</h2>
+ * <p>
+ * The filter can process tickets present on any url. This is useful when wanting to process proxy tickets. In order for proxy
+ * tickets to get processed {@link ServiceProperties#isAuthenticateAllArtifacts()} must return <code>true</code>. Additionally,
+ * if the request is already authenticated, authentication will <b>not</b> occur. Last, {@link AuthenticationDetailsSource#buildDetails(Object)}
+ * must return a {@link ServiceAuthenticationDetails}. This can be accomplished using the {@link ServiceAuthenticationDetailsSource}.
+ * In this case {@link ServiceAuthenticationDetails#getServiceUrl()} will be used for the service url.
+ * <p>
+ * Processing the proxy ticket involves creating a <code>UsernamePasswordAuthenticationToken</code> which
+ * uses {@link #CAS_STATELESS_IDENTIFIER} for the <code>principal</code> and the opaque ticket string as the
+ * <code>credentials</code>. When a proxy ticket is successfully authenticated, the FilterChain continues and the
+ * <code>authenticationSuccessHandler</code> is not used.
+ * <h2>Notes about the <code>AuthenticationManager</code></h2>
  * <p>
  * The configured <code>AuthenticationManager</code> is expected to provide a provider that can recognise
  * <code>UsernamePasswordAuthenticationToken</code>s containing this special <code>principal</code> name, and process
- * them accordingly by validation with the CAS server.
- * <p>
- * By configuring a shared {@link ProxyGrantingTicketStorage} between the {@link TicketValidator} and the
- * CasAuthenticationFilter one can have the CasAuthenticationFilter handle the proxying requirements for CAS. In addition, the
- * URI endpoint for the proxying would also need to be configured (i.e. the part after protocol, hostname, and port).
+ * them accordingly by validation with the CAS server. Additionally, it should be capable of using the result of
+ * {@link ServiceAuthenticationDetails#getServiceUrl()} as the service when validating the ticket.
+ * <h2>Example Configuration</h2>
  * <p>
- * By default this filter processes the URL <tt>/j_spring_cas_security_check</tt>.
+ * An example configuration that supports service tickets, obtaining proxy granting tickets, and proxy tickets is
+ * illustrated below:
+ *
+ * <pre>
+ * &lt;b:bean id=&quot;serviceProperties&quot;
+ *     class=&quot;org.springframework.security.cas.ServiceProperties&quot;
+ *     p:service=&quot;https://service.example.com/cas-sample/j_spring_cas_security_check&quot;
+ *     p:authenticateAllArtifacts=&quot;true&quot;/&gt;
+ * &lt;b:bean id=&quot;casEntryPoint&quot;
+ *     class=&quot;org.springframework.security.cas.web.CasAuthenticationEntryPoint&quot;
+ *     p:serviceProperties-ref=&quot;serviceProperties&quot; p:loginUrl=&quot;https://login.example.org/cas/login&quot; /&gt;
+ * &lt;b:bean id=&quot;casFilter&quot;
+ *     class=&quot;org.springframework.security.cas.web.CasAuthenticationFilter&quot;
+ *     p:authenticationManager-ref=&quot;authManager&quot;
+ *     p:serviceProperties-ref=&quot;serviceProperties&quot;
+ *     p:proxyGrantingTicketStorage-ref=&quot;pgtStorage&quot;
+ *     p:proxyReceptorUrl=&quot;/j_spring_cas_security_proxyreceptor&quot;&gt;
+ *     &lt;b:property name=&quot;authenticationDetailsSource&quot;&gt;
+ *         &lt;b:bean class=&quot;org.springframework.security.cas.web.authentication.ServiceAuthenticationDetailsSource&quot;/&gt;
+ *     &lt;/b:property&gt;
+ *     &lt;b:property name=&quot;authenticationFailureHandler&quot;&gt;
+ *         &lt;b:bean class=&quot;org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler&quot;
+ *             p:defaultFailureUrl=&quot;/casfailed.jsp&quot;/&gt;
+ *     &lt;/b:property&gt;
+ * &lt;/b:bean&gt;
+ * &lt;!--
+ *     NOTE: In a real application you should not use an in memory implementation. You will also want
+ *           to ensure to clean up expired tickets by calling ProxyGrantingTicketStorage.cleanup()
+ *  --&gt;
+ * &lt;b:bean id=&quot;pgtStorage&quot; class=&quot;org.jasig.cas.client.proxy.ProxyGrantingTicketStorageImpl&quot;/&gt;
+ * &lt;b:bean id=&quot;casAuthProvider&quot; class=&quot;org.springframework.security.cas.authentication.CasAuthenticationProvider&quot;
+ *     p:serviceProperties-ref=&quot;serviceProperties&quot;
+ *     p:key=&quot;casAuthProviderKey&quot;&gt;
+ *     &lt;b:property name=&quot;authenticationUserDetailsService&quot;&gt;
+ *         &lt;b:bean
+ *             class=&quot;org.springframework.security.core.userdetails.UserDetailsByNameServiceWrapper&quot;&gt;
+ *             &lt;b:constructor-arg ref=&quot;userService&quot; /&gt;
+ *         &lt;/b:bean&gt;
+ *     &lt;/b:property&gt;
+ *     &lt;b:property name=&quot;ticketValidator&quot;&gt;
+ *         &lt;b:bean
+ *             class=&quot;org.jasig.cas.client.validation.Cas20ProxyTicketValidator&quot;
+ *             p:acceptAnyProxy=&quot;true&quot;
+ *             p:proxyCallbackUrl=&quot;https://service.example.com/cas-sample/j_spring_cas_security_proxyreceptor&quot;
+ *             p:proxyGrantingTicketStorage-ref=&quot;pgtStorage&quot;&gt;
+ *             &lt;b:constructor-arg value=&quot;https://login.example.org/cas&quot; /&gt;
+ *         &lt;/b:bean&gt;
+ *     &lt;/b:property&gt;
+ *     &lt;b:property name=&quot;statelessTicketCache&quot;&gt;
+ *         &lt;b:bean class=&quot;org.springframework.security.cas.authentication.EhCacheBasedTicketCache&quot;&gt;
+ *             &lt;b:property name=&quot;cache&quot;&gt;
+ *                 &lt;b:bean class=&quot;net.sf.ehcache.Cache&quot;
+ *                   init-method=&quot;initialise&quot;
+ *                   destroy-method=&quot;dispose&quot;&gt;
+ *                     &lt;b:constructor-arg value=&quot;casTickets&quot;/&gt;
+ *                     &lt;b:constructor-arg value=&quot;50&quot;/&gt;
+ *                     &lt;b:constructor-arg value=&quot;true&quot;/&gt;
+ *                     &lt;b:constructor-arg value=&quot;false&quot;/&gt;
+ *                     &lt;b:constructor-arg value=&quot;3600&quot;/&gt;
+ *                     &lt;b:constructor-arg value=&quot;900&quot;/&gt;
+ *                 &lt;/b:bean&gt;
+ *             &lt;/b:property&gt;
+ *         &lt;/b:bean&gt;
+ *     &lt;/b:property&gt;
+ * &lt;/b:bean&gt;
+ * </pre>
  *
  * @author Ben Alex
  * @author Rob Winch
@@ -81,26 +179,59 @@ public class CasAuthenticationFilter extends AbstractAuthenticationProcessingFil
 
     private String artifactParameter = ServiceProperties.DEFAULT_CAS_ARTIFACT_PARAMETER;
 
+    private boolean authenticateAllArtifacts;
+
+    private AuthenticationFailureHandler proxyFailureHandler = new SimpleUrlAuthenticationFailureHandler();
+
     //~ Constructors ===================================================================================================
 
     public CasAuthenticationFilter() {
         super("/j_spring_cas_security_check");
+        setAuthenticationFailureHandler(new SimpleUrlAuthenticationFailureHandler());
     }
 
     //~ Methods ========================================================================================================
 
+    @Override
+    protected final void successfulAuthentication(HttpServletRequest request,
+            HttpServletResponse response, FilterChain chain, Authentication authResult)
+            throws IOException, ServletException {
+        boolean continueFilterChain = proxyTicketRequest(serviceTicketRequest(request, response),request);
+        if(!continueFilterChain) {
+            super.successfulAuthentication(request, response, chain, authResult);
+            return;
+        }
+
+        if (logger.isDebugEnabled()) {
+            logger.debug("Authentication success. Updating SecurityContextHolder to contain: " + authResult);
+        }
+
+        SecurityContextHolder.getContext().setAuthentication(authResult);
+
+        // Fire event
+        if (this.eventPublisher != null) {
+            eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(authResult, this.getClass()));
+        }
+
+        chain.doFilter(request, response);
+    }
+
+    @Override
     public Authentication attemptAuthentication(final HttpServletRequest request, final HttpServletResponse response)
             throws AuthenticationException, IOException {
         // if the request is a proxy request process it and return null to indicate the request has been processed
-        if(isProxyRequest(request)) {
+        if(proxyReceptorRequest(request)) {
+            logger.debug("Responding to proxy receptor request");
             CommonUtils.readAndRespondToProxyReceptorRequest(request, response, this.proxyGrantingTicketStorage);
             return null;
         }
 
-        final String username = CAS_STATEFUL_IDENTIFIER;
-        String password = request.getParameter(this.artifactParameter);
+        final boolean serviceTicketRequest = serviceTicketRequest(request, response);
+        final String username = serviceTicketRequest ? CAS_STATEFUL_IDENTIFIER : CAS_STATELESS_IDENTIFIER;
+        String password = obtainArtifact(request);
 
         if (password == null) {
+            logger.debug("Failed to obtain an artifact (cas ticket)");
             password = "";
         }
 
@@ -111,11 +242,47 @@ public class CasAuthenticationFilter extends AbstractAuthenticationProcessingFil
         return this.getAuthenticationManager().authenticate(authRequest);
     }
 
+    /**
+     * If present, gets the artifact (CAS ticket) from the {@link HttpServletRequest}.
+     * @param request
+     * @return if present the artifact from the {@link HttpServletRequest}, else null
+     */
+    protected String obtainArtifact(HttpServletRequest request) {
+        return request.getParameter(artifactParameter);
+    }
+
     /**
      * Overridden to provide proxying capabilities.
      */
+    @Override
     protected boolean requiresAuthentication(final HttpServletRequest request, final HttpServletResponse response) {
-        return isProxyRequest(request) || super.requiresAuthentication(request, response);
+        final boolean serviceTicketRequest = serviceTicketRequest(request, response);
+        final boolean result = serviceTicketRequest || proxyReceptorRequest(request) || (proxyTicketRequest(serviceTicketRequest, request));
+        if(logger.isDebugEnabled()) {
+            logger.debug("requiresAuthentication = "+result);
+        }
+        return result;
+    }
+
+    /**
+     * Sets the {@link AuthenticationFailureHandler} for proxy requests.
+     * @param proxyFailureHandler
+     */
+    public final void setProxyAuthenticationFailureHandler(
+            AuthenticationFailureHandler proxyFailureHandler) {
+        Assert.notNull(proxyFailureHandler,"proxyFailureHandler cannot be null");
+        this.proxyFailureHandler = proxyFailureHandler;
+    }
+
+    /**
+     * Wraps the {@link AuthenticationFailureHandler} to distinguish between
+     * handling proxy ticket authentication failures and service ticket
+     * failures.
+     */
+    @Override
+    public final void setAuthenticationFailureHandler(
+            AuthenticationFailureHandler failureHandler) {
+        super.setAuthenticationFailureHandler(new CasAuthenticationFailureHandler(failureHandler));
     }
 
     public final void setProxyReceptorUrl(final String proxyReceptorUrl) {
@@ -129,15 +296,97 @@ public class CasAuthenticationFilter extends AbstractAuthenticationProcessingFil
 
     public final void setServiceProperties(final ServiceProperties serviceProperties) {
         this.artifactParameter = serviceProperties.getArtifactParameter();
+        this.authenticateAllArtifacts = serviceProperties.isAuthenticateAllArtifacts();
     }
 
     /**
-     * Indicates if the request is eligible to be processed as a proxy request.
+     * Indicates if the request is elgible to process a service ticket. This method exists for readability.
      * @param request
+     * @param response
      * @return
      */
-    private boolean isProxyRequest(final HttpServletRequest request) {
+    private boolean serviceTicketRequest(final HttpServletRequest request, final HttpServletResponse response) {
+        boolean result = super.requiresAuthentication(request, response);
+        if(logger.isDebugEnabled()) {
+            logger.debug("serviceTicketRequest = "+result);
+        }
+        return result;
+    }
+
+    /**
+     * Indicates if the request is elgible to process a proxy ticket.
+     * @param request
+     * @return
+     */
+    private boolean proxyTicketRequest(final boolean serviceTicketRequest, final HttpServletRequest request) {
+        if(serviceTicketRequest) {
+            return false;
+        }
+        final boolean result = authenticateAllArtifacts && obtainArtifact(request) != null && !authenticated();
+        if(logger.isDebugEnabled()) {
+            logger.debug("proxyTicketRequest = "+result);
+        }
+        return result;
+    }
+
+    /**
+     * Determines if a user is already authenticated.
+     * @return
+     */
+    private boolean authenticated() {
+        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
+        return authentication != null && authentication.isAuthenticated() && !(authentication instanceof AnonymousAuthenticationToken);
+    }
+    /**
+     * Indicates if the request is elgible to be processed as the proxy receptor.
+     * @param request
+     * @return
+     */
+    private boolean proxyReceptorRequest(final HttpServletRequest request) {
         final String requestUri = request.getRequestURI();
-        return this.proxyGrantingTicketStorage != null && !CommonUtils.isEmpty(this.proxyReceptorUrl) && requestUri.endsWith(this.proxyReceptorUrl);
+        final boolean result = proxyReceptorConfigured() && requestUri.endsWith(this.proxyReceptorUrl);
+        if(logger.isDebugEnabled()) {
+            logger.debug("proxyReceptorRequest = "+result);
+        }
+        return result;
+    }
+
+    /**
+     * Determines if the {@link CasAuthenticationFilter} is configured to handle the proxy receptor requests.
+     *
+     * @return
+     */
+    private boolean proxyReceptorConfigured() {
+        final boolean result = this.proxyGrantingTicketStorage != null && !CommonUtils.isEmpty(this.proxyReceptorUrl);
+        if(logger.isDebugEnabled()) {
+            logger.debug("proxyReceptorConfigured = "+result);
+        }
+        return result;
+    }
+
+    /**
+     * A wrapper for the AuthenticationFailureHandler that will flex the {@link AuthenticationFailureHandler} that is used. The value
+     * {@link CasAuthenticationFilter#setProxyAuthenticationFailureHandler(AuthenticationFailureHandler) will be used for proxy requests
+     * that fail. The value {@link CasAuthenticationFilter#setAuthenticationFailureHandler(AuthenticationFailureHandler)} will be used for
+     * service tickets that fail.
+     *
+     * @author Rob Winch
+     */
+    private class CasAuthenticationFailureHandler implements AuthenticationFailureHandler {
+        private final AuthenticationFailureHandler serviceTicketFailureHandler;
+        public CasAuthenticationFailureHandler(AuthenticationFailureHandler failureHandler) {
+            Assert.notNull(failureHandler,"failureHandler");
+            this.serviceTicketFailureHandler = failureHandler;
+        }
+        public void onAuthenticationFailure(HttpServletRequest request,
+                HttpServletResponse response,
+                AuthenticationException exception) throws IOException,
+                ServletException {
+            if(serviceTicketRequest(request, response)) {
+                serviceTicketFailureHandler.onAuthenticationFailure(request, response, exception);
+            }else {
+                proxyFailureHandler.onAuthenticationFailure(request, response, exception);
+            }
+        }
     }
-}
+}

+ 131 - 0
cas/src/main/java/org/springframework/security/cas/web/authentication/DefaultServiceAuthenticationDetails.java

@@ -0,0 +1,131 @@
+/*
+ * Copyright 2011 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.cas.web.authentication;
+
+import java.util.regex.Pattern;
+
+import javax.servlet.http.HttpServletRequest;
+
+import org.springframework.security.web.authentication.WebAuthenticationDetails;
+import org.springframework.security.web.util.UrlUtils;
+import org.springframework.util.Assert;
+
+/**
+ * A default implementation of {@link ServiceAuthenticationDetails} that figures
+ * out the value for {@link #getServiceUrl()} by inspecting the current
+ * {@link HttpServletRequest} and using the current URL minus the artifact and
+ * the corresponding value.
+ *
+ * @author Rob Winch
+ */
+final class DefaultServiceAuthenticationDetails extends WebAuthenticationDetails implements ServiceAuthenticationDetails {
+    private static final long serialVersionUID = 6192409090610517700L;
+
+    //~ Instance fields ================================================================================================
+
+    private final String serviceUrl;
+
+    //~ Constructors ===================================================================================================
+
+    /**
+     * Creates a new instance
+     * @param request
+     *            the current {@link HttpServletRequest} to obtain the
+     *            {@link #getServiceUrl()} from.
+     * @param artifactPattern
+     *            the {@link Pattern} that will be used to clean up the query
+     *            string from containing the artifact name and value. This can
+     *            be created using {@link #createArtifactPattern(String)}.
+     */
+    DefaultServiceAuthenticationDetails(HttpServletRequest request, Pattern artifactPattern) {
+        super(request);
+        final String query = getQueryString(request,artifactPattern);
+        this.serviceUrl = UrlUtils.buildFullRequestUrl(request.getScheme(),
+                request.getServerName(), request.getServerPort(),
+                request.getRequestURI(), query);
+    }
+
+    //~ Methods ========================================================================================================
+
+    /**
+     * Returns the current URL minus the artifact parameter and its value, if present.
+     * @see org.springframework.security.cas.web.authentication.ServiceAuthenticationDetails#getServiceUrl()
+     */
+    public String getServiceUrl() {
+        return serviceUrl;
+    }
+
+    @Override
+    public int hashCode() {
+        final int prime = 31;
+        int result = super.hashCode();
+        result = prime * result
+                + serviceUrl.hashCode();
+        return result;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (!super.equals(obj) || !(obj instanceof DefaultServiceAuthenticationDetails)) {
+            return false;
+        }
+        ServiceAuthenticationDetails that = (ServiceAuthenticationDetails) obj;
+        return serviceUrl.equals(that.getServiceUrl());
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder result = new StringBuilder();
+        result.append(super.toString());
+        result.append("ServiceUrl: ");
+        result.append(serviceUrl);
+        return result.toString();
+    }
+
+    /**
+     * If present, removes the artifactParameterName and the corresponding value from the query String.
+     * @param request
+     * @return the query String minus the artifactParameterName and the corresponding value.
+     */
+    private String getQueryString(final HttpServletRequest request, final Pattern artifactPattern) {
+        final String query = request.getQueryString();
+        if(query == null) {
+            return null;
+        }
+        final String result = artifactPattern.matcher(query).replaceFirst("");
+        if(result.length() == 0) {
+            return null;
+        }
+        // strip off the trailing & only if the artifact was the first query param
+        return result.startsWith("&") ? result.substring(1) : result;
+    }
+
+    /**
+     * Creates a {@link Pattern} that can be passed into the constructor. This
+     * allows the {@link Pattern} to be reused for every instance of
+     * {@link DefaultServiceAuthenticationDetails}.
+     *
+     * @param artifactParameterName
+     * @return
+     */
+    static Pattern createArtifactPattern(String artifactParameterName) {
+        Assert.hasLength(artifactParameterName);
+        return Pattern.compile("&?"+Pattern.quote(artifactParameterName)+"=[^&]*");
+    }
+}

+ 43 - 0
cas/src/main/java/org/springframework/security/cas/web/authentication/ServiceAuthenticationDetails.java

@@ -0,0 +1,43 @@
+/*
+ * Copyright 2011 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.cas.web.authentication;
+
+import java.io.Serializable;
+
+import org.springframework.security.cas.ServiceProperties;
+import org.springframework.security.cas.authentication.CasAuthenticationProvider;
+import org.springframework.security.core.Authentication;
+
+/**
+ * In order for the {@link CasAuthenticationProvider} to provide the correct
+ * service url to authenticate the ticket, the returned value of
+ * {@link Authentication#getDetails()} should implement this interface when
+ * tickets can be sent to any URL rather than only
+ * {@link ServiceProperties#getService()}.
+ *
+ * @author Rob Winch
+ *
+ * @see ServiceAuthenticationDetailsSource
+ */
+public interface ServiceAuthenticationDetails extends Serializable {
+
+    /**
+     * Gets the absolute service url (i.e. https://example.com/service/).
+     *
+     * @return the service url. Cannot be <code>null</code>.
+     */
+    String getServiceUrl();
+}

+ 71 - 0
cas/src/main/java/org/springframework/security/cas/web/authentication/ServiceAuthenticationDetailsSource.java

@@ -0,0 +1,71 @@
+/*
+ * Copyright 2011 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.cas.web.authentication;
+
+import java.util.regex.Pattern;
+
+import javax.servlet.http.HttpServletRequest;
+
+import org.springframework.security.authentication.AuthenticationDetailsSource;
+import org.springframework.security.cas.ServiceProperties;
+
+/**
+ * The {@code AuthenticationDetailsSource} that is set on the
+ * {@code CasAuthenticationFilter} should return a value that implements
+ * {@code ServiceAuthenticationDetails} if the application needs to authenticate
+ * dynamic service urls. The
+ * {@code ServiceAuthenticationDetailsSource#buildDetails(HttpServletRequest)}
+ * creates a default {@code ServiceAuthenticationDetails}.
+ *
+ * @author Rob Winch
+ */
+public class ServiceAuthenticationDetailsSource implements AuthenticationDetailsSource<HttpServletRequest,
+        ServiceAuthenticationDetails> {
+    //~ Instance fields ================================================================================================
+
+    private final Pattern artifactPattern;
+
+    //~ Constructors ===================================================================================================
+
+    /**
+     * Creates an implementation that uses the default CAS artifactParameterName.
+     */
+    public ServiceAuthenticationDetailsSource() {
+        this(ServiceProperties.DEFAULT_CAS_ARTIFACT_PARAMETER);
+    }
+
+    /**
+     * Creates an implementation that uses the specified artifactParameterName
+     *
+     * @param artifactParameterName
+     *            the artifactParameterName that is removed from the current
+     *            URL. The result becomes the service url. Cannot be null and
+     *            cannot be an empty String.
+     */
+    public ServiceAuthenticationDetailsSource(final String artifactParameterName) {
+        this.artifactPattern = DefaultServiceAuthenticationDetails.createArtifactPattern(artifactParameterName);
+    }
+
+    //~ Methods ========================================================================================================
+
+    /**
+     * @param context the {@code HttpServletRequest} object.
+     * @return the {@code ServiceAuthenticationDetails} containing information about the current request
+     */
+    public ServiceAuthenticationDetails buildDetails(HttpServletRequest context) {
+        return new DefaultServiceAuthenticationDetails(context,artifactPattern);
+    }
+}

+ 5 - 0
cas/src/main/java/org/springframework/security/cas/web/authentication/package-info.java

@@ -0,0 +1,5 @@
+/**
+ * Authentication processing mechanisms which respond to the submission of authentication
+ * credentials using CAS.
+ */
+package org.springframework.security.cas.web.authentication;

+ 85 - 1
cas/src/test/java/org/springframework/security/cas/authentication/CasAuthenticationProviderTests.java

@@ -15,7 +15,7 @@
 
 package org.springframework.security.cas.authentication;
 
-
+import static org.mockito.Mockito.*;
 import static org.junit.Assert.*;
 
 import org.jasig.cas.client.validation.Assertion;
@@ -23,11 +23,13 @@ import org.jasig.cas.client.validation.AssertionImpl;
 import org.jasig.cas.client.validation.TicketValidationException;
 import org.jasig.cas.client.validation.TicketValidator;
 import org.junit.*;
+import org.springframework.mock.web.MockHttpServletRequest;
 import org.springframework.security.authentication.BadCredentialsException;
 import org.springframework.security.authentication.TestingAuthenticationToken;
 import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
 import org.springframework.security.cas.ServiceProperties;
 import org.springframework.security.cas.web.CasAuthenticationFilter;
+import org.springframework.security.cas.web.authentication.ServiceAuthenticationDetails;
 import org.springframework.security.core.Authentication;
 import org.springframework.security.core.authority.AuthorityUtils;
 import org.springframework.security.core.authority.SimpleGrantedAuthority;
@@ -35,6 +37,7 @@ import org.springframework.security.core.userdetails.AuthenticationUserDetailsSe
 import org.springframework.security.core.userdetails.User;
 import org.springframework.security.core.userdetails.UserDetails;
 import org.springframework.security.core.userdetails.UsernameNotFoundException;
+import org.springframework.security.web.authentication.WebAuthenticationDetails;
 
 import java.util.*;
 
@@ -148,6 +151,87 @@ public class CasAuthenticationProviderTests {
         assertEquals("ST-456", newResult.getCredentials());
     }
 
+    @Test
+    public void authenticateAllNullService() throws Exception {
+        String serviceUrl = "https://service/context";
+        ServiceAuthenticationDetails details = mock(ServiceAuthenticationDetails.class);
+        when(details.getServiceUrl()).thenReturn(serviceUrl);
+        TicketValidator validator = mock(TicketValidator.class);
+        when(validator.validate(any(String.class),any(String.class))).thenReturn(new AssertionImpl("rod"));
+
+        ServiceProperties serviceProperties = makeServiceProperties();
+        serviceProperties.setAuthenticateAllArtifacts(true);
+
+        CasAuthenticationProvider cap = new CasAuthenticationProvider();
+        cap.setAuthenticationUserDetailsService(new MockAuthoritiesPopulator());
+        cap.setKey("qwerty");
+
+        cap.setTicketValidator(validator);
+        cap.setServiceProperties(serviceProperties);
+        cap.afterPropertiesSet();
+
+        String ticket = "ST-456";
+        UsernamePasswordAuthenticationToken token =
+            new UsernamePasswordAuthenticationToken(CasAuthenticationFilter.CAS_STATELESS_IDENTIFIER, ticket);
+
+        Authentication result = cap.authenticate(token);
+    }
+
+    @Test
+    public void authenticateAllAuthenticationIsSuccessful() throws Exception {
+        String serviceUrl = "https://service/context";
+        ServiceAuthenticationDetails details = mock(ServiceAuthenticationDetails.class);
+        when(details.getServiceUrl()).thenReturn(serviceUrl);
+        TicketValidator validator = mock(TicketValidator.class);
+        when(validator.validate(any(String.class),any(String.class))).thenReturn(new AssertionImpl("rod"));
+
+        ServiceProperties serviceProperties = makeServiceProperties();
+        serviceProperties.setAuthenticateAllArtifacts(true);
+
+        CasAuthenticationProvider cap = new CasAuthenticationProvider();
+        cap.setAuthenticationUserDetailsService(new MockAuthoritiesPopulator());
+        cap.setKey("qwerty");
+
+        cap.setTicketValidator(validator);
+        cap.setServiceProperties(serviceProperties);
+        cap.afterPropertiesSet();
+
+        String ticket = "ST-456";
+        UsernamePasswordAuthenticationToken token =
+            new UsernamePasswordAuthenticationToken(CasAuthenticationFilter.CAS_STATELESS_IDENTIFIER, ticket);
+
+        Authentication result = cap.authenticate(token);
+        verify(validator).validate(ticket, serviceProperties.getService());
+
+        serviceProperties.setAuthenticateAllArtifacts(true);
+        result = cap.authenticate(token);
+        verify(validator,times(2)).validate(ticket, serviceProperties.getService());
+
+        token.setDetails(details);
+        result = cap.authenticate(token);
+        verify(validator).validate(ticket, serviceUrl);
+
+        serviceProperties.setAuthenticateAllArtifacts(false);
+        serviceProperties.setService(null);
+        cap.setServiceProperties(serviceProperties);
+        cap.afterPropertiesSet();
+        result = cap.authenticate(token);
+        verify(validator,times(2)).validate(ticket, serviceUrl);
+
+        token.setDetails(new WebAuthenticationDetails(new MockHttpServletRequest()));
+        try {
+            cap.authenticate(token);
+            fail("Expected Exception");
+        }catch(IllegalStateException success) {}
+
+        cap.setServiceProperties(null);
+        cap.afterPropertiesSet();
+        try {
+            cap.authenticate(token);
+            fail("Expected Exception");
+        }catch(IllegalStateException success) {}
+    }
+
     @Test(expected = BadCredentialsException.class)
     public void missingTicketIdIsDetected() throws Exception {
         CasAuthenticationProvider cap = new CasAuthenticationProvider();

+ 79 - 1
cas/src/test/java/org/springframework/security/cas/web/CasAuthenticationFilterTests.java

@@ -16,19 +16,34 @@
 package org.springframework.security.cas.web;
 
 import static org.junit.Assert.*;
+import static org.mockito.Matchers.any;
 import static org.mockito.Mockito.*;
 
+import java.lang.reflect.Method;
+
 import javax.servlet.FilterChain;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
 
 import org.jasig.cas.client.proxy.ProxyGrantingTicketStorage;
+import org.junit.After;
 import org.junit.Test;
 import org.springframework.mock.web.MockHttpServletRequest;
 import org.springframework.mock.web.MockHttpServletResponse;
+import org.springframework.security.authentication.AnonymousAuthenticationToken;
 import org.springframework.security.authentication.AuthenticationManager;
 import org.springframework.security.authentication.BadCredentialsException;
+import org.springframework.security.authentication.TestingAuthenticationToken;
 import org.springframework.security.cas.ServiceProperties;
 import org.springframework.security.core.Authentication;
 import org.springframework.security.core.AuthenticationException;
+import org.springframework.security.core.authority.AuthorityUtils;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
+import org.springframework.security.web.authentication.AuthenticationFailureHandler;
+import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
+import org.springframework.security.web.authentication.NullRememberMeServices;
+import org.springframework.util.ReflectionUtils;
 
 
 /**
@@ -40,6 +55,11 @@ import org.springframework.security.core.AuthenticationException;
 public class CasAuthenticationFilterTests {
     //~ Methods ========================================================================================================
 
+    @After
+    public void tearDown() {
+        SecurityContextHolder.clearContext();
+    }
+
     @Test
     public void testGettersSetters() {
         CasAuthenticationFilter filter = new CasAuthenticationFilter();
@@ -105,6 +125,31 @@ public class CasAuthenticationFilterTests {
         assertFalse(filter.requiresAuthentication(request, response));
     }
 
+    @Test
+    public void testRequiresAuthenticationAuthAll() {
+        ServiceProperties properties = new ServiceProperties();
+        properties.setAuthenticateAllArtifacts(true);
+
+        CasAuthenticationFilter filter = new CasAuthenticationFilter();
+        filter.setServiceProperties(properties);
+        MockHttpServletRequest request = new MockHttpServletRequest();
+        MockHttpServletResponse response = new MockHttpServletResponse();
+
+        request.setRequestURI(filter.getFilterProcessesUrl());
+        assertTrue(filter.requiresAuthentication(request, response));
+
+        request.setRequestURI("/other");
+        assertFalse(filter.requiresAuthentication(request, response));
+        request.setParameter(properties.getArtifactParameter(), "value");
+        assertTrue(filter.requiresAuthentication(request, response));
+        SecurityContextHolder.getContext().setAuthentication(new AnonymousAuthenticationToken("key", "principal", AuthorityUtils.createAuthorityList("ROLE_ANONYMOUS")));
+        assertTrue(filter.requiresAuthentication(request, response));
+        SecurityContextHolder.getContext().setAuthentication(new TestingAuthenticationToken("un", "principal", AuthorityUtils.createAuthorityList("ROLE_ANONYMOUS")));
+        assertTrue(filter.requiresAuthentication(request, response));
+        SecurityContextHolder.getContext().setAuthentication(new TestingAuthenticationToken("un", "principal", "ROLE_ANONYMOUS"));
+        assertFalse(filter.requiresAuthentication(request, response));
+    }
+
     @Test
     public void testAuthenticateProxyUrl() throws Exception {
         CasAuthenticationFilter filter = new CasAuthenticationFilter();
@@ -117,9 +162,42 @@ public class CasAuthenticationFilterTests {
         assertNull(filter.attemptAuthentication(request, response));
     }
 
+    @Test
+    public void testDoFilterAuthenticateAll() throws Exception {
+        AuthenticationSuccessHandler successHandler = mock(AuthenticationSuccessHandler.class);
+        AuthenticationManager manager = mock(AuthenticationManager.class);
+        Authentication authentication = new TestingAuthenticationToken("un", "pwd","ROLE_USER");
+        when(manager.authenticate(any(Authentication.class))).thenReturn(authentication);
+        ServiceProperties serviceProperties = new ServiceProperties();
+        serviceProperties.setAuthenticateAllArtifacts(true);
+        MockHttpServletRequest request = new MockHttpServletRequest();
+        request.setParameter("ticket", "ST-1-123");
+        MockHttpServletResponse response = new MockHttpServletResponse();
+        FilterChain chain = mock(FilterChain.class);
+
+        CasAuthenticationFilter filter = new CasAuthenticationFilter();
+        filter.setServiceProperties(serviceProperties);
+        filter.setAuthenticationSuccessHandler(successHandler);
+        filter.setProxyGrantingTicketStorage(mock(ProxyGrantingTicketStorage.class));
+        filter.setAuthenticationManager(manager);
+        filter.afterPropertiesSet();
+
+        filter.doFilter(request,response,chain);
+        assertFalse("Authentication should not be null",SecurityContextHolder.getContext().getAuthentication() == null);
+        verify(chain).doFilter(request, response);
+        verifyZeroInteractions(successHandler);
+
+        // validate for when the filterProcessUrl matches
+        filter.setFilterProcessesUrl(request.getRequestURI());
+        SecurityContextHolder.clearContext();
+        filter.doFilter(request,response,chain);
+        verifyNoMoreInteractions(chain);
+        verify(successHandler).onAuthenticationSuccess(request, response, authentication);
+    }
+
     // SEC-1592
     @Test
-    public void testChainNotInvokedForProxy() throws Exception {
+    public void testChainNotInvokedForProxyReceptor() throws Exception {
         CasAuthenticationFilter filter = new CasAuthenticationFilter();
         MockHttpServletRequest request = new MockHttpServletRequest();
         MockHttpServletResponse response = new MockHttpServletResponse();

+ 12 - 0
cas/src/test/java/org/springframework/security/cas/web/ServicePropertiesTests.java

@@ -35,6 +35,18 @@ public class ServicePropertiesTests {
         sp.afterPropertiesSet();
     }
 
+    @Test
+    public void allowNullServiceWhenAuthenticateAllTokens() throws Exception {
+        ServiceProperties sp = new ServiceProperties();
+        sp.setAuthenticateAllArtifacts(true);
+        sp.afterPropertiesSet();
+        sp.setAuthenticateAllArtifacts(false);
+        try {
+            sp.afterPropertiesSet();
+            fail("Expected Exception");
+        }catch(IllegalArgumentException success) {}
+    }
+
     @Test
     public void testGettersSetters() throws Exception {
         ServiceProperties[] sps = {new ServiceProperties(), new SamlServiceProperties()};

+ 88 - 0
cas/src/test/java/org/springframework/security/cas/web/authentication/DefaultServiceAuthenticationDetailsTests.java

@@ -0,0 +1,88 @@
+/*
+ * Copyright 2011 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.cas.web.authentication;
+import static org.junit.Assert.assertEquals;
+
+import java.util.regex.Pattern;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.springframework.mock.web.MockHttpServletRequest;
+import org.springframework.security.cas.ServiceProperties;
+import org.springframework.security.web.util.UrlUtils;
+
+/**
+ *
+ * @author Rob Winch
+ */
+public class DefaultServiceAuthenticationDetailsTests {
+    private DefaultServiceAuthenticationDetails details;
+    private MockHttpServletRequest request;
+    private Pattern artifactPattern;
+
+    @Before
+    public void setUp() {
+        request = new MockHttpServletRequest();
+        request.setScheme("https");
+        request.setServerName("localhost");
+        request.setServerPort(8443);
+        request.setRequestURI("/cas-sample/secure/");
+        artifactPattern = DefaultServiceAuthenticationDetails.createArtifactPattern(ServiceProperties.DEFAULT_CAS_ARTIFACT_PARAMETER);
+
+    }
+
+    @Test
+    public void getServiceUrlNullQuery() throws Exception {
+        details = new DefaultServiceAuthenticationDetails(request,artifactPattern);
+        assertEquals(UrlUtils.buildFullRequestUrl(request),details.getServiceUrl());
+    }
+
+    @Test
+    public void getServiceUrlTicketOnlyParam() {
+        request.setQueryString("ticket=123");
+        details = new DefaultServiceAuthenticationDetails(request,artifactPattern);
+        String serviceUrl = details.getServiceUrl();
+        request.setQueryString(null);
+        assertEquals(UrlUtils.buildFullRequestUrl(request),serviceUrl);
+    }
+
+    @Test
+    public void getServiceUrlTicketFirstMultiParam() {
+        request.setQueryString("ticket=123&other=value");
+        details = new DefaultServiceAuthenticationDetails(request,artifactPattern);
+        String serviceUrl = details.getServiceUrl();
+        request.setQueryString("other=value");
+        assertEquals(UrlUtils.buildFullRequestUrl(request),serviceUrl);
+    }
+
+    @Test
+    public void getServiceUrlTicketLastMultiParam() {
+        request.setQueryString("other=value&ticket=123");
+        details = new DefaultServiceAuthenticationDetails(request,artifactPattern);
+        String serviceUrl = details.getServiceUrl();
+        request.setQueryString("other=value");
+        assertEquals(UrlUtils.buildFullRequestUrl(request),serviceUrl);
+    }
+
+    @Test
+    public void getServiceUrlTicketMiddleMultiParam() {
+        request.setQueryString("other=value&ticket=123&last=this");
+        details = new DefaultServiceAuthenticationDetails(request,artifactPattern);
+        String serviceUrl = details.getServiceUrl();
+        request.setQueryString("other=value&last=this");
+        assertEquals(UrlUtils.buildFullRequestUrl(request),serviceUrl);
+    }
+}

+ 28 - 1
web/src/main/java/org/springframework/security/web/authentication/AbstractAuthenticationProcessingFilter.java

@@ -215,7 +215,7 @@ public abstract class AbstractAuthenticationProcessingFilter extends GenericFilt
             chain.doFilter(request, response);
         }
 
-        successfulAuthentication(request, response, authResult);
+        successfulAuthentication(request, response, chain, authResult);
     }
 
     /**
@@ -280,8 +280,35 @@ public abstract class AbstractAuthenticationProcessingFilter extends GenericFilt
      * <li>Delegates additional behaviour to the {@link AuthenticationSuccessHandler}.</li>
      * </ol>
      *
+     * Subclasses can override this method to continue the {@link FilterChain} after successful authentication.
+     * @param request
+     * @param response
+     * @param chain
      * @param authResult the object returned from the <tt>attemptAuthentication</tt> method.
+     * @throws IOException
+     * @throws ServletException
      */
+    protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
+            Authentication authResult) throws IOException, ServletException{
+        successfulAuthentication(request, response, authResult);
+    }
+
+    /**
+     * Default behaviour for successful authentication.
+     * <ol>
+     * <li>Sets the successful <tt>Authentication</tt> object on the {@link SecurityContextHolder}</li>
+     * <li>Invokes the configured {@link SessionAuthenticationStrategy} to handle any session-related behaviour
+     * (such as creating a new session to protect against session-fixation attacks).</li>
+     * <li>Informs the configured <tt>RememberMeServices</tt> of the successful login</li>
+     * <li>Fires an {@link InteractiveAuthenticationSuccessEvent} via the configured
+     * <tt>ApplicationEventPublisher</tt></li>
+     * <li>Delegates additional behaviour to the {@link AuthenticationSuccessHandler}.</li>
+     * </ol>
+     *
+     * @param authResult the object returned from the <tt>attemptAuthentication</tt> method.
+     * @deprecated since 3.1. Use {@link #successfulAuthentication(HttpServletRequest, HttpServletResponse, FilterChain, Authentication)} instead.
+     */
+    @Deprecated
     protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response,
             Authentication authResult) throws IOException, ServletException {