WS-Security

WS-Security is a family of specifications (see Figure 6-9) designed to augment wire-level security (e.g., HTTPS) and container-managed security (e.g., Tomcat) by providing a unified, transport-neutral, container-neutral, end-to-end framework for higher levels of security such as message confidentiality and authentication/authorization.

The WS-Security specifications

Figure 6-9. The WS-Security specifications

The layered blocks above WS-Security in Figure 6-9 can be clarified briefly as follows. The first layer consists of WS-Policy, WS-Trust, and WS-Privacy. The second layer of WS-SecureConversation, WS-Federation, and WS-Authorization builds upon this first layer. The architecture is thus modular but also complicated. Here is a short description of each specification, starting with the first layer:

WS-Policy
This specification describes general security capabilities, constraints, and policies. For example, a WS-Policy assertion could stipulate that a message requires security tokens or that a particular encryption algorithm be used.
WS-Trust
This specification deals primarily with how security tokens are to be issued, renewed, and validated. In general, the specification covers brokered trust relationships.
WS-Privacy
This specification explains how services can state and enforce privacy policies. The specification also covers how a service can determine whether a requester intends to follow such policies.
WS-SecureConversation
This specification covers, as the name indicates, secure web service conversations across different sites and, therefore, across different security contexts and trust domains. The specification focuses on how a security context is created and how security keys are derived and exchanged.
WS-Federation
This specification addresses the challenge of managing security identities across different platforms and organizations. At the heart of the challenge is how to maintain a single, authenticated identity (for example, Alice’s security identity) in a heterogeneous security environment.
WS-Authorization
This specification covers the management of authorization data such as security tokens and underlying policies for granting access to secured resources.

WS-Security is often associated with federated security in the broad sense, which has the goal of cleanly separating web service logic from the high-level security concerns, in particular authentication/authorization, that challenge web service deployment. This separation of concerns is meant to ease collaboration across computer systems and trust realms.

Recall that SOAP-based web services are meant to be transport-neutral. Accordingly, SOAP-based services cannot depend simply on the reliable transport that HTTP and HTTPS provide, although most SOAP messages are transported over HTTP. HTTP and HTTPS rest on TCP/IP (Transmission Control Protocol/Internet Protocol), which supports reliable messaging. What if TCP/IP infrastructure is not available? The WS-ReliableMessaging specification addresses precisely the issue of delivering SOAP-based services over unreliable infrastructure.

A SOAP-based service can rely on the authentication/authorization support that a web container such as Tomcat or an application server such as Oracle WebLogic, JBoss, GlassFish, or WebSphere may provide. In this case, the service outsources users/roles security to the service container. The WS-Security specifications are a guide to how security in general can be handled from within SOAP messaging. Accordingly, the WS-Security specifications address security issues as part of SOAP itself rather than as the part of the infrastructure that happens to be in place for a particular SOAP-based service. The goals of WS-Security are often summarized with the phrase end-to-end security, which means that security matters are not delegated to either the transport level (e.g., HTTPS) or a particular service container (e.g., Tomcat) but, rather, handled directly through an appropriate security API. A framework for end-to-end security needs to cover the situation in which a message is routed through intermediaries, each of which may have to process the message, before reaching the ultimate receiver; thus, end-to-end security focuses on message content rather than on the underlying transport or the service container. As a result, SOAP messaging becomes considerably more complicated.

In order to focus squarely on WS-Security, the sample web service (see Example 6-16) is deliberately bare bones. Further, the Endpoint publisher is used to host the service despite the fact that Endpoint supports neither wire-level security nor users/roles authentication and authorization. The very point of WS-Security is to provide security within SOAP messaging. The Echo service focuses on how WS-Security supports user authentication in particular.

The publisher (see Example 6-17) first sets an Echo instance as the service endpoint (line 1) and then gets the Binding (line 2) in order to register a service-side handler (lines 3, 4, and 5). The publisher finishes its work by publishing the service at the specified URL (line 6).

The Echo class gives no hint of WS-Security, which is delegated to the handler level, in this case to the message handler ServiceHandler. This separation of concerns means that, at the application level, the Echo service looks like any other @WebService: the service is a collection of operations, in this case only the echo method.

