Spring WS Username Password Authentication Wss4j
This tutorial shows how to secure Spring WS Soap Services using Ws-Security username and password authentication. This password can either be in plain text or in a digest. Remember, plain text passwords are not secure. You should always make additional security measurements when sending clear passwords over the network. Use HTTPS instead of plain HTTP.
Dependencies
In order to use apache’s Wss4j
implementation, we use the following dependencies. Make sure all these dependencies are on the class path. We also use the jaxb2-maven-plugin
to generate our java classes from an XSD schema.
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.memorynotfound.spring.ws</groupId>
<artifactId>ws-security-username-password-authentication-Wss4jSecurityInterceptor</artifactId>
<version>1.0.0-SNAPSHOT</version>
<name>SPRING-WS - ${project.artifactId}</name>
<url>https://memorynotfound.com</url>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.3.3.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-ws</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.ws</groupId>
<artifactId>spring-ws-security</artifactId>
</dependency>
<dependency>
<groupId>org.apache.ws.security</groupId>
<artifactId>wss4j</artifactId>
<version>1.6.19</version>
</dependency>
<dependency>
<groupId>wsdl4j</groupId>
<artifactId>wsdl4j</artifactId>
<version>1.6.3</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>jaxb2-maven-plugin</artifactId>
<version>2.2</version>
<executions>
<execution>
<id>xjc</id>
<goals>
<goal>xjc</goal>
</goals>
</execution>
</executions>
<configuration>
<sources>
<source>src/main/resources/xsd</source>
</sources>
</configuration>
</plugin>
</plugins>
</build>
</project>
Ws-Security Username Password Authentication on Server
First, lets look at our service which we want to secure with a username password authentication mechanism. Note that the service isn’t aware of any security measurements. This is a great example of separation of concerns.
package com.memorynotfound.server;
import com.memorynotfound.beer.Beer;
import com.memorynotfound.beer.GetBeerRequest;
import com.memorynotfound.beer.GetBeerResponse;
import org.springframework.ws.server.endpoint.annotation.Endpoint;
import org.springframework.ws.server.endpoint.annotation.PayloadRoot;
import org.springframework.ws.server.endpoint.annotation.RequestPayload;
import org.springframework.ws.server.endpoint.annotation.ResponsePayload;
@Endpoint
public class BeerEndpoint {
public static final String NAMESPACE_URI = "https://memorynotfound.com/beer";
@PayloadRoot(namespace = NAMESPACE_URI, localPart = "getBeerRequest")
@ResponsePayload
public GetBeerResponse getBeer(@RequestPayload GetBeerRequest request) {
GetBeerResponse beerResponse = new GetBeerResponse();
Beer beer = new Beer();
beer.setId(request.getId());
beer.setName("Beer name");
beerResponse.setBeer(beer);
return beerResponse;
}
}
Configure Spring Ws Ws-Security Username Password
Apache Wss4j
Ws-Security implementation does not need an external configuration file. It can be completely configured using properties. We secure our server’s endpoint using a Wss4jSecurityInterceptor
. In this interceptor we should add validation actions in order to validate if the request is able to proceed. We can add these actions via the setValidationActions()
and specify a space delimited list of actions to perform. In this example we pass Timestamp
, which validates the timestamp and UsernameToken
, which validates the Username and Password using the SimplePasswordValidationCallbackHandler
. For simplicity we used the SimplePasswordValidationCallbackHandler
, which has a list of known users. When using spring security you can better use the SpringSecurityPasswordValidationCallbackHandler
which you can register the UserDetailsService
to retrieve your user information.
package com.memorynotfound.server;
import org.springframework.boot.context.embedded.ServletRegistrationBean;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.ws.config.annotation.EnableWs;
import org.springframework.ws.config.annotation.WsConfigurerAdapter;
import org.springframework.ws.server.EndpointInterceptor;
import org.springframework.ws.soap.security.wss4j.Wss4jSecurityInterceptor;
import org.springframework.ws.soap.security.wss4j.callback.SimplePasswordValidationCallbackHandler;
import org.springframework.ws.transport.http.MessageDispatcherServlet;
import org.springframework.ws.wsdl.wsdl11.DefaultWsdl11Definition;
import org.springframework.xml.xsd.SimpleXsdSchema;
import org.springframework.xml.xsd.XsdSchema;
import java.util.List;
import java.util.Properties;
@EnableWs
@Configuration
public class SoapServerConfig extends WsConfigurerAdapter {
@Bean
public SimplePasswordValidationCallbackHandler securityCallbackHandler(){
SimplePasswordValidationCallbackHandler callbackHandler = new SimplePasswordValidationCallbackHandler();
Properties users = new Properties();
users.setProperty("admin", "secret");
callbackHandler.setUsers(users);
return callbackHandler;
}
@Bean
public Wss4jSecurityInterceptor securityInterceptor(){
Wss4jSecurityInterceptor securityInterceptor = new Wss4jSecurityInterceptor();
securityInterceptor.setValidationActions("Timestamp UsernameToken");
securityInterceptor.setValidationCallbackHandler(securityCallbackHandler());
return securityInterceptor;
}
@Override
public void addInterceptors(List interceptors) {
interceptors.add(securityInterceptor());
}
@Bean
public ServletRegistrationBean messageDispatcherServlet(ApplicationContext appContext){
MessageDispatcherServlet servlet = new MessageDispatcherServlet();
servlet.setApplicationContext(appContext);
servlet.setTransformWsdlLocations(true);
return new ServletRegistrationBean(servlet, "/ws/*");
}
@Bean(name = "beers")
public DefaultWsdl11Definition defaultWsdl11Definition(XsdSchema schema){
DefaultWsdl11Definition wsdl11Definition = new DefaultWsdl11Definition();
wsdl11Definition.setPortTypeName("BeersPort");
wsdl11Definition.setLocationUri("/ws");
wsdl11Definition.setTargetNamespace(BeerEndpoint.NAMESPACE_URI);
wsdl11Definition.setSchema(schema);
return wsdl11Definition;
}
@Bean
public XsdSchema beersSchema(){
return new SimpleXsdSchema(new org.springframework.core.io.ClassPathResource("xsd/beers.xsd"));
}
}
Finally, we bootstrap this application using spring-boot. When we start this application our server is ready to process soap requests.
package com.memorynotfound.server;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class RunServer {
public static void main(String[] args) {
SpringApplication.run(RunServer.class);
}
}
Authenticate Client With WS-Security Username Password Token
We use the following client to make a request to our previously configured server. We obtain a reference of the WebServiceTemplate
to make a request to the server. Note that this code isn’t aware of any security mechanism.
package com.memorynotfound.client;
import com.memorynotfound.beer.GetBeerRequest;
import com.memorynotfound.beer.GetBeerResponse;
import org.springframework.ws.client.core.support.WebServiceGatewaySupport;
public class BeerClient extends WebServiceGatewaySupport {
public GetBeerResponse getBeer(GetBeerRequest request){
return (GetBeerResponse) getWebServiceTemplate().marshalSendAndReceive(request);
}
}
Configure Client to use Ws-Security Username Password Authentication
In the client we use the same Wss4jSecurityInterceptor
we used on the server. Only this time we use the securement actions instead of the validation actions. By setting the securement actions via the setSecurementActions()
we tell Wss4j
to add these actions to the request. In this example we add a Timestamp
and a UsernameToken
. By default Wss4j
uses a digest.
package com.memorynotfound.client;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.oxm.jaxb.Jaxb2Marshaller;
import org.springframework.ws.client.support.interceptor.ClientInterceptor;
import org.springframework.ws.soap.security.wss4j.Wss4jSecurityInterceptor;
@Configuration
public class SoapClientConfig {
@Bean
public Wss4jSecurityInterceptor securityInterceptor(){
Wss4jSecurityInterceptor wss4jSecurityInterceptor = new Wss4jSecurityInterceptor();
wss4jSecurityInterceptor.setSecurementActions("Timestamp UsernameToken");
wss4jSecurityInterceptor.setSecurementUsername("admin");
wss4jSecurityInterceptor.setSecurementPassword("secret");
return wss4jSecurityInterceptor;
}
@Bean
public Jaxb2Marshaller getMarshaller(){
Jaxb2Marshaller marshaller = new Jaxb2Marshaller();
marshaller.setContextPath("com.memorynotfound.beer");
return marshaller;
}
@Bean
public BeerClient getBeerClient(){
BeerClient beerClient = new BeerClient();
beerClient.setMarshaller(getMarshaller());
beerClient.setUnmarshaller(getMarshaller());
beerClient.setDefaultUri("http://localhost:8080/ws/beers");
ClientInterceptor[] interceptors = new ClientInterceptor[] {securityInterceptor()};
beerClient.setInterceptors(interceptors);
return beerClient;
}
}
Finally, here is the code to make a client request to the server.
package com.memorynotfound.client;
import com.memorynotfound.beer.*;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
public class RunClient {
public static void main(String[] args) {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(SoapClientConfig.class);
System.out.println(context.getBeanDefinitionCount());
BeerClient client = context.getBean(BeerClient.class);
GetBeerRequest request = new GetBeerRequest();
request.setId(2);
GetBeerResponse resp = client.getBeer(request);
System.out.println("response: " + resp);
}
}
Example Ws-Security Username Password Authentication Request
When the previous client code is executed, the following request is sent to the server. Note that a Security
element is added to the soap header. This header contains a UsernameToken
element containing a Username
and Password
combination. The Security
element also contains a Timestamp
element which we configured earlier.
<?xml version="1.0" encoding="UTF-8"?>
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/">
<SOAP-ENV:Header>
<wsse:Security 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:mustUnderstand="1">
<wsse:UsernameToken wsu:Id="UsernameToken-EC9F7473E024359C6A14589178984712">
<wsse:Username>admin</wsse:Username>
<wsse:Password Type="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordDigest">Uq6pANLo2rGD1rfnOlvGHVHq7m0=</wsse:Password>
<wsse:Nonce EncodingType="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-message-security-1.0#Base64Binary">VNRqB4w0jhpDz92hdpjXkQ==</wsse:Nonce>
<wsu:Created>2016-03-25T14:58:18.471Z</wsu:Created>
</wsse:UsernameToken>
<wsu:Timestamp wsu:Id="TS-EC9F7473E024359C6A14589178984531">
<wsu:Created>2016-03-25T14:58:18.452Z</wsu:Created>
<wsu:Expires>2016-03-25T15:03:18.452Z</wsu:Expires>
</wsu:Timestamp>
</wsse:Security>
</SOAP-ENV:Header>
<SOAP-ENV:Body>
<ns2:getBeerRequest xmlns:ns2="https://memorynotfound.com/beer">
<ns2:id>2</ns2:id>
</ns2:getBeerRequest>
</SOAP-ENV:Body>
</SOAP-ENV:Envelope>
We receive the following response.
<?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>
<ns2:getBeerResponse xmlns:ns2="https://memorynotfound.com/beer">
<ns2:beer>
<ns2:id>2</ns2:id>
<ns2:name>Beer name</ns2:name>
</ns2:beer>
</ns2:getBeerResponse>
</SOAP-ENV:Body>
</SOAP-ENV:Envelope>
How I can work with https?
You need to configure your application server (Tomcat or JBoss, or … ) to support secured socket layer (SSL/HTTPS) transportation.
Hello,
When i access the above sample service from SoapUI the request that is generated with out security header.
”
?
”
What changes are required to make the security header available as sample for user?
Thanks,
Unfortunately, spring-ws does not support WS-Policy (yet). So the information needed, cannot be specified in the WSDL by default. You could however, enhance the WSDL with your own WS-Policy implementation by extending the DefaultWsdl11Definition.
You can manually add a ws-security-header using SoapUI. Please read the following documentation: https://www.soapui.org/soapui-projects/ws-security.html
Hello guys,
thank you for the great article! :) I have one question though: Why do you need that “wss4j” dependency in pom.xml? It should be a compile time dependency of “spring-ws-security”, right? Also, it would be great to write a follow-up article with credentials provided using the UserDetailService… ;-)
With kind regards,
Petr Dvorak, Lime
Hi,
Thanks for your articles.
GetBeerRequest and GetBeerResponse files are missing.
Could you add them please?
Thanks
How did you generate your sample request from Java code. I need to create client something similar to mention in the example. I can generate my request however i am not sure how can i see the request with header details.
love it
It looks like the example request won’t get generated with the given security interceptor.
I had added these to get the nonce and created:
wss4jSecurityInterceptor.setSecurementUsernameTokenCreated(true);
wss4jSecurityInterceptor.setSecurementUsernameTokenNonce(true);