Spring WS returning "No adapter for endpoint" after decrypting SOAP message with Wss4jSecurityInterceptor

636 Views Asked by At

I've created a simple Spring WS that sign and decrypt a SOAP message. The message is decrypted but somehow it doesn't hit my Endpoint class. Here is my code:

pom.xml

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.7.9</version>
        <relativePath />
    </parent>
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.example.hr</groupId>
    <artifactId>countryService</artifactId>
    <packaging>war</packaging>
    <version>1.0.0</version>
    <name>holidayService Spring-WS Application</name>
    <url>http://www.springframework.org/spring-ws</url>
    <properties>
        <java.version>17</java.version>
    </properties>
    <build>
        <resources>
            <resource>
                <directory>src/main/resources</directory>
            </resource>
        </resources>
        <finalName>countryService</finalName>
        <plugins>
            <plugin>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <source>${java.version}</source>
                    <target>${java.version}</target>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.codehaus.mojo</groupId>
                <artifactId>jaxb2-maven-plugin</artifactId>
                <version>1.5</version>
                <executions>
                    <execution>
                        <id>xjc</id>
                        <goals>
                            <goal>xjc</goal>
                        </goals>
                    </execution>
                </executions>
                <configuration>
                    <schemaDirectory>${project.basedir}/src/main/resources/templates/</schemaDirectory>
                    <outputDirectory>${project.basedir}/src/main/java</outputDirectory>
                    <clearOutputDir>false</clearOutputDir>
                </configuration>
            </plugin>
        </plugins>
    </build>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-tomcat</artifactId>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web-services</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.ws</groupId>
            <artifactId>spring-ws-core</artifactId>
        </dependency>
        
        <dependency>
            <groupId>org.jdom</groupId>
            <artifactId>jdom</artifactId>
            <version>2.0.2</version>
        </dependency>
        <dependency>
            <groupId>jaxen</groupId>
            <artifactId>jaxen</artifactId>
        </dependency>
        <dependency>
            <groupId>javax.xml.bind</groupId>
            <artifactId>jaxb-api</artifactId>
        </dependency>
        <dependency>
            <groupId>com.sun.xml.bind</groupId>
            <artifactId>jaxb-impl</artifactId>
            <version>2.3.4</version>
        </dependency>
        <dependency>
            <groupId>wsdl4j</groupId>
            <artifactId>wsdl4j</artifactId>
        </dependency>
        
        
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
            <optional>true</optional>
        </dependency>
        
        
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
            <exclusions>
                <exclusion>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-starter-logging</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-log4j2</artifactId>
        </dependency>
        
        <dependency>
            <groupId>org.springframework.ws</groupId>
            <artifactId>spring-ws-security</artifactId>
        </dependency>
        <dependency>
            <groupId>org.apache.wss4j</groupId>
            <artifactId>wss4j</artifactId>
            <version>3.0.0</version>
            <type>pom</type>
        </dependency>
        
    </dependencies>

