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.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
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.

001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
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:

001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
<?xml version="1.0" encoding="UTF-8"?>
       xmlns:context="http://www.springframework.org/schema/context"
              xmlns:s="http://www.springframework.org/schema/security"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
  
  
    <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:

1
2
3
4
5
6
7
8
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.

01
02
03
04
05
06
07
08
09
10
11
   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:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
<?xml version="1.0" encoding="UTF-8"?>
  
  
    <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:

1
2
3
4
5
6
7
8
<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:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
        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
12 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
11 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
10 years ago

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

Back to top button