Versions Compared

Key

  • This line was added.
  • This line was removed.
  • Formatting was changed.
Comment: Migrated to Confluence 5.3

AuthHandler revisited

The purpose of this paper is to apply twenty-twenty hindsight and the perspective of the Design Patterns and Dependency Injection movements to the Central Authentication Service version 2.0 "auth" authentication handling package. CAS has served Yale and others well since its inception and those involved in its design, implementation, and maintenance deserve a great deal of credit for its success. Nonetheless, an examination of the CAS 2.0 "AuthHandler" interface and its extensions and implementations reveals opportunites for improvement.

...

AuthHandler is a marker interface which declares no methods.

Code Block
titleAuthHandler
package edu.yale.its.tp.cas.auth;

/** Marker interface for authentication handlers. */
public interface AuthHandler { }

A PasswordHandler interface:

Code Block
titlePasswordHandler
package edu.yale.its.tp.cas.auth;

/** Interface for password-based authentication handlers. */
public interface PasswordHandler extends AuthHandler {

  /**
   * Authenticates the given username/password pair, returning true
   * on success and false on failure.
   */
  boolean authenticate(javax.servlet.ServletRequest request,
                       String username,
                       String password);

}

...

TrustHandler is probably named as it was because it might have authenticated services to one another based upon their IP addresses, say. However, it could also be used to implement arbitrary other authentication schemes, such as examination of a persistent cookie, validation of a ServiceTicket from another CAS server, examination of a response to a one-time-pad or other challenge, etc.

Code Block
titleTrustHandler
package edu.yale.its.tp.cas.auth;

/** Interface for server-based authentication handlers. */
public interface TrustHandler extends AuthHandler {

  /**
   * Allows arbitrary logic to compute an authenticated user from
   * a ServletRequest.
   */
  String getUsername(javax.servlet.ServletRequest request);

}

...

WatchfulPasswordHandler is a concrete an abstract implementation of the PasswordHandler interface. It is watchful in the sense that you can register with it failed authentication requests and ask it whether it wishes to veto a particular authentication request on the basis of there having been too many recent failed authentication requests from the apparent originating IP address of the request.

Code Block
titleWatchfulPasswordHandler
package edu.yale.its.tp.cas.auth.provider;

import java.util.*;
import edu.yale.its.tp.cas.auth.*;

/**
 * A PasswordHandler base class that implements logic to block IP addresses
 * that engage in too many unsuccessful login attempts.  The goal is to
 * limit the damage that a dictionary-based password attack can achieve.
 * We implement this with a token-based strategy; failures are regularly
 * forgotten, and only build up when they occur faster than expiry.
 */
public abstract class WatchfulPasswordHandler implements PasswordHandler {

  //*********************************************************************
  // Constants

  /**
   * The number of failed login attempts to allow before locking out
   * the source IP address.  (Note that failed login attempts "expire"
   * regularly.)
   */
  private static final int FAILURE_THRESHOLD = 100;

  /**
   * The interval to wait before expiring recorded failure attempts.
   */
  private static final int FAILURE_TIMEOUT = 60;


  //*********************************************************************
  // Private state

  /** Map of offenders to the number of their offenses. */
  private static Map offenders = new HashMap();

  /** Thread to manage offenders. */
  private static Thread offenderThread = new Thread() {
    public void run() {
      try {
        while (true) {
          Thread.sleep(FAILURE_TIMEOUT * 1000);
          expireFailures();
        }
      } catch (InterruptedException ex) {
        // ignore
      }
    }
  };

   static {
     offenderThread.setDaemon(true);
     offenderThread.start();
   }

  //*********************************************************************
  // Gating logic

  /**
   * Returns true if the given request comes from an IP address whose
   * allotment of failed login attemps is within reasonable bounds; 
   * false otherwise.  Note:  We don't actually validate the user
   * and password; this functionality must be implemented by subclasses.
   */
  public synchronized boolean authenticate(javax.servlet.ServletRequest request,
                              String netid,
                              String password) {
    return (getFailures(request.getRemoteAddr()) < FAILURE_THRESHOLD);
  }

