Apache CXF: Provider Services and WS-Security

Provider Services

“Provider service” is CXF-speak for a web service that implements the Provider interface. This interface only has one method: invoke. This interface is used when your web service is defined as passing fully-formed documents around – usually a SOAP document.

Coding a provider service

Here’s sample code for a server:

@WebServiceProvider(portName="helloWorldPort", serviceName="helloWorld")
@ServiceMode(value=Service.Mode.MESSAGE)
public class SampleWebServiceProvider implements Provider<SOAPMessage> {

	public SampleWebServiceProvider() {
	}

	public SOAPMessage invoke(SOAPMessage soapMessage) {
		try {
			return processSOAPMessage(soapMessage);
		} catch (SOAPException e) {
			throw new MessageException(e);
		} catch (RuntimeException e) {
			throw new MessageException(e);
		}
	}

	public static SOAPMessage processSOAPMessage(SOAPMessage request) throws SOAPException {
	    SOAPMessage response = MessageFactory.newInstance().createMessage();
	    Name bodyName = SOAPFactory.newInstance().createName("helloWorldResponse");
	    response.getSOAPBody().addBodyElement(bodyName);
	    SOAPElement respContent = response.getSOAPBody().addChildElement("hello");
	    respContent.setValue("world");
	    response.saveChanges();
	    return response;
    }
}

Note that the @WebServiceProvider annotation can be used to specify the WSDL that you’re using to define your webservice if that’s the way you roll.

In your server-side Spring configuration file, you need to define the web service endpoint:

<jaxws:endpoint address="/helloWorld">
	<jaxws:implementor>
		<bean class="ca.intelliware.example.webservice.SampleWebServiceProvider">
		</bean>
	</jaxws:implementor>
</jaxws:endpoint>

This method can be invoked simply by posting a well-formatted SOAP message to the appropriate URL. Here’s an example message:

<?xml version="1.0" encoding="UTF-8"?>
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/">
  <SOAP-ENV:Header/>
  <SOAP-ENV:Body>
    <helloWorld>
      <stuff>stuff goes here</stuff>
    </helloWorld>
  </SOAP-ENV:Body>
</SOAP-ENV:Envelope>

Which returns:

<?xml version="1.0" encoding="UTF-8"?>
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/">
  <SOAP-ENV:Header/>
  <SOAP-ENV:Body>
    <helloWorldResponse/>
    <hello>world</hello>
  </SOAP-ENV:Body>
</SOAP-ENV:Envelope>

WS-Security

Now we’d like to add some security to our web service. CXF takes care of this automatically on the server side.

Adding authentication

First off, we’ll add a requirement that users of the service must have a valid username and password. In WS-Security lingo, this is UsernameToken authentication. There are two different ways of passing the password in the message: either in the clear, or as a digest.

According to the UserNameToken spec:

&#8220 Note that PasswordDigest can only be used if the plain text password (or password equivalent) is available to both the requestor and the recipient. &#8221

It should be noted that if the server only has access to the digest of a password:

&#8220 For example, if a server does not have access to the clear text of a password but does have the hash, then the hash is considered a password equivalent and can be used anywhere where a “password” is indicated in this specification. It is not the intention of this specification to require that all implementations have access to clear text passwords. &#8221

In my case, the use of PasswordDigest was mandated by the client. So I modified the Spring configuration file to look this:

<jaxws:endpoint address="/helloWorld">
  <jaxws:implementor>
    <bean class="ca.intelliware.example.webservice.SampleWebServiceProvider">
    </bean>
  </jaxws:implementor>
  <jaxws:inInterceptors>
    <bean class="org.apache.cxf.ws.security.wss4j.WSS4JInInterceptor">
      <constructor-arg>
        <map>
          <entry key="action" value="UsernameToken"/>
          <entry key="passwordType" value="PasswordDigest"/>
          <entry key="passwordCallbackRef">
            <ref bean="examplePasswordCallback"/>
          </entry>
        </map>
      </constructor-arg>
    </bean>
  </jaxws:inInterceptors>
</jaxws:endpoint>

<bean id="examplePasswordCallback" class="ca.intelliware.example.webservice.PasswordCallback">
  <constructor-arg ref="ca.intelliware.example.service.UserService"/>
</bean>

The password callback is the class that CXF uses to determine the password for the specified user. It then creates a hash with a couple of other elements and compares it to the hash that was sent in the message.

public class PasswordCallback implements CallbackHandler {

    private final UserService userService;

    public PasswordCallback(UserService userService) {
        this.userService = userService;
    }

