Securing a webservice at method level : Apache CXF + Spring Security + JSR-250 + wss4j

Hi reader,

I was once told to write a service. It was initialy aimed to a web application. Following the YAGNI principle, so far, no need to remote anything, the service was jvm co-located.
The requirements evolved such that I the same service was need by other apps. I knew I had to remote the service. The remoting technology I’m the most familiar with and comfortable is webservice mainly thanks to Apache CXF.

I was used to configuring security with spring-security in a webapp at servlet and filter level.
With this service no logging page, no session, no filter (not exactly true but true in the way we think about them).
I found out that WSS4J was exactly designed for my need : it allows one to apply request and response interceptors. One can implement any security behaviour : transmitting credentials (client part). spring-security takes it from here : it tries to match credentials against a user repository then enables fined grained security at level method.
The absolute solution adds an HTTPS channel between client and server.
I will focus on the wss4j part, as the https part is usually configured in Apache Httpd by enabling mod_ssl, mod_rewrite, etc.

For the example I came up with those stories :
– As anonymous I should find a product by description
– As anonymous I should find a product by id
– As anonymous I should not add a product
– As anonymous I should not delete a product
– As anonymous I should not update a product

These following steps allowed me to test such stories :

– define the functionnalities to protect

..... omitted for clarity
   /**
    * @see org.diveintojee.poc.wss4j.domain.services.ProductService#add(org.diveintojee.poc.wss4j.domain.Product)
    */
   @Override
   @RolesAllowed( {Role.ADMIN_ROLE_ID} )
   public Long add(Product product) {

      if (product == null)
         return null;

      return persistenceManager.persist(product);

   }
..... omitted for clarity

– define users, roles

<?xml version="1.0" encoding="UTF-8"?>

<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context"
	xmlns:sec="http://www.springframework.org/schema/security"
	xsi:schemaLocation="http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
        http://www.springframework.org/schema/context
        http://www.springframework.org/schema/context/spring-context-3.0.xsd
        http://www.springframework.org/schema/security
        http://www.springframework.org/schema/security/spring-security-3.0.xsd">

	<sec:global-method-security
		jsr250-annotations="enabled" />

	<bean
		class="org.springframework.beans.factory.config.MethodInvokingFactoryBean">
		<property name="targetClass"
			value="org.springframework.security.core.context.SecurityContextHolder" />
		<property name="targetMethod" value="setStrategyName" />
		<property name="arguments" value="MODE_INHERITABLETHREADLOCAL" />
	</bean>

	<sec:authentication-manager>
		<sec:authentication-provider>
			<sec:user-service id="userDetailsService">
				<sec:user name="anonymous" password="anonymous" authorities="ROLE_ANY" />
				<sec:user name="admin" password="*password@0" authorities="ROLE_ADM" />
			</sec:user-service>
		</sec:authentication-provider>
	</sec:authentication-manager>
</beans>

– activate protections against roles

	<jaxws:endpoint id="productServiceWsEndpoint"
		implementor="#productService" address="/ProductService">
		
		<jaxws:serviceFactory>
			<ref bean="jaxws-service-factory" />
		</jaxws:serviceFactory>

		<jaxws:inInterceptors>
			<bean class="org.apache.cxf.binding.soap.saaj.SAAJInInterceptor" />
			<bean class="org.apache.cxf.ws.security.wss4j.WSS4JInInterceptor">
				<constructor-arg>
					<map>
						<entry key="action" value="UsernameToken" />
						<entry key="passwordType" value="PasswordText" />
						<entry>
							<key>
								<value>passwordCallbackRef</value>
							</key>
							<ref bean="serverPasswordMatcherHandler" />
						</entry>
					</map>
				</constructor-arg>
			</bean>
		</jaxws:inInterceptors>
		
	</jaxws:endpoint>
package org.diveintojee.poc.wss4j.security;
/**
 * 
 */


import java.io.IOException;

import javax.security.auth.callback.Callback;
import javax.security.auth.callback.CallbackHandler;
import javax.security.auth.callback.UnsupportedCallbackException;

import org.apache.ws.security.WSPasswordCallback;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

/**
 * @author louis.gueye@gmail.com
 *
 */
@Component(ServerPasswordMatcherHandler.BEAN_ID)
public class ServerPasswordMatcherHandler implements CallbackHandler {

	public static final String BEAN_ID = "serverPasswordMatcherHandler"; 
	
	@Autowired
	private AuthenticationManager providerManager;
	
