Wednesday, November 18, 2009

Single Sign On with CAS and RememberMe Part 1

Overview   
As an avid internet user I have a lot of different sites that I use, for most sites I have a separate user account and password.
With lots of accounts, it can be hard to remember all my different user names and passwords.
 With Endurotracker.com, we want to make you user experience as enjoyable as possible. So, unlike some sites where you need to 3 different username and passwords one for main site, one for forum , and one for blog engine, we have made so that you only need a Single Sign On or Username/password combo for all the different applications.
  In this series of blog entries, I will discuss how I implement Single Sign across 3 different applications, Roller ( a Open Source Blogging Engine),
JForum ( a Open Source Forum Engine), and our main application Endurotracker.com.
  The way we implement Single Sign On (SSO) will be to use CAS 4, Central Authentication System 4 written by www.jasig.org and the
Spring Framework, specifically Spring Security formerly Aegis Security.
 At a high level the way SSO works is : if a user is not logged in and they go to a secure area of the Roller application, then they will get prompted
to log in (via a redirect to the CAS 4 application), after successful login they get redirected back to where they where in the Roller application.
SSO will be implemented on 3 applications so this type of workflow will occur for the 3 applications, Roller, JForum, and Endurotracker.



Setting of CAS4

 In this section we will go over how CAS4 was setup.

Step 1: Get CAS4 from jasig.org's SVN trunk repository

Step 2: Configure your Eclipse IDE and setup the CAS4 project : cas-server-webapp.
           Optional (but good idea for debugging) , pull in all the CAS4 projects (over a dozen projects).

RememberMe Implementation

 CAS4 takes advantage of the Open Source framework Spring. Specifically it uses Spring Web Flow and Spring Security.
(see www.springsource.org for full documenation).
In a nutshell, the way we implement RememberMe is by creating a RememberMe cookie and then utilize this cookie to determine
who the user is. If the RememberMe cookie is not found or is expired we force the user to login.
You can use the standard RememberMeServices class from Spring Security or you can create your own RememberMeServices class
if you need a implement things like encrypting your cookies. We implement our own RememberMeServices class to encrypt the values
stored in our cookies. Creating your own RememberMeServices class is optional.

Add this html snippet to promptForCredentials.jsp which will add a remember me check box to the login form:



For Spring Web Flow, to store whether the user has checked rememberMe check box, we need to create a small class, CaptchaRememberMeCredentialImpl.java, here is the source (small class):

@Component
@ValidateDefinition
public class CaptchaRememberMeAuthenticationResponsePluginImpl extends CaptchaAuthenticationResponsePluginImpl implements AuthenticationResponsePlugin {


//WARNING: implement all required methods this is just a SNIPPET for illustration purposes, this code will not compile, you will
// need to implement all required methods and add your imports, etc.


public static final String SPRING_SECURITY_REMEMBER_ME_COOKIE_KEY = "SPRING_SECURITY_REMEMBER_ME_COOKIE";

@Override
public void handle(final LoginRequest loginRequest, final AuthenticationResponse response) {
if (response.succeeded()) {

String username ="";
String password ="";
List creds = loginRequest.getCredentials();
if(creds.size() > 0)
{
CaptchaRememberMeCredentialImpl userCred = (CaptchaRememberMeCredentialImpl) creds.get(0);
username = userCred.getUsername();
password = userCred.getPassword();

if (!StringUtils.hasLength(username) || !StringUtils.hasLength(password)) {

return;
}

//if isRememberMe is false then return (don't set rememberme cookie)
if(userCred.isRememberMe()==false)
{

return;
}

//need to implement your own UserDetails Class , see Spring Security's UserDetails javadoc for more info
UserDetails userDetails = getUserDetailsService().loadUserByUsername(username);
if(userDetails != null){

Users user = userDetails .getUser();

RequestContext context = RequestContextHolder.getRequestContext();
ExternalContext externalContext = context.getExternalContext();

//determine what data you wish to store in cookie value
//typically store username or email as the key, or userid possibly
String cookieValue = user.getUserName().toString();
response.getAttributes().put(SPRING_SECURITY_REMEMBER_ME_COOKIE_KEY, cookieValue);

}


return;
}
else
{

return;

}
}

}


}


