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.

No comments: