From a76a947b12e7a85ab34fb5379e8d5e6b7c06604f Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Sun, 3 Apr 2011 18:54:02 -0500 Subject: [PATCH] SEC-965: Added support for CAS proxy ticket authentication on any URL --- .../security/cas/ServiceProperties.java | 21 +- .../CasAuthenticationProvider.java | 35 ++- .../cas/web/CasAuthenticationEntryPoint.java | 1 + .../cas/web/CasAuthenticationFilter.java | 289 ++++++++++++++++-- .../DefaultServiceAuthenticationDetails.java | 131 ++++++++ .../ServiceAuthenticationDetails.java | 43 +++ .../ServiceAuthenticationDetailsSource.java | 71 +++++ .../cas/web/authentication/package-info.java | 5 + .../CasAuthenticationProviderTests.java | 86 +++++- .../cas/web/CasAuthenticationFilterTests.java | 80 ++++- .../cas/web/ServicePropertiesTests.java | 12 + ...aultServiceAuthenticationDetailsTests.java | 88 ++++++ ...bstractAuthenticationProcessingFilter.java | 29 +- 13 files changed, 865 insertions(+), 26 deletions(-) create mode 100644 cas/src/main/java/org/springframework/security/cas/web/authentication/DefaultServiceAuthenticationDetails.java create mode 100644 cas/src/main/java/org/springframework/security/cas/web/authentication/ServiceAuthenticationDetails.java create mode 100644 cas/src/main/java/org/springframework/security/cas/web/authentication/ServiceAuthenticationDetailsSource.java create mode 100644 cas/src/main/java/org/springframework/security/cas/web/authentication/package-info.java create mode 100644 cas/src/test/java/org/springframework/security/cas/web/authentication/DefaultServiceAuthenticationDetailsTests.java diff --git a/cas/src/main/java/org/springframework/security/cas/ServiceProperties.java b/cas/src/main/java/org/springframework/security/cas/ServiceProperties.java index f6488df0aa..d49e98ccc4 100644 --- a/cas/src/main/java/org/springframework/security/cas/ServiceProperties.java +++ b/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; + } } diff --git a/cas/src/main/java/org/springframework/security/cas/authentication/CasAuthenticationProvider.java b/cas/src/main/java/org/springframework/security/cas/authentication/CasAuthenticationProvider.java index 9b324718b0..b35199c582 100644 --- a/cas/src/main/java/org/springframework/security/cas/authentication/CasAuthenticationProvider.java +++ b/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. diff --git a/cas/src/main/java/org/springframework/security/cas/web/CasAuthenticationEntryPoint.java b/cas/src/main/java/org/springframework/security/cas/web/CasAuthenticationEntryPoint.java index cb4a8a2146..a06b73dfad 100644 --- a/cas/src/main/java/org/springframework/security/cas/web/CasAuthenticationEntryPoint.java +++ b/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, diff --git a/cas/src/main/java/org/springframework/security/cas/web/CasAuthenticationFilter.java b/cas/src/main/java/org/springframework/security/cas/web/CasAuthenticationFilter.java index 12157b8ce0..ed2e53126d 100644 --- a/cas/src/main/java/org/springframework/security/cas/web/CasAuthenticationFilter.java +++ b/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. + *

Service Tickets

*

