You are viewing an old version of this page. View the current version.

Compare with Current View Page History

« Previous Version 22 Next »

WS-Security provides means to secure your services above and beyond transport level protocols such as HTTPS. Through a number of standards such as XML-Encryption, and headers defined in the WS-Security standard, it allows you to:

  • Pass authentication tokens between services
  • Encrypt messages or parts of messages
  • Sign messages
  • Timestamp messages

Currently, CXF implements WS-Security by integrating WSS4J. To use the integration, you'll need to configure these interceptors and add them to your service and/or client.

Overview of encryption and signing

WS-Security makes heavy use of public/private key cryptography. To really understand how to configure WS-Security, it is helpful - if not necessary - to understand these basics. The Wikipedia has an excellent entry on this, but we'll try to summarize the relevant basics here (This content is a modified version of the wikipedia content..)

With public key cryptography, a user has a pair of public and private keys. These are generated using a large prime number and a key function.

The keys are related mathematically, but cannot be derived from one another. With these keys we can encrypt messages. For example, if Bob wants to send a message to Alice, he can encrypt a message using her public key. Alice can then decrypt this message using her private key. Only Alice can decrypt this message as she is the only one with the private key.

Messages can also be signed. This allows you to ensure the authenticity of the message. If Alice wants to send a message to Bob, and Bob wants to be sure that it is from Alice, Alice can sign the message using her private key. Bob can then verify that the message is from Alice by using her public key.

Configuring the WSS4J Interceptors

To enable WS-Security within CXF for a server or a client, you'll need to set up the WSS4J interceptors. You can either do this via the API or via Spring XML configuration. This section will provide an overview of how to do this, and the following sections will go into more detail about configuring the interceptors for specific security actions.

It is important to note that:

  1. If you are using CXF 2.0.x, you must add the SAAJ(In/Out)Interceptors if you're using WS-Security. (This is done automatically for you with CXF 2.1) These enable creation of a DOM tree for each request/response. The support libraries for WS-Security require DOM trees.
  2. You may not need both in and out WS-Security. For instance, if you are just requiring signatures on incoming messages, you'll just need the incoming interceptors.

Adding the interceptors via the API

On the Server side, you'll want to add the interceptors to your CXF Endpoint. If you're publishing your service using the JAX-WS APIs, you can get your CXF endpoint like this:

import org.apache.cxf.endpoint.Endpoint;
import org.apache.cxf.jaxws.EndpointImpl;

EndpointImpl jaxWsEndpoint = (EndpointImpl) Endpoint.publish("http://host/service", myServiceImpl);
Endpoint cxfEndpoint = jaxWsEndpoint.getServer().getEndpoint();

If you've used the (JaxWs)ServerFactoryBean, you can simply access it via the Server object:

import org.apache.cxf.endpoint.Endpoint;
import org.apache.cxf.endpoint.Server;
import org.apache.cxf.frontend.ServerFactoryBean;

ServerFactoryBean factory = ...;
...
Server server = factory.create();
Endpoint cxfEndpoint = server.getEndpoint();

On the client side, you can obtain a reference to the CXF endpoint using the ClientProxy helper:

GreeterService gs = new GreeterService();
Greeter greeter = gs.getGreeterPort();
...
org.apache.cxf.endpoint.Client client = org.apache.cxf.frontend.ClientProxy.getClient(greeter);
org.apache.cxf.endpoint.Endpoint cxfEndpoint = client.getEndpoint();

Now you're ready to add the interceptors:

Map<String,Object> inProps= new HashMap<String,Object>();
... // how to configure the properties is outlined below;

WSS4JInInterceptor wssIn = new WSS4JInInterceptor(inProps);
cxfEndpoint.getInInterceptors().add(wssIn);
cxfEndpoint.getInInterceptors().add(new SAAJInInterceptor()); // 2.0.x only; not needed in 2.1+

Map<String,Object> outProps = new HashMap<String,Object>();
... // how to configure the properties is outlined below;

WSS4JOutInterceptor wssOut = new WSS4JOutInterceptor(outProps);
cxfEndpoint.getOutInterceptors().add(wssOut);
cxfEndpoint.getOutInterceptors().add(new SAAJOutInterceptor()); // 2.0.x only; not needed in 2.1+

Spring XML Configuration

If you're using Spring to build endpoints, you can easily integrate accomplish the above in your bean definitions as well.