To set the cookie we need to implement an Authentication plugin and a cookie creator class.
CAS4 has implemented a plugin architecture that allows developers to create custom plugins.
Once you create a plugin, to get CAS4 to use it you just reference the plugin in the your deployment configuration file (in CAS3 called deployerConfigContext.xml) or if you utilize annotations will get picked up automagically. (i.e @Component, etc)
In this case our plugin will need to get called during the authentication process, so we will extend the CaptchaAuthenticationResponsePluginImpl
and implement AuthenticationResponsePlugin.
In a nutshell the plugin calculates the Cookie's value and stores it in the HttpResponse.
During the workflow after successful authentication the RememberMeCookieCreater class , extracts the Cookie's value from
the HttpResponse and creates new RememberMe Cookie using this value as input.

@Component
@ValidateDefinition
public class CaptchaRememberMeAuthenticationResponsePluginImpl extends CaptchaAuthenticationResponsePluginImpl implements AuthenticationResponsePlugin {


//WARNING: implement all required methods this is just a SNIPPET for illustration purposes, this code will not compile, you will
// need to implement all required methods and add your imports, etc.


public static final String SPRING_SECURITY_REMEMBER_ME_COOKIE_KEY = "SPRING_SECURITY_REMEMBER_ME_COOKIE";

@Override
public void handle(final LoginRequest loginRequest, final AuthenticationResponse response) {
if (response.succeeded()) {

String username ="";
String password ="";
List creds = loginRequest.getCredentials();
if(creds.size() > 0)
{
CaptchaRememberMeCredentialImpl userCred = (CaptchaRememberMeCredentialImpl) creds.get(0);
username = userCred.getUsername();
password = userCred.getPassword();

if (!StringUtils.hasLength(username) || !StringUtils.hasLength(password)) {

return;
}

//if isRememberMe is false then return (don't set rememberme cookie)
if(userCred.isRememberMe()==false)
{

return;
}

//need to implement your own UserDetails Class , see Spring Security's UserDetails javadoc for more info
UserDetails userDetails = getUserDetailsService().loadUserByUsername(username);
if(userDetails != null){

Users user = userDetails .getUser();

RequestContext context = RequestContextHolder.getRequestContext();
ExternalContext externalContext = context.getExternalContext();

//determine what data you wish to store in cookie value
//typically store username or email as the key, or userid possibly
String cookieValue = user.getUserName().toString();
response.getAttributes().put(SPRING_SECURITY_REMEMBER_ME_COOKIE_KEY, cookieValue);

}


return;
}
else
{

return;

}
}

}


}

We use org.springframework.web.util.CookieGenerator to generate the remember me cookie, and see configuration section further down.

We then have to configure the Spring Web Flow configuration file, login.xml (locate under /webapp/WEB-INF/login/login.xml) to call this class during our workflow.

Here is the xml snippets:


xml snippet 1, update credentials var to use CaptchaRememberMeCredentialsImpl:

<var name="credentials" class="org.jasig.cas.server.authentication.CaptchaRememberMeCredentialImpl" />



xml snippet 2, add rememberme property:
<binder>
            <binding property="username" required="true" />
            <binding property="password" required="true" />
            <binding property="captchaResponse" required="false" />
            <binding property="rememberMe" required="true" />
</binder>
           
           
           
           


xml snippet 3:
<transition on="submit" to="determineIfSessionCreated">
            <evaluate expression="loginRequest.credentials.add(credentials)" />
            <evaluate expression="centralAuthenticationService.login(loginRequest)" result="requestScope.loginResponse" result-type="org.jasig.cas.server.login.LoginResponse" />
            <evaluate expression="sessionCookieCreater.createSessionCookie(loginResponse, null, externalContext)" />
            <!--RememberMe Cookie Logical Step Here-->
            <evaluate expression="rememberMeCookieCreater.createSessionCookie(loginResponse, null, externalContext)" />
            <evaluate expression="loginRequest.setSessionId(loginResponse.sessionId)" />
        </transition>

Editing Configuration:

Step 3: Edit cas-server-webapp's pom.xml to allow for use of jetty (web server used in eclipse for debugging)
pom.xml Snippet:
<plugin>
                <groupId>org.mortbay.jetty</groupId>
                <artifactId>jetty-maven-plugin</artifactId>
                <version>7.0.0.1beta2</version>
                <configuration>
                    <webAppConfig>
                      <contextPath>/cas</contextPath>
                    </webAppConfig>
                    <connectors>
                       <connector implementation="org.eclipse.jetty.server.nio.SelectChannelConnector">
                            <port>8080</port>
                         </connector>
                    </connectors>                  
                </configuration>