  /** Registers a login failure initiated by the given address. */
  protected synchronized void registerFailure(javax.servlet.ServletRequest r) {
    String address = r.getRemoteAddr();
    offenders.put(address, new Integer(getFailures(address) + 1));
  }

  /** Returns the number of "active" failures for the given address. */
  private synchronized static int getFailures(String address) {
    Object o = offenders.get(address);
    if (o == null)
      return 0;
    else
      return ((Integer) o).intValue();
  }

  /**
   * Removes one failure record from each offender; if any offender's
   * resulting total is zero, remove it from the list.
   */
  private synchronized static void expireFailures() {
    // scoop up addresses from Map so as to avoid modifying the Map in-place
    Set keys = offenders.keySet();
    Iterator ki = keys.iterator();
    List l = new ArrayList();
    while (ki.hasNext())
      l.add(ki.next());

    // now, decrement and prune as appropriate
    for (int i = 0; i < l.size(); i++) {
      String address = (String) l.get(i);
      int failures = getFailures(address) - 1;
      if (failures > 0)
        offenders.put(address, new Integer(failures));
      else
        offenders.remove(address);
    }
  }

}

...

The distributed SampleHandler extends WatchfulPasswordHandler but does not use its watchful behavior:

Code Block
titleSampleHandler
package edu.yale.its.tp.cas.auth.provider;

import edu.yale.its.tp.cas.auth.*;

/** A simple, dummy authenticator. */
public class SampleHandler extends WatchfulPasswordHandler {

    public boolean authenticate(javax.servlet.ServletRequest request,
                                String username,
                                String password) {

        /*
         * As a demonstration, accept any username/password combination
         * where the username matches the password.
         */
        if (username.equals(password))
            return true;

        return false;
    }
}

A WatchfulSampleHandler would look something more like this:

Code Block
titleSampleHandler
package edu.yale.its.tp.cas.auth.provider;

import edu.yale.its.tp.cas.auth.*;

/** A simple, dummy authenticator. */
public class SampleHandler extends WatchfulPasswordHandler {

    public boolean authenticate(javax.servlet.ServletRequest request,
                                String username,
                                String password) {

        /*
         *  If the username doesn't equal the password, register the failure with our superclass
         *  and return false.
         */
        if (!username.equals(password)) {
                super.registerFailure(request);
                return false;
        }

        /* Allow WatchfulPasswordHandler to veto the authentication */
        return super.authenticate(request, username, password);
    }
}

...

I would therefore suggest a single interface for CAS authentication handlers:

Code Block
titleAuthenticationHandler

public interface AuthenticationHandler {

      /*
       * Examine a Request to determine whether it presents credentials
       * sufficient to athenticate a username.
       * @param request The request to examine for proof of identity
       * @return the username of the authenticated user, or null if none
       */
     public String authenticate(HttpServletRequest request);

}

...

The PasswordHandler interface might be fine as is:

Code Block
titlePasswordHandler
package edu.yale.its.tp.cas.auth;

/** Interface for password-based authentication handlers. */
public interface PasswordHandler {

  /**
   * Authenticates the given username/password pair, returning true
   * on success and false on failure.
   */
  boolean authenticate(javax.servlet.ServletRequest request,
                       String username,
                       String password);

}

...

To balance the above feature creep, I would strongly prefer to leave the HttpServletRequest out of the information available to a PasswordHandler. Need the HttpServletRequest? Then you're not a PasswordHandler, you're an AuthenticationHandler.

Code Block
titlePasswordHandler
package edu.yale.its.tp.cas.auth;

/** Interface for password-based authentication handlers. */
public interface PasswordHandler {

  /**
   * Authenticates the given username/password pair,
   * returning the authenticated username, or null if no user is authenticated
   * by the request.
   */
  String authenticate(
                       String username,
                       String password);

}

