Skip navigation.
Home

How Spring Security hooks to Central Authentication Service (CAS)

CAS High Level Collaboration DiagramOur latest software project used CAS for authentication and single sign-on. I couldn't find any good documentation on how Spring Security and CAS played together. There is some documentation about how to configure your application to use CAS but not much on what messages go back and forth by whom and when.

Normally, we would drop in the CAS war, do a little configuration and use it for single sign-on for a suite of applications. I had a basic understanding of how it worked conceptually, but not really a complete understanding of how it worked in practice. This was okay provided that we didn't run into any issues with authentication or needed to extend CAS functionality.

This understanding gap started to show up when I needed to use Spring Security as the CAS client. I was able able to configure the client easily enough, but couldn't quite get my head around how Spring Security interacted with CAS at a message level. I put together the following diagrams to address that gap.

High Level Collaboration Diagram

This diagram doesn't show the internals of the Spring Security client but it is very helpful if you aren't all that familiar with how CAS works in general. The main takeaways are how involved the browser is in the process. There are a bunch of redirects that happen (really without the user noticing) during an login/authentication. The second thing is step #8. This is the only time where the Application (Spring Security client) talks directly to CAS. This is also why the Application's JVM needs to be accept the SSL certificate of the CAS application. This seems to trip people up.

CAS High Level Collaboration Diagram

Sequence Diagram

This diagram has the three main actors in the Spring Security CAS client and shows which classes actually initiate redirects. This is very helpful if you need to modify the CAS application. I put it together as a first step in determining how to add password expiration detection with force password change into CAS instead of having them in each application.

CAS Sequence Diagram

Matt Fleming's picture

Non-Simple security.xml

This is the real security.xml file that we use to configure Spring Security. Normally we wouldn't need to do this expanded version, but I wanted the channelProcessingFilter to fire (which switches the protocol from http to https) on all non-secured resources. If the concise configuration actually used the filters attribute on the sec:intercept-url element, I wouldn't have to break it all out. It was good to figure out anyway.

-Matt

