Enterprise Java

Spring 3, Spring Web Services 2 & LDAP Security

This year started on a good note, another one of those “the deadline won’t change” / “skip all the red tape” / “Wild West” type of projects in which I got to figure out and implement some functionality using some relatively new libraries and tech for a change, well Spring 3 ain’t new but in the Java 5, weblogic 10(.01), Spring 2.5.6 slow corporate kind of world it is all relative.

Due to general time constraints I am not including too much “fluff” in this post, just the nitty gritty of creating and securing a Spring 3 , Spring WS 2 web service using multiple XSDs and LDAP security.

The Code:

The Service Endpoint: ExampleServiceEndpoint
This is the class that will be exposed as web service using the configuration later in the post.

package javaitzen.spring.ws;

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;

import javax.annotation.Resource;


@Endpoint
public class ExampleServiceEndpoint {

    private static final String NAMESPACE_URI = "http://www.briandupreez.net";

    /**
     * Autowire a POJO to handle the business logic
    @Resource(name = "businessComponent")
    private ComponentInterface businessComponent;
   */

    public ExampleServiceEndpoint() {
        System.out.println(">>  javaitzen.spring.ws.ExampleServiceEndpoint loaded.");
    }

    @PayloadRoot(localPart = "ProcessExample1Request", namespace = NAMESPACE_URI + "/example1")
    @ResponsePayload
    public Example1Response processExample1Request(@RequestPayload final Example1 request) {
        System.out.println(">> process example request1 ran.");
        return new Example1Response();
    }

    @PayloadRoot(localPart = "ProcessExample2Request", namespace = NAMESPACE_URI + "/example2")
    @ResponsePayload
    public Example2Response processExample2Request(@RequestPayload final Example2 request) {
        System.out.println(">> process example request2 ran.");
        return new Example2Response();
    }

}

The Code: CustomValidationCallbackHandler

This was my bit of custom code I wrote to extend the AbstactCallbackHandler allowing us to use LDAP.
As per the comments in the CallbackHandler below, it’s probably a good idea to have a cache manager, something like Hazelcast or Ehcache to cache authenticated users, depending on security / performance considerations.

The Digest Validator below can just be used directly from the Sun library, I was just wanted to see how it worked.

package javaitzen.spring.ws;

import com.sun.org.apache.xml.internal.security.exceptions.Base64DecodingException;
import com.sun.xml.wss.impl.callback.PasswordValidationCallback;
import com.sun.xml.wss.impl.misc.Base64;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.util.Assert;
import org.springframework.ws.soap.security.callback.AbstractCallbackHandler;

import javax.security.auth.callback.Callback;
import javax.security.auth.callback.UnsupportedCallbackException;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.security.MessageDigest;
import java.util.Properties;


public class CustomValidationCallbackHandler extends AbstractCallbackHandler implements InitializingBean {

    private Properties users = new Properties();
    private AuthenticationManager ldapAuthenticationManager;

    @Override
    protected void handleInternal(final Callback callback) throws IOException, UnsupportedCallbackException {

        if (callback instanceof PasswordValidationCallback) {
            final PasswordValidationCallback passwordCallback = (PasswordValidationCallback) callback;
            if (passwordCallback.getRequest() instanceof PasswordValidationCallback.DigestPasswordRequest) {
                final PasswordValidationCallback.DigestPasswordRequest digestPasswordRequest =
                        (PasswordValidationCallback.DigestPasswordRequest) passwordCallback.getRequest();
                final String password = users
                        .getProperty(digestPasswordRequest
                                .getUsername());
                digestPasswordRequest.setPassword(password);
                passwordCallback
                        .setValidator(new CustomDigestPasswordValidator());

            }
            if (passwordCallback.getRequest() instanceof PasswordValidationCallback.PlainTextPasswordRequest) {
                passwordCallback
                        .setValidator(new LDAPPlainTextPasswordValidator());


            }
        } else {
            throw new UnsupportedCallbackException(callback);
        }

    }

