SPNEGO authentication

Posted on December 13, 2009 by Tommy McGuire
Labels: authentication, protocols, http, active directory, java
I have written previously about authenticating against Active Directory using LDAP, and I will probably be writing in the future about authenticating using a username and password against AD's Kerberos, but I have not mentioned the original reason for my interest in the grinding toil of groveling through Microsoft authentication protocols.

The Simple and Protected GSSAPI Negotiation scheme and its application to HTTP provide a nice solution to single sign-on to web applications in a Kerberos-based authentication environment. It works basically like this:

  1. The browser connects to the server and makes a request.

  2. The server determines that the client is not authorized and the request has no authorization information in it, and denies the request with a 401 status code and a "WWW-Authenticate" header of "Negotiate".

  3. The browser identifies that it has Kerberos authentication information available (a cached TGT, for example). It requests a ticket for the server from the ticket-grating authority and re-sends the original request with the ticket (wrapped in some GSS-API nonsense and base-64 encoded) in the "Authorization" header.

  4. The server recovers the ticket and validates it, based on its pre-authentication with the same Kerberos authority. Assuming the ticket validates, the server replies successfully to the request, possibly adding some authentication-negotiation-related information to the response.



There are some complications; the negotiation may take more than one round, etc., but for the most-common, successful case, it is a straightforward, relatively lightweight protocol that does not require (nor allow, for that matter) any user effort. So, how do you do this for a Java web application? Well, I have a servlet filter that implements the authentication protocol, and I would like to describe it here.

A servlet filter implementation requires three methods, and destroy() is currently unused. That leaves init(FilterConfig) and doFilter(ServletRequest, ServletResponse, FilterChain). init() handles the configuration, based on the configuration in the web app's web.xml file.

The first part of init recovers four pieces of information from the configuration:


final String spn = filterConfig.getInitParameter(SERVICE_PRINCIPAL_NAME_PARAMETER);
final String keyTab = filterConfig.getInitParameter(SERVICE_KEYTAB_FILE_PARAMETER);
if (spn == null || keyTab == null)
{
throw new ServletException( String.format("both %s and %s must be set", SERVICE_PRINCIPAL_NAME_PARAMETER, SERVICE_KEYTAB_FILE_PARAMETER) );
}
String passExpression = filterConfig.getInitParameter(NON_AUTHENTICATED_REGEX_PATTERN);
if (passExpression != null)
{
try
{
passPattern = Pattern.compile(passExpression);
}
catch (PatternSyntaxException e)
{
throw new ServletException( String.format("invalid %s expression: %s", NON_AUTHENTICATED_REGEX_PATTERN, passExpression), e);
}
}
final String debugParameter = filterConfig.getInitParameter(DEBUG_PARAMETER);
final boolean debug = (debugParameter != null && debugParameter.equalsIgnoreCase("true"));

Given these four items (actually, just the first three), I can create a Java standard Subject object which can validate incoming tickets.

final Configuration loginConfig = new KerberosLoginConfiguration(spn, keyTab, debug);
final Set principals = new HashSet(1);
principals.add(new KerberosPrincipal(spn));
final Subject subject = new Subject(false, principals, Collections.EMPTY_SET, Collections.EMPTY_SET);
try
{
final LoginContext loginContext = new LoginContext("", subject, null, loginConfig);
loginContext.login();
serviceSubject = loginContext.getSubject();
}
catch (LoginException e)
{
throw new ServletException( String.format("cannot login to %s with %s", spn, keyTab), e);
}

(This implementation is based on an example from Spring Security; my original implementation required several much uglier custom classes.)

The SPN, keytab file path, and debug flag are encoded in a KerberosLoginConfiguration which is used to conceptually pre-authenticate the server's resulting serviceSubject. The key is the keytab file, which contains the private key for the SPN, which is associated with a Kerberos account and exported from Active Directory. This keytab file should be should be protected in much the same way as an SSL key.

The KerberosLoginConfiguration is

public class KerberosLoginConfiguration extends Configuration
{
private final Map options = new HashMap();

public KerberosLoginConfiguration(String spn, String keyTabFile, boolean debug)
{
options.put("useKeyTab", "true");
options.put("storeKey", "true");
options.put("doNotPrompt", "true");
options.put("isInitiator", "false");
options.put("keyTab", keyTabFile);
options.put("principal", spn);
options.put("debug", Boolean.toString(debug));
}

@Override
public AppConfigurationEntry[] getAppConfigurationEntry(String name)
{
final AppConfigurationEntry[] entries = {
new AppConfigurationEntry(BaseKerberosSpnegoFilter.KERBEROS_LOGIN_MODULE, AppConfigurationEntry.LoginModuleControlFlag.REQUIRED, options)
};
return entries;
}
}