<?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"
       xmlns:util="http://www.springframework.org/schema/util"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
                        http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security-2.0.xsd
                        http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util-2.5.xsd">
 
    <bean id="securityFilter" class="org.springframework.security.util.FilterChainProxy">
        <sec:filter-chain-map path-type="ant">
            <sec:filter-chain pattern="/images/**" filters="channelProcessingFilter"/>
            <sec:filter-chain pattern="/css/**" filters="channelProcessingFilter"/>
            <sec:filter-chain pattern="/js/**" filters="channelProcessingFilter"/>
            <sec:filter-chain pattern="/403.jsp" filters="channelProcessingFilter"/>
            <sec:filter-chain pattern="/404.jsp" filters="channelProcessingFilter"/>
            <sec:filter-chain pattern="/error.jsp" filters="channelProcessingFilter"/>
            <sec:filter-chain pattern="/**/cas/changePassword.htm*" filters="channelProcessingFilter"/>
            <sec:filter-chain pattern="/**/cas/login.htm*" filters="channelProcessingFilter"/>
            <sec:filter-chain pattern="/**/cas/passwordExpired.htm*" filters="channelProcessingFilter"/>
            <sec:filter-chain pattern="/**/*.html*" filters="channelProcessingFilter"/>
            <sec:filter-chain pattern="/**"
                              filters="channelProcessingFilter,httpSessionContextIntegrationFilter,logoutFilter,casSingleSignOutFilter,casProcessingFilter,securityContextHolderAwareRequestFilter,exceptionTranslationFilter,filterInvocationInterceptor"/>
        </sec:filter-chain-map>
    </bean>
 
    <!-- this is what hooks up the CAS entry point -->
    <bean id="exceptionTranslationFilter" class="org.springframework.security.ui.ExceptionTranslationFilter">
        <property name="authenticationEntryPoint">
            <ref local="casProcessingFilterEntryPoint"/>
        </property>
    </bean>
 
    <!-- where do I go when I need authentication from CAS-->
    <bean id="casProcessingFilterEntryPoint" class="org.springframework.security.ui.cas.CasProcessingFilterEntryPoint">
        <property name="loginUrl" value="https://localhost:5543/cas/login"/>
        <property name="serviceProperties" ref="serviceProperties"/>
    </bean>
 
    <!-- defines which roles are allowed to access http resources -->
    <bean id="filterInvocationInterceptor" class="org.springframework.security.intercept.web.FilterSecurityInterceptor">
        <property name="authenticationManager" ref="authenticationManager"/>
        <property name="accessDecisionManager" ref="accessDecisionManager"/>
        <property name="objectDefinitionSource">
            <value>
                PATTERN_TYPE_APACHE_ANT
                **=ROLE_ALLOWED_ROLES_HERE
            </value>
        </property>
    </bean>
 
    <!-- hooks up CAS ticket validator and user details loader -->
    <bean id="authenticationManager" class="org.springframework.security.providers.ProviderManager">
        <property name="providers">
            <list>
                <ref bean="casAuthenticationProvider"/>
            </list>
        </property>
    </bean>
 
    <!-- supporting class for filterInvocationInterceptor -->
    <bean id="accessDecisionManager" class="org.springframework.security.vote.AffirmativeBased">
        <property name="allowIfAllAbstainDecisions" value="false"/>
        <property name="decisionVoters">
            <list>
                <ref local="roleVoter"/>
            </list>
        </property>
    </bean>
 
    <bean id="roleVoter" class="org.springframework.security.vote.RoleVoter">
        <property name="rolePrefix" value=""/>
    </bean>
 
    <!-- setup method level security using annotations -->
    <sec:global-method-security jsr250-annotations="enabled" secured-annotations="enabled"/>
    <alias name="authenticationManager" alias="_authenticationManager"/>
 
    <bean id="passwordEncoder" class="org.springframework.security.providers.encoding.ShaPasswordEncoder"/>
 
    <!-- which service (application) am I authenticating -->
    <bean id="serviceProperties" class="org.springframework.security.ui.cas.ServiceProperties">
        <property name="service" value="https://localhost:5543/for/j_spring_cas_security_check"/>
        <property name="sendRenew" value="false"/>
    </bean>
 
    <!-- handles a logout request from the CAS server -->
    <bean id="casSingleSignOutFilter" class="org.jasig.cas.client.session.SingleSignOutFilter"/>
 
    <!-- performs CAS authentication -->
    <bean id="casProcessingFilter" class="org.springframework.security.ui.cas.CasProcessingFilter">
        <property name="authenticationManager" ref="authenticationManager"/>
        <property name="authenticationFailureUrl" value="/403.jsp"/>
        <property name="alwaysUseDefaultTargetUrl" value="false"/>
        <property name="defaultTargetUrl" value="/"/>
    </bean>
 
    <!-- Does the CAS ticket validation and user details loading -->
    <bean id="casAuthenticationProvider" class="org.springframework.security.providers.cas.CasAuthenticationProvider">
        <property name="userDetailsService" ref="cansAgencyPersonnelProfileDao"/>
        <property name="serviceProperties" ref="serviceProperties"/>
        <property name="ticketValidator">
            <bean class="org.jasig.cas.client.validation.Cas20ServiceTicketValidator">
                <constructor-arg index="0" value="https://localhost:5543/cas"/>
            </bean>
        </property>
        <property name="key" value="my_password_for_this_auth_provider_only"/>
    </bean>
 
    <!-- Log failed authentication attempts to commons-logging -->
    <bean id="loggerListener" class="org.springframework.security.event.authentication.LoggerListener"/>
 
    <bean id="httpSessionContextIntegrationFilter"
          class="org.springframework.security.context.HttpSessionContextIntegrationFilter"/>
 
    <bean id="securityContextHolderAwareRequestFilter"
          class="org.springframework.security.wrapper.SecurityContextHolderAwareRequestFilter"/>
 
    <!-- ===================== SSL SWITCHING ==================== -->
    <bean id="channelProcessingFilter" class="org.springframework.security.securechannel.ChannelProcessingFilter">
        <property name="channelDecisionManager" ref="channelDecisionManager"/>
        <property name="filterInvocationDefinitionSource">
            <value>
                PATTERN_TYPE_APACHE_ANT
                **=REQUIRES_SECURE_CHANNEL
            </value>
        </property>
    </bean>
 
    <bean id="channelDecisionManager" class="org.springframework.security.securechannel.ChannelDecisionManagerImpl">
        <property name="channelProcessors">
            <list>
                <bean class="org.springframework.security.securechannel.SecureChannelProcessor">
                    <property name="entryPoint" ref="channelEntryPoint"/>
                </bean>
                <bean class="org.springframework.security.securechannel.InsecureChannelProcessor">
                    <property name="entryPoint" ref="channelEntryPoint"/>
                </bean>
            </list>
        </property>
    </bean>
 
    <bean id="channelEntryPoint" class="org.springframework.security.securechannel.RetryWithHttpsEntryPoint">
        <property name="portMapper" ref="portMapper"/>
    </bean>
 
    <bean id="portMapper" class="org.springframework.security.util.PortMapperImpl">
        <property name="portMappings">
            <map>
                <entry key="80" value="443"/>
                <entry key="8080" value="8443"/>
                <entry key="5580" value="5543"/>
            </map>
        </property>
    </bean>
 
    <!-- Invoked when the user clicks logout -->
    <bean id="logoutFilter" class="org.springframework.security.ui.logout.LogoutFilter">
        <!-- URL redirected to after logout success -->
        <constructor-arg value="https://localhost:5543/cas/logout"/>
        <constructor-arg>
            <list>
                <bean class="org.springframework.security.ui.logout.SecurityContextLogoutHandler">
                    <property name="invalidateHttpSession" value="false"/>
                </bean>
            </list>
        </constructor-arg>
    </bean>
 
