Java WSS in CDI application
When SOAP web services are used in Jakarta EE CDI application some or all of them has to be secured. There are several possible ways to do it but one of the most “standard” is Apache WSS4J™ - Web Services Security for Java. In case when Apache CXF™ is used as JAX-WS implementation it is really easy to implement.
Let’s create simplest SOAP web service.
@WebService
@BindingType(value = SOAPBinding.SOAP12HTTP_BINDING)
public class HelloWS {
@WebMethod
public String getGreetings() {
return "Hello, world!";
}
}
Web or application server can automatically create WSDL for this web service. So starting from this point things may be tested in practice.
Now let’s make this SOAP web service more secure step by step.
Retrieve authenticated user name
Usually information about authenticated user may be retrieved using java.security.Principal interface. In its turn javax.xml.ws.WebServiceContext is used to store all the web service related data including request. So it is used to get principal also:
Optional.ofNullable(context.getUserPrincipal()).map(Principal::getName).orElse("anonymus")+ "!";
Now greetings are personalized. But user information should be delivered to principal.
Enable WSS in WSDL
When the application containing SOAP web services is deployed to some web or application server this server automatically generates WSDL for each particular web service. Java type is considered to be web service if it’s annotated with @WebService. WSS is not enabled in WSDL by default. To make it available WSS policy should be included into WSDL. First, XML file (let’s say usernameTokenPolicy.xml) with WSS policy should be defined.
Recommended by LinkedIn
<wsp:Policy xmlns:wsp="http://schemas.xmlsoap.org/ws/2004/09/policy">
<wsp:ExactlyOne>
<wsp:All>
<sp:SupportingTokens xmlns:sp="http://docs.oasis-open.org/ws-sx/ws-securitypolicy/200702" wsp:Optional="true">
<wsp:Policy>
<sp:UsernameToken sp:IncludeToken="http://docs.oasis-open.org/ws-sx/ws-securitypolicy/200702/IncludeToken/AlwaysToRecipient">
<wsp:Policy>
<sp:WssUsernameToken10/>
</wsp:Policy>
</sp:UsernameToken>
</wsp:Policy>
</sp:SupportingTokens>
</wsp:All>
</wsp:ExactlyOne>
</wsp:Policy>
Important thing here is attribute wsp:Optional="true". It allows to receive and process SOAP messages without WSS block(s). HelloWS.getGreetings() will return "Hello, anonymus!" in this case.
In order to include this piece of XML into WSDL we can use approach provided by Apache CXF WS-SecurityPolicy. The only thing we need is to annotate our web service type with
@org.apache.cxf.annotations.Policy(uri = "classpath/to/usernameTokenPolicy.xml")
Password callback handler
Now only couple things are required to finish HelloWS. We just have to add
@EndpointProperties({
@EndpointProperty(key = "security.callback-handler", beanClass = UserPasswordCallbackHandler.class),
@EndpointProperty(key = "ws-security.is-bsp-compliant", value = "false")})
@EndpointProperty(key = "ws-security.is-bsp-compliant", value = "false") means that incoming SOAP request is allowed not to be compliant with Basic Security Profile. In other words WSS implementation accepts not only hashed but also plain text password. Another property is self-explanatory.
When WSS processor receives SOAP message it calls specified callback handler. In its turn callback handler should find plain password for given user and set it to callback object. This password will be hashed and compared to hash provided in SOAP request. If they are not equal exception is thrown.
public class UserPasswordCallbackHandler implements CallbackHandler {
@Override
public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException {
if (callbacks != null && callbacks.length > 0 && callbacks[0] instanceof WSPasswordCallback) {
WSPasswordCallback passwordCallback = (WSPasswordCallback) callbacks[0];
Instance<UserDao> daoProvider = CDI.current().select(UserDao.class);
UserDao dao = daoProvider.get();
try {
User user = dao.getByUsername(passwordCallback.getIdentifier());
passwordCallback.setPassword(user.getPassword());
} finally {
daoProvider.destroy(dao);
}
}
}
}
In case if plain password is not available (let’s assume user.getPassword() returns some hash of the password) hashed hash of password (not hashed password) should be provided in incoming SOAP request.
P.S. You may notice that @org.apache.cxf.annotations.EndpointProperty has ref() attribute. It can be used as reference to Spring Bean. Unfortunately I couldn’t make it work in Jakarta EE. As the result `serPasswordCallbackHandler is not CDI bean, so simple dependency injection or transaction propagation are not allowed here. At the other hand only one callback handler is created for one particular web service so developer should not worry about potential memory leak or instantiation overhead.