SAML Enabled POJO Web Services

In this article, we will explore enabling SAML authentication for POJO based Web Services using JBoss Web Services (JBossWS). This also satisfies the SAML Token Profile of the Oasis Web Services Security Specification.

Requisities

 

POJO Web Services

 

 

Lets write an interface.

 

package org.picketlink.test.trust.ws;
import javax.ejb.Remote;
import javax.jws.WebService;

@Remote
@WebService
public interface WSTest{
   public String echo(String echo);

   public String echoUnchecked(String echo); 
}

 

Lets write a POJO implementing this interface.

 

 

package org.picketlink.test.trust.ws;
import javax.jws.HandlerChain;
import javax.jws.WebMethod;
import javax.jws.WebService;
import javax.jws.soap.SOAPBinding;

@WebService
@SOAPBinding(style = SOAPBinding.Style.RPC)
@HandlerChain(file="authorize-handlers.xml") 
public class POJOBean
{
   @WebMethod
   public String echo(String echo)
   {
      return echo;
   }

   @WebMethod
   public String echoUnchecked(String echo)
   {
      return echo;
   }
}

 

Notice that we have indicated the use of an xml file to describe the handlers.  The handler file is authorize-handlers.xml

 

 

<?xml version="1.0" encoding="UTF-8"?>


<handler-chains xmlns="http://java.sun.com/xml/ns/javaee"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://java.sun.com/xml/ns/javaee javaee_web_services_1_2.xsd">

  <handler-chain>


    <handler>
      <handler-name>WSAuthorizationHandler</handler-name>
      <handler-class>org.picketlink.trust.jbossws.handler.WSAuthorizationHandler</handler-class>
    </handler>

    <handler>
      <handler-name>WSAuthenticationHandler</handler-name>
      <handler-class>org.picketlink.trust.jbossws.handler.WSAuthenticationHandler</handler-class>
    </handler>

    <handler>
      <handler-name>SAML2Handler</handler-name>
      <handler-class>org.picketlink.trust.jbossws.handler.SAML2Handler</handler-class>
    </handler>


  </handler-chain>


</handler-chains>

 

The order of the handlers is very important. They are defined in the reverse order of execution as per the JAX-WS Specification.

See section 9.3.2 of JAXWS 2.2 spec:

 

"For outbound messages handler processing starts with the first handler in the chain and proceeds in the same order as the handler chain. For inbound messages the order of processing is reversed: processing starts with the last handler in the chain and proceeds in the reverse order of the handler chain. E.g., consider a handler chain that consists of six handlers H1 . . .H6 in that order: for outbound messages handler H1 would be

invoked first followed by H2, H3, . . . , and finally handler H6; for inbound messages H6 would be invoked first followed by H5, H4, . . . , and finally H1."

 

Since POJO web services are packaged as web archives (WAR) files,  we need a web.xml also

 

<web-app xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
    version="2.5">

    <servlet>
        <display-name>POJO Web Service</display-name>
        <servlet-name>POJOBeanService</servlet-name>
        <servlet-class>org.picketlink.test.trust.ws.POJOBean</servlet-class>
        <load-on-startup>1</load-on-startup>
    </servlet>
    <servlet-mapping>
        <servlet-name>POJOBeanService</servlet-name>
        <url-pattern>/POJOBeanService</url-pattern>
    </servlet-mapping>
</web-app>

 

Note, we are NOT adding any security-constraint elements in the web.xml

 

Securing the POJO Web Services

 

We need to introduce a jboss-web.xml in the WEB-INF directory of the web archive.

 

<jboss-web>
  <security-domain>sts</security-domain>
</jboss-web>

 

We also need the JBossWS deployment descriptor for Web Services Security,  jboss-wsse.xml in the WEB-INF directory.

 

<jboss-ws-security xmlns="http://www.jboss.com/ws-security/config"
                   xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
                   xsi:schemaLocation="http://www.jboss.com/ws-security/config
                   http://www.jboss.com/ws-security/schema/jboss-ws-security_1_0.xsd">

  <port name="POJOBeanPort">
    <operation name="{http://ws.trust.test.picketlink.org/}echoUnchecked">
      <config>
        <authorize>
          <unchecked/>
        </authorize>
      </config>    
    </operation>

    <operation name="{http://ws.trust.test.picketlink.org/}echo">
      <config>
        <authorize>
          <role>JBossAdmin</role>
        </authorize>
      </config>    
    </operation>        
  </port>

