Tuesday, May 29, 2012

CAS Attribute Release, Backed by Database

Earlier in this post, I provided a CAS security server that would authenticate a user and release their attributes as a list of granted authorities to be used in Spring to provide access control on different parts of the application.

This post will be on something very similar to that, with the difference of going to a database instead. The complete source code of a Maven based project can be seen here, or checked out this way:
svn co http://tinywebgears-samples.googlecode.com/svn/trunk/sso-cas-spring-db sso-cas-spring-db

In order to run this sample application you need to set up a database and change the connection properties in the cas.properties file as necessary (it defaults to MySQL though). The application uses binding method to authenticate the user, meaning you need to be able to log in directly to the database using username/password provided. It then will go to a different database (configured in the same place) and fetch more information about the user from a databse table. This table is very simple and can be genereted using this script (also provided in the source tree):

CREATE TABLE `USER_DATA` (
  `uid` varchar(255) NOT NULL,
  `email` varchar(255) DEFAULT NULL,
  `roles` varchar(255) DEFAULT NULL,
  PRIMARY KEY (`uid`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;

Contents of the roles column in this table will be converted to granted authorities for Spring.

Since the code is very similar to the LDAP-based one, I will onlly show the major differences here. You can easily investigate the source code if you are interested.

The cas.properties is where most of the configuration takes place:
server.prefix=https://localhost:8443/cas

cas.securityContext.serviceProperties.service=${server.prefix}/services/j_acegi_cas_security_check
# Names of roles allowed to access the CAS service manager
cas.securityContext.serviceProperties.adminRoles=ROLE_ADMINISTRATORS
cas.securityContext.casProcessingFilterEntryPoint.loginUrl=${server.prefix}/login
cas.securityContext.ticketValidator.casServerUrlPrefix=${server.prefix}


cas.themeResolver.defaultThemeName=cas-theme-default
cas.viewResolver.basename=default_views

host.name=localhost

#database.hibernate.dialect=org.hibernate.dialect.OracleDialect
database.hibernate.dialect=org.hibernate.dialect.MySQLDialect
#database.hibernate.dialect=org.hibernate.dialect.HSQLDialect
database.url=jdbc:mysql://localhost/cas
database.username=cas
database.password=cas
database.driver.class=com.mysql.jdbc.Driver

users.database.url=jdbc:mysql://localhost/userdata
users.database.username=cas
users.database.password=cas
users.database.driver.class=com.mysql.jdbc.Driver
#users.database.hibernate.dialect=org.hibernate.dialect.OracleDialect
users.database.hibernate.dialect=org.hibernate.dialect.MySQLDialect
#users.database.hibernate.dialect=org.hibernate.dialect.HSQLDialect

The real change goes into the deployerConfigContext.xml file. I have left comments in the file so that you can see what will change if you move from LDAP to database, or vice versa:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:tx="http://www.springframework.org/schema/tx"
       xmlns:p="http://www.springframework.org/schema/p"
       xmlns:sec="http://www.springframework.org/schema/security"
       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
       http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-2.5.xsd">

    <bean id="authenticationManager"
          class="org.jasig.cas.authentication.AuthenticationManagerImpl">
        <property name="credentialsToPrincipalResolvers">
            <list>
                <bean class="org.jasig.cas.authentication.principal.UsernamePasswordCredentialsToPrincipalResolver" >
                    <property name="attributeRepository">
                        <ref bean="attributeRepository"/>
                    </property>
                </bean>
                <!-- LDAP -->
                <!--<bean class="org.jasig.cas.authentication.principal.CredentialsToLDAPAttributePrincipalResolver">-->
                    <!--&lt;!&ndash; The Principal resolver form the credentials &ndash;&gt;-->
                    <!--<property name="credentialsToPrincipalResolver">-->
                        <!--<bean class="org.jasig.cas.authentication.principal.UsernamePasswordCredentialsToPrincipalResolver"/>-->
                    <!--</property>-->
                    <!--&lt;!&ndash; The query made to find the Principal ID. "%u" will be replaced by the resolved Principal &ndash;&gt;-->
                    <!--<property name="filter" value="(uid=%u)"/>-->
                    <!--&lt;!&ndash; The attribute used to define the new Principal ID &ndash;&gt;-->
                    <!--<property name="principalAttributeName" value="cn"/>-->
                    <!--<property name="searchBase" value="ou=users,ou=system"/>-->
                    <!--<property name="contextSource" ref="contextSource"/>-->
                    <!--<property name="attributeRepository">-->
                        <!--<ref bean="attributeRepository"/>-->
                    <!--</property>-->
                <!--</bean>-->
                <bean class="org.jasig.cas.authentication.principal.HttpBasedServiceCredentialsToPrincipalResolver"/>
            </list>
        </property>

        <property name="authenticationHandlers">
            <list>
                <bean class="org.jasig.cas.authentication.handler.support.HttpBasedServiceCredentialsAuthenticationHandler"
                 p:httpClient-ref="httpClient" />
                <!-- DB -->
                <bean class="org.jasig.cas.adaptors.jdbc.BindModeSearchDatabaseAuthenticationHandler">
                    <property name="dataSource" ref="usersDataSource" />
                </bean>
                <!-- LDAP -->
                <!--<bean class="org.jasig.cas.adaptors.ldap.BindLdapAuthenticationHandler">-->
                    <!--<property name="filter" value="uid=%u"/>-->
                    <!--<property name="searchBase" value="ou=users,ou=system"/>-->
                    <!--<property name="contextSource" ref="contextSource"/>-->
                <!--</bean>-->
            </list>
        </property>
    </bean>

    <bean id="userDetailsService"
          class="org.springframework.security.cas.userdetails.GrantedAuthorityFromAssertionAttributesUserDetailsService">
        <constructor-arg>
            <list>
                <value>authorities</value>
            </list>
        </constructor-arg>
    </bean>

    <!-- DB -->
    <bean id="attributeRepository" class="com.tinywebgears.sso.cas.server.DatabasePersonAttributeAndRoleDao">
        <constructor-arg index="0" ref="usersDataSource" />
        <constructor-arg index="1" value="SELECT * FROM USER_DATA WHERE {0}" />
        <property name="authoritiesColumnName" value="roles"/>
        <property name="defaultRole">
            <bean class="org.springframework.security.core.authority.GrantedAuthorityImpl">
                <constructor-arg index="0" value="ROLE_USER"/>
            </bean>
        </property>
        <property name="queryAttributeMapping">
            <map>
                <entry key="username" value="uid" />
            </map>
        </property>
        <property name="resultAttributeMapping">
            <map>
                <entry key="uid" value="username" />
                <entry key="email" value="email" />
                <entry key="roles" value="authorities" />
            </map>
        </property>
    </bean>
    <bean id="usersDataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource" destroy-method="close">
        <property name="driverClass" value="${users.database.driver.class}"/>
        <property name="jdbcUrl" value="${users.database.url}"/>
        <property name="user" value="${users.database.username}"/>
        <property name="password" value="${users.database.password}"/>
    </bean>
    <!-- LDAP -->
    <!--<bean id="attributeRepository" class="org.jasig.services.persondir.support.ldap.LdapPersonAttributeAndRoleDao">-->
        <!--<property name="contextSource" ref="contextSource"/>-->
        <!--<property name="baseDN" value="ou=users,ou=system"/>-->
        <!--<property name="requireAllQueryAttributes" value="true"/>-->
        <!--&lt;!&ndash; Attribute mapping between principal (key) and LDAP (value) names used to perform the LDAP search.-->
<!--By default, multiple search criteria are ANDed together.  Set the queryType property to change to OR. &ndash;&gt;-->
        <!--<property name="queryAttributeMapping">-->
            <!--<map>-->
                <!--<entry key="username" value="uid"/>-->
            <!--</map>-->
        <!--</property>-->
        <!--<property name="resultAttributeMapping">-->
            <!--<map>-->
                <!--&lt;!&ndash; Mapping beetween LDAP entry attributes (key) and Principal's (value) &ndash;&gt;-->
                <!--<entry key="mail" value="email"/>-->
                <!--<entry key="authorities" value="authorities"/>-->
            <!--</map>-->
        <!--</property>-->
        <!--<property name="ldapAuthoritiesPopulator" ref="ldapAuthoritiesPopulator"/>-->
    <!--</bean>-->
    <!--<bean id="ldapAuthoritiesPopulator"-->
          <!--class="org.springframework.security.ldap.userdetails.DefaultLdapAuthoritiesPopulator">-->
        <!--<constructor-arg ref="contextSource"/>-->
        <!--<constructor-arg value="ou=groups,ou=system"/>-->
        <!--<property name="groupRoleAttribute" value="cn"/>-->
        <!--<property name="groupSearchFilter" value="(uniqueMember={0})"/>-->
    <!--</bean>-->
    <!--<bean id="contextSource" class="org.springframework.ldap.core.support.LdapContextSource">-->
        <!--<property name="pooled" value="false"/>-->
        <!--<property name="urls">-->
            <!--<list>-->
                <!--<value>ldap://localhost:10389</value>-->
            <!--</list>-->
        <!--</property>-->
        <!--<property name="userDn" value="uid=admin,ou=system"/>-->
        <!--<property name="password" value="secret"/>-->
        <!--<property name="baseEnvironmentProperties">-->
            <!--<map>-->
                <!--<entry>-->
                    <!--<key>-->
                        <!--<value>java.naming.security.authentication</value>-->
                    <!--</key>-->
                    <!--<value>simple</value>-->
                <!--</entry>-->
            <!--</map>-->
        <!--</property>-->
    <!--</bean>-->

    <bean id="serviceRegistryDao" class="org.jasig.cas.services.JpaServiceRegistryDaoImpl"
          p:entityManagerFactory-ref="entityManagerFactory"/>
    <!-- This is the EntityManagerFactory configuration for Hibernate -->
    <bean id="entityManagerFactory" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
        <property name="dataSource" ref="casDataSource"/>
        <property name="jpaVendorAdapter">
            <bean class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter">
                <property name="generateDdl" value="true"/>
                <property name="showSql" value="false"/>
            </bean>
        </property>
        <property name="jpaProperties">
            <props>
                <prop key="hibernate.dialect">org.hibernate.dialect.MySQL5Dialect</prop>
                <prop key="hibernate.hbm2ddl.auto">update</prop>
            </props>
        </property>
    </bean>
    <bean id="transactionManager" class="org.springframework.orm.jpa.JpaTransactionManager">
        <property name="entityManagerFactory" ref="entityManagerFactory"/>
    </bean>
    <tx:annotation-driven transaction-manager="transactionManager"/>
    <bean id="auditTrailManager" class="com.github.inspektr.audit.support.Slf4jLoggingAuditTrailManager"/>
    <bean id="casDataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource" destroy-method="close">
        <property name="driverClass" value="${database.driver.class}"/>
        <property name="jdbcUrl" value="${database.url}"/>
        <property name="user" value="${database.username}"/>
        <property name="password" value="${database.password}"/>
    </bean>

</beans>
Please note that this application is set-up for HTTPS. If you haven't yet set up your application server for HTTPS yet, simply replace URLs from https://localhost:8443/ to http://localhost:8080/ in a few places.