sping-ws-servlet.xml


    <context:component-scan base-package="http://com.example.hr" />
    
    
    <sws:annotation-driven />


    <sws:dynamic-wsdl id="countries" portTypeName="CountriesPort" locationUri="/" targetNamespace="example.com/hr">
        <sws:xsd location="/WEB-INF/countries.xsd" />
    </sws:dynamic-wsdl>
    
    <bean id="messageFactory" class="org.springframework.ws.soap.saaj.SaajSoapMessageFactory"/>
    
    
    <bean id="propertyPlaceholderConfigurer"
        class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
        <property name="locations">
            <list>
                <value>classpath*:application.properties</value>
            </list>
        </property>
    </bean>


    <sws:interceptors>
        <bean class="org.springframework.ws.soap.server.endpoint.interceptor.SoapEnvelopeLoggingInterceptor" />
    </sws:interceptors>

    
    <bean id="keyStore" class="org.springframework.ws.soap.security.support.KeyStoreFactoryBean">
        <property name="location" value="${sec.EncryptionKeyStore}"/>
        <property name="password" value="${sec.KeyStorePassword}"/>
        <property name="type" value="JKS"/>
    </bean>
    
    
    <bean id="keyStoreCallbackHandler" class="org.springframework.ws.soap.security.wss4j2.callback.KeyStoreCallbackHandler">
      <property name="keyStore" ref="keyStore"/>
      <property name="privateKeyPassword" value="${sec.PrivateKeyPassword}"/>
    </bean>
    
    
    <bean id="cryptoFactory" class="org.springframework.ws.soap.security.wss4j2.support.CryptoFactoryBean">
        <property name="keyStoreLocation" value="${sec.EncryptionKeyStore}"/>
        <property name="keyStorePassword" value="${sec.KeyStorePassword}"/>
        <property name="keyStoreType" value="JKS"/>
    </bean>
    
    
    <bean id="wss4jSecurityInterceptor" class="com.example.hr.ws.CustomWss4jSecurityInterceptor">
        <property name="validationActions" value="${sec.ValidationActions}"/>
        <property name="securementActions" value="${sec.SecurementActions}"/>
        <property name="validationTimeToLive" value="${sec.ValidationTTL}"/>
        <property name="securementTimeToLive" value="${sec.SecurementTTL}"/>
        <property name="securementUsername" value="${sec.SecurementUsername}"/>
        <property name="securementPassword" value="${sec.SecurementPassword}"/>
        <property name="securementEncryptionKeyIdentifier" value="${sec.SecurementEncryptionKeyIdentifier}"/>
        <property name="securementSignatureKeyIdentifier" value="${sec.SecurementSignatureKeyIdentifier}"/>
        <property name="securementSignatureParts" value="${sec.SecurementSignatureParts}"/>
        <property name="securementSignatureCrypto" ref="cryptoFactory" />
        <property name="securementEncryptionCrypto" ref="cryptoFactory" />
        <property name="validationDecryptionCrypto" ref="cryptoFactory" />
        <property name="validationSignatureCrypto" ref="cryptoFactory" />
        <property name="validationCallbackHandler" ref="keyStoreCallbackHandler" />
    </bean>
    
    
    <bean id="countryRepository" class="com.example.hr.repository.CountryRepository" /> 
        
    <bean id="countryEndpoint" class="com.example.hr.ws.CountryEndpoint">
        <constructor-arg ref="countryRepository"/>
    </bean>
    
    <bean id="endpointMapping" class="org.springframework.ws.server.endpoint.mapping.PayloadRootAnnotationMethodEndpointMapping">
        <property name="interceptors">
            <list>
                <ref bean="wss4jSecurityInterceptor" />
            </list>
        </property>
        <property name="defaultEndpoint" ref="countryEndpoint" />
    </bean>

    
    <bean id="messageDispatcher" class="org.springframework.ws.server.MessageDispatcher">
        <property name="endpointMappings" ref="endpointMapping" />
    </bean>
    
</beans>

CountryEndpoint.java

@Endpoint
public class CountryEndpoint {
   private static final String NAMESPACE_URI = "example.com/hr/schemas";
   private CountryRepository countryRepository;
   private static final Logger logger = LoggerFactory.getLogger(CountryEndpoint.class);


   public CountryEndpoint(CountryRepository countryRepository) throws JDOMException {
      this.countryRepository = countryRepository;
   }
   
   
   @PayloadRoot(namespace = NAMESPACE_URI, localPart = "getCountryRequest")
   @ResponsePayload
   public GetCountryResponse getCountry(@RequestPayload GetCountryRequest request) 
      throws JDOMException {
      
      Country country = countryRepository.findCountry(request.getName());
      GetCountryResponse response = new GetCountryResponse();
      response.setCountry(country);
      logger.info("========================================");
      logger.info("============== RESPONSE ================");
      logger.info("========================================");
      return response;
   }

}

countries.xsd

<xs:schema xmlns:xs = "http://www.w3.org/2001/XMLSchema" 
   xmlns:tns = "example.com/hr/schemas"
   xmlns:wsu = "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd"
   targetNamespace = "example.com/hr/schemas" 
   elementFormDefault = "qualified">

   <xs:element name = "getCountryRequest">
      <xs:complexType>
         <xs:sequence>
            <xs:element name = "name" type = "xs:string"/>
         </xs:sequence>
      </xs:complexType>
   </xs:element>

   <xs:element name = "getCountryResponse">
      <xs:complexType>
         <xs:sequence>
            <xs:element name = "country" type = "tns:country"/>
         </xs:sequence>
      </xs:complexType>
   </xs:element>

   <xs:complexType name = "country">
      <xs:sequence>
         <xs:element name = "name" type = "xs:string"/>
         <xs:element name = "population" type = "xs:int"/>
         <xs:element name = "capital" type = "xs:string"/>
         <xs:element name = "currency" type = "tns:currency"/>
      </xs:sequence>
   </xs:complexType>

   <xs:simpleType name = "currency">
      <xs:restriction base = "xs:string">
         <xs:enumeration value = "GBP"/>
         <xs:enumeration value = "USD"/>
         <xs:enumeration value = "INR"/>
      </xs:restriction>
   </xs:simpleType>