    public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException {
        WSPasswordCallback pc = (WSPasswordCallback) callbacks[0];
        User user = this.userService.findByName(pc.getIdentifer());

        if (user == null) {
            throw new SecurityException("unknown user");
        }

        // set the password on the callback. This will be compared to the
        // password which was sent from the client.
        pc.setPassword(user.getPassword());
    }
}

So now we need to add the WS-Security header to our SOAP message. Besides user name and password digest, there are two optional fields that can be specified: data/time and nonce. Here’s a sample of what it looks like:

<?xml version="1.0" encoding="UTF-8"?>
<SOAP-ENV:Envelope
    xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/"
    xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"
    xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd">
  <SOAP-ENV:Header>
    <wsse:Security SOAP-ENV:mustUnderstand="1">
      <wsse:UsernameToken wsu:Id="UsernameToken-9949215">
        <wsse:Username>username</wsse:Username>
        <wsse:Password Type="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordDigest">bHDQDWtI1/GAmkS1N9VvIxSu65g=</wsse:Password>
        <wsse:Nonce>T97bvF3vLvItbcs39k/CYQ==</wsse:Nonce>
        <wsu:Created>2008-08-27T20:50:47.084Z</wsu:Created>
      </wsse:UsernameToken>
    </wsse:Security>
  </SOAP-ENV:Header>
  [body as before]
</SOAP-ENV:Envelope>

While it is possible to hand-code the password digest each time, there is an easier way to add the security header.

SOAPMessage soapMessage = MessageFactory.newInstance().createMessage();
soapMessage.getSOAPBody().addDocument(
  DocumentFactory.createDocumentFromStream(SoapSigner.class.getResourceAsStream("/sample_hello_world_request.xml")));

Document document = soapMessage.getSOAPPart();

WSSecHeader secHeader = new WSSecHeader();
secHeader.insertSecurityHeader(document);

// UsernameToken
WSSecUsernameToken ut = new WSSecUsernameToken();
ut.setPasswordType(WSConstants.PASSWORD_DIGEST);
ut.setUserInfo("username", "password");
ut.addCreated();
ut.addNonce();
ut.prepare(document);

ut.prependToHeader(secHeader);

The SOAPMessage will now be ready to send. You can access the raw XML document via getSOAPPart().

Adding signature

The next security to be added to my service is a signature, to ensure that the message body was not tampered with in transit. In the WS-Security standard, most signing is handled via a public key infrastructure (PKI) where a message is signed with a private key and checked with the public key. In addition, Microsoft’s .NET specifies a method to create a signature with a UsernameToken. This is the method that my specifications require.

I modified a single line in my Spring config file:

<entry key="action" value="UsernameTokenSignature UsernameToken"/>

Note that the order of the actions is very important. I had the order reversed and it basically had me hung up for a week.

This tells the server that as well as authenticating the username and password, the digital signature (as signed by the usernameToken) must also be verified.

Now we need our SOAP request to look like this:

<?xml version="1.0" encoding="UTF-8"?>
<SOAP-ENV:Envelope
    xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/"
    xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"
    xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd"
    xmlns:ds="http://www.w3.org/2000/09/xmldsig#" >
  <SOAP-ENV:Header>
    <wsse:Security SOAP-ENV:mustUnderstand="1">
      <wsse:UsernameToken wsu:Id="UsernameToken-14721926">
        <wsse:Username>username</wsse:Username>
        <wsse:Password Type="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordDigest">7qSMV2ole4ftWtvaxFMBYX5/YLc=</wsse:Password>
        <wsse:Nonce>jaXk03JWGVBsxJ1TKv0z7Q==</wsse:Nonce>
        <wsu:Created>2008-08-27T21:42:43.018Z</wsu:Created>
      </wsse:UsernameToken>
      <ds:Signature Id="Signature-20079748">
        <ds:SignedInfo>
          <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
          <ds:SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#hmac-sha1"/>
          <ds:Reference URI="#id-25229676">
            <ds:Transforms>
              <ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
            </ds:Transforms>
            <ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
            <ds:DigestValue>r4+RREePIRtwt9E2FTQ0zUMV42s=</ds:DigestValue>
          </ds:Reference>
        </ds:SignedInfo>
        <ds:SignatureValue>StxdgoAOTQZjorNr4vs0EqjEWQ4=</ds:SignatureValue>
        <ds:KeyInfo Id="KeyId-25840096">
          <wsse:SecurityTokenReference wsu:Id="STRId-23930419">
            <wsse:Reference URI="#UsernameToken-14721926" ValueType="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#UsernameToken"/>
          </wsse:SecurityTokenReference>
        </ds:KeyInfo>
      </ds:Signature>
    </wsse:Security>
  </SOAP-ENV:Header>
  <SOAP-ENV:Body wsu:Id="id-25229676">
    <helloWorld>
      <stuff>stuff goes here</stuff>
    </helloWorld>
  </SOAP-ENV:Body>