</jboss-ws-security>

 

 

As you can see, we have defined the access control rules for the two operations for a port, POJOBeanPort.

 

The operation, echoUnchecked gives unlimited access whereas the operation, echo needs a role "JBossAdmin" in the caller.

 

 

Package the Web Archive

You will need to package the Web Archive with the web.xml, jboss-web.xml and jboss-wsse.xml in the WEB-INF directory.  Also package the compiled classes for WSTest interface and POJOBean in the classes directory.

 

Client/ Test Classes

 

We are going to write a test case using JUnit.

 

package org.picketlink.test.trust.tests;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;

import java.net.URL;
import java.util.List;

import javax.xml.namespace.QName;
import javax.xml.ws.BindingProvider;
import javax.xml.ws.Service;
import javax.xml.ws.handler.Handler;
import javax.xml.ws.soap.SOAPFaultException;

import org.junit.Test;
import org.picketlink.test.trust.ws.WSTest;
import org.picketlink.trust.jbossws.SAML2Constants;
import org.picketlink.trust.jbossws.handler.SAML2Handler;
import org.w3c.dom.Element;

/**
 * A Simple WS Test for POJO WS Authorization using PicketLink
 */public class POJOWSAuthorizationTestCase extends TrustTestsBase
{  
   @SuppressWarnings("rawtypes")
   @Test
   public void testWSInteraction() throws Exception 
   {
      Element assertion = getAssertionFromSTS("UserA", "PassA");

      // Step 2: Stuff the Assertion on the SOAP message context and add the SAML2Handler to client side handlers
      URL wsdl = new URL("http://localhost:8080/pojo-test/POJOBeanService?wsdl");
      QName serviceName = new QName("http://ws.trust.test.picketlink.org/", "POJOBeanService");
      Service service = Service.create(wsdl, serviceName);
      WSTest port = service.getPort(new QName("http://ws.trust.test.picketlink.org/", "POJOBeanPort"), WSTest.class);
      BindingProvider bp = (BindingProvider)port;
      bp.getRequestContext().put(SAML2Constants.SAML2_ASSERTION_PROPERTY, assertion);
      List<Handler> handlers = bp.getBinding().getHandlerChain();
      handlers.add(new SAML2Handler());
      bp.getBinding().setHandlerChain(handlers); 

      //Step 3: Access the WS. Exceptions will be thrown anyway.
      assertEquals( "Test", port.echo("Test"));
   }


   @SuppressWarnings("rawtypes")
   @Test
   public void testWSAccessDeniedInteraction() throws Exception 
   {
      Element assertion = getAssertionFromSTS("UserB", "PassB");

      // Step 2: Stuff the Assertion on the SOAP message context and add the SAML2Handler to client side handlers
      URL wsdl = new URL("http://localhost:8080/pojo-test/POJOBeanService?wsdl");
      QName serviceName = new QName("http://ws.trust.test.picketlink.org/", "POJOBeanService");
      Service service = Service.create(wsdl, serviceName);
      WSTest port = service.getPort(new QName("http://ws.trust.test.picketlink.org/", "POJOBeanPort"), WSTest.class);
      BindingProvider bp = (BindingProvider)port;
      bp.getRequestContext().put(SAML2Constants.SAML2_ASSERTION_PROPERTY, assertion);
      List<Handler> handlers = bp.getBinding().getHandlerChain();
      handlers.add(new SAML2Handler());
      bp.getBinding().setHandlerChain(handlers); 

      try
      {
         port.echo("Test");
         fail( "Should have thrown exception");
      }
      catch( Exception e)
      {
         if(e instanceof SOAPFaultException)
         {
            //pass

         }
         else
            fail( "Wrong Exception:"+e);      
      }
   }

   @SuppressWarnings("rawtypes")
   @Test
   public void testWSUncheckedInteraction() throws Exception 
   {
      Element assertion = getAssertionFromSTS("UserB", "PassB");

      // Step 2: Stuff the Assertion on the SOAP message context and add the SAML2Handler to client side handlers
      URL wsdl = new URL("http://localhost:8080/pojo-test/POJOBeanService?wsdl");
      QName serviceName = new QName("http://ws.trust.test.picketlink.org/", "POJOBeanService");
      Service service = Service.create(wsdl, serviceName);
      WSTest port = service.getPort(new QName("http://ws.trust.test.picketlink.org/", "POJOBeanPort"), WSTest.class);
      BindingProvider bp = (BindingProvider)port;
      bp.getRequestContext().put(SAML2Constants.SAML2_ASSERTION_PROPERTY, assertion);
      List<Handler> handlers = bp.getBinding().getHandlerChain();
      handlers.add(new SAML2Handler());
      bp.getBinding().setHandlerChain(handlers); 

      //Step 3: Access the WS. Exceptions will be thrown anyway.
      assertEquals( "Test", port.echoUnchecked("Test"));
   }
}

 

 

 

 package org.picketlink.test.trust.tests;
