Container-Managed Security

Wire-level security and users/roles security are related as follows. Under users/roles security, a client furnishes an identification such as a username or even a digital certificate together with a security credential that vouches for the identification (for instance, a password or a signature on the digital certificate from a certificate authority). To avoid hijacking, the identification and the credential should be sent from the client to the server through a secure channel, for instance, over an HTTPS connection. Wire-level security is thus the foundation upon which users/roles security should be implemented, and HTTPS is an ideal way to provide wire-level security for web-based systems such as web services.

Users/roles security is a two-phase process (see Figure 6-8). In the first and required phase, the user provides an identification and a credential that vouches for the identification. A successful user authentication phase results in an authenticated subject. In the optional second phase, role authorization, the access permissions of the authenticated subject can be refined as needed. For example, in a software development organization there might be a distinction between a senior engineer and a starting programmer, in that the former can access resources (for instance, sensitive records in a database) that the latter cannot access. This distinction could be implemented with different authorization roles.

Users/roles security

Figure 6-8. Users/roles security

At what level should users/roles security be enforced? Enforcement at the application level does not scale easily, in that every web service (or website) would require code, perhaps consolidated into a library, dedicated to security; a web service still would need to link to such library code. The preferred approach is to hand over the security concerns to the service container—to Tomcat or Jetty. This is container-managed security, which is considered best practice. Tomcat’s implementation of container-managed security, like its management of wire-level security, is unobtrusive at the service level: no changes are required in the web service code to enable users/roles security. Once again, the configuration document web.xml is the key.

The RESTful predictions2 service once again can be augmented with container-managed security—and without any change to the code. The revised web.xml document is displayed in Example 6-9.

Example 6-9. The revised web.xml to support both HTTPS and users/roles security

The numbered lines in the revised web.xml need clarification.

Security role declaration
Line 1 declares a security-role, which is an authorization role, and line 2 sets the role’s name to bigshot. On the Tomcat side, a data store must contain the same role name, with details to follow.
Authorization constraint
The security-constraint element, introduced earlier, now contains two specific constraints: the user-data-constraint, which enforces HTTPS transport, from the earlier example; and the new auth-constraint (line 3), which is an authorization rather than an authentication constraint in the context of users/roles security. The authorization constraint specifies that access to the predictions2 resource, the service and its operations, is restricted to a client authorized as a bigshot.
User authentication method

The login-config element (line 4) designates BASIC as the user authentication method (line 5). HTTP 1.1 supports four authentication types: BASIC, FORM, DIGEST, and CLIENT-CERT. These four types were designed with websites in mind but are adaptable to web services as well. Here is a summary of the differences:

The very simplicity of the BASIC type is attractive for clients against RESTful services, especially if BASIC authentication is combined with HTTPS transport, which then provides the required username/password encryption.

The revised web.xml document specifies the type of HTTP authentication in use, BASIC, as well as the authorization role, bigshot, required of the client that accesses the predictions2 service. The next question is how Tomcat puts this security information to use, in other words, how Tomcat’s container-managed security works under the hood. Tomcat implements container-managed security with realms, which are akin to groups in Unix-type operating systems. In simplest form, a realm is a collection of usernames and passwords together the authorization roles, if any, associated with the usernames. The purpose of a realm is to coordinate various security resources in support of a single policy on access control. On the service side, security information needs to be saved in a data store such as a relational database system; Tomcat realms provide the details about how the security information is to be saved and accessed.

Tomcat7 comes with six standard plug-ins, all of which have Realm in their names. Developers are free to develop additional Realm plug-ins. Here are the six native Tomcat plug-ins with a short description of each:

Under any of these choices, it is the Tomcat container rather than the application that becomes the security provider. With respect to the options, the path of least resistance leads to the default, the UserDatabaseRealm. Here is the data store, the XML file tomcat-users.xml. The five elements commented out act as Tomcat’s tutorial about how the file is to be used. My additions are lines 1 and 2. Line 1 declares the security role used in line 2, which specifies a username and an associated password:

<tomcat-users>
  <role rolename="bigshot"/>                                         1
  <user username="moe" password="MoeMoeMoe" roles="bigshot"/>        2
</tomcat-users>

With the UserDatabaseRealm now configured, the security process can be summarized as follows:

On the service side, Tomcat is responsible for conducting the user authentication and role authorization. The burden now shifts to the client, which must properly format, within an HTTPS request, the username and password information. On the service side, the required changes are limited to the web service’s configuration file, web.xml, and to the Tomcat UserDatabaseRealm file, tomcat-users.xml. No code in the predictions2 service needs to change.

The PredictionsHttpsClientAA (see Example 6-10) adds users/roles security on the client side to the earlier HTTPS client against the predictions2 service. The changes are quite small.

