Monday, October 26, 2009

CXF, WSS4J & Spring Security Recipe


How to use Spring Security to authenticate a user/password web service implemented with CXF using WSS4J (Apache Java WS-Security):

In the Spring Application context, we define the namespaces, import CXF's xml and define the "Interceptor" to deal with security:
<?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:p="http://www.springframework.org/schema/p"
    xmlns:cxf="http://cxf.apache.org/core"
    xmlns:util="http://www.springframework.org/schema/util"
    xmlns:jaxws="http://cxf.apache.org/jaxws"
    xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
        http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-2.5.xsd
        http://cxf.apache.org/core http://cxf.apache.org/schemas/core.xsd
        http://cxf.apache.org/jaxws http://cxf.apache.org/schemas/jaxws.xsd
        http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util-2.5.xsd"
    >

    <!-- Load CXF modules from cxf.jar -->
    <import resource="classpath:META-INF/cxf/cxf.xml" />
    <import resource="classpath:META-INF/cxf/cxf-extension-soap.xml" />
    <import resource="classpath:META-INF/cxf/cxf-servlet.xml" />

    <bean id="WSS4JInInterceptor" class="org.apache.cxf.ws.security.wss4j.WSS4JInInterceptor">
     <property name="properties">
      <map>
          <entry key="action" value="UsernameToken Timestamp"/>
          <entry key="passwordType" value="PasswordDigest"/>
          <entry key="passwordCallbackRef">
           <bean class="my.company.SecurityInPasswordHandler"/>
          </entry>
      </map>
     </property>
   </bean>
</beans>

And then we add the interceptor to the webservice:
<jaxws:endpoint id="salaWebService" implementor="#salaService" address="/salas">
    <jaxws:inInterceptors>
        <ref bean="WSS4JInInterceptor"/>
    </jaxws:inInterceptors>
</jaxws:endpoint>

Assuming we have the following service:
@WebService
@SOAPBinding
public interface SalaService {
    @WebResult(name = "sala")
    public abstract Sala get(@WebParam(name = "id") Long id);
}

@Service("salaService")
@WebService(serviceName = "SalaService", portName = "SalaPort", endpointInterface = "my.company.service.SalaService")
public class SalaServiceImpl implements SalaService {
    public Sala get(Long id) {
        return (Sala) executor.execute(SalaBusinessOperations.GET, new Long(id));
    }
}

And CXF configured in web.xml as follows:
<servlet>
    <servlet-name>CXFServlet</servlet-name>
    <servlet-class>org.apache.cxf.transport.servlet.CXFServlet</servlet-class>
</servlet>    
<servlet-mapping>
    <servlet-name>CXFServlet</servlet-name>
    <url-pattern>/services/*</url-pattern>
</servlet-mapping>

The spring security should be configured with something like this:
<http >
    <intercept-url pattern="/services/**" access="ROLE_ANONYMOUS" />
    <intercept-url pattern="/**" access="ROLE_USER" />
    <logout/>
    <anonymous/>
</http>

Finally we could write the interceptor class:
public class SecurityInPasswordHandler implements CallbackHandler {
    @Autowired
    private AuthenticationManager authenticationManager;
    @Autowired
    private UserDetailsService userService;

    public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException, AuthenticationException {

        WSPasswordCallback pwdCallback = (WSPasswordCallback) callbacks[0];

        int usage = pwdCallback.getUsage();
        if ((usage == WSPasswordCallback.USERNAME_TOKEN) || (usage == WSPasswordCallback.USERNAME_TOKEN_UNKNOWN)) {
            String password = pwdCallback.getPassword();
            if (usage == WSPasswordCallback.USERNAME_TOKEN) {
                UserDetails userDetails = userService.loadUserByUsername(pwdCallback.getIdentifier());
                password = userDetails.getPassword();
            }
            Authentication authentication = new UsernamePasswordAuthenticationToken(pwdCallback.getIdentifier(), password);
            authentication = authenticationManager.authenticate(authentication); //throws AuthenticationException
            SecurityContextHolder.getContext().setAuthentication(authentication);
            // Return the password to the caller
            pwdCallback.setPassword(password);
        }
    }
}

Now we could handle plain (PasswordText) or encrypted (PasswordDigest) passwords. In both cases creating a SecurityContext for the request to have the user roles and use them in our Spring Security "@Secured" annotations.