Yuriy Zubarev

CAS - Non Sharable Credentials

2010-02-26

We have a requirement that a certain type of users cannot use the same credentials to have two or more simultaneous active sessions. I called this feature as “Non Sharable Credentials”. Probably there is a better name for it, but this one will do.

I first posted a question to cas-user mailing list and got a rather encouraging response. Even though CAS doesn’t provide such functionality right out of the box, it seems possible to search ticket registries to find existing ticket for a given principal. Luckily we have JPA-backed registry and at the time I didn’t quite understand why Scott Battaglia cautioned against loading the entire table of tickets. Couldn’t I just craft an appropriate JPA query?

Upon further inspection of database table-backed ticket registries it was quickly discovered that a principal for a ticket doesn’t have a dedicated column. Instead, this information is a small part of a serialized object stored in a BLOB field. I couldn’t query for it directly, so I had no choice but load records into memory, de-serialize the BLOB field and then check for my principal until the match is found. This approach could seriously slow down authentication process and therefore was quickly ruled out.

I really needed an efficient way to search for tickets belonging to a specified principal. I didn’t want to modify CAS code and change structure of database tables for the ticket registries. There is always a new version of CAS and an upgrade should be as easy as possible. Instead, I create another simple table:

CREATE TABLE `principalticket` (
  `ticket_id` varchar(255) NOT NULL,
  `principal` varchar(255) default NULL,
  PRIMARY KEY  (`ticket_id`)
)

The purpose for this table is self-evident. The real question is how to populate it every time a new ticket is issued. CAS is a perfect example of well written code thoroughly utilizing Spring framework. I decided to use Spring AOP and have an aspect that would be run as a ticket gets persisted. A snippet from deployerConfigContext.xml:

<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:aop="http://www.springframework.org/schema/aop"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.0.xsd
       http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-2.5.xsd">

  <aop:aspectj-autoproxy/>

  <aop:config>
    <aop:aspect id="ticketAddAspect" ref="nscBean">
      <aop:pointcut id="interceptingAddTicket" expression="bean(ticketRegistry) and execution(public * add*(..))"/>
      <aop:around pointcut-ref="interceptingAddTicket" method="addTicket"/>
    </aop:aspect>
  </aop:config>

  <bean id="nscBean" class="com...NSCBean">
    <constructor-arg index="0" ref="entityManagerFactory" />
  </bean>

  ...

I wanted to use the same approach for deleteTicket pointcut on ticketRegistry bean but I kept running into AOP related exceptions when a scheduled quartz job was trying to clean up ticket registries. I was not AOP guru and I had to finish the task in 2 days and I opted for a less glamorous solution of removing records from principalticket table. If I get back to this task and refactor the approach then I will update this post as well.

NSCBean (NonSharableCredentialsBean) is rather simple:

package ...;

import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import org.aspectj.lang.JoinPoint;
import org.jasig.cas.ticket.TicketGrantingTicket;
import org.springframework.aop.aspectj.MethodInvocationProceedingJoinPoint;
import org.springframework.orm.jpa.JpaTemplate;

public class NSCBean {

  private JpaTemplate jpaTemplate;

  public NSCBean(final EntityManagerFactory factory) {
    this.jpaTemplate = new JpaTemplate(factory);
  }

  public void addTicket(JoinPoint point) throws Throwable {
    if (point.getArgs() != null && point.getArgs().length > 0 && point.getArgs()[0] instanceof TicketGrantingTicket) {
      TicketGrantingTicket ticket = (TicketGrantingTicket) point.getArgs()[0];
      if (ticket.getAuthentication() != null && ticket.getAuthentication().getPrincipal() != null) {
        String principal = ticket.getAuthentication().getPrincipal().getId();
        EntityManager em = this.jpaTemplate.getEntityManagerFactory().createEntityManager();
        if (em.createNativeQuery("select * from principalticket where ticket_id = '" + ticket.getId() + "'").getResultList().size() == 0) {
          em.getTransaction().begin();
          em.createNativeQuery("insert into principalticket (ticket_id, principal) values ('" + ticket.getId() + "', '" + principal + "')").executeUpdate();
          em.getTransaction().commit();
        } else {
          // ticket/principal is already recorded
        }
      }
    } 

    if (point instanceof MethodInvocationProceedingJoinPoint) {
        ((MethodInvocationProceedingJoinPoint) p).proceed(p.getArgs());
    } 
  }

}

Records from principalticket table are cleaned up in a rather contentious way. During authentication process, if a ticket doesn’t exist in ticketgrantingticket but exists in principalticket then it gets deleted from the latter. Authentication itself, checking for non sharable credentials and cleaning up the new table is all part of a custom AuthenticationHandler. There is no point providing its code here as it deals with lots of logic particular to my company.

This post explains one way of achieving non-sharable credentials with CAS. At the heart of the approach is a usage of Spring AOP. AOP is usually advocated while dealing with cross-cutting concerns but it also comes very handy when you want to extend functionality of an existent product beyond its predefined extension hooks.

 

Comments :

blog comments powered by Disqus