Example 6-10. The PredictionsHttpsClientAA client against the predictions2 service

import java.net.URL;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import java.security.KeyStore;
import javax.net.ssl.TrustManagerFactory;
import javax.net.ssl.X509TrustManager;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.SSLSession;
import java.security.cert.X509Certificate;
import java.security.SecureRandom;
import java.io.FileInputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.ByteArrayOutputStream;
import org.apache.commons.codec.binary.Base64;

public class PredictionsHttpsClientAA {
    private static final String endpoint = "https://localhost:8443/predictions2";
    private static final String truststore = "test.keystore";

    public static void main(String[ ] args) {
        new PredictionsHttpsClientAA().runTests();
    }
    private void runTests() {
        try {
            SSLContext sslCtx = SSLContext.getInstance("TLS");
            char[ ] password = "qubits".toCharArray();
            KeyStore ks = KeyStore.getInstance("JKS");
            FileInputStream fis = new FileInputStream(truststore);
            ks.load(fis, password);
            TrustManagerFactory tmf = TrustManagerFactory.getInstance("SunX509");
            tmf.init(ks); // same as keystore
            sslCtx.init(null,                   // not needed, not challenged
                        tmf.getTrustManagers(),
                        new SecureRandom());
            HttpsURLConnection.setDefaultSSLSocketFactory(sslCtx.getSocketFactory());
            // Proof of concept tests.
            String uname = "moe";
            String passwd = "MoeMoeMoe";
            getTest(uname, passwd);
            postTest(uname, passwd);
            getTestAll(uname, passwd);     // confirm POST test
            deleteTest(uname, passwd, "31");
            getTestAll(uname, passwd);     // confirm DELETE test
        }
        catch(Exception e) { throw new RuntimeException(e); }
    }
    private HttpsURLConnection getConnection(URL url,
                                             String verb,
                                             String uname,
                                             String passwd) {
        try {
            HttpsURLConnection conn = (HttpsURLConnection) url.openConnection();
            conn.setDoInput(true);
            conn.setDoOutput(true);
            conn.setRequestMethod(verb);
            // authentication (although header name is Authorization)
            String userpass = uname + ":" + passwd;
            String basicAuth = "Basic " +
               new String(new Base64().encode(userpass.getBytes()));
            conn.setRequestProperty ("Authorization", basicAuth);
            conn.setHostnameVerifier(new HostnameVerifier() {
                    public boolean verify(String host, SSLSession session) {
                        return host.equals("localhost"); // for development
                    }
                });
            return conn;
        }
        catch(Exception e) { throw new RuntimeException(e); }
    }
    private void getTest(String uname, String passwd) {
        getTestAll(uname, passwd);
        getTestOne(uname, passwd, "31");
    }
    private void getTestAll(String uname, String passwd) {
        try {
            URL url = new URL(endpoint);
            HttpsURLConnection conn = getConnection(url, "GET", uname, passwd);
            conn.connect();
            readResponse("GET all request:\n", conn);
            conn.disconnect();
        }
        catch(Exception e) { throw new RuntimeException(e); }
    }
    private void getTestOne(String uname, String passwd, String id) {
        try {
            URL url = new URL(endpoint + "?id=" + id);
            HttpsURLConnection conn = getConnection(url, "GET", uname, passwd);
            conn.connect();
            readResponse("GET request for " + id + ":\n", conn);
            conn.disconnect();
        }
        catch(Exception e) { throw new RuntimeException(e); }
    }
    private void postTest(String uname, String passwd) {
        try {
            URL url = new URL(endpoint);
            HttpsURLConnection conn = getConnection(url, "POST", uname, passwd);
            conn.connect();
            writeBody(conn);
            readResponse("POST request:\n", conn);
            conn.disconnect();
        }
        catch(Exception e) { throw new RuntimeException(e); }
    }
    private void deleteTest(String uname, String passwd, String id) {
        try {
            URL url = new URL(endpoint + "?id=" + id);
            HttpsURLConnection conn = getConnection(url, "DELETE", uname, passwd);
            conn.connect();
            readResponse("DELETE request:\n", conn);
            conn.disconnect();
        }
        catch(Exception e) { throw new RuntimeException(e); }
    }
    private void writeBody(HttpsURLConnection conn) {
        try {
            String pairs = "who=Freddy&what=Avoid Friday nights if possible.";
            OutputStream out = conn.getOutputStream();
            out.write(pairs.getBytes());
            out.flush();
        }
        catch(Exception e) { throw new RuntimeException(e); }
    }
    private void readResponse(String msg, HttpsURLConnection conn) {
        try {
            byte[ ] buffer = new byte[4096];
            InputStream in = conn.getInputStream();
            ByteArrayOutputStream out = new ByteArrayOutputStream();
            int n = 0;
            while ((n = in.read(buffer)) != -1) out.write(buffer, 0, n);
            in.close();
            System.out.println(new String(out.toByteArray())); // stringify and print
        }
        catch(Exception e) { throw new RuntimeException(e); }
    }
}

