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>

Final source here



This entry was posted on Tuesday, July 7th, 2009 at 8:04 am and is filed under Uncategorized. You can follow any responses to this entry through the RSS 2.0 feed. You can leave a response, or trackback from your own site.

One Response to “Protecting Webservices with Acegi in Grails”

  1. Hay

    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.

Leave a Reply

Your comment