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