Step 4: Update web.xml
             Update the section like this:

 <context-param>
              <param-name>contextConfigLocation</param-name>
             <param-value>
               WEB-INF/cas-servlet.xml,
               WEB-INF/spring/applicationContext.xml,
               WEB-INF/spring/jcaptcha-configuration.xml,
               WEB-INF/spring/testAuthenticationHandler-configuration.xml,
               WEB-INF/spring/urlCredentialAuthenticationHandler-configuration.xml,
               /WEB-INF/deployerConfigContext.xml,
               /WEB-INF/jndi.xml         
              </param-value>
            </context-param>

Step 5: Example cas-servlet.xml (if you run into issues):
 
<?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:webflow="http://www.springframework.org/schema/webflow-config"
       xmlns:p="http://www.springframework.org/schema/p"
       xmlns:util="http://www.springframework.org/schema/util"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:security="http://www.springframework.org/schema/security"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
                           http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
                           http://www.springframework.org/schema/webflow-config
                           http://www.springframework.org/schema/webflow-config/spring-webflow-config-2.0.xsd
                           http://www.springframework.org/schema/util
                           http://www.springframework.org/schema/util/spring-util-2.5.xsd
                           http://www.springframework.org/schema/context
                           http://www.springframework.org/schema/context/spring-context.xsd
                           http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security-2.0.4.xsd">

     
   
    <!-- Web flow 2.0 schema -->
    <webflow:flow-registry id="flowRegistry">
        <webflow:flow-location path="/WEB-INF/login/login.xml" id="login"/>
    </webflow:flow-registry>   
    <webflow:flow-executor id="flowExecutor" />
   
    <bean class="org.springframework.webflow.mvc.servlet.FlowHandlerMapping" >
        <property name="order" value="0" />
        <property name="flowRegistry" ref="flowRegistry" />
    </bean>

    <bean class="org.springframework.web.servlet.i18n.CookieLocaleResolver" />
   

    <bean class="org.springframework.webflow.mvc.servlet.FlowHandlerAdapter" >
        <property name="flowExecutor" ref="flowExecutor" />
    </bean>
   
      
    <bean class="org.springframework.web.servlet.mvc.annotation.DefaultAnnotationHandlerMapping" >
       <property name="order" value="0" />
    </bean>
 
    <bean class="org.springframework.web.servlet.view.UrlBasedViewResolver" >
             <property name="prefix" value="/WEB-INF/jsp/" />
             <property name="suffix" value=".jsp" />
             <property name="viewClass" value="org.springframework.web.servlet.view.JstlView" />
    </bean>
  
   
    <!-- Dispatches requests mapped to POJO @Controllers implementations -->
    <bean class="org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter" />
   
    <!-- Dispatches requests mapped to org.springframework.web.servlet.mvc.Controller implementations -->
    <bean class="org.springframework.web.servlet.mvc.SimpleControllerHandlerAdapter" />
  
  
</beans>
Step 6: Update spring/applicationContext.xml:
<?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:p="http://www.springframework.org/schema/p"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
       http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">

    <context:component-scan base-package="org.jasig.cas.server.authentication"/>
    <context:component-scan base-package="org.jasig.cas.server.login"/>
    <context:component-scan base-package="org.jasig.cas.server.logout"/>
    <context:component-scan base-package="org.jasig.cas.server.session"/>
    <context:component-scan base-package="org.jasig.cas.server.util"/>
    <context:component-scan base-package="org.jasig.cas.server"/>
    <context:component-scan base-package="org.jasig.cas.server.login"/>
    <context:component-scan base-package="org.jasig.cas.server.session"/>
    <context:component-scan base-package="org.jasig.cas.server.web"/> 
  
   
    <context:annotation-config />

   
    <!--Default Auth. Manager, can override default in deployerConfigContext.xml-->
    <bean id="authenticationManager" class="org.jasig.cas.server.authentication.DefaultAuthenticationManagerImpl">
    </bean>
   
    <bean id="messageSource" class="org.springframework.context.support.ResourceBundleMessageSource">
        <property name="basenames">
            <list>
                <value>org/javalid/core/validator/jv_messages</value>
                <value>messages</value>
                <value>org/jasig/cas/server/messages/log</value>
                <value>org/jasig/cas/server/session/messages/cas_messages</value>
            </list>
        </property>
    </bean>

    <bean id="casMessageSource" class="org.jasig.cas.server.util.CasMessageSourceAccessor" />

   
