Apache CXF Tutorial – WS-Security with Spring
This tutorial will cover adding an authentication component to your web service though WS-Security. If you need an overview of how to setup CXF then you may find our previous tutorial helpful. Another helpful resource is CXF’s own WS-Security tutorial. However, it does not include information on how to setup the client through Spring.
To begin with, make sure you have at least the following .jars in addition to the required base CXF .jars:
spring-beans-2.0.6.jar
spring-context-2.0.6.jar
spring-core-2.0.6.jar
spring-web-2.0.6.jar
wss4j-1.5.1.jar
xmlsec-1.3.0.jar
Now we will add a security interceptor to the server’s Spring configuration file, which we named cxf.xml in the last tutorial in order to match the CXF documentation.
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:jaxws="http://cxf.apache.org/jaxws"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://cxf.apache.org/jaxws
http://cxf.apache.org/schemas/jaxws.xsd">
<import resource="classpath:META-INF/cxf/cxf.xml" />
<import resource="classpath:META-INF/cxf/cxf-extension-soap.xml"/>
<import resource="classpath:META-INF/cxf/cxf-servlet.xml" />
<jaxws:endpoint id="auth"
implementor="com.company.auth.service.AuthServiceImpl"
address="/corporateAuth">
<jaxws:inInterceptors>
<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="PasswordText" />
<entry key="passwordCallbackClass" value="com.company.auth.service.ServerPasswordCallback" />
</map>
</constructor-arg>
</bean>
</jaxws:inInterceptors>
</jaxws:endpoint>
</beans>
You can change the action and passwordType to do more advanced authentication. In this example, we will simply require all authenticating clients to know a single password specified by the server. If you’d like each client to have it’s own password you can specify that in the callback, which is the next thing we must implement:
package com.company.auth.service;
import java.io.IOException;
import java.util.ResourceBundle;
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 {
private static final String BUNDLE_LOCATION = "com.company.auth.authServer";
private static final String PASSWORD_PROPERTY_NAME = "auth.manager.password";
private static String password;
static {
final ResourceBundle bundle = ResourceBundle.getBundle(BUNDLE_LOCATION);
password = bundle.getString(PASSWORD_PROPERTY_NAME);
}
public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException {
WSPasswordCallback pc = (WSPasswordCallback) callbacks[0];
// Set the password on the callback. This will be compared to the
// password which was sent from the client.
// We can call pc.getIdentifer() right here to check the username
// if we want each client to have it's own password.
pc.setPassword(password);
}
}
The server is now setup to require a password. The password we are requiring is one that we specified in a properties file and then read in through a ResourceBundle. You may find it easier to simply hard code the password on the initial run and then replace it with your own means of authentication once the service is up and running.
If you are running on WebLogic 9, as I was, then you will get an error “java.lang.UnsupportedOperationException: This class does not support SAAJ 1.1“. In order to correct that, make sure your version of the SAAJ classes are being used by adding the following to your weblogic.xml descriptor file:
<container-descriptor>
<prefer-web-inf-classes>true</prefer-web-inf-classes>
</container-descriptor>
You WebLogic folks must also then set two properties in your WebLogic JDK:
-Djavax.xml.soap.MessageFactory=com.sun.xml.messaging.saaj.soap.ver1_1.SOAPMessageFactory1_1Impl
-Djavax.xml.soap.SOAPConnectionFactory=weblogic.wsee.saaj.SOAPConnectionFactoryImpl
We now have to setup the client to supply a password. Firstly, we will create another Spring file at com/company/auth/service/cxfClient.xml to setup the application context for the client:
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:jaxws="http://cxf.apache.org/jaxws"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.0.xsd
http://cxf.apache.org/jaxws http://cxf.apache.org/schemas/jaxws.xsd">
<bean id="proxyFactory" class="org.apache.cxf.jaxws.JaxWsProxyFactoryBean">
<property name="serviceClass" value="com.company.auth.service.AuthService"/>
<property name="address" value="http://localhost:7001/authManager/services/corporateAuth"/>
<property name="inInterceptors">
<list>
<ref bean="logIn" />
</list>
</property>
<property name="outInterceptors">
<list>
<ref bean="logOut" />
<ref bean="saajOut" />
<ref bean="wss4jOut" />
</list>
</property>
</bean>
<bean id="client" class="org.apache.cxf.jaxws.JaxWsProxyFactoryBean" factory-bean="proxyFactory" factory-method="create" />
<bean id="logIn" class="org.apache.cxf.interceptor.LoggingInInterceptor" />
<bean id="logOut" class="org.apache.cxf.interceptor.LoggingOutInterceptor" />
<bean id="saajOut" class="org.apache.cxf.binding.soap.saaj.SAAJOutInterceptor" />
<bean id="wss4jOut" class="org.apache.cxf.ws.security.wss4j.WSS4JOutInterceptor">
<constructor-arg>
<map>
<entry key="action" value="UsernameToken" />
<entry key="user" value="ws-client" />
<entry key="passwordType" value="PasswordText" />
<entry key="passwordCallbackClass" value="com.company.auth.service.ClientPasswordCallback" />
</map>
</constructor-arg>
</bean>
</beans>
We then need to set the password for our message:
package com.company.auth.service;
import java.io.IOException;
import java.util.ResourceBundle;
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 {
private static final String BUNDLE_LOCATION = "com.company.auth.authClient";
private static final String PASSWORD_PROPERTY_NAME = "auth.manager.password";
private static String password;
static {
final ResourceBundle bundle = ResourceBundle.getBundle(BUNDLE_LOCATION);
password = bundle.getString(PASSWORD_PROPERTY_NAME);
}
public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException {
WSPasswordCallback pc = (WSPasswordCallback) callbacks[0];
// set the password for our message.
pc.setPassword(password);
}
}
Finally, we create the service factory, which is extremely easy since all the work was done in the Spring file:
package com.company.auth.service;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public final class AuthServiceFactory {
private static final ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext(new String[] {
"com/company/auth/service/cxfClient.xml"
});
public AuthServiceFactory() {
}
public AuthService getService() {
return (AuthService) context.getBean("client");
}
}
Congratulations. Your web service now utilizes a basic implementation of WS-Security. Hopefully, that will be enough background to get you on your way.
I am getting following exception while invoking the client
8/03/2011 18:06:43 org.apache.cxf.phase.PhaseInterceptorChain doIntercept
INFO: Interceptor has thrown exception, unwinding now Problems creating SAAJ object model
I using CXF 2.1.4 on Weblogic 10.0. I am using following Server side XML:
Error Stack Trace is as follows:
Exception in thread “Main Thread” javax.xml.ws.soap.SOAPFaultException: Problems creating SAAJ object model
at org.apache.cxf.jaxws.JaxWsClientProxy.invoke(JaxWsClientProxy.java:145)
at $Proxy39.myFile(Unknown Source)
at wsdlclient.myService_myServiceImplPort_Client.main(myService_myServiceImplPort_Client.java:73)
Caused by: org.apache.cxf.binding.soap.SoapFault: Problems creating SAAJ object model
at org.apache.cxf.binding.soap.interceptor.Soap11FaultInInterceptor.unmarshalFault(Soap11FaultInInterceptor.java:75)
at org.apache.cxf.binding.soap.interceptor.Soap11FaultInInterceptor.handleMessage(Soap11FaultInInterceptor.java:46)
at org.apache.cxf.binding.soap.interceptor.Soap11FaultInInterceptor.handleMessage(Soap11FaultInInterceptor.java:35)
at org.apache.cxf.phase.PhaseInterceptorChain.doIntercept(PhaseInterceptorChain.java:226)
at org.apache.cxf.interceptor.AbstractFaultChainInitiatorObserver.onMessage(AbstractFaultChainInitiatorObserver.java:96)
at org.apache.cxf.binding.soap.interceptor.CheckFaultInterceptor.handleMessage(CheckFaultInterceptor.java:69)
at org.apache.cxf.binding.soap.interceptor.CheckFaultInterceptor.handleMessage(CheckFaultInterceptor.java:34)
at org.apache.cxf.phase.PhaseInterceptorChain.doIntercept(PhaseInterceptorChain.java:226)
at org.apache.cxf.endpoint.ClientImpl.onMessage(ClientImpl.java:641)
at org.apache.cxf.transport.http.HTTPConduit$WrappedOutputStream.handleResponseInternal(HTTPConduit.java:2102)
at org.apache.cxf.transport.http.HTTPConduit$WrappedOutputStream.handleResponse(HTTPConduit.java:1980)
at org.apache.cxf.transport.http.HTTPConduit$WrappedOutputStream.close(HTTPConduit.java:1905)
at org.apache.cxf.transport.AbstractConduit.close(AbstractConduit.java:66)
at org.apache.cxf.transport.http.HTTPConduit.close(HTTPConduit.java:600)
at org.apache.cxf.interceptor.MessageSenderInterceptor$MessageSenderEndingInterceptor.handleMessage(MessageSenderInterceptor.java:62)
at org.apache.cxf.phase.PhaseInterceptorChain.doIntercept(PhaseInterceptorChain.java:226)
at org.apache.cxf.endpoint.ClientImpl.invoke(ClientImpl.java:469)
at org.apache.cxf.endpoint.ClientImpl.invoke(ClientImpl.java:299)
at org.apache.cxf.endpoint.ClientImpl.invoke(ClientImpl.java:251)
at org.apache.cxf.frontend.ClientProxy.invokeSync(ClientProxy.java:73)
at org.apache.cxf.jaxws.JaxWsClientProxy.invoke(JaxWsClientProxy.java:124)
I have added the Weblogic properties in JDK as well:
-Djavax.xml.soap.MessageFactory=com.sun.xml.messaging.saaj.soap.ver1_1.SOAPMessageFactory1_1Impl
-Djavax.xml.soap.SOAPConnectionFactory=weblogic.wsee.saaj.SOAPConnectionFactoryImpl
and also set the prefer-application-packages to:
javax.jws.*
org.apache.*
Could you please tell where the issue could be. Have spent lot of time on this, still no luck 🙁
Hi Ben,
Thanks a lot. You have done a great job for us.
This tutorial is really helpful to test the security. I could set up this very quickly and finish the testing easily.
I don’t understand one thing why it is giving the outbound message 2 times. It would be great if you provide your comments.
Once again thanks a lot.
Hi,
Let’s say i want to distribute the wsdl file to some client who doesn’t support Spring & uses a simple CXF generator (wsdl2java).
Will he be required to supply the ws with the auth password in addition to the service arguments?
Where & how would it be supplied?
Thanks & great job.
Liav.
Hi there
Thanks for this tutorial. I had to adapt it a little because my app is currently using the simple front-end, however I doubt it may be the reason why if is giving an unexpected (to me at least!) behavior.
What happens if I leave both handle methods exactly as you provided them, is, the WS responds no matter which password is supplied by the client. In order to have the WS to actually check for the password being correct I had to modify the server-side handle method as follows:
public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException {
WSPasswordCallback pc = (WSPasswordCallback) callbacks[0];
// We can call pc.getIdentifer() right here to check the username
// if we want each client to have it’s own password.
if (!pc.getPassword().equals(password))
throw new IOException(“Wrong password!”);
}
So did I miss something? Any ideas?
Thanks in advance.
Guilhem
I got the following exception:
“java.util.MissingResourceException:Can’t find bundle for base name…”, anyone know how to name the bundle? Thx.
Can anyone help regarding my post from Aug. 15th?
how can we user cxf-rest server and client
Hello!
I’m a confused about the server’s callback handler. The matter is I can’t get to it in the debug mode. After some time researching I found, that WSS4JInInterceptor doesn’t have a callback. Please, see http://stackoverflow.com/questions/9035520/apache-cxf-spring-simple-certificate-authentication/9062076#9062076 for more details.
Can you, please, answer these questions:
1. When do the WSS4JInInterceptor’s callback is called
2. What password do the client sets in WSPasswordCallback pc = (WSPasswordCallback) callbacks[0]; pc.setPassword(“”)
Hi All,
I am getting below error please help me out i m using 2.7 apache cxf…
Caused by: org.xml.sax.SAXParseException: cvc-complex-type.2.4.c: The matching wildcard is strict, but no declaration can be found for element ‘jaxws:inInterceptors’.
at com.sun.org.apache.xerces.internal.util.ErrorHandlerWrapper.createSAXParseException(Unknown Source)
at com.sun.org.apache.xerces.internal.util.ErrorHandlerWrapper.error(Unknown Source)
at com.sun.org.apache.xerces.internal.impl.XMLErrorReporter.reportError(Unknown Source)
at com.sun.org.apache.xerces.internal.impl.XMLErrorReporter.reportError(Unknown Source)
at com.sun.org.apache.xerces.internal.impl.xs.XMLSchemaValidator$XSIErrorReporter.reportError(Unknown Source)
at com.sun.org.apache.xerces.internal.impl.xs.XMLSchemaValidator.reportSchemaError(Unknown Source)
at com.sun.org.apache.xerces.internal.impl.xs.XMLSchemaValidator.handleStartElement(Unknown Source)
at com.sun.org.apache.xerces.internal.impl.xs.XMLSchemaValidator.startElement(Unknown Source)
Thanks Ben. This is a pretty useful and very quick to understand article.
Hi Ben,
From where can I download the source code for this ?
Appreciate if you have provide zip or something.