Using the CAS Client 3.1 with Spring Security

This was tested against the sample application that is included with Spring Security. As of this writing, replacing the applicationContext-security.xml in the sample application with the one below would enable this alternative configuration. We can not guarantee this version will work without modification in future versions of Spring Security.

web.xml
<?xml version="1.0" encoding="UTF-8"?>
<!--
  - Tutorial web application
  -
  -->

<web-app xmlns="http://java.sun.com/xml/ns/j2ee"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd" version="2.4">

    <display-name>Spring Security CAS Demo Application</display-name>

    <!--
      - Location of the XML file that defines the root application context
      - Applied by ContextLoaderListener.
      -->
    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>
            /WEB-INF/applicationContext-security.xml
        </param-value>
    </context-param>

    <context-param>
        <param-name>log4jConfigLocation</param-name>
        <param-value>/WEB-INF/classes/log4j.properties</param-value>
    </context-param>

    <context-param>
        <param-name>webAppRootKey</param-name>
        <param-value>cas.root</param-value>
    </context-param>

    <filter>
       <filter-name>CAS Single Sign Out Filter</filter-name>
       <filter-class>org.jasig.cas.client.session.SingleSignOutFilter</filter-class>
    </filter>

    <filter>
        <filter-name>springSecurityFilterChain</filter-name>
        <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
    </filter>

    <filter-mapping>
       <filter-name>CAS Single Sign Out Filter</filter-name>
       <url-pattern>/*</url-pattern>
    </filter-mapping>

    <filter-mapping>
      <filter-name>springSecurityFilterChain</filter-name>
      <url-pattern>/*</url-pattern>
    </filter-mapping>

    <!--
      - Loads the root application context of this web app at startup.
      - The application context is then available via
      - WebApplicationContextUtils.getWebApplicationContext(servletContext).
    -->
    <listener>
        <listener-class>org.jasig.cas.client.session.SingleSignOutHttpSessionListener</listener-class>
    </listener>

    <listener>
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>

    <listener>
        <listener-class>org.springframework.web.util.Log4jConfigListener</listener-class>
    </listener>

    <error-page>
        <error-code>403</error-code>
        <location>/casfailed.jsp</location>
    </error-page>

</web-app>

The important additions to the web.xml include the addition of the 403 error page. 403 is what the CAS Validation Filter will throw if it has a problem with the ticket. Also, if you want Single Sign Out, you should enable the SingleSignOutHttpSessionListener.

applicationContext-security.xml
<?xml version="1.0" encoding="UTF-8"?>

<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:sec="http://www.springframework.org/schema/security"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
                        http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security-3.0.xsd">

    <bean id="springSecurityFilterChain" class="org.springframework.security.web.FilterChainProxy">
        <sec:filter-chain-map path-type="ant">
            <sec:filter-chain pattern="/" filters="casValidationFilter, wrappingFilter" />
            <sec:filter-chain pattern="/secure/receptor" filters="casValidationFilter" />
            <sec:filter-chain pattern="/j_spring_security_logout" filters="logoutFilter,etf,fsi" />
            <sec:filter-chain pattern="/**" filters="casAuthenticationFilter, casValidationFilter, wrappingFilter, sif,j2eePreAuthFilter,logoutFilter,etf,fsi"/>
        </sec:filter-chain-map>
    </bean>

    <bean id="sif" class="org.springframework.security.web.context.SecurityContextPersistenceFilter"/>


    <sec:authentication-manager alias="authenticationManager">
        <sec:authentication-provider ref="preAuthAuthProvider"/>
    </sec:authentication-manager>

     <bean id="preAuthAuthProvider" class="org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationProvider">
        <property name="preAuthenticatedUserDetailsService">
            <bean id="userDetailsServiceWrapper" class="org.springframework.security.core.userdetails.UserDetailsByNameServiceWrapper">
                <property name="userDetailsService" ref="userService"/>
            </bean>
        </property>
    </bean>

    <bean id="preAuthEntryPoint" class="org.springframework.security.web.authentication.Http403ForbiddenEntryPoint" />

    <bean id="j2eePreAuthFilter" class="org.springframework.security.web.authentication.preauth.j2ee.J2eePreAuthenticatedProcessingFilter">
        <property name="authenticationManager" ref="authenticationManager"/>
        <property name="authenticationDetailsSource">
            <bean class="org.springframework.security.web.authentication.WebAuthenticationDetailsSource" />
        </property>
    </bean>

    <bean id="logoutFilter" class="org.springframework.security.web.authentication.logout.LogoutFilter">
        <constructor-arg value="/"/>
        <constructor-arg>
            <list>
                <bean class="org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler"/>
            </list>
        </constructor-arg>
    </bean>

    <bean id="servletContext" class="org.springframework.web.context.support.ServletContextFactoryBean"/>

    <bean id="etf" class="org.springframework.security.web.access.ExceptionTranslationFilter">
        <property name="authenticationEntryPoint" ref="preAuthEntryPoint"/>
    </bean>

    <bean id="httpRequestAccessDecisionManager" class="org.springframework.security.access.vote.AffirmativeBased">
        <property name="allowIfAllAbstainDecisions" value="false"/>
        <property name="decisionVoters">
            <list>
                <ref bean="roleVoter"/>
            </list>
        </property>
    </bean>

   <bean id="fsi" class="org.springframework.security.web.access.intercept.FilterSecurityInterceptor">
        <property name="authenticationManager" ref="authenticationManager"/>
        <property name="accessDecisionManager" ref="httpRequestAccessDecisionManager"/>
        <property name="securityMetadataSource">
            <sec:filter-invocation-definition-source>
                <sec:intercept-url pattern="/secure/extreme/**" access="ROLE_SUPERVISOR"/>
                <sec:intercept-url pattern="/secure/**" access="ROLE_USER"/>
                <sec:intercept-url pattern="/**" access="ROLE_USER"/>
            </sec:filter-invocation-definition-source>
        </property>
    </bean>

    <bean id="roleVoter" class="org.springframework.security.access.vote.RoleVoter"/>

    <bean id="securityContextHolderAwareRequestFilter" class="org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter"/>
    
    <bean class="org.jasig.cas.client.validation.Cas20ServiceTicketValidator" id="ticketValidator">
        <constructor-arg index="0" value="https://localhost:9443/cas" />
        <property name="proxyGrantingTicketStorage" ref="proxyGrantingTicketStorage" />
        <property name="proxyCallbackUrl" value="https://localhost:8443/cas-sample/secure/receptor" />
    </bean>

    <bean id="proxyGrantingTicketStorage" class="org.jasig.cas.client.proxy.ProxyGrantingTicketStorageImpl" />

    <sec:user-service id="userService">
        <sec:user name="rod" password="rod" authorities="ROLE_SUPERVISOR,ROLE_USER" />
        <sec:user name="dianne" password="dianne" authorities="ROLE_USER" />
        <sec:user name="scott" password="scott" authorities="ROLE_USER" />
    </sec:user-service>

    <bean id="casAuthenticationFilter" class="org.jasig.cas.client.authentication.AuthenticationFilter">
        <property name="casServerLoginUrl" value="https://localhost:9443/cas/login" />
        <property name="serverName" value="https://localhost:8443" />
    </bean>

    <bean id="casValidationFilter" class="org.jasig.cas.client.validation.Cas20ProxyReceivingTicketValidationFilter">
        <property name="serverName" value="https://localhost:8443" />
        <property name="exceptionOnValidationFailure" value="true" />
        <property name="proxyGrantingTicketStorage" ref="proxyGrantingTicketStorage" />
        <property name="redirectAfterValidation" value="true" />
        <property name="ticketValidator" ref="ticketValidator" />
        <property name="proxyReceptorUrl" value="/secure/receptor" />
    </bean>

    <bean id="wrappingFilter" class="org.jasig.cas.client.util.HttpServletRequestWrapperFilter" />
</beans>

Replacement Steps

  1. You should replace the userService with something that checks your user storage.
  2. Replace the serverName and casServerLoginUrl with your values (or better yet, externalize them).
  3. Replace the URLs with the URL configuration for your application.

Future Improvements

This relies on the CAS2 protocol. One could imagine swapping in the SAML1.1 response, obtaining attributes, and then using more of the J2EE PreAuth filter support to load the roles from that.