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.