Securing SOAP communication in CXF using standard WSSE is not a trivial task, especially considering the documentation is quite old and often shows XML Spring bean configuration and other, a bit outdated, stuff.
We will see how to configure the security both on the server and the client side.
Short explanation of terms used:
- WSS - Web Service Security - Official specification of securing SOAP Web Services
- Apache CXF - implementation of SOAP protocol in Java. Used both for server side and client
- Apache WSS4J - A library from Apache that integrates with CXF and implements WSS specification. We will use the UsernameToken method.
(I'm showing Kotlin code but Java should be very similar and easy to convert to)
Setup
So at the start you should have your classes generated from a wsdl file. Then the CXF's Endpoint
should be configured with a CXF Bus and implementation of the main WSDL-generated interface.
I used cxf-spring-boot-starter-jaxws
to have the Bus
already configured. I declared the SomePort
implementation before and EndpointImpl
like that:
// in Kotlin apply block just makes the object available as this
@Bean
fun endpoint(port: SomePort){
EndpointImpl(bus, port).apply {
this.inInterceptors.add(LoggingInInterceptor())
this.publish("/ws")
}
}
With the CXF Spring Boot starter mentioned above, it's all that is needed to have the service up and running.
We will work on this definition to include the WS-security.
Server
So, first things first - let's add the needed dependencies. Here's what you need to add to your maven pom.xml (check if newer versions are available) :
<dependency>
<groupId>org.apache.wss4j</groupId>
<artifactId>wss4j</artifactId>
<version>2.3.0</version>
<type>pom</type>
</dependency>
<dependency>
<groupId>org.apache.cxf</groupId>
<artifactId>cxf-rt-ws-security</artifactId>
<version>3.4.1</version>
</dependency>
Next, we will add the WSS4JInInterceptor
to the endpoint, just like the LoggingInInterceptor
was already added.
But before that, we will need to declare some properties for this interceptor :
val wss4jProps = mapOf(
WSHandlerConstants.ACTION to WSHandlerConstants.USERNAME_TOKEN,
WSHandlerConstants.PASSWORD_TYPE to WSConstants.PASSWORD_DIGEST,
WSHandlerConstants.PW_CALLBACK_REF to PasswordCallback(username, password)
)
Here we are declaring the UsernameToken
method, the type of password and the password callback reference. It's a class we'll have to implement by ourselves. Here the username and password are predefined and constant, but we may as well use database for verification of multiple users:
class PasswordCallback(
private val username: String,
private val password: String,
) : CallbackHandler {
override fun handle(callbacks: Array<out Callback>) {
val pc = callbacks[0] as WSPasswordCallback
if (pc.identifier == username) {
pc.password = password
}
}
}
Now, finally, we can add our new interceptor to the endpoint configuration:
// in java you will access inInterceptors through the getter
this.inInterceptors.add(WSS4JInInterceptor(wss4jProps))
Client
To configure the client we'll use JaxWsProxyFactoryBean
and WSS4JOutInterceptor
.
So we're starting from a plain configuration without security which looks like this:
JaxWsProxyFactoryBean().apply {
this.serviceClass = SomePort::class.java
this.address = wsAddress
this.outInterceptors.add(LoggingOutInterceptor())
this.inInterceptors.add(LoggingInInterceptor())
}.create() as SmpsPort
Once again - SomePort
is an interface generated from wsdl. wsAddress
parameter is the url to our service. There are also in and out logging interceptors.
Security
Here we also have to configure a map with configuration for our WSS4J interceptor, but this time it looks a bit different:
private val wss4jProps = mapOf(
WSHandlerConstants.ACTION to WSHandlerConstants.USERNAME_TOKEN,
WSHandlerConstants.USER to username,
WSHandlerConstants.PW_CALLBACK_REF to PasswordCallback(username, password)
)
Then, we can configure the interceptor in JaxWsProxyFactoryBean
:
this.outInterceptors.add(WSS4JOutInterceptor(wss4jProps))
HTTP Basic Authorization
If you need an HTTP Basic authorization then there is a fast way to do so on the client side.
On a previously configured JaxWsProxyFactoryBean
, instead of adding interceptor, there are fields for username and password:
jaxWsProxyFactoryBean.username = username
jaxWsProxyFactoryBean.password = password
Summary
We have seen how to configure CXF's UsernameToken security.
We ended up with the endpoint configured:
// in Kotlin apply block just makes the object available as this
@Bean
fun endpoint(port: SomePort){
EndpointImpl(bus, port).apply {
this.inInterceptors.add(LoggingInInterceptor())
this.inInterceptors.add(WSS4JInInterceptor(wss4jProps))
this.publish("/ws")
}
}
And the client for use in other modules or for test purposes:
@Bean("smpsProxy")
fun smpsProxy(): SmpsPort =
JaxWsProxyFactoryBean().apply {
this.serviceClass = SmpsPort::class.java
this.address = wsAddress
this.outInterceptors.add(LoggingOutInterceptor())
this.outInterceptors.add(WSS4JOutInterceptor(wss4jProps))
this.inInterceptors.add(LoggingInInterceptor())
}.create() as SmpsPort
Reference
Web Service Security Specification
Apache CXF WSS
Using Apache WSS4j
More advanced examples of WSS4j
Thanks for reading
If you see anything is missing in this guide, please let me know in the comments. And if you found it useful then give a heart, I'll be grateful :)
Top comments (0)