Monday, October 23, 2006

Implementing Web Service Security: Accessing a Secure Service

In my last post I finished a handler to verify the signature in a SOAP message following a simple web service security scheme.

Next I was ready to use JAX-WS 2.0 to create a client to call my secure service. The wsimport tool does the work of generating the client code, but I needed to figure out how to insert my custom security element into the message header before it gets sent to the server.

The process of preparing to send a message from the client should look something like this:

  1. Create the message body, specifying the operation and relevant data or parameters.
  2. Add a SOAP Header element to the message if it does not already have one.
  3. Add a Security element to the header.
  4. Add the Timestamp and AccessID elements to the Security element.
  5. Create the Signature element based on the access ID, timestamp and canonicalized text of the message body and add it to the Security element.

The client code that invokes a specific web service operation should only have to worry about the first step of this process. The remaining steps are the same for all operations and should be handled transparently from the viewpoint of the caller.

The message processing pipeline for JAX-WS is similar whether you are sending or receiving messages. A message always passes through a handler chain before and after the message is actually processed. In this case, I needed to create a handler to insert the security element into the message header as described above.

My signature creation handler extended the same base handler I used on the server. In this handler the action happens in the handleOutbound method:

package member.client;

import java.util.Iterator;
import java.util.Map;
import java.util.Set;

import java.util.logging.Logger;

import org.w3c.dom.*;

import javax.xml.namespace.QName;
import javax.xml.ws.handler.MessageContext;
import javax.xml.ws.handler.soap.SOAPHandler;

import javax.xml.soap.SOAPBody;
import javax.xml.soap.SOAPElement;
import javax.xml.soap.SOAPException;
import javax.xml.soap.SOAPMessage;
import javax.xml.soap.SOAPPart;
import javax.xml.soap.SOAPEnvelope;
import javax.xml.soap.SOAPHeader;
import javax.xml.soap.SOAPHeaderElement;

import javax.xml.ws.handler.soap.SOAPMessageContext;

import member.common.Canonicalizer;
import member.common.Signature;
import member.common.BaseHandler;
import member.common.Timestamp;

/*
 * Adds a signature to outbound messages.
 */
public class SignatureCreationHandler extends BaseHandler {

  /**
   * TO DO: replace these with configuration properties
   **/
  private static final String ACCESS_ID = "karl";
  private static final String SECRET_KEY = "secret";

  private static final Logger log = Logger.getLogger("member");

  public boolean handleOutbound(SOAPMessageContext smc) throws SOAPException {

    SOAPMessage message = smc.getMessage();
    SOAPPart part = message.getSOAPPart();
    SOAPEnvelope envelope = part.getEnvelope();
    
    SOAPBody body = envelope.getBody();
    if (body == null) { 
      body = envelope.addBody();
    }
    
    SOAPHeader header = envelope.getHeader();
    if (header == null) {
      header = envelope.addHeader();
    }

    addSecurityElement(header, body);

    return true;
  }

  private void addSecurityElement(SOAPHeader header, SOAPBody body) 
    throws SOAPException {

    QName qName = getQName(body, SECURITY_ELEMENT);
    if (qName == null) { return; }
    SOAPHeaderElement securityElement = header.addHeaderElement(qName);
    
    qName = new QName("", TIMESTAMP_ELEMENT, "");
    SOAPElement timestampElement = securityElement.addChildElement(qName);
    
    String timestamp = Timestamp.create();
    timestampElement.addTextNode(timestamp);
    
    qName = new QName("", ACCESS_ID_ELEMENT, "");
    SOAPElement clientIDElement = securityElement.addChildElement(qName);
    clientIDElement.addTextNode(ACCESS_ID);

    qName = new QName("", SIGNATURE_ELEMENT, "");
    SOAPElement signatureElement = securityElement.addChildElement(qName);

    String bodyText = Canonicalizer.toText(body);
    String signedText = timestamp + ":" + ACCESS_ID + ":" + bodyText;

    String signature = Signature.create(SECRET_KEY, signedText);

    signatureElement.addTextNode(signature);
  }
}

Once the handler was written, the next challenge was to incorporate it into the service endpoint interface (SEI) that the wsimport tool generates based on a WSDL file. The endpoint interface is what a client uses to access the service.

To customize the endpoint interface, I created a binding file declaring a handler-chains element and passed it to the wsimport tool. The tool then added the specified nodes to the WSDL before generating the SEI:

<bindings 
    xmlns:xsd="http://www.w3.org/2001/XMLSchema"
    xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/"
    wsdlLocation="http://localhost:8082/member?wsdl"
    xmlns="http://java.sun.com/xml/ns/jaxws">    
  <bindings node="wsdl:definitions">
    <package name="member.client"/>
  </bindings>
  <bindings node="wsdl:definitions" 
    xmlns:javaee="http://java.sun.com/xml/ns/javaee">
    <javaee:handler-chains>
      <javaee:handler-chain>
 <javaee:handler>
   <javaee:handler-class>member.client.SignatureCreationHandler</javaee:handler-class>
 </javaee:handler>
      </javaee:handler-chain>
    </javaee:handler-chains>
  </bindings>