At this point, a shift to the client side may be helpful because the client, too, has a handler; the service-side handler validates the information that the client-side handler puts into the SOAP request message. The client-side handler inserts a username and a password into the header of every SOAP request from the client. The service-side handler then verifies the identity of the user by using the password as the credential that vouches for the identity. The client’s request hits the Echo service only if the service-side handler is successful in its verification.

On the client side and on the service side, the labor is divided in similar ways. The client-side message handler inserts the username and password into the outgoing SOAP message but relies upon the Prompter, which in turn is a CallbackHandler, to prompt for and read in the username and password; this CallbackHandler obscures but, in this example, does not encrypt the password. The client-side message handler also inserts other security information into the SOAP request message (see Figure 6-10).

On the service side, the message handler delegates verification to a CallbackHandler of its own, the Verifier (see Figure 6-11). The Verifier, in turn, relies on other CallbackHandler instances to extract the authentication information and to verify the sent username/password against service-side copies of these. The architecture on the service side thereby complements the architecture on the client side.

An examination of a familiar request/response exchange, starting from a client request through the service response, should cast light on the implementation details. The EchoClientWSS client (see Example 6-18) relies on the usual wsimport-generated artifacts (lines 1 and 2) to get a port reference (line 3), which is cast to the data type BindingProvider (line 4) so that the client-side SOAPHandler, an instance of the ClientHandler class, can be linked dynamically with the client (line 5). With this setup in place, the EchoClientWSS then makes a call against the Echo service (line 6) and prints the response for confirmation (line 7). All of the WS-Security code is relegated to the ClientHandler.

The ClientHandler receives, from the underlying SOAP libraries on the client side, a SOAP message that represents a call to the echo operation in the Echo service. This message is passed to the ClientHandler (see Example 6-19), which does the WS-Security work. The result of this work impacts only the SOAP header, not the SOAP body; hence, the ClientHandler must be a SOAPHandler in order to access the SOAP header.

Example 6-19. The client-side ClientHandler, which uses the Prompter

import java.util.Set;
import java.util.HashSet;
import javax.xml.namespace.QName;
import javax.xml.soap.SOAPMessage;
import javax.xml.ws.handler.MessageContext;
import javax.xml.ws.handler.soap.SOAPHandler;
import javax.xml.ws.handler.soap.SOAPMessageContext;
import java.io.FileInputStream;
import java.io.File;
import com.sun.xml.wss.ProcessingContext;
import com.sun.xml.wss.SubjectAccessor;
import com.sun.xml.wss.XWSSProcessorFactory;
import com.sun.xml.wss.XWSSProcessor;

public class ClientHandler implements SOAPHandler<SOAPMessageContext> {
    private XWSSProcessor xwssClient;
    private boolean trace;

    public ClientHandler() {
        XWSSProcessorFactory fact = null;
        try {
            fact = XWSSProcessorFactory.newInstance();                            1
            FileInputStream config = new FileInputStream(new File("client.xml")); 2
            xwssClient =                                                          3
                fact.createProcessorForSecurityConfiguration(config, new Prompter());
            config.close();
        }
        catch (Exception e) { throw new RuntimeException(e); }
        trace = true; // set to true to enable message dumps
    }
    // Add a security header block
    public Set<QName> getHeaders() {                                             4
        String uri = "http://docs.oasis-open.org/wss/2004/01/" +
                     "oasis-200401-wss-wssecurity-secext-1.0.xsd";
        QName securityHdr = new QName(uri, "Security", "wsse");
        HashSet<QName> headers = new HashSet<QName>();
        headers.add(securityHdr);
        return headers;
    }
    public boolean handleMessage(SOAPMessageContext msgCtx) {
        Boolean outbound = (Boolean)
            msgCtx.get (MessageContext.MESSAGE_OUTBOUND_PROPERTY);
        SOAPMessage msg = msgCtx.getMessage();
        if (outbound.booleanValue()) {
            ProcessingContext pCtx = null;
            try {
                pCtx = xwssClient.createProcessingContext(msg);                   5
                pCtx.setSOAPMessage(msg);                                         6
                SOAPMessage secureMsg = xwssClient.secureOutboundMessage(pCtx);   7
                msgCtx.setMessage(secureMsg);                                     8

                if (trace) dump("Outgoing message:", secureMsg);
            }
            catch (Exception e) { throw new RuntimeException(e); }
        }
        return true;
    }
    public boolean handleFault(SOAPMessageContext msgCtx) { return true; }
    public void close(MessageContext msgCtx) { }
    private void dump(String msg, SOAPMessage soapMsg) {
        try {
            System.out.println(msg);
            soapMsg.writeTo(System.out);
            System.out.println();
        }
        catch(Exception e) { throw new RuntimeException(e); }
    }
}

