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.