Spring WS: How to apply Interceptor to a specific endpoint
Asked Answered
C

2

13

I have multiple working SOAP Web Services on a Spring application, using httpBasic authentication, and I need to use WS-Security instead on one of them to allow authentication with the following Soap Header.

<soap:Header><wsse:Security xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd" soap:mustUnderstand="1">
  <wsse:UsernameToken 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" wsu:Id="UsernameToken-1">
    <wsse:Username>username</wsse:Username>
    <wsse:Password Type="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordText">password</wsse:Password>
  </wsse:UsernameToken>
</wsse:Security></soap:Header>

Current WSConfiguration was done according to https://github.com/spring-projects/spring-boot/blob/master/spring-boot-samples/spring-boot-sample-ws/ giving something like

@EnableWs
@Configuration
public class WebServiceConfig extends WsConfigurerAdapter {

    @Bean
    public ServletRegistrationBean dispatcherServlet(ApplicationContext applicationContext) {
        MessageDispatcherServlet servlet = new MessageDispatcherServlet();
        servlet.setApplicationContext(applicationContext);
        return new ServletRegistrationBean(servlet, "/services/*");
    }

    @Bean(name = "SOAP1")
    public DefaultWsdl11Definition defaultWsdl11Definition(XsdSchema soap1) {
        DefaultWsdl11Definition wsdl11Definition = new DefaultWsdl11Definition();
        wsdl11Definition.setPortTypeName("Soap1");
        wsdl11Definition.setLocationUri("/soap1/");
        wsdl11Definition.setTargetNamespace("http://mycompany.com/hr/definitions");
        wsdl11Definition.setSchema(soap1);
        return wsdl11Definition;
    }

    @Bean
    public XsdSchema soap1() {
        return new SimpleXsdSchema(new ClassPathResource("META-INF/schemas/hr.xsd"));
    }

}

and Web Security according to http://spring.io/blog/2013/07/03/spring-security-java-config-preview-web-security/ looks like this

@EnableWebSecurity
@Configuration
public class CustomWebSecurityConfigurerAdapter extends
   WebSecurityConfigurerAdapter {
  @Autowired
  public void configureGlobal(AuthenticationManagerBuilder auth) {
    auth
      .inMemoryAuthentication()
        .withUser("user1")  
          .password("password")
          .roles("SOAP1")
          .and()
        .withUser("user2") 
          .password("password")
          .roles("SOAP2");
  }

  @Override
  protected void configure(HttpSecurity http) throws Exception {
    http
      .authorizeUrls()
        .antMatchers("/soap/soap1").hasRole("SOAP1") 
        .antMatchers("/soap/soap2").hasRole("SOAP2") 
        .anyRequest().authenticated() 
        .and().httpBasic();
  }
}

After some searches, I found that Wss4J provides a UsernameToken authentication, but can't figure out how to use it. What I'm trying to do is the following https://sites.google.com/site/ddmwsst/ws-security-impl/ws-security-with-usernametoken but without XML files with bean definitions.

What I plan to do:

  • Create the Callback Handler.
  • Create a Wss4jSecurityInterceptor, setting "setValidationActions" to "UsernameToken", "setValidationCallbackHandler" to my callback handler, and then add it by overriding addInterceptors on my WebServiceConfig.

(I tried something like that, but I just realised my callback was using a deprecated method)

Problem : Even if it works, it would then apply to all my webservices on "WebServiceConfig".

Update :

The implementation does work, but as expected it is applied to all my Web Services. How could I add my interceptor only to 1 Web Service ?

Following, the code I added in WebServiceConfig

 @Bean
    public Wss4jSecurityInterceptor wss4jSecurityInterceptor() throws IOException, Exception{
        Wss4jSecurityInterceptor interceptor = new Wss4jSecurityInterceptor();
        interceptor.setValidationActions("UsernameToken");
        interceptor.setValidationCallbackHandler(new Wss4jSecurityCallbackImpl());

    return interceptor;
}

@Override
public void addInterceptors(List<EndpointInterceptor> interceptors)  {
    try {
        interceptors.add(wss4jSecurityInterceptor());
    } catch (Exception e) {
        e.printStackTrace();
    }
}
Codon answered 24/9, 2015 at 21:37 Comment(0)
C
17

Sorry, I totally forgot to answer this, but in case it helps someone :

We got it working by creating a new SmartEndpointInterceptor, and applying it only to our endpoint:

public class CustomSmartEndpointInterceptor extends Wss4jSecurityInterceptor implements SmartEndpointInterceptor {

    //CustomEndpoint is your @Endpoint class
    @Override
    public boolean shouldIntercept(MessageContext messageContext, Object endpoint) {
        if (endpoint instanceof MethodEndpoint) {
            MethodEndpoint methodEndpoint = (MethodEndpoint)endpoint;
            return methodEndpoint.getMethod().getDeclaringClass() == CustomEndpoint.class; 
        }
        return false;
    }
}

instead of adding a wss4j bean to the WebServiceConfig, we added our SmartEndpointInterceptor :

@Configuration
public class SoapWebServiceConfig extends WsConfigurationSupport {

