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.
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:
Hopefully now the code will show:
Hello,
I’d like to help U but nothing displays …
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
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?
Hi Hugo,
Sorry I was of no help but I didn’t dive enough into your problem.
My bet is that the PasswordDigest is not supported yet … Take a look at http://www.mail-archive.com/users@cxf.apache.org/msg08615.html
Hope this helps …
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!
Glad I could help ! I wish u success in your project
Due to https://issues.apache.org/jira/browse/CXF-3484
Hope this link may help you
Thx for the reference
Constructive feedback is always appreciated !
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