</SOAP-ENV:Envelope>

Note that the SecurityToken reference (UsernameToken-14721926) goes back to the usernameToken block earlier in the message, and the ds:reference URI points to the element that was signed (the SOAP body).

Again, we need some code to generate this signature. Here’s what I used (this would go right after setting up the UsernameToken above, but before ut.prependToHeader(secHeader)):

WSSecSignature sign = new WSSecSignature();
Vector<WSEncryptionPart> signParts = new Vector<WSEncryptionPart>();

String name = "Body";
String namespace = "http://schemas.xmlsoap.org/soap/envelope/";
String encMod = "Content";
WSEncryptionPart part = new WSEncryptionPart(name, namespace, encMod);
signParts.add(part);

if(signParts.size() > 0) {
    sign.setParts(signParts);
}

sign.setUsernameToken(ut);
sign.setKeyIdentifierType (WSConstants.UT_SIGNING);
sign.setSignatureAlgorithm(XMLSignature.ALGO_ID_MAC_HMAC_SHA1);

try {
    sign.build(document, null, secHeader);
} catch (WSSecurityException e) {
    e.printStackTrace();
}

After a small tweak (with help from the cxf-users mailing list), this code now works. The trick was in the order of the WS actions in the Spring configuration file.

Encrypting the SOAP body

This may be a bit more difficult than I had imagined after my eventual success at getting the signature working. Here’s a quote from a mailing list:

&#8220 WSS4J 1.1 does not support the feature to encrypt the body with the key dereived [sic] from the Usernametoken. This will (probably) implemented with the Policy Language enhanced version if the Policy Language spec becomes somewhat more stable (and readable 🙂 ) &#8221

I’ve managed to cobble something together with help from this sample on the WSS4J mailing list. Here’s the relevant client-side code:

Reference reference = new Reference(document);
reference.setURI("#" + ut.getId());
reference.setValueType("http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#UsernameToken");

SecurityTokenReference securityTokenReference = new SecurityTokenReference(document);
securityTokenReference.setReference(reference);

WSSecurityUtil.setNamespace(securityTokenReference.getElement(), WSConstants.WSSE_NS, WSConstants.WSSE_PREFIX);

WSSecEncrypt secEncrypt = new WSSecEncrypt();
secEncrypt.setKeyIdentifierType(WSConstants.EMBED_SECURITY_TOKEN_REF);
secEncrypt.setSecurityTokenReference(securityTokenReference);
secEncrypt.setKey(secretKey);
secEncrypt.setSymmetricEncAlgorithm(WSConstants.AES_128);
secEncrypt.setDocument(document);

// this specifies that the encryptMe element should be encrypted, but
// it could be any combination of elements that gets encrypted
Vector<WSEncryptionPart> encryptionParts = new Vector<WSEncryptionPart>();
String name = "encryptMe";
String namespace = null;
String encMod = "Content";
WSEncryptionPart encryptionPart = new WSEncryptionPart(name, namespace, encMod);
encryptionParts.add(encryptionPart);
secEncrypt.setParts(encryptionParts);

secEncrypt.build(document, null, secHeader);

The spec I was working from specified that the encryption occurs before the signature (i.e., the encrypted message is the one that is signed), so I slotted this code in right before the signature code above.

Here’s a sample of a message that I generated:

[...signature block]
      </ds:Signature>
      <xenc:ReferenceList>
        <xenc:DataReference URI="#EncDataId-1281123"/>
      </xenc:ReferenceList>
    </wsse:Security>
    </SOAP-ENV:Header>
    <SOAP-ENV:Body xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd" wsu:Id="id-20000831">
      <helloWorld>
        <stuff>The following bit is encrypted</stuff>
        <encryptMe>
          <xenc:EncryptedData Id="EncDataId-1281123" Type="http://www.w3.org/2001/04/xmlenc#Content" xmlns:xenc="http://www.w3.org/2001/04/xmlenc#">
            <xenc:EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#aes128-cbc" xmlns:xenc="http://www.w3.org/2001/04/xmlenc#"/>
            <ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
              <wsse:SecurityTokenReference xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd">
                <wsse:Reference URI="#UsernameToken-14721926" ValueType="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#UsernameToken" xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"/>
              </wsse:SecurityTokenReference>
            </ds:KeyInfo>
            <xenc:CipherData xmlns:xenc="http://www.w3.org/2001/04/xmlenc#">
              <xenc:CipherValue xmlns:xenc="http://www.w3.org/2001/04/xmlenc#">5lEPD1JcaArVNk9imTSsHLW7n8lTY8F1HGmdkU2gZSNtosRtdA9E3umEjXp4xyeJ</xenc:CipherValue>
            </xenc:CipherData>
          </xenc:EncryptedData>
        </encryptMe>
      </helloWorld>
    </SOAP-ENV:Body>
  </SOAP-ENV:Envelope>