* 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 service. The opaque ticket string is - * presented in the ticket request parameter. This filter monitors the service URL so it can - * receive the service ticket and process it. The CAS server knows which service URL to use via the - * {@link ServiceProperties#getService()} method. + * presented in the ticket request parameter. + *

+ * This filter monitors the service URL so it can + * receive the service ticket and process it. By default this filter processes the URL /j_spring_cas_security_check. + * When processing this URL, the value of {@link ServiceProperties#getService()} is used as the service when validating + * the ticket. This means that it is important that {@link ServiceProperties#getService()} specifies the same value + * as the filterProcessesUrl. *

* Processing the service ticket involves creating a UsernamePasswordAuthenticationToken which * uses {@link #CAS_STATEFUL_IDENTIFIER} for the principal and the opaque ticket string as the * credentials. + *

Obtaining Proxy Granting Tickets

+ *

+ * If specified, the filter can also monitor the proxyReceptorUrl. 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 proxyReceptorUrl a non-null + * proxyGrantingTicketStorage 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. + *

Proxy Tickets

+ *

+ * 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 true. Additionally, + * if the request is already authenticated, authentication will not 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. + *

+ * Processing the proxy ticket involves creating a UsernamePasswordAuthenticationToken which + * uses {@link #CAS_STATELESS_IDENTIFIER} for the principal and the opaque ticket string as the + * credentials. When a proxy ticket is successfully authenticated, the FilterChain continues and the + * authenticationSuccessHandler is not used. + *

Notes about the AuthenticationManager

*

* The configured AuthenticationManager is expected to provide a provider that can recognise * UsernamePasswordAuthenticationTokens containing this special principal name, and process - * them accordingly by validation with the CAS server. + * 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. + *

Example Configuration

*

- * 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). - *

- * By default this filter processes the URL /j_spring_cas_security_check. + * An example configuration that supports service tickets, obtaining proxy granting tickets, and proxy tickets is + * illustrated below: + * + *

+ * <b:bean id="serviceProperties"
+ *     class="org.springframework.security.cas.ServiceProperties"
+ *     p:service="https://service.example.com/cas-sample/j_spring_cas_security_check"
+ *     p:authenticateAllArtifacts="true"/>
+ * <b:bean id="casEntryPoint"
+ *     class="org.springframework.security.cas.web.CasAuthenticationEntryPoint"
+ *     p:serviceProperties-ref="serviceProperties" p:loginUrl="https://login.example.org/cas/login" />
+ * <b:bean id="casFilter"
+ *     class="org.springframework.security.cas.web.CasAuthenticationFilter"
+ *     p:authenticationManager-ref="authManager"
+ *     p:serviceProperties-ref="serviceProperties"
+ *     p:proxyGrantingTicketStorage-ref="pgtStorage"
+ *     p:proxyReceptorUrl="/j_spring_cas_security_proxyreceptor">
+ *     <b:property name="authenticationDetailsSource">
+ *         <b:bean class="org.springframework.security.cas.web.authentication.ServiceAuthenticationDetailsSource"/>
+ *     </b:property>
+ *     <b:property name="authenticationFailureHandler">
+ *         <b:bean class="org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler"
+ *             p:defaultFailureUrl="/casfailed.jsp"/>
+ *     </b:property>
+ * </b:bean>
+ * <!--
+ *     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()
+ *  -->
+ * <b:bean id="pgtStorage" class="org.jasig.cas.client.proxy.ProxyGrantingTicketStorageImpl"/>
+ * <b:bean id="casAuthProvider" class="org.springframework.security.cas.authentication.CasAuthenticationProvider"
+ *     p:serviceProperties-ref="serviceProperties"
+ *     p:key="casAuthProviderKey">
+ *     <b:property name="authenticationUserDetailsService">
+ *         <b:bean
+ *             class="org.springframework.security.core.userdetails.UserDetailsByNameServiceWrapper">
+ *             <b:constructor-arg ref="userService" />
+ *         </b:bean>
+ *     </b:property>
+ *     <b:property name="ticketValidator">
+ *         <b:bean
+ *             class="org.jasig.cas.client.validation.Cas20ProxyTicketValidator"
+ *             p:acceptAnyProxy="true"
+ *             p:proxyCallbackUrl="https://service.example.com/cas-sample/j_spring_cas_security_proxyreceptor"
+ *             p:proxyGrantingTicketStorage-ref="pgtStorage">
+ *             <b:constructor-arg value="https://login.example.org/cas" />
+ *         </b:bean>
+ *     </b:property>
+ *     <b:property name="statelessTicketCache">
+ *         <b:bean class="org.springframework.security.cas.authentication.EhCacheBasedTicketCache">
+ *             <b:property name="cache">
+ *                 <b:bean class="net.sf.ehcache.Cache"
+ *                   init-method="initialise"
+ *                   destroy-method="dispose">
+ *                     <b:constructor-arg value="casTickets"/>
+ *                     <b:constructor-arg value="50"/>
+ *                     <b:constructor-arg value="true"/>
+ *                     <b:constructor-arg value="false"/>
+ *                     <b:constructor-arg value="3600"/>
+ *                     <b:constructor-arg value="900"/>
+ *                 </b:bean>
+ *             </b:property>
+ *         </b:bean>
+ *     </b:property>
+ * </b:bean>
+ * 
* * @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 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 isProxyRequest(final HttpServletRequest request) { - final String requestUri = request.getRequestURI(); - return this.proxyGrantingTicketStorage != null && !CommonUtils.isEmpty(this.proxyReceptorUrl) && requestUri.endsWith(this.proxyReceptorUrl); + 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(); + 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); + } + } + } +} \ No newline at end of file diff --git a/cas/src/main/java/org/springframework/security/cas/web/authentication/DefaultServiceAuthenticationDetails.java b/cas/src/main/java/org/springframework/security/cas/web/authentication/DefaultServiceAuthenticationDetails.java new file mode 100644 index 0000000000..f404510eaa --- /dev/null +++ b/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)+"=[^&]*"); + } +} \ No newline at end of file diff --git a/cas/src/main/java/org/springframework/security/cas/web/authentication/ServiceAuthenticationDetails.java b/cas/src/main/java/org/springframework/security/cas/web/authentication/ServiceAuthenticationDetails.java new file mode 100644 index 0000000000..b242023c01 --- /dev/null +++ b/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 null. + */ + String getServiceUrl(); +} \ No newline at end of file diff --git a/cas/src/main/java/org/springframework/security/cas/web/authentication/ServiceAuthenticationDetailsSource.java b/cas/src/main/java/org/springframework/security/cas/web/authentication/ServiceAuthenticationDetailsSource.java new file mode 100644 index 0000000000..546a160e2b --- /dev/null +++ b/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 { + //~ 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); + } +} \ No newline at end of file diff --git a/cas/src/main/java/org/springframework/security/cas/web/authentication/package-info.java b/cas/src/main/java/org/springframework/security/cas/web/authentication/package-info.java new file mode 100644 index 0000000000..6a26354bf0 --- /dev/null +++ b/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; diff --git a/cas/src/test/java/org/springframework/security/cas/authentication/CasAuthenticationProviderTests.java b/cas/src/test/java/org/springframework/security/cas/authentication/CasAuthenticationProviderTests.java index e77f897d3a..495ded74d8 100644 --- a/cas/src/test/java/org/springframework/security/cas/authentication/CasAuthenticationProviderTests.java +++ b/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(); diff --git a/cas/src/test/java/org/springframework/security/cas/web/CasAuthenticationFilterTests.java b/cas/src/test/java/org/springframework/security/cas/web/CasAuthenticationFilterTests.java index 11328c3641..e6ddf8fde2 100644 --- a/cas/src/test/java/org/springframework/security/cas/web/CasAuthenticationFilterTests.java +++ b/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(); diff --git a/cas/src/test/java/org/springframework/security/cas/web/ServicePropertiesTests.java b/cas/src/test/java/org/springframework/security/cas/web/ServicePropertiesTests.java index 81a07dfe98..7c4f3bde1c 100644 --- a/cas/src/test/java/org/springframework/security/cas/web/ServicePropertiesTests.java +++ b/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()}; diff --git a/cas/src/test/java/org/springframework/security/cas/web/authentication/DefaultServiceAuthenticationDetailsTests.java b/cas/src/test/java/org/springframework/security/cas/web/authentication/DefaultServiceAuthenticationDetailsTests.java new file mode 100644 index 0000000000..095caa4195 --- /dev/null +++ b/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); + } +} diff --git a/web/src/main/java/org/springframework/security/web/authentication/AbstractAuthenticationProcessingFilter.java b/web/src/main/java/org/springframework/security/web/authentication/AbstractAuthenticationProcessingFilter.java index ddb6af3e47..9d17f739cd 100644 --- a/web/src/main/java/org/springframework/security/web/authentication/AbstractAuthenticationProcessingFilter.java +++ b/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 *
  • Delegates additional behaviour to the {@link AuthenticationSuccessHandler}.
  • * * + * 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 attemptAuthentication 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. + *
      + *
    1. Sets the successful Authentication object on the {@link SecurityContextHolder}
    2. + *
    3. Invokes the configured {@link SessionAuthenticationStrategy} to handle any session-related behaviour + * (such as creating a new session to protect against session-fixation attacks).
    4. + *
    5. Informs the configured RememberMeServices of the successful login
    6. + *
    7. Fires an {@link InteractiveAuthenticationSuccessEvent} via the configured + * ApplicationEventPublisher
    8. + *
    9. Delegates additional behaviour to the {@link AuthenticationSuccessHandler}.
    10. + *
    + * + * @param authResult the object returned from the attemptAuthentication 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 {