Friday, October 9, 2009

WS-Security Support in Spring-WS Grails Plugin

I've been busy lately working on adding WS-Security support to the Spring-WS Grails plugin and I'm pleased to tell you that we're just about to release a new version containing that work. When I first learned about the plugin from Russ Miles, I was pretty excited I must say; the perspective of bringing together Grails and Spring-WS opens up a whole new world of possibilities. The goal of the Spring-WS plugin is to make developing contract-first web services easy and productive, and to that end, we wanted to provide an easy way to add security to your endpoints, based on a convention-over-configuration approach and on other niceties that Groovy offers.

 

Features


The WS-Security support is still not 100% complete and we're hoping to get your feedback once it's released. For the moment, let me walk you through the main features that it's going to deliver.

 

Defining WS-Security Configuration


Defining a WS-Security configuration is as easy as creating a WsSecurityConfig.groovy class under grails-app/conf and using a simple DSL just as in the next example:
class WsSecurityConfig {

    // validation actions to apply to incoming messages
    def incomingMessageValidation = {
        usernameToken(users: ['Gort': 'Klaatu barada nikto'])
        timestamp()
    }

    // securement actions to apply to outgoing messages
    def outgoingMessageSecurement = {
        mustUnderstand = true
        timestamp()
    }
}
I think that the configuration is quite explicit but just in case, here's what it says: the incomingMessageValidation closure defines how to validate incoming messages; here, authentication via a username token plus timestamp checking. Security actions are defined as method calls while SOAP attributes are defined as properties (see mustUnderstand).

Similarly, the outgoingMessageSecurement closure defines the security actions to apply to outgoing messages. Currently only usernameToken, timestamp and signature are defined both for securement and validation actions. Encryption and decryption are to follow shortly.

Security actions can be parametrized by passing a map of arguments. For example, in the previous example, the users property passed to usernameToken() defines the users who are authorized to access the web service. Here's a richer example of how to secure outgoing messages with a signature and a username token:
def outgoingMessageSecurement = {
    signature(keyStore: myKeyStore, keyAlias: 'mykey', keyPassword: '123456', parts:['{http://schemas.xmlsoap.org/soap/envelope/}Body','Token'])
        keyReference: 'IssuerSerial', signatureAlgorithm: 'http://www.w3.org/2000/09/xmldsig#rsa-sha1'
    usernameToken(username:'bob', password:'hotdog', passwordType:'Text')
}
Enabling security on an endpoint is equally easy: just define a static wsSecurity property in your endpoint as follows:
static wsSecurity = true
It's also possible to configure multiple security configurations. To do so, create new security definition classes under grails-app/conf and make sure to respect to naming convention by suffixing their names with WsSecurityConfig as in MyOtherWsSecurityConfig.groovy. This time you should set your wsSecurity static property to class you're targeting:
static wsSecurity = MyOtherWsSecurityConfig
WsSecurityConfig.groovy is always considered as the default security configuration and you can still assign true to the wsSecurity property in order to reference it.

If you're curious to know how things work under the hood, the plugin detects security config classes and creates a Wss4jSecurityInterceptor for each one it finds. the plugin also takes care of creating other useful objects such as callback handlers and of adding the resulting interceptor to the interceptors chain.

 

Key Stores


Key stores are useful to store keys for signature, encryption and decryption operations. The Spring-WS plugin greatly simplifies defining key stores by using a property based syntax in the standard Config.groovy file. For example, to define a simple key store called myKeyStore, add something similar to the following:
springws.security.keyStore.myKeyStore.location='file:grails-app/keys/mykeystore.jks'
springws.security.keyStore.myKeyStore.password='123456'
Now, the plugin will configure the key store and create a corresponding bean called myKeyStore that you can simply inject in your security config classes by defining a property having the same name:
class WsSecurityConfig {

    def myKeyStore

    def incomingMessageValidation = {
        signature(keyStore: keyStore)
    }
}
With this approach you can take advantage of environment-scoped definitions to define different locations for the same key store, depending on the deployment environment, and thus preserving your security configuration classes from change.

 

Automatic Spring Security Integration


This feature gave me a lot of trouble to implement but now I think it was worth the effort :) If the Spring Security plugin (acegi) happens to be installed in the same application, the Spring-WS plugin detects it and integrates with its authentication and authorization schemes automatically.
What this means is that you can take advantage of the domain classes and the management interface of the Spring Security plugin to manage the users of your web services without writing a single line of code. Of course, you'll have to install the Spring Security plugin and run the appropriate scripts before. Once this is done, you can add a useSpringSecurity parameter to any validating usernameToken action:
 
class WsSecurityConfig {

    // validation actions to apply to incoming messages
    def incomingMessageValidation = {
        usernameToken(useSpringSecurity:true)
    }
}
Now, incoming messages will authenticate against the users defined with Spring Security. You can even configure authorization rules for those users. Currently, authorization is only supported through requestMapString, as you can see in the next snippet, but we will certainly support other options such as @Secured on endpoints.
security {

  active = true

  loginUserDomainClass = "Person"
  authorityDomainClass = "Authority"
  requestMapClass = "Requestmap"
  useRequestMapDomainClass = false
  requestMapString = """CONVERT_URL_TO_LOWERCASE_BEFORE_COMPARISON
PATTERN_TYPE_APACHE_ANT
/services/**=ROLE_WSUSER 
"""
}
This configuration only authorizes users having the role ROLE_WSUSER to invoke your web services.

 

Functional Testing Support


There is a new withSecuredEndpointRequest construct in EndpointFunctionalTestCase that allows to test secured endpoints. It is very similar to withEndpointRequest but it accepts additionally an instance of a security configuration class and applies it to the WebServiceTemplate used to invoke and test the web service. See the next code snippet for an example. 
class HolidayEndpointFunctionalTests extends EndpointFunctionalTestCase {
    void testSOAPDocumentService() {

      def security = new ClientWsSecurityConfig(keyStore: keyStore)

      def response = withSecuredEndpointRequest(serviceURL, security) {
               HolidayRequest(xmlns: namespace) {
                   Holiday {
                     StartDate("2006-07-03")
                     EndDate("2006-07-07")
                   }
                   Employee {
                     Number("42")
                     FirstName("Russ")
                     LastName("Miles")
                   }
                 }
              }

      def status = response.status
      assert status == 'canceled'
    }
}

class ClientWsSecurityConfig {

    def keyStore

    def outgoingMessageSecurement = {
        usernameToken(username:'Gort', password:'Klaatu barada nikto')
        timestamp()
    }

    def incomingMessageValidation = {
        signature(keyStore: keyStore, keyAlias: 'mykey')
        timestamp()
    }
}
I've omitted some details to simplify but you got the idea.

Upcoming Talks


I hope you like it. I will be happily presenting the plugin during a Devoxx 09 quickie. I will also do a presentation with Russ Miles during the Groovy & Grails eXchange 2009. Be sure to drop by if you are around!