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>http://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 = "http://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="http://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="http://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>

Download

You may also like...

  • Alexey

    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.

  • iSyed

    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