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 |
---|
|
package edu.yale.its.tp.cas.auth;
/** Marker interface for authentication handlers. */
public interface AuthHandler { }
|
A PasswordHandler interface:
Code Block |
---|
|
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 |
---|
|
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 |
---|
title | WatchfulPasswordHandler |
---|
|
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 |
---|
|
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 |
---|
|
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 |
---|
title | AuthenticationHandler |
---|
|
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 |
---|
|
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 |
---|
|
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 |
---|
title | PasswordAuthenticationAdaptor, 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 |
---|
title | PasswordAuthenticationAdaptor, 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 |
---|
title | InjectablePasswordAuthenticationHandler |
---|
|
/**
* 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 |
---|
title | Being 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 |
---|
title | Switching 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 |
---|
title | LoginServlet 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 |
---|
title | LoginServlet'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 |
---|
|
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 |
---|
title | InjectableWatchfulHandler |
---|
|
public class InjectableWatchfulHandler extends WatchfulHandler {
private AuthenticationHandler handler;
protected final AuthenticationHandler perAuthenticationHandler(){
return this.handler;
}
public void setHandler(AuthenticationHandler handler) {
this.handler = handler;
}
}
|