    //Wss4jSecurityCallbackImpl refers to an implementation of https://sites.google.com/site/ddmwsst/ws-security-impl/ws-security-with-usernametoken
    @Bean
    public CustomSmartEndpointInterceptor customSmartEndpointInterceptor() {
        CustomSmartEndpointInterceptor customSmartEndpointInterceptor = new CustomSmartEndpointInterceptor();
        customSmartEndpointInterceptor.setValidationActions("UsernameToken");
        customSmartEndpointInterceptor.setValidationCallbackHandler(new Wss4jSecurityCallbackImpl(login, pwd)); 
        return customSmartEndpointInterceptor;
    }

  [...]
}

Hope this is clear enough :)

Codon answered 3/3, 2016 at 17:41 Comment(3)
I'm running into the same issue. I tried doing exactly as you mentioned above but the shouldIntercept method never gets hit.Demogorgon
Did you follow this spring.io/guides/gs/producing-web-service/… ? It's the configuration I'm running with. You should have a class extending WsConfigurationSupport, and that's where you should add the customSmartEndpointInterceptor Bean.Codon
Chrisophe, it has been a while you answered this question, but can you please look at this question, #72579281Instructive
A
2

It is worthworthy to note that whether is the result of the method shouldIntercept, the program would execute anyways the handleRequest method.
This can be dangerous, for example, in the login process.
In a project that I'm developing, we have only two endpoints:

  • UserLoginEndpoint
  • SomeGeneralEndpoint

The login would be invoked only for logging in purposes and will produce a token that I'll have to parse somehow from the request (this is done via an interceptor, the only one that we need in the application).
Suppose we have the following interceptor, just like Christophe Douy proposed and that our class of interest would be the UserLoginEndpoint.class

public class CustomSmartEndpointInterceptor extends Wss4jSecurityInterceptor implements SmartEndpointInterceptor {

//CustomEndpoint is your @Endpoint class
@Override
public boolean shouldIntercept(MessageContext messageContext, Object endpoint) {
    if (endpoint instanceof MethodEndpoint) {
        MethodEndpoint methodEndpoint = (MethodEndpoint)endpoint;
        return methodEndpoint.getMethod().getDeclaringClass() == UserLoginEndpoint.class; 
    }
    return false;
}

If this returns true, by all means, that's good and the logic defined in the handleRequest method will be executed.
But where's my issue?
For my specific problem, I'm writing an interceptor that should get in the way only if the user has already logged in. This means that the previous snippet code should be the following

public class CustomSmartEndpointInterceptor extends Wss4jSecurityInterceptor implements SmartEndpointInterceptor {

//CustomEndpoint is your @Endpoint class
@Override
public boolean shouldIntercept(MessageContext messageContext, Object endpoint) {
    if (endpoint instanceof MethodEndpoint) {
        MethodEndpoint methodEndpoint = (MethodEndpoint)endpoint;
        return methodEndpoint.getMethod().getDeclaringClass() != UserLoginEndpoint.class; 
    }
    return false;
}

And if that would be true, the handleRequest method would be executed (my implementation is below)

@Override
public boolean handleRequest(MessageContext messageContext, Object endpoint) throws Exception {

    System.out.println("### SOAP REQUEST ###");


    InputStream is = null;
    DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
    factory.setNamespaceAware(true);
    Document doc = null;
    try {
        ByteArrayOutputStream buffer = new ByteArrayOutputStream();
        messageContext.getRequest().writeTo(buffer);
        String payload = buffer.toString(java.nio.charset.StandardCharsets.UTF_8.name());
        System.out.println(payload);
        is = new ByteArrayInputStream(payload.getBytes());
        DocumentBuilder builder = factory.newDocumentBuilder();
        doc = builder.parse(is);
    } catch (IOException ex) {
        ex.printStackTrace();
        return false;
    }

    XPath xpath = XPathFactory.newInstance().newXPath();
    xpath.setNamespaceContext(new NamespaceContext() {
        @Override
        public String getNamespaceURI(String prefix) {
            switch(prefix) {
                case "soapenv":
                    return "http://schemas.xmlsoap.org/soap/envelope/";
                case "it":
                    return "some.fancy.ws";
                default:
                    return null;
            }
        }

        @Override
        public String getPrefix(String namespaceURI) {
            return null;
        }

        @Override
        public Iterator getPrefixes(String namespaceURI) {
            return null;
        }
    });
    XPathExpression expr = xpath.compile("//*//it:accessToken//text()");
    Object result = expr.evaluate(doc, XPathConstants.NODE);
    Node node = (Node) result;
    String token = node.getNodeValue();
    return authUtility.checkTokenIsValid(token);

}

But what happens if shouldIntercept returns false? If the handleRequest method, which is mandatory to implement if you "implements" SmartPointEndPointInterceptor, returns true, the invocation chain will keep on; but if it returns false, it will stop there: I'm in the second case, but the handleRequest still gets executed.
The only workaround that I found is to add a property in the MessageContext which has an arbitrary key and a corresponding value which is the one returned from the shouldIntercept method.
Then negate that value in the very first lines of your handleRequest's implementation to force the return true and have the invocation chain

    if (!(Boolean)messageContext.getProperty("shouldFollow")) {
        return true;
    }

Of course, this will work in projects where only one interceptor is needed (i.e., in my case just to verify if the user is really logged in) and there are many other factors that might influence everything but I felt it was worthy to share in this topic.
I apologize in advance if I made a mistake in answering here instead of opening a new question

Asiaasian answered 13/2, 2019 at 11:10 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.