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 "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.
What is
An AuthHandler marker interface:
AuthHandler is a marker interface which declares no methods.
package edu.yale.its.tp.cas.auth; /** Marker interface for authentication handlers. */ public interface AuthHandler { }
A PasswordHandler interface:
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); }
A TrustHandler interface
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.
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
WatchfulPasswordHandler is a concrete 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.
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 intent here is that subclasses of WatchfulPasswordHandler both register failed authentication requests with it when authentication fails and allow it to veto authentication of requests they would otherwise authenticate.
The distributed SampleHandler extends WatchfulPasswordHandler but does not use its watchful behavior:
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:
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); } }
Thoughts
I would suggest that the marker interface is not needed here. The work of a CAS AuthHandler is to examine an HttpServletRequest and to assert one of the following:
- The request presents credentials sufficient to declare the request to have been made by some particular user. (e.g., the request declares a username and presents an associated password such that we are authenticating the request as originating from the user with the given username).
- The request does not present credentials sufficient to declare the request to have been made by any particular user.
I would therefore suggest a single interface for CAS authentication handlers:
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); }
What of PasswordHandlers?
Username/Password authentication can be thought of as a Strategy for implementing the AuthenticationHandler interface.
The PasswordHandler interface might be fine as is:
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); }
However, I think I would prefer something more like the following, adding the ability to alias the user's asserted username to a canonical representation. For instance, a given institution might have the rule that all usernames are lowercase alphanumerics with no spaces. The PasswordHandler implementation might normalize the username before attempting to authenticate it against the password. The end user experience of having accidentally prefaced his username with a space character or having typed his username in capital letters becomes one of the system "knowing what he meant". Or an institution might choose to allow users to present either their netids or their email addresses. Feature creep, perhaps.
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.
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.
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:
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(); }
Subclasses must now implement perAuthenticationPasswordHandler() to provide a (potentially new) instance of your PasswordHandler for each call to authenticate(HttpServletRequest request). This incidentally supports non-threadsafe PasswordHandler implementations.
A SamplePasswordHandler might look like this:
Error rendering macro 'code': Invalid value specified for parameter 'lang'/** * 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.
Error rendering macro 'code': Invalid value specified for parameter 'lang'/** * 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:
/** * 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; } }
Convergence of TrustHandler, AuthHandler, and PasswordHandler to one interface (AuthenticationHandler) which provides the actual method CAS needs simplifies LoginServlet. LoginServlet currently switches on the implementation of AuthHandler.
Note that implementing AuthHandler is not sufficient for CAS's needs:
// 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:
// if not, then see if our AuthHandler can help if (handler instanceof TrustHandler) { ... } else if (handler instanceof PasswordHandler ....
This is no longer necessary.
Instead, CAS LoginServlet asks its instance of AuthenticationHandler for the authenticated username, if any.
// 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():
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); }
Watchfulness
But what of watchfulness? Watchfulness can be implemented as an abstract class which wraps an implementation of AuthenticationHandler which its concrete subclass is required to provide.
A Strategy Implementation
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(); }
The advantages of this approach:
Subclasses of WatchfulHandler must now implement perAuthenticationHandler(). They cannot help but properly check whether the Watchfulness behavior would veto authentication and cannot fail to report failed authentications. Additionally, developers of concrete implementations extending WatchfulHandler might choose to use a non-threadsafe AuthenticationHandler to accomplish the actual authentication, returning a new handler instance on each invocation of perAuthenticationHandler().
Applying IoC / dependency injection:
A subclass of WatchfulHandler suitable for configuration using dependency injection would be:
public class InjectableWatchfulHandler extends WatchfulHandler { private AuthenticationHandler handler; protected final AuthenticationHandler perAuthenticationHandler(){ return this.handler; } public void setHandler(AuthenticationHandler handler) { this.handler = handler; } }