Saturday 3 September 2011

CXF, WS-Security and Spring Security

I wanted to find a way to integrate CXF and Spring Security so that CXF would take the WS-Security details and populate the SecurityContextHolder; leaving the service implementation to carry out authentication/authorisation processes using Spring Security features. I found a post on the Spring forums: http://forum.springsource.org/showthread.php?64492-WS-Security-integration that overrode the WSS4JInInterceptor and populated the SecurityContext here, but I wasn't totally happy with this solution: in theory it was possible that a different thread could handle a different interceptor in the chain further down the line possibly causing elevated privileges for a thread/caller that doesn't have the permissions. Furthermore the thread didn't remove the SpringContext details after execution which might cause security problems on outgoing interceptors if care wasn't taken. Inspired by the RMI ContextPropagatingRemoteInvocation offered by Spring I changed the CXF method invoker to set the SecurityContext for the calling thread, execute the method and then remove the SecurityContext. I made modifications to the request chain to make this happen; while all my code is in Groovy I've tried to make it as close to Java as possible, I suspect translation should be a trivial exercise.

I override the WSS4JInInterceptor to get the WS-Security details from the message, these are put into a Spring UsernamePasswordAuthenticationToken then placed into the messages's exchange under the key "WS_TOKEN". The CXF Architecture defines an exchange as holding "references to the in, out and fault messages for the current message exchange"; I wanted to include the value in the actual Message object but only the exchange was available to the method invocation (described later) - exchange would have to do. The following is the code for SpringAuthnInterceptor.groovy:
package ws

import org.apache.cxf.ws.security.wss4j.WSS4JInInterceptor
import org.apache.cxf.binding.soap.SoapMessage
import org.apache.cxf.interceptor.Fault
import org.apache.ws.security.handler.WSHandlerConstants
import org.apache.ws.security.handler.WSHandlerResult
import org.apache.ws.security.WSSecurityEngineResult
import org.apache.ws.security.WSConstants
import org.apache.ws.security.WSUsernameTokenPrincipal
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken
import org.apache.cxf.ws.security.SecurityConstants

public class SpringAuthnInterceptor extends WSS4JInInterceptor {

    public SpringAuthnInterceptor() {
        super()
    }

    public SpringAuthnInterceptor(Map properties) {
        super (properties)
    }

    //with help from: http://forum.springsource.org/showthread.php?64492-WS-Security-integration
    public void handleMessage(SoapMessage message) throws Fault {
        message.put(SecurityConstants.VALIDATE_TOKEN, Boolean.FALSE);
        super.handleMessage(message)
        message.getContextualProperty(WSHandlerConstants.RECV_RESULTS).each { WSHandlerResult result ->
            result.getResults().each { WSSecurityEngineResult securityResult ->
                if ((securityResult.get(WSSecurityEngineResult.TAG_ACTION) & WSConstants.UT) > 0) {
                    WSUsernameTokenPrincipal principal = (WSUsernameTokenPrincipal)securityResult.get(WSSecurityEngineResult.TAG_PRINCIPAL)
                    if (!principal.getPassword()) principal.setPassword("")
                    UsernamePasswordAuthenticationToken usernameToken = new UsernamePasswordAuthenticationToken(principal.getName(), principal.getPassword())
                    message.getExchange().put("WS_TOKEN", usernameToken)
                }
            }
        }
    }
}

Once the user details are populated we need to override BeanInvoker to change the way that the WS method is invoked to add, then remove the SecurityContext. The following is SpringSecurityWSInvoker.groovy which carries this out:
package ws

import org.apache.cxf.service.invoker.BeanInvoker
import org.apache.cxf.message.Exchange;
import java.lang.reflect.Method
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken
import org.springframework.security.core.context.SecurityContextHolder

class SpringSecurityWSInvoker extends BeanInvoker {
    public SpringSecurityWSInvoker(Object proxy) {
        super(proxy)
    }

    protected Object performInvocation(Exchange exchange, final Object serviceObject, Method m,
        Object[] paramArray) throws Exception {
        Object invocationResult = null
        try {
            UsernamePasswordAuthenticationToken token = (UsernamePasswordAuthenticationToken)exchange.get("WS_TOKEN")
            if (token) {
                SecurityContextHolder.getContext().setAuthentication(token)
            }
            invocationResult = super.performInvocation(exchange,serviceObject,m,paramArray)
        } finally {
            SecurityContextHolder.clearContext()
        }

        return invocationResult
    }
}

Now we can use Spring to set up the relevant web service with the additional interceptor and overridden method invoker. I've used the following with the Spring DSL (I'm sure it translates to Spring XML easily also):
wsServerFactory(org.apache.cxf.frontend.ServerFactoryBean) {
        serviceClass = wsInterface
        address = "${ws.protocol}://${ws.host}:${ws.port}/${ws.serviceName}"
        invoker = new SpringSecurityWSInvoker(wsInterfaceInstance)
        inInterceptors = [new SpringAuthnInterceptor(["action":"UsernameToken","passwordType":"PasswordText"])]
        serviceName = new QName(ws.namespace,ws.serviceName,"")
    }
Where wsInterface is the webservice interface code that forms the WSDL and wsInterfaceInstance is the instantce of the implementation of this interface. To start the server the following code could be used:
def serverFactory = appContext.getBean("wsServerFactory")
serverFactory.create()

No comments:

Post a Comment