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