</xs:schema>

SOAP ENVELOPE

<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:sch="example.com/hr/schemas">
   <soapenv:Header/>
   <soapenv:Body>
      <sch:getCountryRequest>
         <sch:name>U.S</sch:name>
      </sch:getCountryRequest>
   </soapenv:Body>
</soapenv:Envelope>

The message is decrypted, since I see it printed in the logs, but then I receive the below error message:

   <soapenv:Body xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd" wsu:Id="id-811A8CD4ECA47EB92F1679415081841773">
      <sch:getCountryRequest>
         <sch:name>U.S</sch:name>
      </sch:getCountryRequest>
   </soapenv:Body>
</soapenv:Envelope>
2023-03-21 16:12:42 DEBUG SoapMessageDispatcher - Testing endpoint adapter [org.springframework.ws.server.endpoint.adapter.DefaultMethodEndpointAdapter@60f7ea28]
2023-03-21 16:12:54 DEBUG SoapMessageDispatcher - Testing endpoint adapter [org.springframework.ws.server.endpoint.adapter.GenericMarshallingMethodEndpointAdapter@69afb721]
2023-03-21 16:12:57 DEBUG SoapMessageDispatcher - Testing endpoint adapter [org.springframework.ws.server.endpoint.adapter.MarshallingMethodEndpointAdapter@70c93a12]
2023-03-21 16:13:03 DEBUG SoapFaultAnnotationExceptionResolver - Resolving exception from endpoint [com.example.hr.ws.CountryEndpoint@87248c3]: java.lang.IllegalStateException: No adapter for endpoint [com.example.hr.ws.CountryEndpoint@87248c3]: Is your endpoint annotated with @Endpoint, or does it implement a supported interface like MessageHandler or PayloadEndpoint?
2023-03-21 16:13:03 DEBUG SimpleSoapExceptionResolver - Resolving exception from endpoint [com.example.hr.ws.CountryEndpoint@87248c3]: java.lang.IllegalStateException: No adapter for endpoint [com.example.hr.ws.CountryEndpoint@87248c3]: Is your endpoint annotated with @Endpoint, or does it implement a supported interface like MessageHandler or PayloadEndpoint?
2023-03-21 16:13:03 DEBUG SoapMessageDispatcher - Endpoint invocation resulted in exception - responding with Fault
java.lang.IllegalStateException: No adapter for endpoint [com.example.hr.ws.CountryEndpoint@87248c3]: Is your endpoint annotated with @Endpoint, or does it implement a supported interface like MessageHandler or PayloadEndpoint?
    at deployment.countryService.war//org.springframework.ws.server.MessageDispatcher.getEndpointAdapter(MessageDispatcher.java:291)

My schema classes are correctly annotated with @XmlRootElement from javax.xml.bind.annotation, e.g.

package com.example.hr.schemas;

import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;
import javax.xml.bind.annotation.XmlType;


@XmlAccessorType(XmlAccessType.FIELD)
@XmlType(name = "", propOrder = {
    "name"
})
@XmlRootElement(name = "getCountryRequest")
public class GetCountryRequest {

    @XmlElement(required = true)
    protected String name;

    /**
     * Gets the value of the name property.
     * 
     * @return
     *     possible object is
     *     {@link String }
     *     
     */
    public String getName() {
        return name;
    }

    /**
     * Sets the value of the name property.
     * 
     * @param value
     *     allowed object is
     *     {@link String }
     *     
     */
    public void setName(String value) {
        this.name = value;
    }

}

When I debug the code, the MessageDispatcher.dispatch method fails to get an EndpointAdapter for CountryEndpoint class: when executing getEndpointAdapter method it checks the condition if (endpointAdapter.supports(endpoint))

and it appears that my CountryEndpoint is not an instance of org.springframework.ws.server.endpoint.MethodEndpoint.

I also tried using org.springframework.oxm.jaxb.Jaxb2Marshaller in my Spring WS Configuration and JAXBElement in my CountryEndpoint as shown below but it didn't work:

 @PayloadRoot(namespace = NAMESPACE_URI, localPart = "getCountryRequest")
   @ResponsePayload
   public JAXBElement<GetCountryResponse> getCountry(@RequestPayload JAXBElement<GetCountryRequest> request) 
      throws JDOMException {
      
      Country country = countryRepository.findCountry(request.getValue().getName());
      GetCountryResponse response = new GetCountryResponse();
      response.setCountry(country);
      logger.info("========================================");
      logger.info("============== RESPONSE ================");
      logger.info("========================================");
      return new JAXBElement<GetCountryResponse>(QName.valueOf("GetCountryResponse"), GetCountryResponse.class, response);
   }

