If you are already aware of the Grizzly framework, you can skip forward to the Kerberization process.
The Grizzly NIO framework has become a popular choice for creating scalable web applications in Java. Amongst the many reasons for its growing popularity is the following:
- NIO capabilities
- Easy to use web framework
- Built in support for SSL
- Thrift support as part of the core framework
Security is of the utmost importance for enterprise software, and one feature that keeps enterprise software developers from using Grizzly is the lack of out-of-the-box support for Kerberos–a widely-used, but complex network authentication protocol.
We recently made an attempt to Kerberize one of our REST API servers which was written using the Grizzly framework. Currently, the most widely used Kerberos filter is the KerberosAuthenticationFilter included with Apache CXF.
What are request filters?
REST is based on HTTP, which is commonly served by a web framework. As a developer you can extend web-based applications by adding custom request filters which are invoked by the framework before processing takes place. These filters are used to perform tasks like user authentication.
The filter has been known to play nice with most mature products, including JBoss, Spring and Tomcat. However, Grizzly is left out of the party. The gist of the problem here is the injection of the MessageContext–CXF’s beefed-up implementation of SecurityContext, which Grizzly doesn’t handle gracefully..
At Cerebro, we are a justifiably tenacious bunch, so after evaluating our choices, we figured that the best solution is to solve the problem for everyone, once and for all–and decided to write the Kerberos filter for Grizzly using plain old JAAS constructs.
The Process:
We will break this down into four parts, not at all arranged according to complexity. This post covers only part one. The other three will be covered in the next post. The four parts are:
- Creating the KerberosFilter class
- Creating the KerberosSecurityContext class
- Creating the KerberosPrincipal class
- Exposing a REST Endpoint
1. Creating the KerberosFilter class
Grizzly registers filters just like any other resource. The steps that constitute the Kerberos authentication need to take place before the request reaches the REST endpoint. For this, the KerberosFilter needs to implement the javax.ws.rs.container.ContainerRequestFilter interface. The ContainerRequestFilter contract describes the filter() method that takes in a ContainerRequestContext, which is all we need at this point to authenticate an incoming client request.
As per its own definition, the ContainerRequestContext class is “A mutable class that provides request-specific information for the filter, such as request URI, message headers, message entity or request-scoped properties”.
Kerberos Ticket Exchange Sequence. Details and Image Source: Rutgers University
The first thing that needs to happen in a Kerberos handshake is ensuring that the incoming request has an AUTHORIZATION HTTP Header. In the absence of this, the server needs to send back a response with an HTTP 401 Unauthorized challenge header, containing the Authenticate: Negotiate status which shall elicit a response from the client that contains a well-formatted AUTHORIZATION HTTP Header as described below.
//Step 1 // context is of type javax.ws.rs.container.ContainerRequestContext; ContainerRequestContext context String authHeader = context.getHeaderString(HttpHeaders.AUTHORIZATION);if (authHeader == null) { // A Not Authorized response (HTTP 401) with an Authenticate: Negotiate // status constiutes a server challenge // The client is expected to respond to this with a well formed Authorization // header. LOGGER.debug(“No Authorization header is available ” + getFaultResponse()); throw new NotAuthorizedException(getFaultResponse()); }
Step 1–Request an Authorization Header
private static Response getFaultResponse() { return Response.status(Response.Status.UNAUTHORIZED.getStatusCode()) .header(HttpHeaders.WWW_AUTHENTICATE, NEGOTIATE_SCHEME) .build(); }
Defining getFaultResponse(). This sends the server challenge to elicit a client response
At this point, we expect the client (which has a TGT) to respond to the server’s Authenticate: Negotiate challenge, with a valid 2-part AUTHORIZATION header. A valid AUTHORIZATION header will have the following 2 components:
- The “Negotiate” string
- Base64 encoded service ticket.
Once again, if the required information is not found, the server can challenge the client again, requesting the required information and telling it is currently not authorized to access the resource.
// The authPair must contain two parts. The first is "Negotiate" header // and the second is the Base64 encoded service ticket. String authPair[] = authHeader.split(" "); final String NEGOTIATE_SCHEME = "Negotiate"if (authPair.length != 2 || !NEGOTIATE_SCHEME.equalsIgnoreCase(authPair[0])) { LOGGER.debug(“Negotiate Authorization scheme is expected”); throw new NotAuthorizedException(getFaultResponse()); }
Step 2–Get the header with Base64 encoded ticket and “Negotiate” token
However, if we do receive the properly formatted AUTHORIZATION header, we can safely move to the next step, which is to use the JAAS Configuration object (javax.security.auth.login.Configuration) in order to instantiate a LoginContext. Make sure you use the same name here as your Login Configuration object, as the name serves as an index to the configuration we want to use in this Context. Call LoginContext.login() to perform the actual authentication at this point, and get back the subject using LoginContext.getSubject().
Before we proceed any further, let’s try and explain the new terms that have just been introduced, or will be introduced in this article:
What is JAAS?
JAAS, or Java Authentication and Authorization Service is Java’s integrated service for authentication and authorization purposes. It is written as an implementation of the Pluggable Authentication Module (PAM) framework, allowing applications to remain independent from the underlying authentication technologies.
What is a Subject?
A Subject represents a grouping on related information for a single entity. This information includes the Subject’s identities (called Principals), as well as security-related attributes (called Credentials) (eg. passwords and cryptographic keys).
What is a Principal?
A Principal is a unique identity to which Kerberos can assign tickets. In the context of a Subject, each identity belonging to a subject is represented by a separate Principal.
The next step is to associate the subject with the current AccessControlContext. For this, we need to get the GSSContext and Subject. Follow the steps in the code to get the GSSContext.
private byte[] getServiceTicket(String encodedServiceTicket) { try { // If using Java 8, move to Base64#Decode() return DatatypeConverter.parseBase64Binary(encodedServiceTicket); } catch (IllegalArgumentException e) { throw new NotAuthorizedException(getFaultResponse()); } }byte [] serviceTicket = getServiceTicket(authPair[1]); /* * VERY IMPORTANT * DO NOT PRINT TICKET INFO IN PRODUCTION CODE. * THIS IS FOR TUTORIAL PURPOSES ONLY. * HOPEFULLY THE CODE IS BEING READ. */ System.out.println(“Succesfully received auth pair”, authPair[0] + “\n” + authPair[1]);Subject serviceSubject = loginAndGetSubject(); GSSContext gssContext = createGSSContext(context);private String getCompleteServicePrincipalName(ContainerRequestContext context) { String name = “HTTP/” + context.getUriInfo().getBaseUri().getHost(); if (realm != null) { name += “@” + realm; } return name; }
private GSSContext createGSSContext(ContainerRequestContext context) throws GSSException { /* * To reinvent the wheel, or understand this better * Refer to the following SO question and CXF KerberosAuthenticationFilter * https://stackoverflow.com/questions/25289231/using-gssmanager-to-validate-a-kerberos-ticket */ Oid oid = new Oid(KERBEROS_OID); GSSManager gssManager = GSSManager.getInstance(); String spn = getCompleteServicePrincipalName(context); System.out.println("SPN is: " + spn); // null Oid NameType passed below can be used to specify that a mechanism // specific default printable syntax should be assumed by each mechanism // that examines service principal name. // The mechanism is specified in canonicalize() below, where the Oid // for Krb5 dictates the default mechanism that examines spn. GSSName gssService = gssManager.createName(spn, null); return gssManager.createContext(gssService.canonicalize(oid), oid, null, GSSContext.DEFAULT_LIFETIME); }
Step 3 and 4–Login and get the Subject
Once succesfully done, call the static method Subject.doAs() and associate the subject to the current GSSContext.
Be careful to dispose the GSSContext if the Credential Delegation is not allowed — this will make sure you don’t persist any client principal information if your server is not allowed to.
At this point, we can instantiate the SecurityContext, passing it the ContainerRequestContext and the KerberosPrincipal by calling ContainerRequestContest.setSecurityContext(). This SecurityContext will be injected into REST endpoints that have the @Context SecurityContext annotation description.
Subject.doAs(serviceSubject, new ValidateServiceTicketAction(gssContext, serviceTicket));GSSName srcName = gssContext.getSrcName(); KerberosPrincipal kp = new KerberosPrincipal(gssContext.getSrcName().toString());// We check for credential delegation state here to have the opportunity to preserve // client principal information before GSSContext is lost or disposed if (!gssContext.getCredDelegState()) { LOGGER.debug(“Disposing GSS Context as credential delegation is disabled.”); gssContext.dispose(); gssContext = null; }
Step 4, 5 and 6–Authentication by Subject association and disposal of the GSSContext
Conclusion
In the next post, watch out for how to go about defining the javax.ws.rs.core.SecurityContext and java.security.Principal contracts in a simple way and exposing a Kerberos-enabled REST endpoint.
We are currently busy building a great data product and encounter many interesting challenges on a daily basis. If, like us, you enjoy solving interesting problems, mail us at careers@cerebrodata.com