Note that only the element I specified (encryptMe) is encrypted.

Things get a little trickier on the server side. Right off the bat, CXF and WSS4J do not have the concept of automatically encrypting/decrpting with UsernameToken like they have with signature. And while it appears that WSS4JInInterceptor allows you to slot in different handlers for different actions, there’s also some certificate management that automatically happens that causes errors when processing an incoming message and no crypto properties have been defined.

The CXF and WSS4J code are not very amenable to subclassing so I ended up copying two classes and using those instead of the default classes.

Here are the changes to my local copy of WSS4JInInterceptor:

public WSS4JInInterceptor() {
        super();

        setPhase(Phase.PRE_PROTOCOL);
        getAfter().add(SAAJInInterceptor.class.getName());

        // use our custom ReferenceListProcessor
        // this is a little sloppy, see note below
        secEngine.getWssConfig().setProcessor(
        		WSSecurityEngine.REFERENCE_LIST,
        		ca.intelliware.common.webservice.ReferenceListProcessor.class.getName());
    }

[...]

    public void handleMessage(SoapMessage msg) throws Fault {
        [...]
            // SS: we remove "Encrypt" from the actions before we pass it into decodeAction.
            // The trick here is that if doAction includes the encrypt bit value, lower-level code
            // automatically tries to look up our crypto information and throws an exception. So
            // we use two sets of values here: doAction has the Encrypt action removed, while actions
            // still has it since that is the one that is compared to the wsResult vector later on.

            String noEncryptAction = StringUtils.remove(action, "Encrypt");
            int doAction = WSSecurityUtil.decodeAction(noEncryptAction, new Vector());
            WSSecurityUtil.decodeAction(action, actions);

[...]

Note that the above code is actually pretty sloppy. Not only is there a method to override the security engine with a custom one, but secEngine is basically a static field that I’m modifying for my purposes. However, since I know I’m the only one using it I can do so without feeling too bad about it.

Next we’ll code up our own ReferenceListProcessor. This class is responsible for determining the details of each reference in the message. The one we’re interested in handling is the reference to the username token in the encryption block, which cannot be handled by the default ReferenceListProcessor implementation.

private SecretKey getKeyFromSecurityTokenReference(Element secRefToken, String algorithm,
	        Crypto crypto, CallbackHandler cb) throws WSSecurityException {

                [...]

                    if     (p == null
                        || (!(p instanceof EncryptedKeyProcessor)
                        && !(p instanceof DerivedKeyTokenProcessor)
                        && !(p instanceof SAMLTokenProcessor)
                        // SS support this type of processor now too
                        && !(p instanceof UsernameTokenProcessor))) {

                [...]

                    if(p instanceof EncryptedKeyProcessor) {
                       [...]
                    } else if(p instanceof DerivedKeyTokenProcessor) {
                       [...]
                    } else if(p instanceof SAMLTokenProcessor) {
                       [...]
                    } else if(p instanceof UsernameTokenProcessor) {
                        // SS added UsernameToken handling
            	        UsernameTokenProcessor utp = (UsernameTokenProcessor) p;
                        decryptedData = utp.getUt().getSecretKey();
                    }
[...]

Now all that’s left is for us to specify the correct class in our Spring config file:

<jaxws:inInterceptors>
    <bean class="ca.intelliware.common.webservice.WSS4JInInterceptor">
      <constructor-arg>
        <map>
          <entry key="action" value="Encrypt UsernameTokenSignature UsernameToken"/>
          <entry key="passwordType" value="PasswordDigest"/>
          <entry key="passwordCallbackRef">
            <ref bean="examplePasswordCallback"/>
          </entry>
        </map>
      </constructor-arg>
    </bean>
  </jaxws:inInterceptors>

After starting the server up with this configuration, the message was decrypted and handled properly.

Notes

The code above is quite messy. I’ve cleaned it up a fair amount in my application, but I’ve left this code in its messified state so it’s easier to tell what’s going on without jumping between a lot of methods and classes.

References

  • CXF User guide: Provider Services
  • CXF User guide: WS-Security

It's only fair to share...
Share on FacebookGoogle+Tweet about this on TwitterShare on LinkedIn

One thought on “Apache CXF: Provider Services and WS-Security

  1. Pingback: Adding an interceptor to the CXF fault chain | i-Proving –

Leave a Reply