</beans>

-Matt

Matt Fleming's picture

Here's the concise security.xml configuration

Here's the concise version of the security configuration. This isn't something that you would be able to totally copy and paste of course. At a minimum the ROLEs are going to be different, as well as the service and userDetailsService definition.

<?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-2.0.xsd
                                                http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security-2.0.xsd">
 
    <sec:http lowercase-comparisons="false" entry-point-ref="casProcessingFilterEntryPoint">
        <sec:intercept-url pattern="/images/*" filters="none"/>
        <sec:intercept-url pattern="/styles/*" filters="none"/>
        <sec:intercept-url pattern="/scripts/*" filters="none"/>
        <sec:intercept-url pattern="/403.jsp" filters="none"/>
        <sec:intercept-url pattern="/404.jsp" filters="none"/>
        <sec:intercept-url pattern="/error.jsp" filters="none"/>
        <sec:intercept-url pattern="/**/*.html*"
                           access="ROLE_ALLOWED_ROLES_GO_HERE"
                           requires-channel="https"/>
        <sec:logout logout-success-url="https://localhost:5543/cas/logout" invalidate-session="false"/>
        <sec:port-mappings>
            <sec:port-mapping http="5580" https="5543"/>
            <sec:port-mapping http="80" https="443"/>
        </sec:port-mappings>
    </sec:http>
 
    <bean id="passwordEncoder" class="org.springframework.security.providers.encoding.ShaPasswordEncoder"/>
 
    <!-- where do I go when I need authentication -->
    <bean id="casProcessingFilterEntryPoint" class="org.springframework.security.ui.cas.CasProcessingFilterEntryPoint">
        <property name="loginUrl" value="https://localhost:5543/cas/login"/>
        <property name="serviceProperties" ref="serviceProperties"/>
    </bean>
 
    <!-- which service (application) am I authenticating -->
    <bean id="serviceProperties" class="org.springframework.security.ui.cas.ServiceProperties">
        <property name="service" value="https://localhost:5543/rtos/j_spring_cas_security_check"/>
        <property name="sendRenew" value="false"/>
    </bean>
 
    <sec:authentication-manager alias="authenticationManager"/>
 
    <bean id="casSingleSignOutFilter" class="org.jasig.cas.client.session.SingleSignOutFilter">
        <sec:custom-filter before="CAS_PROCESSING_FILTER"/>
    </bean>
 
    <bean id="casProcessingFilter" class="org.springframework.security.ui.cas.CasProcessingFilter">
        <sec:custom-filter after="CAS_PROCESSING_FILTER"/>
        <property name="authenticationManager" ref="authenticationManager"/>
        <property name="authenticationFailureUrl" value="/403.jsp"/>
        <property name="defaultTargetUrl" value="/"/>       
    </bean>
 
    <bean id="casAuthenticationProvider" class="org.springframework.security.providers.cas.CasAuthenticationProvider">
        <sec:custom-authentication-provider/>
        <property name="userDetailsService" ref="cansAgencyPersonnelProfileDao"/>
        <property name="serviceProperties" ref="serviceProperties"/>
        <property name="ticketValidator">
            <bean class="org.jasig.cas.client.validation.Cas20ServiceTicketValidator">
                <constructor-arg index="0" value="https://localhost:5543/cas"/>
            </bean>
        </property>
        <property name="key" value="my_password_for_this_auth_provider_only"/>
    </bean>
 
    <!-- Log failed authentication attempts to commons-logging -->
    <bean id="loggerListener" class="org.springframework.security.event.authentication.LoggerListener"/>
 
</beans>

-Matt