The ClientHandler no-argument constructor creates an XWSSProcessor (lines 1 through 3), which generates the WS-Security artifacts that go into the revised SOAP message. Two arguments are required for the creation of the XWSSProcessor: a file from which configuration information can be read and a CallbackHandler, in this case the Prompter, that provides the username and password. The configuration file is minimalist:

<xwss:SecurityConfiguration
   xmlns:xwss="http://java.sun.com/xml/ns/xwss/config"
   dumpMessages="true" >
    <xwss:UsernameToken digestPassword="false"/>
</xwss:SecurityConfiguration>

The code for the Prompter is examined shortly.

A SOAP message handler must define four methods: getHeaders, handleMessage, handleFault, and close. Of the four methods, getHeaders executes first. Earlier examples of SOAP handlers defined the getHeaders method but never put this method to work. In this case, the getHeaders method (line 4) is put to work—the method adds an empty header block in the SOAP message:

<S:Header>
  <wsse:Security
     xmlns:wsse="http://docs.oasis-open.org/ \
                 wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"
     S:mustUnderstand="1">
  </wsse:Security>
</S:Header>

Although this header block is empty, it does contain the mustUnderstand attribute, with a value of 1 for true; WS-Security requires the attribute. Once the getHeaders method has done its part, the handleMessage method takes over to complete the work. This method creates a WS-Security processing context (line 5) that is used to transform the current SOAP message (line 6), with its newly added wsse:Security header block, into a secured SOAP message whose header contains the username and password (lines 7 and 8). Behind the scenes, the Prompter instance works with the XWSSProcessor to provide the required username and password. When the handleMessage method exits, the SOAP message has been transformed into something much larger. The client-side SOAP message before the handler operates is small (see Example 6-20), but this message becomes significantly larger after the handler has done its work (see Example 6-21).

The outgoing SOAP request now has, in the header, three items of security interest:

The low-level work on the client side falls to the Prompter class (see Example 6-22), which implements the CallbackHandler interface by defining the handle method. The details are tedious but the gist is clear: the Prompter, in a production environment, would prompt for a username and password by using a UsernameCallback (line 1) and a PasswordCallback, respectively. The XWSSProcessor, which has access to the two callbacks through the processing context, extracts the username and password so that these can be inserted into the outgoing SOAP message.

Example 6-22. The Prompter callback handler, which helps the ClientHandler

import javax.security.auth.callback.Callback;
import javax.security.auth.callback.CallbackHandler;
import com.sun.xml.wss.impl.callback.PasswordCallback;
import com.sun.xml.wss.impl.callback.PasswordValidationCallback;
import com.sun.xml.wss.impl.callback.UsernameCallback;
import java.io.BufferedReader;
import java.io.InputStreamReader;

// For ease of testing, the username and password are
// hard-wired in the handle method with local variables
// username and password. For production, the hard wirings
// would be removed.
public class Prompter implements CallbackHandler {
    private String readLine() {
        String line = null;
        try {
            line = new BufferedReader(new InputStreamReader(System.in)).readLine();
        }
        catch(Exception e) { throw new RuntimeException(e); }
        return line;
    }
    // Prompt for and read the username and the password.
    public void handle(Callback[ ] callbacks) {
        try {
            for (int i = 0; i < callbacks.length; i++) {
                if (callbacks[i] instanceof UsernameCallback) {
                    UsernameCallback cb = (UsernameCallback) callbacks[i];   1
                    /* Disable for testing.
                    System.out.print("Username: ");
                    String username = readLine();
                    */
                    String username = "fred"; // hard-wire for testing
                    if (username != null) cb.setUsername(username);
                }
                else if (callbacks[i] instanceof PasswordCallback) {
                    PasswordCallback cb = (PasswordCallback) callbacks[i];   2
                    /* Disable for testing
                    System.out.print("Password: ");
                    String password = readLine();
                    */
                    String password = "rockbed"; // hard-wire for testing
                    if (password != null) cb.setPassword(password);
                }
            }
        }
        catch(Exception e) { throw new RuntimeException(e); }
    }
}

