/* * 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.security.web.util.matcher.AntPathRequestMatcher; import org.springframework.security.web.util.matcher.RequestMatcher; 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>/login/cas</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/login/cas" * 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="/login/cas/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/login/cas/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 RequestMatcher proxyReceptorMatcher; /** * 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("/login/cas"); 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. */ 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.proxyReceptorMatcher = new AntPathRequestMatcher("/**" + 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 boolean result = proxyReceptorConfigured() && proxyReceptorMatcher.matches(request); 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 && proxyReceptorMatcher != null; 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); } } } }