|
|
@@ -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>
|
|
|
+ * <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>
|
|
|
+ * </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);
|
|
|
+ }
|
|
|
+ }
|
|
|
}
|
|
|
-}
|
|
|
+}
|