    /**
     * Digest Validator.
     * This code is directly from the sun class, I was just curious how it worked.
     */
    private class CustomDigestPasswordValidator implements PasswordValidationCallback.PasswordValidator {
        public boolean validate(final PasswordValidationCallback.Request request) throws PasswordValidationCallback.PasswordValidationException {

            final PasswordValidationCallback.DigestPasswordRequest req = (PasswordValidationCallback.DigestPasswordRequest) request;
            final String passwd = req.getPassword();
            final String nonce = req.getNonce();
            final String created = req.getCreated();
            final String passwordDigest = req.getDigest();
            final String username = req.getUsername();

            if (null == passwd)
                return false;
            byte[] decodedNonce = null;
            if (null != nonce) {
                try {
                    decodedNonce = Base64.decode(nonce);
                } catch (final Base64DecodingException bde) {
                    throw new PasswordValidationCallback.PasswordValidationException(bde);
                }
            }
            String utf8String = "";
            if (created != null) {
                utf8String += created;
            }
            utf8String += passwd;
            final byte[] utf8Bytes;
            try {
                utf8Bytes = utf8String.getBytes("utf-8");
            } catch (final UnsupportedEncodingException uee) {
                throw new PasswordValidationCallback.PasswordValidationException(uee);
            }

            final byte[] bytesToHash;
            if (decodedNonce != null) {
                bytesToHash = new byte[utf8Bytes.length + decodedNonce.length];
                for (int i = 0; i < decodedNonce.length; i++)
                    bytesToHash[i] = decodedNonce[i];
                for (int i = decodedNonce.length;
                     i < utf8Bytes.length + decodedNonce.length;
                     i++)
                    bytesToHash[i] = utf8Bytes[i - decodedNonce.length];
            } else {
                bytesToHash = utf8Bytes;
            }
            final byte[] hash;
            try {
                final MessageDigest sha = MessageDigest.getInstance("SHA-1");
                hash = sha.digest(bytesToHash);
            } catch (final Exception e) {
                throw new PasswordValidationCallback.PasswordValidationException(
                        "Password Digest could not be created" + e);
            }
            return (passwordDigest.equals(Base64.encode(hash)));
        }

    }


    /**
     * LDAP Plain Text validator.
     */
    private class LDAPPlainTextPasswordValidator implements
            PasswordValidationCallback.PasswordValidator {

        /**
         * Validate the callback against the injected LDAP server.
         * Probably a good idea to have a cache manager - ehcache /  hazelcast injected to cache authenticated users.
         *
         * @param request the callback request
         * @return true if login successful
         * @throws PasswordValidationCallback.PasswordValidationException
         *
         */
        public boolean validate(final PasswordValidationCallback.Request request) throws PasswordValidationCallback.PasswordValidationException {
            final PasswordValidationCallback.PlainTextPasswordRequest plainTextPasswordRequest =
                    (PasswordValidationCallback.PlainTextPasswordRequest) request;
            final String username = plainTextPasswordRequest.getUsername();

            final Authentication authentication;
            final Authentication userPassAuth = new UsernamePasswordAuthenticationToken(username, plainTextPasswordRequest.getPassword());
            authentication = ldapAuthenticationManager.authenticate(userPassAuth);

            return authentication.isAuthenticated();

        }
    }

    /**
     * Assert users.
     *
     * @throws Exception error
     */
    public void afterPropertiesSet() throws Exception {
        Assert.notNull(users, "Users is required.");
        Assert.notNull(this.ldapAuthenticationManager, "A LDAP Authentication manager is required.");
    }


    /**
     * Sets the users to validate against. Property names are usernames, property values are passwords.
     *
     * @param users the users
     */
    public void setUsers(final Properties users) {
        this.users = users;
    }

    /**
     * The the authentication manager.
     *
     * @param ldapAuthenticationManager the provider
     */
    public void setLdapAuthenticationManager(final AuthenticationManager ldapAuthenticationManager) {
        this.ldapAuthenticationManager = ldapAuthenticationManager;
    }
}

