/* Copyright 2004, 2005, 2006 Acegi Technology Pty Limited
*
* 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;
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, 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.
* <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. 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>
* 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
*/
public class CasAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
//~ Static fields/initializers =====================================================================================
/** Used to identify a CAS request for a stateful user agent, such as a web browser. */
public static final String CAS_STATEFUL_IDENTIFIER = "_cas_stateful_";
/**
* Used to identify a CAS request for a stateless user agent, such as a remoting protocol client (e.g.
* Hessian, Burlap, SOAP etc). Results in a more aggressive caching strategy being used, as the absence of a
* <code>HttpSession</code> will result in a new authentication attempt on every request.
*/
public static final String CAS_STATELESS_IDENTIFIER = "_cas_stateless_";
/**
* The last portion of the receptor url, i.e. /proxy/receptor
*/
private String proxyReceptorUrl;
/**
* The backing storage to store ProxyGrantingTicket requests.
*/
private ProxyGrantingTicketStorage proxyGrantingTicketStorage;
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(proxyReceptorRequest(request)) {
logger.debug("Responding to proxy receptor request");
CommonUtils.readAndRespondToProxyReceptorRequest(request, response, this.proxyGrantingTicketStorage);
return null;
}
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 = "";
}
final UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
authRequest.setDetails(authenticationDetailsSource.buildDetails(request));
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) {
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) {
this.proxyReceptorUrl = proxyReceptorUrl;
}
public final void setProxyGrantingTicketStorage(
final ProxyGrantingTicketStorage proxyGrantingTicketStorage) {
this.proxyGrantingTicketStorage = proxyGrantingTicketStorage;
}
public final void setServiceProperties(final ServiceProperties serviceProperties) {
this.artifactParameter = serviceProperties.getArtifactParameter();
this.authenticateAllArtifacts = serviceProperties.isAuthenticateAllArtifacts();
}
/**
* 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 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);
}
}
}
}