</beans>
Step 7: Example spring/jcaptcha-configuration.xml (current trunk version):
 <?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:p="http://www.springframework.org/schema/p"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
       http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">

    <bean class="com.octo.captcha.service.image.DefaultManageableImageCaptchaService" id="imageCaptchaService" />

    <bean
       class="org.jasig.cas.server.authentication.DefaultCaptchaStatusImpl" id="captchaStatus"/>
  
    <bean class="org.jasig.cas.server.authentication.DefaultCaptchaStatusImplFactory" id="captchaStatusFactory" >
          <property name="numberOfFailures" value="2" />
          <property name="numberOfMilliseconds" value="60000" />
    </bean>
 
    <bean class="org.jasig.cas.server.authentication.InMemoryCaptchaStatusStorageImpl" id="captchaStatusStorage" />
</beans>

Step 8: Create deployerConfigContext.xml (main customization config file):
<?xml version="1.0" encoding="UTF-8"?>
<!--
    | deployerConfigContext.xml centralizes into one file some of the declarative configuration that
    | all CAS deployers will need to modify.
    |
    | This file declares some of the Spring-managed JavaBeans that make up a CAS deployment. 
    | The beans declared in this file are instantiated at context initialization time by the Spring
    | ContextLoaderListener declared in web.xml.  It finds this file because this
    | file is among those declared in the context parameter "contextConfigLocation".
    |
    | By far the most common change you will need to make in this file is to change the last bean
    | declaration to replace the default SimpleTestUsernamePasswordAuthenticationHandler with
    | one implementing your approach for authenticating usernames and passwords.
    +-->
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:p="http://www.springframework.org/schema/p"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xmlns:tx="http://www.springframework.org/schema/tx"
       xmlns:jee="http://www.springframework.org/schema/jee"
       xmlns:security="http://www.springframework.org/schema/security"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
                           http://www.springframework.org/schema/beans/spring-beans-2.0.xsd
                           http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-2.5.xsd
                http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-2.5.xsd
                http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-2.5.xsd
                http://www.springframework.org/schema/jee http://www.springframework.org/schema/jee/spring-jee-2.5.xsd
                http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security-2.0.4.xsd">
    <!--
        | This bean declares our AuthenticationManager.  The CentralAuthenticationService service bean
        | declared in applicationContext.xml picks up this AuthenticationManager by reference to its id,
        | "authenticationManager".  Most deployers will be able to use the default AuthenticationManager
        | implementation and so do not need to change the class of this bean.  We include the whole
        | AuthenticationManager here in the deployerConfigContext.xml so that you can see the things you will
        | need to change in context.
        +-->
   
  
  
   
     <bean class="org.springframework.web.util.CookieGenerator" id="sessionCookieGenerator"
          p:cookieName="TGT"
          p:cookieSecure="false"/>
         
              
    <bean class="org.springframework.web.util.CookieGenerator" id="rememberMeCookieGenerator"
          p:cookieName="SPRING_SECURITY_REMEMBER_ME_COOKIE"
          p:cookieDomain=".endurotracker.com"
          p:cookieMaxAge="1209600"
          p:cookieSecure="false" />
   
       
    <!--You Can override Auth. Mgr here if you wish-->
    <!--This is just a example to use as template-->   
    <!--<bean id="authenticationManager"
        class="org.jasig.cas.server.authentication.DefaultAuthenticationManagerImpl">       
    </bean>-->
   
   

    <!--You Can override CredentialToPrincipalResolver here if you wish-->
    <!--This is just a example to use as template-->   
    <!--<bean class="org.jasig.cas.server.authentication.SimpleUsernamePasswordCredentialToPrincipalResolver" id="usernamePasswordCredentialToPrincipalResolver" />-->

    <!--You Can override UsernamePasswordCredentialsAuthenticationHandler here if you wish-->
    <!--This is just a example to use as template-->   
    <!--<bean class="org.jasig.cas.server.authentication.handler.TestUsernamePasswordCredentialsAuthenticationHandler" id="testUsernamePasswordCredentialsAuthenticationHandler"/>-->
   
    <!--your custom AuthenticationHandler bean Here-->
   <bean class="yourPackage.YourCustomAuthenticationHandler" >
               <property name="securityService" ref="securityService" />
    </bean>
             
      
   
     <!--Your customizations here-->
    <!--
        Activates various annotations to be detected in bean classes:
        Spring's @Required and @Autowired, as well as JSR 250's @Resource.
    -->
    <context:annotation-config />
   
    <aop:aspectj-autoproxy />
   
    <!-- Turn on @Required -->
     <bean class="org.springframework.beans.factory.annotation.RequiredAnnotationBeanPostProcessor" />
  
   
    <bean id="securityService"
                 class="yourpackagename.SecurityService">
                <property name="usersDao" ref="usersDao" />     
    </bean>
             
    <bean id="usersDao"
              class="yourpackagename.UsersDao">
           <property name="sessionFactory" ref="sessionFactory"/>
    </bean>
   
    <!--RememberMe Section -->
   
    <bean id="usersInRolesDao"
        class="yourpackagename.UsersInRolesDao">
        <property name="sessionFactory" ref="sessionFactory"/>
    </bean>
  
   <!--NOTE if you use sessionFactories your will need to configure your sessionFactory bean-->

    <bean id="userService" class="yourPackageName.UserService">
          <property name="usersDao" ref="usersDao" />
          <property name="authMgr" ref="yourCustomAuthenticationManager"/>
          <property name="userCache" ref="userCache" />
          <property name="messageSource" ref="messageSource" />
          <property name="userTokenCache" ref="userTokenCache"/>
    </bean>
   
    <bean id="rememberMeAuthenticationProvider" class="org.springframework.security.providers.rememberme.RememberMeAuthenticationProvider">
            <security:custom-authentication-provider />
            <property name="key" value="CHANGE_THIS_2" />
    </bean>
       
                  
     <bean id="yourCustomAuthenticationManager" class="org.springframework.security.providers.ProviderManager">
          <property name="providers">
               <list>
                    <ref local="daoAuthenticationProvider" />
                    <bean class="org.springframework.security.providers.anonymous.AnonymousAuthenticationProvider">
                         <property name="key" value="CHANGE_THIS_1" />
                    </bean>
                    <ref local="rememberMeAuthenticationProvider" />
               </list>
          </property>
     </bean>

    <!--plain vanilla using regular Spring Security-->
     <bean id="rememberMeServices" class="org.springframework.security.ui.rememberme.TokenBasedRememberMeServices">
     <property name="userDetailsService" ref="usersDao"/>
      <property name="key" value="CHANGE_THIS_2"/>
    </bean>


    <!--example of custom rememberMeServices class config-->
     <!--<bean id="rememberMeServices" class="yourPackage.YourCustomRememberMeServices">
          <property name="userDetailsService" ref="usersDao" />
          <property name="userService" ref="userService" />
          <property name="usersInRolesDao" ref="usersInRolesDao" />
          <property name="key" value="CHANGE_THIS_2" />
          <property name="parameter" value="_spring_security_remember_me" />
           <property name="cookieName" value="SPRING_SECURITY_REMEMBER_ME_COOKIE" />
           <property name="tokenValiditySeconds" value="1209600" /><!-- 14 days -->
           <!--<property name="cookieDomain" value="localhost" />-->
           <property name="cookieDomain" value=".yourwebsite.com" />
    </bean>-->
   
    <bean id="daoAuthenticationProvider" class="org.springframework.security.providers.dao.DaoAuthenticationProvider">
          <security:custom-authentication-provider />
          <property name="userDetailsService" ref="usersDao" />
          <property name="passwordEncoder" ref="passwordEncoder" />
          <property name="userCache" ref="userCache" />
        </bean>
       
       
    <bean id="passwordEncoder" class="yourPackage.CustomPasswordEncoder">
          <property name="passwordFormatforBean" value="Hashed" />
    </bean>
   
    <bean id="userTokenCache" class="org.springframework.cache.ehcache.EhCacheFactoryBean">
          <property name="cacheManager" ref="cacheManager" />
          <property name="cacheName" value = "yourPackage.UserTokenCache"/>
     </bean>
   
     <bean id="cacheManager" class="org.springframework.cache.ehcache.EhCacheManagerFactoryBean">
          <property name="configLocation" value="classpath:ehcache.xml" />
     </bean>
       
    <bean id="userCacheBackend" class="org.springframework.cache.ehcache.EhCacheFactoryBean">
          <property name="cacheManager" ref="cacheManager" />
          <property name="cacheName" value="userCache" />
    </bean>

    <bean id="userCache" class="org.springframework.security.providers.dao.cache.EhCacheBasedUserCache">
          <property name="cache">
               <ref local="userCacheBackend" />
          </property>
    </bean>
   
   
   
</beans>

2 comments:

  1. CAS is an authentication system originally created by Yale University to provide a trusted way for an application to authenticate a user. CAS became a Jasig project in December 2004.

    ginko

    ReplyDelete
  2. Hi there to all, the contents present at this site are in fact awesome for people knowledge.
    Denver locksmith

    ReplyDelete