I really don't understand what I'm missing here. Can anybody help me please?

Thank you!

1

There are 1 best solutions below

7
M. Deinum On

First of all I would by starting to cleanup your Spring WS configuration there are conflicting parts in there.

  1. Fix the component-scan it needs a proper base package
  2. Properly configure interceptors
  3. Properly use sws:annotation-driven.
<beans>
    <context:component-scan base-package="com.example.hr" />
    
    <sws:annotation-driven />

    <sws:dynamic-wsdl id="countries" portTypeName="CountriesPort" locationUri="/" targetNamespace="example.com/hr">
        <sws:xsd location="/WEB-INF/countries.xsd" />
    </sws:dynamic-wsdl>
    
    <context:property-placeholder location="classpath*:application.properties"/>

    <sws:interceptors>
        <bean class="org.springframework.ws.soap.server.endpoint.interceptor.SoapEnvelopeLoggingInterceptor" />
        <ref bean="wss4jSecurityInterceptor" />
    </sws:interceptors>
    
    <bean id="keyStore" class="org.springframework.ws.soap.security.support.KeyStoreFactoryBean">
        <property name="location" value="${sec.EncryptionKeyStore}"/>
        <property name="password" value="${sec.KeyStorePassword}"/>
        <property name="type" value="JKS"/>
    </bean>
    
    <bean id="keyStoreCallbackHandler" class="org.springframework.ws.soap.security.wss4j2.callback.KeyStoreCallbackHandler">
      <property name="keyStore" ref="keyStore"/>
      <property name="privateKeyPassword" value="${sec.PrivateKeyPassword}"/>
    </bean>
    
    <bean id="cryptoFactory" class="org.springframework.ws.soap.security.wss4j2.support.CryptoFactoryBean">
        <property name="keyStoreLocation" value="${sec.EncryptionKeyStore}"/>
        <property name="keyStorePassword" value="${sec.KeyStorePassword}"/>
        <property name="keyStoreType" value="JKS"/>
    </bean>
    
    <bean id="wss4jSecurityInterceptor" class="com.example.hr.ws.CustomWss4jSecurityInterceptor">
        <property name="validationActions" value="${sec.ValidationActions}"/>
        <property name="securementActions" value="${sec.SecurementActions}"/>
        <property name="validationTimeToLive" value="${sec.ValidationTTL}"/>
        <property name="securementTimeToLive" value="${sec.SecurementTTL}"/>
        <property name="securementUsername" value="${sec.SecurementUsername}"/>
        <property name="securementPassword" value="${sec.SecurementPassword}"/>
        <property name="securementEncryptionKeyIdentifier" value="${sec.SecurementEncryptionKeyIdentifier}"/>
        <property name="securementSignatureKeyIdentifier" value="${sec.SecurementSignatureKeyIdentifier}"/>
        <property name="securementSignatureParts" value="${sec.SecurementSignatureParts}"/>
        <property name="securementSignatureCrypto" ref="cryptoFactory" />
        <property name="securementEncryptionCrypto" ref="cryptoFactory" />
        <property name="validationDecryptionCrypto" ref="cryptoFactory" />
        <property name="validationSignatureCrypto" ref="cryptoFactory" />
        <property name="validationCallbackHandler" ref="keyStoreCallbackHandler" />
    </bean>
    
</beans>

Next you state that you properly used the @XmlRootElement that way you should be able to remove the JaxbElement.

@Endpoint
public class CountryEndpoint {
   private static final String NAMESPACE_URI = "example.com/hr/schemas";
   private static final Logger logger = LoggerFactory.getLogger(CountryEndpoint.class);
   private CountryRepository countryRepository;

   public CountryEndpoint(CountryRepository countryRepository) {
      this.countryRepository = countryRepository;
   }   
   
   @PayloadRoot(namespace=NAMESPACE_URI, localPart="getCountryRequest")
   @ResponsePayload
   public GetCountryResponse getCountry(@RequestPayload GetCountryRequest request) {
      
      Country country = countryRepository.findCountry(request.getName());
      GetCountryResponse response = new GetCountryResponse();
      response.setCountry(country);
      logger.info("========================================");
      logger.info("============== RESPONSE ================");
      logger.info("========================================");
      return response;
   }
}

Now most of the config in XML isn't needed when using Spring Boot. You would only need the endpoint, the wsdl and the interceptors and all the other stuff is done automatically.