(All of this is using the hideous JAAS interfaces. I'm sorry.)

The Configuration contains a number of elements needed to configure a com.sun.security.auth.module.Krb5LoginModule, the value of KERBEROS_LOGIN_MODULE, to use the SPN and keytab file, and ultimately to handle the SPNEGO negotiation.

Once I have the serviceSubject, which is an attribute of the Filter implementation, I am off to the races. The actual filter starts by managing some necessary trivia.


final HttpServletRequest request = (HttpServletRequest) req;
final HttpServletResponse response = (HttpServletResponse) resp;

if (request.getHeader(KerberosAuthenticatedHttpRequest.AUTHENTICATED_USER_HEADER) != null)
{
// Don't come back now, ya hear?
log.warn(String.format("%s header set in request", KerberosAuthenticatedHttpRequest.AUTHENTICATED_USER_HEADER));
response.sendError(HttpServletResponse.SC_BAD_REQUEST, String.format("%s header set in request", KerberosAuthenticatedHttpRequest.AUTHENTICATED_USER_HEADER));
return;
}
if (passRequest(request))
{
chain.doFilter(request, response);
return;
}
final String header = request.getHeader(AUTHORIZATION_HEADER);
if (header == null)
{
response.setHeader(WWW_AUTHENTICATE_HEADER, NEGOTIATE_HEADER_VALUE);
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
return;
}
final String[] elements = header.split(" +");
if (! elements[0].equalsIgnoreCase("Negotiate"))
{
response.sendError(HttpServletResponse.SC_BAD_REQUEST, String.format("unknown authentication mechanism '%s'", header));
return;
}
else if (elements.length != 2 || elements[1] == null)
{
response.sendError(HttpServletResponse.SC_BAD_REQUEST, String.format("unable to get SPNEGO ticket in '%s'", header));
return;
}


Once the preliminaries are handled, the ticket validation can begin.

try
{
final SpnegoTicketValidator validator = Subject.doAs(serviceSubject, new SpnegoTicketValidator(elements[1]));
if (validator.getNextToken() != null)
{
response.setHeader(WWW_AUTHENTICATE_HEADER, String.format("%s %s", NEGOTIATE_HEADER_VALUE, validator.getNextToken()));
}
final GSSContext context = validator.getContext();
if (! context.isEstablished())
{
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
return;
}
// Authentication accepted; continue with request
chain.doFilter(new KerberosAuthenticatedHttpRequest(request, context), response);
// Dispose of the GSSContext, freeing any resources.
try
{
context.dispose();
}
catch (GSSException e)
{
System.err.println("error disposing of GSSContext");
e.printStackTrace(System.err);
}
}
catch (PrivilegedActionException e)
{
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "authorization failed");
}

Actually, the third line and the SpnegoTicketValidator object handles the important parts. The rest is more details. The ticket negotiation may have another token to pass back to the client, or the negotiation may not be complete; these two situations must be handled before the request is passed on to the client. Once the request is handled, the GSSContext should be free'ed. (And I just noticed a bug; the context should be disposed if the negotiation is not complete. Gotta fix that.)

The request is wrapped by a KerberosAuthenticatedHttpRequest, to pass the authentication information to the application. The important part is the SpnegoTicketValidator.

public class SpnegoTicketValidator implements PrivilegedExceptionAction
{
final private byte[] token;
private GSSContext context;
private byte[] nextToken;

public SpnegoTicketValidator(String ticket)
{
this.token = Base64.decodeBase64(ticket.getBytes());
}

@Override
public SpnegoTicketValidator run() throws Exception
{
context = GSSManager.getInstance().createContext((GSSCredential) null);
nextToken = context.acceptSecContext(token, 0, token.length);
return this;
}

public GSSContext getContext()
{
return context;
}

public String getNextToken()
{
return (nextToken == null) ? null : new String( Base64.encodeBase64(nextToken) );
}
}

The key is the cryptographic context; after it accepts the token, assuming the negotiation is complete, it contains the information about the user on the other end of the connection. This class also hangs onto the possible next token to return to the client.

There remains only a few loose ends. One is the passRequest method, which I wrote in this way to allow subclasses to override it with more complicated pass/authenticate decisions.

protected boolean passRequest(HttpServletRequest request)
{
if (passPattern == null)
{
return false;
}
StringBuffer url = request.getRequestURL();
String queryString = request.getQueryString();
if (queryString != null)
{
url.append('?').append(queryString);
}
return passPattern.matcher(url).matches();
}


The other loose end in the KerberosAuthenticatedHttpRequest, which I am too tired to reproduce at the moment. The highlights are:
active directory applied formal logic ashurbanipal authentication books c c++ comics conference continuations coq data structure digital humanities Dijkstra eclipse virgo electronics emacs goodreads haskell http java job Knuth ldap link linux lisp math naming nimrod notation OpenAM osgi parsing pony programming language protocols python quote R random REST ruby rust SAML scala scheme shell software development system administration theory tip toy problems unix vmware yeti
Member of The Internet Defense League
Site proudly generated by Hakyll.