<import resource="classpath:META-INF/cxf/cxf.xml" />
<import resource="classpath*:META-INF/cxf/cxf-extension-*.xml" />

<jaxws:endpoint id="myService"
  implementor="com.acme.MyServiceImpl"
  address="http://localhost:9001/MyService">
  <jaxws:inInterceptors>
     <!-- SAAJ Interceptor explicitly needed only for 2.0.x --> 
     <bean class="org.apache.cxf.binding.soap.saaj.SAAJInInterceptor"/>
     <bean class="org.apache.cxf.ws.security.wss4j.WSS4JInInterceptor">
       <constructor-arg>
         <map>
           <entry key="action" value="UsernameToken"/>
           <entry key="passwordType" value="PasswordDigest"/>
           <entry key="signaturePropFile" value="..."/>
           ...
         </map>
       </constructor-arg>
     </bean>
  </jaxws:inInterceptors>
</jaxws:endpoint>

The entry keys and values given in the constructor-arg element above (action, signaturePropFile, etc.) map to the text strings in WSS4J's WSHandlerConstants and WSConstants classes for the corresponding WSHandlerConstants.XXXXX and WSConstants.XXXX constants you see in the section below. So by viewing WSHandlerConstants, for example, you can see that the WSHandlerConstants.USERNAME_TOKEN value given below would need to be "UsernameToken" instead when doing Spring configuration.

Configuring WS-Security Actions

Username Token Authentication

WS-Security supports many ways of specifying tokens. One of these is the UsernameToken header. It is a standard way to communicate a username and password or password digest to another endpoint. Be sure to review the OASIS UsernameToken Profile Specification for important security considerations when using UsernameTokens.

For the server side, you'll want to set up the following properties on your WSS4JInInterceptor (see above for code sample):

inProps.setProperty(WSHandlerConstants.ACTION, WSHandlerConstants.USERNAME_TOKEN);
// Password type : plain text
inProps.setProperty(WSHandlerConstants.PASSWORD_TYPE, WSConstants.PW_TEXT);
// for hashed password use:
//properties.setProperty(WSHandlerConstants.PASSWORD_TYPE, WSConstants.PW_DIGEST);
// Callback used to retrieve password for given user.
inProps.setProperty(WSHandlerConstants.PW_CALLBACK_CLASS, ServerPasswordHandler.class.getName());

The password callback class allows you to retrieve to retrieve the password for a given user so that WS-Security can determine if they're authorized. Here is a small example:

import javax.security.auth.callback.Callback;
import javax.security.auth.callback.CallbackHandler;
import javax.security.auth.callback.UnsupportedCallbackException;
import org.apache.ws.security.WSPasswordCallback;

public class ServerPasswordCallback implements CallbackHandler {

    public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException {

        WSPasswordCallback pc = (WSPasswordCallback) callbacks[0];

        if (pc.getId().equals("joe") {
            // set the password on the callback. This will be compared to the
            // password which was sent from the client.
            pc.setPassword("password");
        }
    }

}

Note that for the special case of a plain-text password (or any other yet unknown password type), the password validation is delegated to the callback class, see org.apache.ws.security.processor.UsernameTokenProcessor#handleUsernameToken() method javadoc of the WSS4Jproject. In that case, the ServerPasswordCallback should be something like the following one:

public class ServerPasswordCallback implements CallbackHandler {

    public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException {

        WSPasswordCallback pc = (WSPasswordCallback) callbacks[0];

        if (pc.getId().equals("joe") {
           if (!pc.getPassword().equals("password")) {
                throw new SecurityException("wrong password");
           }
        }
    }

}

On the Client side you'll want to configure the WSS4J outgoing properties:

outProps.setProperty(WSHandlerConstants.ACTION, WSHandlerConstants.USERNAME_TOKEN);
// Specify our username
outProps.setProperty(WSHandlerConstants.USER, "joe");
// Password type : plain text
outProps.setProperty(WSHandlerConstants.PASSWORD_TYPE, WSConstants.PW_TEXT);
// for hashed password use:
//properties.setProperty(WSHandlerConstants.PASSWORD_TYPE, WSConstants.PW_DIGEST);
// Callback used to retrieve password for given user.
outProps.setProperty(WSHandlerConstants.PW_CALLBACK_CLASS, ClientPasswordHandler.class.getName());

Once again we're using a password callback, except this time instead of specifying our password on the server side, we're specifying the password we want sent with the message. This is so we don't have to store our password in our configuration file.

import javax.security.auth.callback.Callback;
import javax.security.auth.callback.CallbackHandler;
import javax.security.auth.callback.UnsupportedCallbackException;
import org.apache.ws.security.WSPasswordCallback;

public class ClientPasswordCallback implements CallbackHandler {