The getConnection method has three newlines:

String userpass = uname + ":" + passwd;                                  1
String basicAuth = "Basic " +
                   new String(new Base64().encode(userpass.getBytes())); 2
conn.setRequestProperty ("Authorization", basicAuth);                    3

A userpass string is created as a key/value pair, with the colon, :, as the separator, from the parameters uname and passwd (line 1). The userpass is then encoded in base64 and has Basic prepended (line 2). The result is inserted into the HTTPS headers, with Authorization as the key. For moe as the username and MoeMoeMoe as the password, the resulting header is:

Authorization: Basic bW9lOk1vZU1vZU1vZQ==

This setup follows HTTP 1.1 guidelines and meets Tomcat expectations about how the authentication/authorization information is to be formatted in the HTTPS request. As usual, a client against a RESTful service needs to stay close to the HTTP/HTTPS metal.

The curl utility is an alternative to a full-blown RESTful client written in Java or some other language. For the predictions2 service accessible through HTTPS and with user-authentication/role authorization in play, this curl command sends a GET request:

% curl --verbose --insecure --user moe:MoeMoeMoe \
       https://localhost:8443/predictions2

The --insecure flag means that curl goes through handshake process but does not verify the digital certificates sent from the server; the verification would require that curl be pointed to the appropriate truststore file. In any case, the output from a sample run, edited slightly for readability, is shown in Example 6-11.

In the curl output, the character > introduces text lines sent from curl to the server, whereas the character < introduces text lines from from the server to curl. The lines that begin with a star, *, trace the TLS handshake process. Although curl recognizes (line 1) that the self-signed certificate from the server is worthless as a security credential, curl continues the process, again because of the --insecure flag, by sending a GET request over HTTPS to the predictions2 service; the service responds with a list of the predictions.

Tomcat supports HTTPS transport and users/roles security for SOAP-based services as well. A SOAP-based client built atop wsimport-generated artifacts can use a slightly higher level API than its REST-style counterpart to insert the required security credentials into an HTTPS request. This section uses a minimal SOAP-based service to focus on security in the client against the service.

The SOAP-based TempConvert service (see Example 6-12) has two operations: f2c converts temperatures from fahrenheit to centigrade and c2f converts them from centigrade to fahrenheit. With respect to security, the web.xml for this service is essentially the same as for the RESTful and secure predictions2 service.

However, the web.xml for the SOAP-based service needs to reference the Metro WSServlet (line 2), which acts as the intermediary between the servlet container and the service (see Example 6-13); the additional configuration file, sun-jaxws.xml, is likewise required.

Lines 1 and 2 are the only changes to the web.xml used in the earlier predictions2 service. With the web.xml and sun-jaxws.xml in place, the TempConvert service can be deployed in the usual way:

% ant deploy -Dwar.name=tc

How should the wsimport-generated artifacts be generated for a service accessible only through HTTPS? The attempt:

% wsimport -p tcClient -keep https://localhost:8443/tc?wsdl

generates a sun.security.validator.ValidatorException precisely because wsimport is unable to conduct the HTTPS handshake: the utility does not have access to a truststore against which the server’s digital certificate(s) can be verified. The service is HTTPS-secured and, therefore, so is the service’s dynamically generated WSDL. The wsgen utility provides a workaround. The command:

% wsgen -cp . tc.TempConvert -wsdl

generates the TempConvertService.wsdl file and the TempConvertService_schema1.xsd file. The wsimport utility can now be targeted at the WSDL:

% wsmport -p tcClient -keep TempConvertService.wsdl

The only drawback is that the service’s URL is not in the class TempConvertService because the WSDL used is not generated dynamically. The TempConvertClient (see Example 6-14) shows how to overcome this drawback.

Example 6-14. The TempConvertClient against the SOAP-based TempConvert service

import tcClient.TempConvertService;
import tcClient.TempConvert;
import javax.xml.ws.BindingProvider;
import java.util.Map;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.SSLSession;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
import javax.net.ssl.HttpsURLConnection;
import java.security.cert.Certificate;
import java.security.cert.X509Certificate;