The service config:
The configuration for the Endpoint, CallbackHandler and the LDAP Authentication manager.
The Application Context – Server Side:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns="http://www.springframework.org/schema/beans"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:sws="http://www.springframework.org/schema/web-services"
              xmlns:s="http://www.springframework.org/schema/security"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd
       http://www.springframework.org/schema/web-services
       http://www.springframework.org/schema/web-services/web-services-2.0.xsd
       http://www.springframework.org/schema/context
       http://www.springframework.org/schema/context/spring-context.xsd
              http://www.springframework.org/schema/security
       http://www.springframework.org/schema/security/spring-security-3.0.xsd">
 
 
    <sws:annotation-driven/>
    <context:component-scan base-package="javaitzen.spring.ws"/>
 
 
    <sws:dynamic-wsdl id="exampleService"
                      portTypeName="javaitzen.spring.ws.ExampleServiceEndpoint"
                      locationUri="/exampleService/"
                      targetNamespace="http://www.briandupreez.net/exampleService">
        <sws:xsd location="classpath:/xsd/Example1Request.xsd"/>
        <sws:xsd location="classpath:/xsd/Example1Response.xsd"/>
        <sws:xsd location="classpath:/xsd/Example2Request.xsd"/>
        <sws:xsd location="classpath:/xsd/Example2Response.xsd"/>
    </sws:dynamic-wsdl>
 
    <sws:interceptors>
        <bean id="validatingInterceptor"
              class="org.springframework.ws.soap.server.endpoint.interceptor.PayloadValidatingInterceptor">
            <property name="schema" value="classpath:/xsd/Example1Request.xsd"/>
            <property name="validateRequest" value="true"/>
            <property name="validateResponse" value="true"/>
        </bean>
        <bean id="loggingInterceptor"
              class="org.springframework.ws.server.endpoint.interceptor.PayloadLoggingInterceptor"/>
           
        <bean class="org.springframework.ws.soap.security.xwss.XwsSecurityInterceptor">
            <property name="policyConfiguration" value="/WEB-INF/securityPolicy.xml"/>
            <property name="callbackHandlers">
                <list>
                    <ref bean="callbackHandler"/>
                </list>
            </property>
        </bean>
         
    </sws:interceptors>
 
 
 
    <bean id="callbackHandler" class="javaitzen.spring.ws.CustomValidationCallbackHandler">
        <property name="ldapAuthenticationManager" ref="authManager" />
    </bean>
 
    <s:authentication-manager alias="authManager">
        <s:ldap-authentication-provider
                user-search-filter="(uid={0})"
                user-search-base="ou=users"
                group-role-attribute="cn"
                role-prefix="ROLE_">
        </s:ldap-authentication-provider>
    </s:authentication-manager>
 
 
   <!-- Example... (inmemory apache ldap service) -->
    <s:ldap-server id="contextSource" root="o=example" ldif="classpath:example.ldif"/>
 
    <!--
    If you want to connect to a real LDAP server it would look more like:
    <s:ldap-server  id="contextSource" url="ldap://localhost:7001/o=example" manager-dn="uid=admin,ou=system" manager-password="secret">
    </s:ldap-server>-->
 
    <bean id="marshallingPayloadMethodProcessor"
          class="org.springframework.ws.server.endpoint.adapter.method.MarshallingPayloadMethodProcessor">
        <constructor-arg ref="serviceMarshaller"/>
        <constructor-arg ref="serviceMarshaller"/>
    </bean>
 
    <bean id="defaultMethodEndpointAdapter"
          class="org.springframework.ws.server.endpoint.adapter.DefaultMethodEndpointAdapter">
        <property name="methodArgumentResolvers">
            <list>
                <ref bean="marshallingPayloadMethodProcessor"/>
            </list>
        </property>
        <property name="methodReturnValueHandlers">
            <list>
                <ref bean="marshallingPayloadMethodProcessor"/>
            </list>
        </property>
    </bean>
 
 
    <bean id="serviceMarshaller" class="org.springframework.oxm.jaxb.Jaxb2Marshaller">
  <property name="classesToBeBound">
   <list>
    <value>javaitzen.spring.ws.Example1</value>
    <value>javaitzen.spring.ws.Example1Response</value>
    <value>javaitzen.spring.ws.Example2</value>
    <value>javaitzen.spring.ws.Example2Response</value>
   </list>
  </property>
        <property name="marshallerProperties">
            <map>
                <entry key="jaxb.formatted.output">
                     <value type="java.lang.Boolean">true</value>
                </entry>
            </map>
        </property>
 </bean>
 
</beans>

The Security Context – Server Side:

xwss:SecurityConfiguration xmlns:xwss="http://java.sun.com/xml/ns/xwss/config">
    <xwss:RequireTimestamp maxClockSkew="60" timestampFreshnessLimit="300"/>
    <!-- Expect plain text tokens from the client -->
    <xwss:RequireUsernameToken passwordDigestRequired="false" nonceRequired="false"/>
    <xwss:Timestamp/>
    <!-- server side reply token -->
    <xwss:UsernameToken name="server" password="server1" digestPassword="false" useNonce="false"/>
</xwss:SecurityConfiguration>