	/**
	 * @see javax.security.auth.callback.CallbackHandler#handle(javax.security.auth.callback.Callback[])
	 */
	public void handle(Callback[] callbacks) throws IOException,
			UnsupportedCallbackException {
		WSPasswordCallback pc = null;
		for (Callback callback : callbacks) {
			if (callback instanceof WSPasswordCallback) {
				pc = (WSPasswordCallback)callback; break;
			}
		}
		
		if (providerManager == null) throw new IllegalStateException("authenticationProvider should've been wired");
		
		if (pc != null && StringUtils.hasText(pc.getIdentifier())) {
			
			Authentication authentication  = providerManager.authenticate(new UsernamePasswordAuthenticationToken(pc.getIdentifier(), pc.getPassword()));
			SecurityContextHolder.getContext().setAuthentication(authentication);
			
		}

	}

}

– test

	/**
	 * 
	 */
	@Test(expected=SOAPFaultException.class)
	public void anonymousShouldNotDelete() {

		String name = "doliprane";

		String description = "Ce médicament est un antalgique et un antipyrétique qui contient du paracétamol."
			+ "\nIl est utilisé pour faire baisser la fièvre et dans le traitement des affections douloureuses.";

		Long productId = addProduct(name, description, ADMIN_LOGIN, ADMIN_PASSWORD);

		securedService = getSecuredProxy(ANONYMOUS_LOGIN, ANONYMOUS_PASSWORD);
		
		assertNotNull(securedService.get(productId));
		
		securedService.delete(productId);

	}

	@SuppressWarnings("rawtypes")
	private ProductService getSecuredProxy(String login, final String password) {
		JaxWsProxyFactoryBean factory = new JaxWsProxyFactoryBean();
		factory.setServiceClass(ProductService.class);
		factory.setAddress(WSS4J_POC_SERVER_URL);
		List<Interceptor> inInterceptors = new ArrayList<Interceptor>();
		inInterceptors.add(new LoggingInInterceptor());
		List<Interceptor> outInterceptors = new ArrayList<Interceptor>();
		outInterceptors.add(new LoggingOutInterceptor());
		outInterceptors.add(new SAAJOutInterceptor());
		final Map<String, Object> authConfig = new HashMap<String, Object>();

		authConfig.put(WSHandlerConstants.ACTION, WSHandlerConstants.USERNAME_TOKEN);
		authConfig.put(WSHandlerConstants.USER, login);
		authConfig.put(WSHandlerConstants.PASSWORD_TYPE, "PasswordText");

		authConfig.put(WSHandlerConstants.PW_CALLBACK_REF, new CallbackHandler()
		{
			public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException
			{
				WSPasswordCallback pc = (WSPasswordCallback) callbacks[0];
				pc.setIdentifier((String)authConfig.get(WSHandlerConstants.USER));
				pc.setPassword(password);
			}
		});

		outInterceptors.add(new WSS4JOutInterceptor(authConfig));
		factory.setInInterceptors(inInterceptors);
		factory.setOutInterceptors(outInterceptors);

		return (ProductService) factory.create();

	}

I did not have time to :
– use spring built-in sha password encoding system
– implement real anonymous : no security check when credentials are not provided.

You can dwnload the full source code here

Enjoy.

Louis.

Advertisements

11 thoughts on “Securing a webservice at method level : Apache CXF + Spring Security + JSR-250 + wss4j

  1. Hi,

    Im having the problem that my pc.getPassword() is empty in my server callback although I do send a password. Can you perhaps help me out here. My callback looks exaclty like yours and my endpoint config looks like:

  2. Hopefully now the code will show:

    [jaxws:endpoint id=”calendarWebService” implementor=”#calendarService”
    address=”/CalendarService”]

    [jaxws:inInterceptors]
    [bean class=”org.apache.cxf.binding.soap.saaj.SAAJInInterceptor” /]
    [bean class=”org.apache.cxf.ws.security.wss4j.WSS4JInInterceptor”]
    [constructor-arg]
    [map]
    [entry key=”action” value=”UsernameToken Timestamp” /]
    [entry key=”passwordType” value=”PasswordDigest” /]
    [entry key=”passwordCallbackRef”]
    [ref bean=”customPasswordCallback” /]
    [/entry]
    [/map]
    [/constructor-arg]
    [/bean]
    [/jaxws:inInterceptors]
    [/jaxws:endpoint]

    Seems like it doesnt like the greater then signs

  3. The probleem seems to be with the passwordType. When i change it to PasswordText, i do get the client password and it works fine. When i set it to PasswordDigest, the pc.getPassword() returns null.

    Do you have a clue how to fix this?

  4. No problem…it works now and by the looks of it, password digest is indeed not supported yet. Not a real issue as we are using SSL. Thanks for your article though!

  5. Hi Louis,

    As you mentioned Basic auth setup is pretty strait forward.
    How do I setup digest auth type to webservice client using cxf ?
    As of now my client by default assumes Basic auth type when I set the username password in the factory object. And the request message header have “Authentication [Basic: ……]” in it.
    I want to setup a cxf client for digest auth. could you please assist ?

    Many thanks

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s