On the client side, the XWSSProcessor could do more than it does in this example. For instance, the security processor could encrypt the blocks in the SOAP header, particularly the one with the password, and encrypt even the payload in the SOAP body. However, this first look at WS-Security is focused on the architecture and flow of control, and these additional steps would distract from that focus. It is now time to move over to the service side.

On the service side, the incoming SOAP message goes to the ServiceHandler (see Example 6-23), which verifies the security header blocks that the ClientHandler injects into the SOAP request. This handler also pares down the incoming message (see Example 6-21) to an ordinary-looking SOAP request:

<S:Envelope xmlns:S="http://schemas.xmlsoap.org/soap/envelope/">
  <S:Header/>
  <S:Body>
    <ns2:echo xmlns:ns2="http://echoService/">
      <arg0>Goodbye, cruel world!</arg0>
    </ns2:echo>
  </S:Body>
</S:Envelope>

This is almost the very request that the EchoClientWSS generates before the client-side handler goes into action. The one difference is that the pared-down, incoming message has a SOAP header—but an empty one.

Example 6-23. The service-side ServiceHandler

package echoService;

import java.util.Set;
import java.util.HashSet;
import javax.xml.namespace.QName;
import javax.xml.soap.SOAPMessage;
import javax.xml.ws.handler.MessageContext;
import javax.xml.ws.handler.soap.SOAPHandler;
import javax.xml.ws.handler.soap.SOAPMessageContext;
import java.io.ByteArrayInputStream;
import com.sun.xml.wss.ProcessingContext;
import com.sun.xml.wss.SubjectAccessor;
import com.sun.xml.wss.XWSSProcessorFactory;
import com.sun.xml.wss.XWSSProcessor;

public class ServiceHandler implements SOAPHandler<SOAPMessageContext> {
    private XWSSProcessor xwssServer = null;
    private boolean trace;

    public ServiceHandler() {
        XWSSProcessorFactory fact = null;
        try {
            fact = XWSSProcessorFactory.newInstance();                     1
            ByteArrayInputStream config = getConfig();                     2
            xwssServer =                                                   3
                fact.createProcessorForSecurityConfiguration(config,
                                                             new Verifier());
        }
        catch (Exception e) { throw new RuntimeException(e); }
        trace = true; // set to true to enable message dumps
    }
    public Set<QName> getHeaders() {                                       4
        String uri = "http://docs.oasis-open.org/wss/2004/01/" +
                     "oasis-200401-wss-wssecurity-secext-1.0.xsd";
        QName securityHdr = new QName(uri, "Security", "wsse");            5
        HashSet<QName> headers = new HashSet<QName>();
        headers.add(securityHdr);
        return headers;                                                    6
    }
    public boolean handleMessage(SOAPMessageContext msgCtx) {
        Boolean outbound = (Boolean)
            msgCtx.get (MessageContext.MESSAGE_OUTBOUND_PROPERTY);
        SOAPMessage msg = msgCtx.getMessage();
        if (!outbound.booleanValue()) {
            // Validate the message.
            try{
                ProcessingContext pCtx =
                   xwssServer.createProcessingContext(msg);
                pCtx.setSOAPMessage(msg);
                SOAPMessage verifiedMsg =
                   xwssServer.verifyInboundMessage(pCtx);                  7
                msgCtx.setMessage(verifiedMsg);                            8
                if (trace) dump("Incoming message:", verifiedMsg);
            }
            catch(Exception e) { throw new RuntimeException(e); }
        }
        return true;
    }
    public boolean handleFault(SOAPMessageContext msgCtx) { return true; }
    public void close(MessageContext msgCtx) { }
    private void dump(String msg, SOAPMessage soapMsg) {
        try {
            System.out.println(msg);
            soapMsg.writeTo(System.out);
            System.out.println();
        }
        catch(Exception e) { throw new RuntimeException(e); }
    }
    private ByteArrayInputStream getConfig() {                             9
        String config =
            "<xwss:SecurityConfiguration " +
            "xmlns:xwss=\"http://java.sun.com/xml/ns/xwss/config\" " +
            "dumpMessages=\"true\"><xwss:RequireUsernameToken " +
            "passwordDigestRequired=\"false\"/> " +
            "</xwss:SecurityConfiguration>";
        return new ByteArrayInputStream(config.getBytes());
    }
}