We can now provide an adaptor to allow PasswordHandler to be the Strategy whereby AuthenticationHandler is fulfilled.

Code Block
titlePasswordAuthenticationAdaptor, take one
public abstract class PasswordAuthenticationAdaptor 
                          implements AuthenticationHandler, PasswordHandler {

     public static final String PASSWORD_REQUEST_PARAM = "password";
     public static final String USERNAME_REQUEST_PARAM = "username";

     public final String authenticate(HttpServletRequest request) {

           return authenticate(request.getParameter(USERNAME_REQUEST_PARAM, 
                                request.getParameter(PASSWORD_REQUEST_PARAM);

     }

     /*
      * Concrete classes extending this must implement this method,
      * fulfilling the PasswordHandler interface.
      */
     protected abstract authenticate(String username, String password);

}

This is perhaps improvement, but awkward. We might be better off requiring the subclass to provide us with an instance of the interface:

Code Block
titlePasswordAuthenticationAdaptor, take two
public abstract class PasswordAuthenticationAdaptor
                          implements AuthenticationHandler {

     public static final String PASSWORD_REQUEST_PARAM = "password";
     public static final String USERNAME_REQUEST_PARAM = "username";

     public final String authenticate(HttpServletRequest request) {

           PasswordHandler passwordHandler = perAuthenticationPasswordHandler();

           return passwordHandler.authenticate(
                                request.getParameter(USERNAME_REQUEST_PARAM, 
                                request.getParameter(PASSWORD_REQUEST_PARAM);

     }

      /**
        * Override this method to provide a new instance of 
        * PasswordHandler for each call to authenticate().
        *  Suitable for when your PasswordHandler is not threadsafe.
        */
      abstract protected PasswordHandler perAuthenticationPasswordHandler();

}

...

A SamplePasswordHandler might look like this:

Code Block
SamplePasswordHandler
SamplePasswordHandler
/** 
 * Sample implementaiton of PasswordHandler which authenticates 
 * usernames which equal the presented password.
 */ 
public class SamplePasswordHandler implements PasswordHandler {

    public String authenticate(String username, String password) {
         if (username.equals(password))
              return username;
         return null;
    }

}

A SamplePasswordAuthenticationHandler suitable for instantiation and usage by CAS using its Class.forName() behavior in LoginServlet would look like this. Note that it chooses to re-use a single instance of SamplePasswordHandler, since SamplePasswordHandler is eminently threadsafe.

Code Block
SamplePasswordAuthenticationhandler
SamplePasswordAuthenticationhandler
/**
 * An example of extending PasswordAuthenticationAdaptor to inject
 * the SamplePasswordHandler as its PasswordHandler implementation.
public class SamplePasswordAuthenticationHandler 
                      extends PasswordAuthenticationAdaptor{

    private PasswordHandler passwordHandler = new SamplePasswordHandler();

    protected final PasswordHandler perAuthenticationPasswordHandler(){
         return passwordHandler;
    }
}

Incidentally, a PasswordAuthenticationHandler suitable for configuration in an IoC / Dependency Injection container would look like:

Code Block
titleInjectablePasswordAuthenticationHandler
/**
 * An example of extending PasswordAuthenticationAdaptor to provide
 *  setter method to inject the PasswordHandler.
public class InjectablePasswordAuthenticationHandler 
                      extends PasswordAuthenticationAdaptor{

    private PasswordHandler passwordHandler;

    protected final PasswordHandler perAuthenticationPasswordHandler(){
         return passwordHandler;
    }

    public void setPasswordHandler(PasswordHandler handler) {
         this.passwordHandler = handler;
    }
}

...

Note that implementing AuthHandler is not sufficient for CAS's needs:

Code Block
titleBeing an AuthHandler isn't good enough
      // create an instance of the right authentication handler
      String handlerName =
        app.getInitParameter("edu.yale.its.tp.cas.authHandler");
      if (handlerName == null)
        throw new ServletException("need edu.yale.its.tp.cas.authHandler");
      handler = (AuthHandler) Class.forName(handlerName).newInstance();
      if (!(handler instanceof TrustHandler)
          && !(handler instanceof PasswordHandler))
        throw new ServletException("unrecognized handler type: " + 
          handlerName);

And CAS LoginServlet switches on the implementation of AuthHandler:

Code Block
titleSwitching on the AuthHandler
 // if not, then see if our AuthHandler can help
    if (handler instanceof TrustHandler) {

   ...   

    } else if (handler instanceof PasswordHandler

    ....

...

Instead, CAS LoginServlet asks its instance of AuthenticationHandler for the authenticated username, if any.

Code Block
titleLoginServlet inititialization

      // create an instance of the authentication handler
      String handlerName =
        app.getInitParameter("edu.yale.its.tp.cas.authHandler");
      if (handlerName == null)
        throw new ServletException("need edu.yale.its.tp.cas.authHandler");
      handler = (AuthenticationHandler) Class.forName(handlerName).newInstance();

and in doGet():

Code Block
titleLoginServlet's doGet() implementation
  public void doGet(HttpServletRequest request, HttpServletResponse response)
      throws ServletException, IOException {

    // avoid caching (in the stupidly numerous ways we must)
    response.setHeader("Pragma", "no-cache");
    response.setHeader("Cache-Control","no-store");
    response.setDateHeader("Expires",-1);

    // check to see whether we've been sent a valid TGC
    Cookie[] cookies = request.getCookies();
    TicketGrantingTicket tgt = null;
    if (cookies != null) {
      for (int i = 0; i < cookies.length; i++) {
        if (cookies[i].getName().equals(TGC_ID)) {
          tgt = 
            (TicketGrantingTicket) tgcCache.getTicket(cookies[i].getValue());
          if (tgt == null)
            continue;
          // unless RENEW is set, let the user through to the service.
          // otherwise, fall through and we'll be handled by authentication
          // below.  Note that tgt is still active.
          if (request.getParameter(RENEW) == null) {
            grantForService(request, response, tgt,
              request.getParameter(SERVICE), false);
            return;
          }
        }
      }
    }

    // if not, but if we're passed "gateway", then simply bounce back
    if (request.getParameter(SERVICE) != null &&
        request.getParameter(GATEWAY) != null) {
      request.setAttribute("serviceId", request.getParameter(SERVICE));
      app.getRequestDispatcher(redirect).forward(request, response);
      return;
    }

    // check for valid loginTicket

    if (ltCache.getTicket(request.getParameter("lt")) != null) {
      // loginTicket was valid.
      // see if our AuthenticationHandler can help
    
      // try to get a trusted username by interpreting the request
      String username = handler.authenicate(request);
      if (username != null) {
          // success: send a new TGC if we don't have a valid TGT from above
          if (tgt == null) {
            tgt = sendTgc(username, request, response);
          } else if (!tgt.getUsername().equals(username)) {
            // we're coming into a renew=true as a different user...
            // expire the old tgt
            tgt.expire();
            // and send a new one
            tgt = sendTgc(username, request, response);
          }
          sendPrivacyCookie(request, response);
          grantForService(request, response, tgt, request.getParameter(SERVICE),
            true);
          return;
        } else {
          request.setAttribute("edu.yale.its.tp.cas.failedAuthentication", "");
        }
      } 

      }

    // record the service in the request
    request.setAttribute("edu.yale.its.tp.cas.service",
      request.getParameter(SERVICE));

    // no success yet, so generate a login ticket and forward to the 
    // login form
    try {
      String lt = ltCache.addTicket();
      request.setAttribute("edu.yale.its.tp.cas.lt",lt);
    } catch(TicketException ex) {
      throw new ServletException(ex);
    }
    app.getRequestDispatcher(loginForm).forward(request, response);
  }

...

A Strategy Implementation

Code Block
titleWatchfulHandler
package edu.yale.its.tp.cas.auth.provider;

import java.util.*;
import edu.yale.its.tp.cas.auth.*;

/**
 * An AuthenticationHandler base class that implements logic to block IP addresses
 * that engage in too many unsuccessful login attempts.  The goal is to
 * limit the damage that a brute-force attack can achieve.
 * We implement this with a token-based strategy; failures are regularly
 * forgotten, and only build up when they occur faster than expiry.
 */
public abstract class WatchfulHandler implements AuthenticationHandler {

  //*********************************************************************
  // Constants

  /**
   * The number of failed login attempts to allow before locking out
   * the source IP address.  (Note that failed login attempts "expire"
   * regularly.)
   */
  private static final int FAILURE_THRESHOLD = 100;

  /**
   * The interval to wait before expiring recorded failure attempts.
   */
  private static final int FAILURE_TIMEOUT = 60;


  //*********************************************************************
  // Private state

  /** Map of offenders to the number of their offenses. */
  private static Map offenders = new HashMap();

  /** Thread to manage offenders. */
  private static Thread offenderThread = new Thread() {
    public void run() {
      try {
        while (true) {
          Thread.sleep(FAILURE_TIMEOUT * 1000);
          expireFailures();
        }
      } catch (InterruptedException ex) {
        // ignore
      }
    }
  };

   static {
     offenderThread.setDaemon(true);
     offenderThread.start();
   }

  //*********************************************************************
  // Gating logic

  /**
   * Delegates to the wrapped AuthenticationHandler in the case where the 
   * number of recent observed failures from the remote address from which
   * the request purports to originate is within reasonable bounds.
   */
  public final synchronized String authenticate(javax.servlet.ServletRequest request) {
    if (getFailures(request.getRemoteAddr()) > FAILURE_THRESHOLD)
        return null; // veto authentication
    AuthenticationHandler handler = perAuthenticationHandler();
    String authenticatedUsername = handler.authenticate(request);
    if (authenticatedUsername == null)
        registerFailure(request);
    return authenticatedUsername;
  }

  /** Registers a login failure initiated by the given address. */
  private synchronized void registerFailure(javax.servlet.ServletRequest r) {
    String address = r.getRemoteAddr();
    offenders.put(address, new Integer(getFailures(address) + 1));
  }

  /** Returns the number of "active" failures for the given address. */
  private synchronized static int getFailures(String address) {
    Object o = offenders.get(address);
    if (o == null)
      return 0;
    else
      return ((Integer) o).intValue();
  }

  /**
   * Removes one failure record from each offender; if any offender's
   * resulting total is zero, remove it from the list.
   */
  private synchronized static void expireFailures() {
    // scoop up addresses from Map so as to avoid modifying the Map in-place
    Set keys = offenders.keySet();
    Iterator ki = keys.iterator();
    List l = new ArrayList();
    while (ki.hasNext())
      l.add(ki.next());

    // now, decrement and prune as appropriate
    for (int i = 0; i < l.size(); i++) {
      String address = (String) l.get(i);
      int failures = getFailures(address) - 1;
      if (failures > 0)
        offenders.put(address, new Integer(failures));
      else
        offenders.remove(address);
    }
  }

   /**
    * Subclasses may override this method to provide either 
    *  a new AuthenticationHandler for each
    *  request (suitable for making otherwise non-threadsafe 
    *  AuthenticationHandlers usable) or the same
    *  AuthenticationHandler instance on each invocation 
    *  of this method (suitable for subclasses which are
    *  merely implementing this method to inject their 
    *  AuthenticationHandler to be wrapped by this class.
    */ 
   abstract protected AuthenticationHandler perAuthenticationHandler();

}

...

A subclass of WatchfulHandler suitable for configuration using dependency injection would be:

Code Block
titleInjectableWatchfulHandler

public class InjectableWatchfulHandler extends WatchfulHandler {
    private AuthenticationHandler handler;

    protected final AuthenticationHandler perAuthenticationHandler(){
        return this.handler;
    }

     public void setHandler(AuthenticationHandler handler) {
         this.handler = handler;
     }

}