The Web XML:
Nothing really special here, just the Spring WS MessageDispatcherServlet.

        spring-ws
        org.springframework.ws.transport.http.MessageDispatcherServlet
     
        transformWsdlLocationstrue
     1
   

   
        spring-ws
        /*
   

The client config:
To test or use the service you’ll need the following:
The Application Context – Client Side Test:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
 
 
    <bean id="messageFactory" class="org.springframework.ws.soap.saaj.SaajSoapMessageFactory"/>
 
    <bean id="webServiceTemplate" class="org.springframework.ws.client.core.WebServiceTemplate">
        <constructor-arg ref="messageFactory"/>
        <property name="marshaller" ref="serviceMarshaller"/>
        <property name="unmarshaller" ref="serviceMarshaller"/>
        <property name="defaultUri" value="http://localhost:7001/example/spring-ws/exampleService"/>
        <property name="interceptors">
            <list>
                <ref local="xwsSecurityInterceptor"/>
            </list>
        </property>
    </bean>
 
 
    <bean id="xwsSecurityInterceptor"
          class="org.springframework.ws.soap.security.xwss.XwsSecurityInterceptor">
        <property name="policyConfiguration" value="testSecurityPolicy.xml"/>
        <property name="callbackHandlers">
            <list>
                <ref bean="callbackHandler"/>
            </list>
        </property>
    </bean>
 
    <!--  As a client the username and password generated by the server must match with the client! -->
    <!-- a simple callback handler to configure users and passwords with an in-memory Properties object. -->
    <bean id="callbackHandler"
          class="org.springframework.ws.soap.security.xwss.callback.SimplePasswordValidationCallbackHandler">
        <property name="users">
            <props>
             <prop key="server">server1</prop>
            </props>
        </property>
    </bean>
 
 
    <bean id="serviceMarshaller" class="org.springframework.oxm.jaxb.Jaxb2Marshaller">
  <property name="classesToBeBound">
   <list>
    <value>javaitzen.spring.ws.Example1</value>
    <value>javaitzen.spring.ws.Example1Response</value>
    <value>javaitzen.spring.ws.Example2</value>
    <value>javaitzen.spring.ws.Example2Response</value>
   </list>
  </property>
        <property name="marshallerProperties">
            <map>
                <entry key="jaxb.formatted.output">
                     <value type="java.lang.Boolean">true</value>
                </entry>
            </map>
        </property>
 </bean>

The Security Context – Client Side:

<xwss:SecurityConfiguration xmlns:xwss="http://java.sun.com/xml/ns/xwss/config">
    <xwss:RequireTimestamp maxClockSkew="60" timestampFreshnessLimit="300"/>
    <!-- Expect a plain text reply from the server -->
    <xwss:RequireUsernameToken passwordDigestRequired="false" nonceRequired="false"/>
    <xwss:Timestamp/>
    <!-- Client sending to server -->
    <xwss:UsernameToken name="example" password="pass" digestPassword="false" useNonce="false"/>
</xwss:SecurityConfiguration>

As usual with Java there can be a couple little nuances when it comes to jars and versions so below is part of the pom I used.
The Dependencies:

        3.0.6.RELEASE
        2.0.2.RELEASE
    


    
        
        
            org.apache.directory.server
            apacheds-all
            1.5.5
            jar
            compile
        
        
            org.springframework.ws
            spring-ws-core
            ${spring-ws-version}
        
        
            org.springframework
            spring-webmvc
            ${spring-version}
        
        
            org.springframework
            spring-web
            ${spring-version}
        
        
            org.springframework
            spring-context
            ${spring-version}
        
        
            org.springframework
            spring-core
            ${spring-version}
        
        
            org.springframework
            spring-beans
            ${spring-version}
        
        
            org.springframework
            spring-oxm
            ${spring-version}
        
        
            org.springframework.ws
            spring-ws-security
            ${spring-ws-version}
        
        
            org.springframework.security
            spring-security-core
            ${spring-version}
        
        
            org.springframework.security
            spring-security-ldap
            ${spring-version}
        
        
            org.springframework.ldap
            spring-ldap-core
            1.3.0.RELEASE
        
        
            org.apache.ws.security
            wss4j
            1.5.12
        
        
            com.sun.xml.wss
            xws-security
            3.0
        
        
        
            org.apache.ws.commons.schema
            XmlSchema
            1.4.2
        
    

</project>

Reference: Spring 3, Spring Web Services 2 & LDAP Security. from our JCG partner Brian Du Preez at the Zen in the art of IT blog.

Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

3 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Jay Khimani
Jay Khimani
11 years ago

Nice post. Is there a way to secure endpoint methods using RolesAllowed and see if the requesting user has specific permission then only allow invoking of that endpoint? Like we normally do with Spring service?

peto
peto
10 years ago

Dude, is something wrong with Spring, or you really need 20 pages of code and xml to run simple ldap auth?

Mat
Mat
9 years ago

Good simple example of integrating spring ws with ldap security.
Keep up the good work.

Back to top button