import org.picketlink.identity.federation.api.wstrust.WSTrustClient;
import org.picketlink.identity.federation.api.wstrust.WSTrustClient.SecurityInfo;
import org.picketlink.identity.federation.core.wstrust.WSTrustException;
import org.picketlink.identity.federation.core.wstrust.plugins.saml.SAMLUtil;
import org.w3c.dom.Element;

/**
 * Base class for the PicketLink trust tests
 */
public class TrustTestsBase
{
   /**
    * Method gets a SAML assertion from the PicketLink STS
    * @param username username to send to STS
    * @param password password to send to STS
    * @return
    * @throws Exception
    */
   protected Element getAssertionFromSTS(String username, String password) throws Exception
   {
   // Step 1:  Get a SAML2 Assertion Token from the STS
      WSTrustClient client = new WSTrustClient("PicketLinkSTS", "PicketLinkSTSPort",
            "http://localhost:8080/picketlink-sts/PicketLinkSTS",
            new SecurityInfo(username, password));
      Element assertion = null;
      try {
         System.out.println("Invoking token service to get SAML assertion for " + username);
         assertion = client.issueToken(SAMLUtil.SAML2_TOKEN_TYPE);
         System.out.println("SAML assertion for " + username + " successfully obtained!");
      } catch (WSTrustException wse) {
         System.out.println("Unable to issue assertion: " + wse.getMessage());
         wse.printStackTrace();
         System.exit(1);
      } 
      return assertion;
   }
}

 

 

The test case calls the PicketLink STS to obtain a SAML Assertion and then it is sent on the WS call to the POJO bean.  We add the SAML2Handler on the client side also.  The SAML2Handler will pick up the SAML assertion and then send it as part of the WS request.

 

 

The STS running on JBoss AS can be configured with the following security domain definition.  (sts-jboss-beans.xml  added to the deploy directory)

 

<?xml version="1.0" encoding="UTF-8"?>

<deployment xmlns="urn:jboss:bean-deployer:2.0">

   <!-- ejb3 test application-policy definition -->
   <application-policy xmlns="urn:jboss:security-beans:1.0" name="sts">
      <authentication>
         <login-module code="org.picketlink.identity.federation.bindings.jboss.auth.SAML2STSLoginModule" flag="required">
            <module-option name="configFile">sts-config.properties</module-option>
            <module-option name="password-stacking">useFirstPass</module-option>
         </login-module>
         <login-module code="org.jboss.security.auth.spi.UsersRolesLoginModule" flag="required">
            <module-option name="usersProperties">sts-users.properties</module-option>
            <module-option name="rolesProperties">sts-roles.properties</module-option>
            <module-option name="password-stacking">useFirstPass</module-option>
         </login-module>
      </authentication>
   </application-policy>

   <!-- ejb3 test application-policy definition -->
   <application-policy xmlns="urn:jboss:security-beans:1.0" name="jmx-console">
      <authentication>
         <login-module code="org.jboss.security.auth.spi.UsersRolesLoginModule" flag="required">
            <module-option name="usersProperties">sts-users.properties</module-option>
            <module-option name="rolesProperties">sts-roles.properties</module-option>
         </login-module>
      </authentication>
   </application-policy>

</deployment>

 

In the "sts" security domain, we have defined two property files, sts-users.properties and sts-roles.properties which can be dropped into the conf directory of JBoss AS and will look as follows:

 

 

sts-users.properties

 

JBoss=JBoss
UserA=PassA
UserB=PassB
UserC=PassC
admin=admin

 

sts-roles.properties

 

JBoss=STSClient
UserA=STSClient,testRole,JBossAdmin
UserB=STSClient
UserC=STSClient
admin=JBossAdmin

References

 

  1. WSAuthenticationHandler
  2. WSAuthorizationHandler
  3. SAML2Handler
  4. JBossWS POJO Authentication and Authorization