Announcement Announcement Module
Collapse
No announcement yet.
CAS Single Sign-out in Acegi Page Title Module
Move Remove Collapse
This topic is closed
X
X
Conversation Detail Module
Collapse
  • Filter
  • Time
  • Show
Clear All
new posts

  • CAS Single Sign-out in Acegi

    Hello Guys!

    I have already set-up JA-SIG CAS to work with Acegi, all our applications used its the single sign-in feature. But I have problem in signing-out, because when I sign-out in one application using the "https://localhost/cas/logout" url, the browser redirects me to the CAS logout page but when I tried to access other applications, it still accepts me. Please help me on this guys, I am badly needing it to implement. Thanks!

  • #2
    Currently CAS doesn't support SSO. This link from their fourm says that they should have it by the 3.1 release which is due by June 2007

    http://tp.its.yale.edu/pipermail/cas...ry/004381.html

    Comment


    • #3
      Now that CAS 3.1 is out (we are using CAS 3.1.1) single sign-out is somehow supported.

      It seems like CAS keeps track of all the issued service tickets. When a user performs a CAS logout, a POST request is submitted to all CAS services with a body content XML containing the service ticket. This will allow all CAS services (SSO applications) to perform a logout - based on the supplied service ticket. The current CAS single sign-out is described here: http://www.ja-sig.org/wiki/display/C...ingle+Sign+Out

      Are there any plans/initiatives to add support for this logout "callback" in Acegi?

      I am probably going to implement something simple until this is supported by Acegi. The simplest I can think of now is to keep track of the "expired" (because of CAS logout) service tickets in each application. I will add a filter sufficiently early i the filter chain to make sure a user is "logged out" if he tries to access the application with an expired service ticket (credentials in a CasAuthenticationToken).

      Any comments or ideas?

      Comment


      • #4
        That approach seems like a good idea, at least until/unless support for the CAS 3 logout callback is implemented in Acegi. I tried it out in my app(s), and it appears to work in my case. As for details:

        I created an ExpiredTicketCache interface with methods to check if a service ticket exists in the cache, and add/remove tickets from the cache. I also created an ehcache-based implementation of ExpiredTicketCache. (See StatelessTicketCache/EhCacheBasedTicketCache for an analogy).

        Then, I created a Filter, CasLogoutCallbackFilter, that handles the "/j_acegi_cas_security_check" POST request and stores the service ticket in an ExpiredTicketCache injected by Spring. I put this filter before the CasProcessingFilter, which also handles the "/j_acegi_cas_security_check" request.

        I already had a LogoutFilter (configured with my app's LogoutHandlers) that redirected the user to CAS to logout, so I modified it to also check if the current SecurityContext's Authentication was a CasAuthenticationToken whose service ticket had expired (by examining the same ExpiredTicketCache used by the CasLogoutCallbackFilter above). If so, I treated this as a "logout" by invoking all of the filter's existing LogoutHandlers.

        I might be missing something(?) since I've only been using Acegi for a couple months, but this appears to be a working single sign-out implementation.

        Comment


        • #5
          Your solution sounds like a good implementation of single sign-out. Would it be possible for you to share the details on the forum? I think it would solve the problem for me (and I'm sure many others too).

          Would you be willing to post your configuration file and/or java classes?

          Thanks!

          Comment


          • #6
            Sure thing. Here's the (slightly modified) code and configuration that I'm using. I've altered the code to boil it down to the bare essentials (removed logging, obvious import statements, etc.).

            1. Here's the "expired ticket cache":

            Code:
            public interface ExpiredTicketCache
            {
              boolean isTicketExpired( String serviceTicket );
            
              void putTicketInCache( String serviceTicket );
            
              public void removeTicketFromCache( String serviceTicket);
            }
            This is the store of tickets that the webapp has received from the CAS server during a "logoutRequest" callback.

            2. Here's an ehcache-based implementation:

            Code:
            import net.sf.ehcache.Cache;
            import net.sf.ehcache.CacheException;
            import net.sf.ehcache.Element;
            
            import org.springframework.beans.factory.InitializingBean;
            import org.springframework.dao.DataRetrievalFailureException;
            import org.springframework.util.Assert;
            
            public class EhCacheBasedExpiredTicketCache
            implements ExpiredTicketCache, InitializingBean
            {
              private Cache cache;
            
              //-------------------------------------------------------------------------
              // Properties
              //-------------------------------------------------------------------------
            
              public void setCache( Cache cache ) { this.cache = cache; }
            
            
              //-------------------------------------------------------------------------
              // Implements InitializingBean
              //-------------------------------------------------------------------------
            
              public void afterPropertiesSet() throws Exception
              {
                Assert.notNull(this.cache, "cache mandatory");
              }
            
            
              //-------------------------------------------------------------------------
              // Implements ExpiredTicketCache
              //-------------------------------------------------------------------------
            
              public boolean isTicketExpired( String serviceTicket )
              {
                Element element = null;
            
                try { element = this.cache.get(serviceTicket); }
                catch( CacheException cacheException ) {
                  throw new DataRetrievalFailureException("Cache failure: "+
                    cacheException.getMessage());
                }
            
                return (element != null);
              }
            
              public void putTicketInCache( String serviceTicket )
              {
                this.cache.put( new Element(serviceTicket, serviceTicket));
              }
            
              public void removeTicketFromCache( String serviceTicket )
              {
                this.cache.remove(serviceTicket);
              }
            
            }
            3. Here's the Spring bean definition(s) for the "expiredTicketCache":

            Code:
            <bean id="cacheManager"
                class="org.springframework.cache.ehcache.EhCacheManagerFactoryBean">
              <property name="configLocation">
                <value>classpath:/ehcache-failsafe.xml</value>
              </property>
            </bean>
                
            <bean id="expiredTicketCache"
                class="example.EhCacheBasedExpiredTicketCache">
              <property name="cache">
                <bean class="org.springframework.cache.ehcache.EhCacheFactoryBean">
                  <property name="cacheManager">
                    <ref bean="cacheManager" />
                  </property>
                  <property name="cacheName">
                    <value>expiredTicketCache</value>
                  </property>
                </bean>
              </property>
            </bean>
            (To be continued)

            Comment


            • #7
              4. This is the "logout callback" filter:

              Code:
              import org.springframework.beans.factory.InitializingBean;
              import org.springframework.util.Assert;
              
              public class CasLogoutCallbackFilter implements Filter, InitializingBean
              {
                private String filterProcessesUrl;
                private ExpiredTicketCache expiredTicketCache;
              
              
                //-------------------------------------------------------------------------
                // Properties
                //-------------------------------------------------------------------------
              
                public void setFilterProcessesUrl( String s )
                {
                  this.filterProcessesUrl = s;
                }
              
                public void setExpiredTicketCache( ExpiredTicketCache cache )
                {
                  this.expiredTicketCache = cache;
                }
              
              
                //-------------------------------------------------------------------------
                // Implements InitializingBean
                //-------------------------------------------------------------------------
              
                public void afterPropertiesSet() throws Exception
                {
                  Assert.hasLength(this.filterProcessesUrl,
                    "filterProcessesUrl must be specified");
                  Assert.notNull(this.expiredTicketCache, "cache mandatory");
                }
              
              
                //-------------------------------------------------------------------------
                // Implements Filter
                //-------------------------------------------------------------------------
              
                public void init( FilterConfig config ) throws ServletException { }
              
                public void destroy() { }
              
                public void doFilter(
                  ServletRequest request, ServletResponse response, FilterChain chain )
                throws ServletException, IOException
                {
                  if(! (request instanceof HttpServletRequest) )
                  {
                    throw new ServletException("Can only process HttpServletRequest");
                  }
              
                  if(! (response instanceof HttpServletResponse) )
                  {
                    throw new ServletException("Can only process HttpServletResponse");
                  }
              
                  HttpServletRequest httpRequest = (HttpServletRequest) request;
                  HttpServletResponse httpResponse = (HttpServletResponse) response;
              
                  if( processLogout(httpRequest) )
                  {
                    return;
                  }
              
                  chain.doFilter(request, response);
                }
              
              
                //-------------------------------------------------------------------------
                // Methods
                //-------------------------------------------------------------------------
              
                protected boolean processLogout( HttpServletRequest request )
                throws IOException
                {
                  if(! request.getMethod().equalsIgnoreCase("POST") )
                  {
                    return false;
                  }
              
                  String uri = request.getRequestURI();
              
                  // strip everything after the first semi-colon
                  int pathParamIndex = uri.indexOf(';');
                  if( pathParamIndex > 0 )
                  {
                    uri = uri.substring(0, pathParamIndex);
                  }
              
                  if(! uri.endsWith(request.getContextPath() + this.filterProcessesUrl) )
                  {
                    return false;
                  }
              
                  String sTicket = null;
              
                  BufferedReader reader = request.getReader();
              
                  String line = null;
                  while( (line = reader.readLine()) != null )
                  {
                    if( line.startsWith("logoutRequest=") )
                    {
                      int start = line.indexOf("<samlp:SessionIndex>");
                      int end = line.indexOf("</samlp:SessionIndex>");
              
                      if( start > -1 && start < end )
                      {
                        sTicket = line.substring(
                          start + "<samlp:SessionIndex>".length(),
                          end);
                      }
                    }
                  }
              
                  reader.close();
              
                  if( sTicket != null )
                  {
                    this.expiredTicketCache.putTicketInCache(sTicket);
                  }
              
                  return true;
                }
              
              }
              This filter processes the "logoutRequest" sent by CAS to registered services during the "/logout" process. It extracts the ticket from the request (you could use regular expressions, here, admittedly) and stores it in the "expired ticket cache".

              5. Here's the Spring bean definition for the "logout callback" filter:

              Code:
              <bean id="casLogoutCallbackFilter"
                    class="example.CasLogoutCallbackFilter">
                <property name="filterProcessesUrl">
                  <value>/j_acegi_cas_security_check</value>
                </property>
                <property name="expiredTicketCache">
                  <ref local="expiredTicketCache" />
                </property>
              </bean>
              The filter's URL must match the "filterProcessesUrl" assigned to your CASProcessingFilter, since CAS sends the "logoutRequest" back to the same service URL with which the webapp registered itself.

              (To be continued)

              Comment


              • #8
                6. Here's the "logout" filter:

                Code:
                import org.springframework.beans.factory.InitializingBean;
                import org.springframework.dao.DataRetrievalFailureException;
                import org.springframework.util.Assert;
                
                import org.acegisecurity.Authentication;
                import org.acegisecurity.context.SecurityContextHolder;
                import org.acegisecurity.providers.cas.CasAuthenticationToken;
                import org.acegisecurity.ui.cas.ServiceProperties;
                import org.acegisecurity.ui.logout.LogoutHandler;
                
                public class CasLogoutFilter implements Filter, InitializingBean
                {
                  private String filterProcessesUrl;
                  private String logoutSuccessUrl;
                  private LogoutHandler[] logoutHandlers;
                  private ServiceProperties serviceProperties;
                  private String logoutUrl;
                  private ExpiredTicketCache expiredTicketCache;
                
                
                  //-------------------------------------------------------------------------
                  // Methods
                  //-------------------------------------------------------------------------
                
                  /**
                   * The "magic" URL that triggers a CAS logout
                   */
                  public void setFilterProcessesUrl( String s )
                  {
                    this.filterProcessesUrl = s;
                  }
                
                  /**
                   * Where to go after user successfully logs out from CAS
                   */
                  public void setLogoutSuccessUrl( String s ) { this.logoutSuccessUrl = s; }
                
                  /**
                   * Logout handlers that clean up after logout
                   */
                  public void setLogoutHandlers( LogoutHandler[] handlers )
                  {
                    this.logoutHandlers = handlers;
                  }
                
                  /**
                   * Provides the "service URL" to send to CAS
                   */
                  public void setServiceProperties( ServiceProperties sp )
                  {
                    this.serviceProperties = sp;
                  }
                
                  /**
                   * The CAS logout URL
                   */
                  public void setLogoutUrl( String s ) { this.logoutUrl = s; }
                
                  /**
                   * The store of expired tickets received from CAS
                   */
                  public void setExpiredTicketCache( ExpiredTicketCache cache )
                  {
                    this.expiredTicketCache = cache;
                  }
                
                
                  //-------------------------------------------------------------------------
                  // Implements InitializingBean
                  //-------------------------------------------------------------------------
                
                  public void afterPropertiesSet() throws Exception
                  {
                    Assert.hasText(this.filterProcessesUrl, "filterProcessesUrl required");
                    Assert.hasText(this.logoutSuccessUrl, "logoutSuccessUrl required");
                    Assert.notEmpty(this.logoutHandlers, "logoutHandlers are required");
                    Assert.notNull(this.serviceProperties, "serviceProperties required");
                    Assert.notNull(this.logoutUrl, "logoutUrl required");
                  }
                
                
                  //-------------------------------------------------------------------------
                  // Implements Filter
                  //-------------------------------------------------------------------------
                
                  public void init( FilterConfig config ) throws ServletException { }
                
                  public void destroy() { }
                
                  public void doFilter(
                    ServletRequest request, ServletResponse response, FilterChain chain )
                  throws IOException, ServletException
                  {
                    if(! (request instanceof HttpServletRequest) )
                    {
                      throw new ServletException("Can only process HttpServletRequest");
                    }
                
                    if(! (response instanceof HttpServletResponse) )
                    {
                      throw new ServletException("Can only process HttpServletResponse");
                    }
                
                    HttpServletRequest httpRequest = (HttpServletRequest) request;
                    HttpServletResponse httpResponse = (HttpServletResponse) response;
                
                    boolean loggedOut = false;
                
                    Authentication auth =
                      SecurityContextHolder.getContext().getAuthentication();
                
                    // has the authentication's ticket expired because of a CAS logout
                    // initiated from another webapp?
                
                    if( auth instanceof CasAuthenticationToken &&
                      this.expiredTicketCache != null )
                    {
                      String serviceTicket = auth.getCredentials().toString();
                
                      if( this.expiredTicketCache.isTicketExpired(serviceTicket) )
                      {
                        for( int i = 0; i < this.logoutHandlers.length; i++ )
                        {
                          this.logoutHandlers[i].logout(httpRequest, httpResponse, auth);
                        }
                
                        this.expiredTicketCache.removeTicketFromCache(serviceTicket);
                
                        loggedOut = true;
                      }
                    }
                
                    // is the user explicitly requesting logout?
                
                    if( requiresLogout(httpRequest) )
                    {
                      if( loggedOut == false )
                      {
                        // we haven't called the logout handlers above, so do so now
                        for( int i = 0; i < this.logoutHandlers.length; i++ )
                        {
                          this.logoutHandlers[i].logout(httpRequest, httpResponse, auth);
                        }
                      }
                
                      String urlEncodedService = httpResponse.encodeURL(
                        this.serviceProperties.getService());
                
                      StringBuffer buffer = new StringBuffer(255);
                
                      synchronized( buffer )
                      {
                        buffer.append(this.logoutUrl);
                        buffer.append("?service=");
                        buffer.append(URLEncoder.encode(urlEncodedService, "UTF-8"));
                        buffer.append("&url=");
                
                        String successUrl = this.logoutSuccessUrl;
                
                        // ensure success URL is an absolute URL
                
                        if( !successUrl.startsWith("http://") &&
                          !successUrl.startsWith("https://") )
                        {
                          buffer.append(httpRequest.getContextPath());
                        }
                
                        buffer.append(successUrl);
                      }
                
                      // have browser tell CAS to log out
                      httpResponse.sendRedirect(buffer.toString());
                
                      return;
                    }
                
                    chain.doFilter(request, response);
                  }
                
                  protected boolean requiresLogout( HttpServletRequest request )
                  {
                    String uri = request.getRequestURI();
                
                    // strip everything after the first semi-colon
                    int pathParamIndex = uri.indexOf(';');
                    if( pathParamIndex > 0 )
                    {
                      uri = uri.substring(0, pathParamIndex);
                    }
                
                    return uri.endsWith(
                      request.getContextPath() + this.filterProcessesUrl);
                  }
                
                }
                7. Here's the Spring bean definition for the "logout" filter:

                Code:
                <bean id="casLogoutFilter"
                    class="example.CasLogoutFilter">
                  <property name="filterProcessesUrl">
                    <value>/Logout.html</value>
                  </property>
                  <property name="logoutSuccessUrl">
                    <value>/Home.html</value>
                  </property>
                  <property name="logoutHandlers">
                    <list>
                      <bean class="org.acegisecurity.ui.logout.SecurityContextLogoutHandler">
                        <property name="invalidateHttpSession"><value>true</value></property>
                      </bean>
                    </list>
                  </property>
                  <property name="serviceProperties">
                    <ref local="serviceProperties" />
                  </property>
                  <property name="logoutUrl">
                    <value>https://example.com/cas/logout</value>
                  </property>
                  <property name="expiredTicketCache">
                    <ref local="expiredTicketCache" />
                  </property>
                </bean>
                This filter does two things:
                a) when the user activates the "logout" link, "Logout.html", they are redirected to CAS' "/logout" page
                b) if the incoming request is from a previously-authenticated user whose ticket has expired because of a CAS logout initiated elsewhere, the user is logged out of the webapp.

                8. Here's the FilterChainProxy bean:

                Code:
                <bean id="filterChainProxy"
                    class="org.acegisecurity.util.FilterChainProxy">
                  <property name="filterInvocationDefinitionSource">
                    <value>
                      CONVERT_URL_TO_LOWERCASE_BEFORE_COMPARISON
                      PATTERN_TYPE_APACHE_ANT
                      /**=httpSessionContextIntegrationFilter,casLogoutFilter,casLogoutCallbackFilter,authenticationProcessingFilter,exceptionTranslationFilter,filterInvocationInterceptor,casSignonFilter
                    </value>
                  </property>
                </bean>
                The casLogoutFilter is pretty early in the chain because we want to be sure to log the expired user out before checking any authentication.

                The casLogoutCallbackFilter comes before the authenticationProcessingFilter, because we don't want the authenticationProcessingFilter to try to process the "/j_acegi_cas_security_check" request that CAS sends for the logout callback.

                Again, this appears to be working for me in my environment, which I have not yet deployed to a production server. Your mileage may vary (and if it does, I'd like to hear about it).

                (Sorry for the long post(s)!)

                Comment


                • #9
                  An small correction I has to add add to the CallBackLogoutFilter. Received message lines shoud be decoded, before it they are analyzed (processLogout method)

                  while( (line = reader.readLine()) != null )
                  {
                  line = URLDecoder.decode(line, "UTF-8");
                  if( line.startsWith("logoutRequest=") )
                  {
                  ....
                  }
                  }

                  Comment


                  • #10
                    This example seems great, I have one question though. When I log-off on one application I am not getting the POST message from CAS and yes I am using 3.1.1. I've noticed that a person has to register the applications with CAS services for this to happen but I have no idea where to register these services. Single SignOn is currently working as expected it's just that I cannot get the Single SignOff to work and there is very little to no docs on this. Is Acegi supporting this yet?

                    Thanks,
                    Colin

                    Comment


                    • #11
                      the POST message from CAS is not working in the previuos version of CAS. In cas-server-3.2.1-release the POST call is made and this filter works great to get the ticketid. I modify this lines to correct the filter
                      int start = line.indexOf("%3Csamlp%3ASessionIndex%3E");
                      int end = line.indexOf("%3C%2Fsamlp%3ASessionIndex%3E");
                      because I did not decode the line, but all work great.
                      Also I continue to working with sessionregistry and in the sessionController.registerSuccessfulAuthentication (Authentication request) I save to the sessionregistry some extra session information and request.getCredentials() in the case of CAS is the extra session information. with this filter i get the credential to logout directly obtaining SessionInformation and proceed to .expireNow();
                      thanks for all

                      Comment


                      • #12
                        where is ExpiredTickedCache

                        I could not found expired ticked cache. Where is ExpiredTickedCache class?

                        Uuganbold

                        Comment


                        • #13
                          Hi, lastbubble

                          Your solution is very greate. Thank you for your share.!!!!!!!!!

                          Comment

                          Working...
                          X