public class TempConvertClient {
    private static final String endpoint = "https://localhost:8443/tc";
    // Make the client "trusting" and handle the hostname verification.
    static {                                                                   1
        HttpsURLConnection.setDefaultHostnameVerifier(new HostnameVerifier() {
                public boolean verify(String name, SSLSession session) {
                    return true; // allow everything
                }
            });
        try {
            TrustManager[ ] trustMgr = new TrustManager[ ] {
                new X509TrustManager() {
                    public X509Certificate[ ] getAcceptedIssuers() { return null; }
                    public void checkClientTrusted(X509Certificate[ ] cs, String t)
                        { }
                    public void checkServerTrusted(X509Certificate[ ] cs, String t)
                        { }
                }
            };
            SSLContext sslCtx = SSLContext.getInstance("TLS");
            sslCtx.init(null, trustMgr, null);
            HttpsURLConnection.setDefaultSSLSocketFactory(sslCtx.getSocketFactory());
        }
        catch(Exception e) { throw new RuntimeException(e); }
    }
    public static void main(String args[ ]) {
        if (args.length < 2) {
            System.err.println("Usage: TempConvertClient <uname> <passwd>");
            return;
        }
        String uname = args[0];
        String passwd = args[1];
        TempConvertService service = new TempConvertService();
        TempConvert port = service.getTempConvertPort();
        BindingProvider prov = (BindingProvider) port;                         2
        prov.getRequestContext().put(BindingProvider.ENDPOINT_ADDRESS_PROPERTY,
                                     endpoint);                                3
        prov.getRequestContext().put(BindingProvider.USERNAME_PROPERTY,
                                     uname);                                   4
        prov.getRequestContext().put(BindingProvider.PASSWORD_PROPERTY,
                                     passwd);                                  5
        System.out.println("f2c(-40.1) = " + port.f2C(-40.1f));
        System.out.println("c2f(-40.1) = " + port.c2F(-40.1f));
        System.out.println("f2c(+98.7) = " + port.f2C(+98.7f));
    }
}

The TempConvertClient uses a static block (line 1) to make itself into a trusting client that does not check the server’s digital certificate during the HTTPS handshake; the static block also instructs the HostnameVerifier to allow client access to any host, including localhost. The static block isolates the transport-level security so that the focus can be kept on the users/roles security. By the way, the static block exploits the fact that a JAX-WS client uses, under the hood, the HttpsURLConnection of earlier RESTful examples.

To gain access to the transport level, in particular to the headers in the HTTPS request, the TempConvertClient casts the port reference to a BindingProvider (line 2). The endpoint then is set (line 3) to the correct URL because the wsimport-generated classes do not have a usable URL. The username and password, entered as command-line arguments, are likewise placed in the HTTPS headers (lines 4 and 5). This SOAP-based client need not bother with creating a single string out of the username and password or with encoding these in base64. Instead, the client uses the intuitive:

BindingProvider.USERNAME_PROPERTY
BindingProvider.PASSWORD_PROPERTY

keys and sets the value for each. After the setup, the client makes three calls against the SOAP-based service. The output is:

f2c(-40.1) = -40.055557
c2f(-40.1) = -40.18
f2c(+98.7) = 37.055557

A downside of BASIC authentication is that a client’s password must be stored, as is, on the server side so that the received password can be compared against the stored password. The DIGEST option requires only that the hash value of the password be stored on the server. The setup for the DIGEST option is trickier than for the BASIC option, however. Yet the BASIC option can be tweaked so that it behaves just like the DIGEST option. This section illustrates.

Tomcat comes with a digest utility: digest.sh for Unixy systems and digest.bat for Windows. The command:

% digest.sh -a SHA MoeMoeMoe

generates a 20-byte hash value, in hex, using the SHA-1 algorithm. Here is the value:

0f9e52090a322d7f788db2ae6b603e8efbd7fbd1

In the TOMCAT_HOME/conf/tomcat-users.xml file, this value replaces the password for moe (line 1):

<?xml version='1.0' encoding='utf-8'?>
<tomcat-users>
  <role rolename="bigshot"/>
  <user username="moe"
        password="0f9e52090a322d7f788db2ae6b603e8efbd7fbd1"  1
        roles="bigshot"/>
</tomcat-users>

The file is otherwise unchanged.

The digest utility is implemented with the RealmBase.Digest method, which can be used in a Java client. The revised client against the TempConvertService, named TempConvertClient2 (see Example 6-15), illustrates.

Most of the code in the TempConvertClient2 client is the same as that in the original. The import in line 1 is the first difference: the Tomcat libraries include the RealmBase class whose Digest method is of interest (line 2). The Digest method generates the hash value for the sample password, in this case MoeMoeMoe, which is given as a command-line argument. The hash value instead of the actual password then is placed in the HTTPS headers (line 3). On the service side, the send hash value is compared against the hash value stored in the revised tomcat-users.xml. The ZIP with the sample code includes runClient.xml, an Ant script to compile and execute the TempConvertClient2. A sample invocation with output is:

% ant -f runClient.xml -Darg1=moe -Darg2=MoeMoeMoe

Buildfile: run.xml
compile:
run:
     [java] f2c(-40.1) = -40.055557
     [java] c2f(-40.1) = -40.18
     [java] f2c(+98.7) = 37.055557