Protecting Webservices with Acegi in Grails
This is a continuation of my previous post “Publishing and Consuming Webservices with Grails“.
Lets suppose you already have experience in your favorite Authentication provider and want to ensure that your webservices use the same users and authentication engine. For my purposes, Acegi (Spring Security) is that engine. I’m going to assume we’ll start from where the last post left off, (here’s the zip if you would like to follow along).
So first, install the plugin for Acegi with
grails install-plugin acegi
And then run
grails create-auth-domains
Before we get to far, I want to be sure to setup the Bootstrap so I can be sure that Acegi is doing its job (/grails-app/conf/BootStrap.groovy).
class BootStrap {
def init = { servletContext ->
//Protect everything
new Requestmap(url:"/**",configAttribute:"ROLE_ADMIN").save()
//Allow a user to login
new Requestmap(url:"/login/auth**",configAttribute:"IS_AUTHENTICATED_ANONYMOUSLY").save()
//And have a nice looking login page
new Requestmap(url:"/images/**",configAttribute:"IS_AUTHENTICATED_ANONYMOUSLY").save()
new Requestmap(url:"/css/**",configAttribute:"IS_AUTHENTICATED_ANONYMOUSLY").save()
//Our Webservices can be seen publicly, but require an authentication header
new Requestmap(url:"/ws/**",configAttribute:"IS_AUTHENTICATED_ANONYMOUSLY").save()
//Credentials Information
def pass = DU.shaHex("passw0rd")
//Create some users
def person1 = new Person(username:"user1", userRealName:"user", passwd:pass, email:"admin@site.com",emailShow:true, enabled:true, description:"a user")
def person2 = new Person(username:"user2", userRealName:"user two", passwd:pass, email:"user@user.com",emailShow:true, enabled:true, description:"user")
def clientAuth = new Authority(description:"Users",authority:"ROLE_USER")
.addToPeople(person1)
.addToPeople(person2)
.save()
}
def destroy = {
}
}
This will give us users and permissions when we run.
Alright, nothing too intense yet. But now we have to worry about securing our CXF web services with Acegi as an authentication provider. The first step involves getting dirty in the Spring xml config again. We need to add some in interceptors to the server. Pretty straight forward if you know what you are doing (I still don’t understand it great). Edit /web-app/WEB-INF/cxf-servlet.xml to look like the following:
<?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:simple="http://cxf.apache.org/simple" xmlns:soap="http://cxf.apache.org/bindings/soap" xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://cxf.apache.org/bindings/soap http://cxf.apache.org/schemas/configuration/soap.xsd http://cxf.apache.org/simple http://cxf.apache.org/schemas/simple.xsd"> <!--create CXF service--> <simple:server id="samplePublishedService" serviceClass="com.domain.services.Sample" address="/sample"> <simple:serviceBean> <bean class="SampleService" /> </simple:serviceBean> <simple: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="passwordCallbackRef"> <bean class="com.domain.security.PasswordHandler"/> </entry> </map> </constructor-arg> </bean> <bean class="com.domain.security.ValidateUserTokenInterceptor" /> </simple:inInterceptors> </simple:server> </beans>
I just referenced a couple classes that don’t exist, so I get you know whats next. Add those classes to /src/groovy/ with the following code:
PasswordHandler.groovy
package com.domain.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.codehaus.groovy.grails.commons.GrailsApplication;
import org.codehaus.groovy.grails.plugins.springsecurity.GrailsDaoAuthenticationProvider;
import org.springframework.security.AuthenticationManager;
import org.springframework.context.ApplicationContext;
import org.springframework.security.context.SecurityContextHolder;
import org.springframework.security.providers.UsernamePasswordAuthenticationToken;
import org.apache.ws.security.WSPasswordCallback;
/**
* This is passed to the WSS4JInHandler in the server configuration.
*
* @author bowlinguru based on XFire implementation by Michael Vorburger, based on XFire sample by <a href="mailto:tsztelak@gmail.com">Tomasz Sztelak</a>
*/
public class PasswordHandler implements CallbackHandler {
public PasswordHandler() {
}
public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException {
// There is nothing we could actually do here...
WSPasswordCallback pc = (WSPasswordCallback) callbacks[0];
String uid = pc.getIdentifer();
System.out.println(uid);
System.out.println(pc.getPassword());
// No pc.setPassword(password); - we don't have a pwd (and don't need one)
}
}
ValidateUserTokenInterceptor.groovy
package com.stewie.security
import org.apache.cxf.phase.AbstractPhaseInterceptor;
import org.apache.cxf.phase.Phase;
import org.apache.cxf.message.*;
import org.apache.cxf.interceptor.Fault;
import org.springframework.security.context.SecurityContextHolder;
import org.springframework.security.providers.UsernamePasswordAuthenticationToken;
import org.springframework.security.AuthenticationManager;
import org.apache.ws.security.WSConstants;
import org.apache.ws.security.WSSecurityEngineResult;
import org.apache.ws.security.WSUsernameTokenPrincipal;
import org.apache.ws.security.handler.WSHandlerConstants;
import org.apache.ws.security.handler.WSHandlerResult;
import org.codehaus.groovy.grails.plugins.springsecurity.GrailsDaoAuthenticationProvider;
import org.springframework.security.AuthenticationManager;
import org.springframework.context.ApplicationContext;
class ValidateUserTokenInterceptor extends AbstractPhaseInterceptor {
public ValidateUserTokenInterceptor(){
super(Phase.UNMARSHAL);
}
public ValidateUserTokenInterceptor(java.lang.String phase){
super(Phase.UNMARSHAL);
}
public ValidateUserTokenInterceptor(java.lang.String phase, boolean uniqueId){
super(Phase.UNMARSHAL);
}
public ValidateUserTokenInterceptor(java.lang.String i, java.lang.String p){
super(Phase.UNMARSHAL);
}
public ValidateUserTokenInterceptor(java.lang.String i, java.lang.String p, boolean uniqueId){
super(Phase.UNMARSHAL);
}
void handleMessage(Message message) throws Fault{
println message
Vector result = (Vector) message.getContextualProperty(WSHandlerConstants.RECV_RESULTS);
if (result==null) {
throw new IllegalArgumentException(WSHandlerConstants.RECV_RESULTS + " Property not found in MessageContext?!");
}
for (int i = 0; i < result.size(); i++) {
WSHandlerResult res = (WSHandlerResult) result.get(i);
for (int j = 0; j < res.getResults().size(); j++) {
WSSecurityEngineResult secRes = (WSSecurityEngineResult) res.getResults().get(j);
int action = secRes.getAction();
// USER TOKEN
if ((action & WSConstants.UT) > 0) {
WSUsernameTokenPrincipal principal = (WSUsernameTokenPrincipal) secRes.getPrincipal();
// "Set user property to user from UT to allow response encryption" (probably not needed at this point at Visana)
//context.setProperty(WSHandlerConstants.ENCRYPTION_USER, principal.getName());
org.codehaus.groovy.grails.commons.GrailsApplication dga = org.codehaus.groovy.grails.commons.ApplicationHolder.getApplication();
//grails.spring.BeanBuilder bb = new grails.spring.BeanBuilder();
ApplicationContext appContext = dga.getMainContext();//bb.createApplicationContext();
appContext.getBeanDefinitionNames().each{println it};
def sessionFactory = appContext.getBean("sessionFactory");
//Make sure we have a session
def session = sessionFactory.openSession();
get the authenticationManager
AuthenticationManager manager = (AuthenticationManager)appContext.getBean("authenticationManager");
// Set the Acegi Security Context
SecurityContextHolder.getContext().setAuthentication(
manager.authenticate(new UsernamePasswordAuthenticationToken(principal.getName(),principal.getPassword())));
}
}
}
}
}
Now we call the PasswordHandler, which makes sure there is a username and password, and then the authenticator, which calls our Acegi to create the session. We can get the user principal from the same way we would if it was a manual (webpage) log-in.
The beauty of this solution is that security is all handled discretely. All a user needs to do is call the web method with their SOAP envelope with an authentication header.
Here is a sample soap envelope (I use soapUI to test):
POST http://localhost:8080/testApp/ws/sample HTTP/1.1 Content-Type: text/xml;charset=UTF-8 SOAPAction: "" User-Agent: Jakarta Commons-HttpClient/3.1 Host: localhost:8080 Content-Length: 892 <soapenv:Envelope xmlns:ser="http://services.domain.com/" xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"> <soapenv:Header><wsse:Security xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"><wsse:UsernameToken wsu:Id="UsernameToken-14535355" xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd"><wsse:Username>user1</wsse:Username><wsse:Password Type="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordText">passw0rd</wsse:Password><wsse:Nonce>083LNLHB9OW2YKc85/TbhA==</wsse:Nonce><wsu:Created>2009-07-07T15:03:26.218Z</wsu:Created></wsse:UsernameToken></wsse:Security></soapenv:Header> <soapenv:Body> <ser:methOne> <ser:arg0>0</ser:arg0> <ser:arg1>"whit"</ser:arg1> </ser:methOne> </soapenv:Body> </soapenv:Envelope>
I tried out with a new app, usign grails 1.2.2
# grails create-app testApp
# grails install-plugin acegi
# grails install-plugin cxf
# grails create-auth-domains
Since you mentioned to edit the web-app/WEB-INF/cxf-servlet.xml file, I assumed a default cxf-servlet.xml file should have been already been created in web-app/WEB-INF direcotry by installing the cxf plugin. However when I look at the web-app/WEB-INF folder, I still do not see a cxf-servlet.xml in that directory. So now I wander is this file to be created manually by us or it should have been created automatically and we just have to get in there to add more entries for configuring the WS-Security related entries.
By the way, your sample cxf-servelt.xml file looks like it has to add SampleService bean into the file; but when I looked at some source code in the CXF plugin, it looks like all the cfx related services were introspected and added to the app context by default.
So may be your sample is based on 1.1 and an older version of CFX plugin?
Any way, it would be good if someone can write a ws-security grails plugin so that it does all these configuration automatically.
That is, if I just do the steps above installing the acegi, cfx plugins, then isntall ws-security plugin, everything would just work without any additional line of code change.