    public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException {

        WSPasswordCallback pc = (WSPasswordCallback) callbacks[0];

        // set the password for our message.
        pc.setPassword("password");
    }

}

Here is an example of WS-Security implemented using annotations for interceptors (uses UsernameToken).

Generating keys using keytool

For the Signature and Encryption actions, you'll need to create a public & private key for the entities involved. You can generate a self-signed key pair for your development environment via the following steps. Keep in mind these will not be signed by an external authority like Verisign, so are inappropriate for production use.

1. Creating private key with given alias and password like "myAlias"/"myAliasPassword" in keystore (protected by password for
security reasons)

keytool -genkey -alias myAlias -keypass myAliasPassword -keystore privatestore.jks \
  -storepass keyStorePassword -dname "cn=myAlias" -keyalg RSA

The alias is simply a way to identify the key pair. In this instance we are using the RSA algorithm.

2. Self-sign our certificate (in production environment this will be done by a company like Verisign).

keytool -selfcert -alias myAlias -keystore privatestore.jks -storepass keyStorePassword -keypass myAliasPassword

3. Export the public key from our private keystore to file named key.rsa

keytool -export -alias myAlias -file key.rsa -keystore privatestore.jks -storepass keyStorePassword

4. Import the public key to new keystore:

keytool -import -alias myAlias  -file key.rsa -keystore publicstore.jks -storepass keyStorePassword

So now we have two keystores containing our keys - a public one (publicstore.jks) and a private one (privatestore.jks). Both of them have keystore password set to keyStorePass (this not recommended for production but ok for development) and alias set to myAlias. The file key.rsa can removed from filesystem, since it used only temporarily. Storing keys in keystores is strongly advised because a keystore is protected by a password.

A more detailed description of key generation can be found here:
http://java.sun.com/j2se/1.4.2/docs/tooldocs/windows/keytool.html

How to create a production certificate can be found here:
http://support.globalsign.net/en/objectsign/java.cfm

Signing

To sign our message, we'll want to configure our client to sign the message via its private key and configure the server to verify the signature using the Client's public key. To do this you must ensure that the Client's public key has been imported into the server's keystore using keytool.

On the client side, our outgoing WS-Security properties will look like so (see above for code sample):

outProps.put(WSHandlerConstants.ACTION, "Signature");
outProps.put(WSHandlerConstants.USER, "myAlias");
outProps.put(WSHandlerConstants.PW_CALLBACK_CLASS, ClientCallbackHandler.class.getName());
outProps.put(WSHandlerConstants.SIG_PROP_FILE, "client_sign.properties");

The USER that is specified is the key alias that you used when creating your keys. The password callback class is responsible for providing the key's password.

Our client_sign.properties file contains several settings to configure WSS4J:

org.apache.ws.security.crypto.provider=org.apache.ws.security.components.crypto.Merlin
org.apache.ws.security.crypto.merlin.keystore.type=jks
org.apache.ws.security.crypto.merlin.keystore.password=keyStorePassword
org.apache.ws.security.crypto.merlin.keystore.alias=myAlias
org.apache.ws.security.crypto.merlin.file=client_keystore.jks

On the server side, we need to configure our incoming WSS4J interceptor to verify the signature using the Client's public key.

inProps.put(WSHandlerConstants.ACTION, "Signature");
inProps.put(WSHandlerConstants.SIG_PROP_FILE, "server.properties");

Our server_sign.properties file contains several settings to configure WSS4J:

org.apache.ws.security.crypto.provider=org.apache.ws.security.components.crypto.Merlin
org.apache.ws.security.crypto.merlin.keystore.type=jks
org.apache.ws.security.crypto.merlin.keystore.password=amex123
org.apache.ws.security.crypto.merlin.file=server_keystore.jks

Encryption

Until our documentation is filled out further here, our WS-Security test sample provides an example of encrypting requests and responses.

  • No labels