The structure of the ServiceHandler is very close to that of the ClientHandler. In the ServiceHandler, the handleMessage method is interested only in incoming SOAP messages, that is, requests. This handler has a XWSSProcessor (lines 1 through 3) created from a hard-wired configuration document (line 9) and associated with a Verifier instance, a Callbackhandler that extracts the security information—the nonce, the username, and the password—from the SOAP header for verification. Once the SOAP request has been validated, the newly verified and simplified SOAP message is passed on to the usual SOAP libraries, which transform the XML document into the appropriate Java objects so that the Echo service can do its thing.

The ServiceHandler also makes use of the getHeaders method, which is particularly important with respect to the SOAP response from the EchoService. Recall that the ServiceHandler, like every handler, is inherently bidirectional. The handleMessage method is coded so that this method ignores outgoing messages, but the getHeaders method injects, into the SOAP response from the Echo service, a WS-Security header with the mustUnderstand attribute set to true (line 5). In effect, the ServiceHandler is demanding that any receiver of the SOAP response, including the EchoClientWSS, stick by the WS-Security rules. If the getHeaders method simply returned null, a client-side exception would be thrown because the incoming message would not be formatted according to WS-Security standards.

The service-side Verifier, like the client-side Prompter, is a CallbackHandler delegated to do grunt work. In a production environment, the Verifier might check the username and password against a database record, but here, for simplicity, these are hard-wired in the code. The Verifier also uses a PlainTextPasswordVerifier because the password itself rather than a hash value of the password is sent in the message (Example 6-24).

Example 6-24. The service-side Verifier, a callback handler that helps the ServiceHandler

package echoService;

import javax.security.auth.callback.Callback;
import javax.security.auth.callback.CallbackHandler;
import javax.security.auth.callback.UnsupportedCallbackException;
import com.sun.xml.wss.impl.callback.PasswordCallback;
import com.sun.xml.wss.impl.callback.PasswordValidationCallback;
import com.sun.xml.wss.impl.callback.UsernameCallback;

// Verifier handles service-side callbacks for password validation.
public class Verifier implements CallbackHandler {
    // Username/password hardcoded for simplicity and clarity.
    private static final String _username = "fred";
    private static final String _password = "rockbed";

    // For password validation, set the validator to the inner class below.
    public void handle(Callback[ ] callbacks) throws UnsupportedCallbackException {
        for (int i = 0; i < callbacks.length; i++) {
            if (callbacks[i] instanceof PasswordValidationCallback) {
                PasswordValidationCallback cb =
                    (PasswordValidationCallback) callbacks[i];
                if (cb.getRequest() instanceof
                    PasswordValidationCallback.PlainTextPasswordRequest)
                    cb.setValidator(new PlainTextPasswordVerifier());
            }
            else
                throw new UnsupportedCallbackException(null, "Not needed");
        }
    }
    // Encapsulated validate method verifies the username/password.
    private class PlainTextPasswordVerifier
        implements PasswordValidationCallback.PasswordValidator {
        public boolean validate(PasswordValidationCallback.Request req)
              throws PasswordValidationCallback.PasswordValidationException {
            PasswordValidationCallback.PlainTextPasswordRequest plain_pwd =
                (PasswordValidationCallback.PlainTextPasswordRequest) req;
            return_username.equals(plain_pwd.getUsername()) &&
                  _password.equals(plain_pwd.getPassword());
        }
    }
}

On a successful verification, the Verifier validates fred (the username) as an authenticated subject whose public credential is the name fred and whose private credential is the password that vouches for Fred’s identity.

The security illustrated in this sample could be ratcheted up to Mutual Challenge Security (MCS) with digital certificates used on both sides for peer authentication. Further, the contents of SOAP messages could be encrypted at the SOAP level, which would result in significantly larger SOAP headers that specified all of the cryptographic information: encryption and message digest algorithms, digital certificate formats, policies on confidentiality, encoding practices, specification of which parts of the SOAP message are to be encrypted and even digitally signed, and so on.