</bindings>

After running the wsimport tool, I was able to use the generated classes to run a simple test program that successfully executed an operation on the secure service:

package member.client;

import java.math.BigInteger;

import java.util.List;
import java.rmi.RemoteException;

public class Client {

  public static void main (String[] args) throws Exception {

    MemberService service = new MemberService();
    MemberPort port = service.getMemberServicePort();

    BigInteger memberID = new BigInteger("2");

    List sn = port.memberGroups(memberID);
  }
}

As illustrated by this simple test program, the handler manages security transparently for the client.

Implementing Web Service Security: Verifying the Message Signature

In my last post I sketched out some of the implementation details for a simple web service security scheme. Next I wanted to flesh out the implementation and incorporate it into my sample web service.

The process of verifying a message from the client should look something like this:

  1. Receive the message and parse it into a DOM.
  2. Retrieve the security element from the header.
  3. Retrieve the signature submitted by the client from the security element.
  4. Recreate the signature based on the submitted access ID, timestamp and canonicalized text of the message body (along with the shared secret key).
  5. If the signatures match, allow processing to continue. Otherwise generate a fault and abort processing.

The verification process should happen transparently from the standpoint of any particular web service operation, such as the getMembers method I wrote about earlier.

JAX-WS provides handlers to support this type of transparent pre- or post-processing. A handler that has access to the entire SOAP message (including the header) must implement the javax.xml.ws.handler.soap.SOAPHandler interface.

The SOAPHandler interface honestly seems a bit odd to me. The same method handleMessage is invoked on each message both before and after processing (See the Tech Tip for some good diagrams). Except in the seemingly unlikely event that you want the same behavior in both these situations, you need to test a context property and split the implementation of handleMessage into two branches. There is probably an explanation for this, but for the moment it is lost on me.

I started by implementing a generic starting point for this and other handlers I might need to write. The base handler simplifies handling of either the inbound or outbound processing phase:

package member.common;

import java.util.Iterator;
import java.util.Set;

import javax.xml.namespace.QName;
import javax.xml.ws.handler.MessageContext;
import javax.xml.ws.handler.soap.SOAPHandler;

import javax.xml.soap.*;

import javax.xml.ws.handler.soap.SOAPMessageContext;

public abstract class BaseHandler 
  implements SOAPHandler {

  protected static final String SECURITY_ELEMENT = "Security";
  protected static final String ACCESS_ID_ELEMENT = "AccessID";
  protected static final String TIMESTAMP_ELEMENT = "Timestamp";
  protected static final String SIGNATURE_ELEMENT = "Signature";

  private static SOAPFactory factory;

  public Set getHeaders() {
    return null;
  }

  public boolean handleMessage(SOAPMessageContext smc) {

    try {
      return isOutbound(smc) ? handleOutbound(smc) : handleInbound(smc);
    } catch (SOAPException e) {
      throw new RuntimeException(e);
    }
  }

  /**
   * Handles pre-processing of messages before being sent by a client
   * or processed by a server.
   **/
  protected boolean handleInbound(SOAPMessageContext smc) 
    throws SOAPException {
    return true;
  }

  /**
   * Handles post-processing of messages after being received by a client
   * or processed by a server.
   **/
  protected boolean handleOutbound(SOAPMessageContext smc) 
    throws SOAPException {
    return true;
  }

  public boolean handleFault(SOAPMessageContext smc) {
    return true;
  }
    
  public void close(MessageContext messageContext) {
  }
    
  /**
   * Tests whether the message is being sent or received.
   **/
  private boolean isOutbound(SOAPMessageContext smc) {

    Boolean outboundProperty = (Boolean)
      smc.get(MessageContext.MESSAGE_OUTBOUND_PROPERTY);
        
    return outboundProperty.booleanValue();
  }

  /**
   * Returns a factory required for generating SOAP faults.
   **/
  protected SOAPFactory getFactory() throws SOAPException {

    if (factory == null) {
      factory = SOAPFactory.newInstance();
    }

    return factory;
  }

  /**
   * Returns a qualified name based on the namespace declared by the
   * root element in the SOAP body.
   **/
  protected QName getQName(SOAPBody body, String localName) {

    Iterator i = body.getChildElements();
    if  (! i.hasNext()) { return null; }

    SOAPElement methodElement = (SOAPElement) i.next();
    String namespaceURI = methodElement.getNamespaceURI();
    String prefix = methodElement.getPrefix();

    return new QName(namespaceURI, localName, prefix);    
  }
}

Once I had the base handler in hand, I could write a signature verification handler focused exclusively on pre-processing of each incoming message. The handleInbound method does the actual work:

package member.server;

import java.util.Date;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;

import java.util.logging.Logger;

import org.w3c.dom.*;

import javax.xml.namespace.QName;
import javax.xml.ws.handler.MessageContext;
import javax.xml.ws.handler.soap.SOAPHandler;

import javax.xml.soap.SOAPBody;
import javax.xml.soap.SOAPElement;
import javax.xml.soap.SOAPException;
import javax.xml.soap.SOAPFault;
import javax.xml.soap.SOAPMessage;
import javax.xml.soap.SOAPPart;
import javax.xml.soap.SOAPEnvelope;
import javax.xml.soap.SOAPHeader;
import javax.xml.soap.SOAPHeaderElement;

import javax.xml.ws.soap.SOAPFaultException;

import javax.xml.ws.handler.soap.SOAPMessageContext;

import member.common.Canonicalizer;
import member.common.Signature;
import member.common.BaseHandler;
import member.common.Timestamp;

/*
 * Verifies the signature on inbound messages.
 */
public class SignatureVerificationHandler extends BaseHandler {

  private static final Logger log = Logger.getLogger("member");

  private static final long MAX_TIME_RANGE_MILLIS = 1000 * 60 * 5;

  public boolean handleInbound(SOAPMessageContext smc) throws SOAPException {

    SOAPMessage message = smc.getMessage();
    SOAPPart part = message.getSOAPPart();

    SOAPEnvelope envelope = part.getEnvelope();
    
    SOAPBody body = envelope.getBody();
    if (body == null) { 
      throw new RuntimeException("Missing required message body.");
    }
    
    SOAPElement securityElement = getSecurityElement(envelope);
    String accessID = getChildText(securityElement, ACCESS_ID_ELEMENT);
    String timestamp = getChildText(securityElement, TIMESTAMP_ELEMENT);
    Date submittedDate = Timestamp.parse(timestamp);
    
    long timeRangeMillis = 
      Math.abs(System.currentTimeMillis() - submittedDate.getTime());
    if (timeRangeMillis > MAX_TIME_RANGE_MILLIS) {
      SOAPFault fault = 
        getFactory().createFault("Timestamp outside allowed range",
     getQName(body, "InvalidTimestamp"));
      throw new SOAPFaultException(fault);
    }
    
    String submittedSignature = 
      getChildText(securityElement, SIGNATURE_ELEMENT);
    
    String bodyText = Canonicalizer.toText(body);
    String signedText = timestamp + ":" + accessID + ":" + bodyText;
    String secretKey = getSecretKey(accessID);
    String verifiedSignature = Signature.create(secretKey, signedText);
    
    if (! submittedSignature.equals(verifiedSignature)) {
      SOAPFault fault = 
        getFactory().createFault("Failed to verify the message signature",
     getQName(body, "InvalidSignature"));
      throw new SOAPFaultException(fault);
    }

    return true;
  }

  private SOAPElement getSecurityElement(SOAPEnvelope envelope) 
    throws SOAPException {

    SOAPHeader header = envelope.getHeader();

    if (header == null) {
      throw new RuntimeException("Missing required message header");
    }

    QName qName = getQName(envelope.getBody(), SECURITY_ELEMENT);
    if (qName == null) {
      throw new RuntimeException("Missing required method element.");
    }

    Iterator i = header.getChildElements(qName);
    if (! i.hasNext()) {
      throw new RuntimeException("Missing required security element.");
    }

    SOAPElement securityElement = (SOAPElement) i.next();

    return securityElement;
  }

  private String getChildText(SOAPElement element, String name) {
    
    Iterator i = element.getChildElements(new QName("", name, ""));
    if (! i.hasNext()) {
      return "";
    }
    
    SOAPElement child = (SOAPElement) i.next();

    Node textNode = child.getFirstChild();
    if (textNode == null) { 
      return ""; 
    }

    if (textNode.getNodeType() != Node.TEXT_NODE) {
      return "";
    }

    return ((CharacterData) textNode).getData();
  }

  /**
    * Placeholder method.
    **/
  private String getSecretKey(String accessID) {
    return "secret";
  }
}

The handler throws a fault if the timestamp or the signature is invalid. A real implementation of the getSecretKey method would also throw a fault if the access ID was invalid.

To add the handler to the message processing chain, I created a handlers.xml file in the member.server package directory:

<?xml version="1.0" encoding="UTF-8"?>

<handler-chains xmlns="http://java.sun.com/xml/ns/javaee">
  <handler-chain>
    <handler>
      <handler-class>member.server.SignatureVerificationHandler</handler-class>
    </handler>
  </handler-chain>
</handler-chains>

This apparently gets picked up by the apt tool and applied to all the web services in that package.

Before I could actually test the security scheme, I had to create a a client that would supply the correct security element in the message header. Keep reading for the shocking finale.