SAML WS Integration with PicketLink STS

In this document we show how to use PicketLink STS to validate SAML assertions and authenticate WS clients.

Required software: JDK 6, PicketLink 1.0.3 or superior and PicketLink Trust JBossWS 1.0.0.CR3 or superior.


Process Overview

The following picture illustrates the process of using SAML assertions to authenticate clients of EJB applications:

saml-sts-module.png

The client must first obtain the SAML assertion from PicketLink STS by sending a WS-Trust request to the token service. This process usually involves authentication of the client. After obtaining the SAML assertion from the STS, the client includes the assertion in the context of the WS request before invoking any operation. The client side WS handler will take care of including the assertion in the SOAP payload. Upon receiving the invocation, the server side WS handler extracts the assertion and validates it by sending a WS-Trust validate message to the  STS. If the assertion is considered valid by the STS (and the proof of  possession token has been verified if needed), the client is  authenticated.

 

On JBoss, the SAML assertion validation process is handled by the SAML2STSLoginModule. It reads properties from a configurable file (specified by the configFile option) and establishes communication with the STS based on these  properties. We will see how a configuration file looks like later on. If  the assertion is valid, a Principal is created using the assertion subject name and if the assertion contains roles, these roles are also extracted and associated with the caller's Subject.


WS Integration Example

In this section we present a sample WS application that authenticates  clients by validating their SAML assertions with PicketLink STS. The  deployments for both the WS application and the STS can be found  attached in this document.


WS Sample App

Our WS application consists of a simple EJB3 stateless session bean. The session interface can be seen bellow:

 

package webservice.test;

import javax.ejb.Remote;
import javax.jws.WebService;

@Remote
@WebService
public interface WSTest {

    public void echo(String echo);
}

And this is the implementation class:

 

package webservice.test;

import javax.annotation.Resource;
import javax.annotation.security.RolesAllowed;
import javax.ejb.Stateless;
import javax.jws.WebMethod;
import javax.jws.WebService;
import javax.xml.ws.WebServiceContext;

import org.jboss.ejb3.annotation.SecurityDomain;
import org.jboss.ws.annotation.EndpointConfig;

@Stateless
@WebService
@EndpointConfig(configName = "SAML WSSecurity Endpoint")
@SecurityDomain("sts")
@RolesAllowed("testRole")
public class WSTestBean implements WSTest {
    
    @Resource
    WebServiceContext wsCtx;

    @WebMethod
    public void echo(String echo) {
        System.out.println("WSTest: " + echo);
        System.out.println("Principal: " + wsCtx.getUserPrincipal());
        System.out.println("Principal.getName(): " + wsCtx.getUserPrincipal().getName());
        System.out.println("isUserInRole('testRole'): " + wsCtx.isUserInRole("testRole"));
    }
}

The session requires authentication using the sts security domain and just prints whatever string the client sent and also the Principal on the server side.

 

This sts security domain is deployed along with the example wstest.jar file attached here but here is:

 

<?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>

</deployment>

The policy defines two login modules: SAML2STSLoginModule and UsersRolesLoginModule. The former will be responsible for validating the assertion with the STS in order to authenticate the client and the latter is responsible for adding roles. To validate the SAML assertions the login module needs information about the STS, like its endpoint URL, service name, port name, etc. This information is supplied by the sts-config.properties file:

 

serviceName=PicketLinkSTS
portName=PicketLinkSTSPort
endpointAddress=http://localhost:8080/picketlink-sts-1.0.0/PicketLinkSTS
username=JBoss
password=JBoss

The last two properties specify the username and password that will be  used to authenticate the JBoss server to the STS when the WS-Trust  validate message is dispatched. In other words, SAML2STSLoginModule needs to authenticate to the STS when validating the SAML assertions  and these properties specify the username and password that will be used  for that.

The deployment wstest.jar also contains the files to map roles to the Subject. The file sts-users.properies is just an empty file as authentication is handled by PicketLink's login module. sts-roles.properties looks like this:

 

UserA=testRole

 

Notice also that the WS uses a custom endpoint (SAML WSSecurity Endpoint). This endpoint definition must be included in .../deployers/jbossws.deployer/META-INF/standard-jaxws-endpoint-config.xml:

 

<endpoint-config>
    <config-name>SAML WSSecurity Endpoint</config-name>
    <post-handler-chains>
      <javaee:handler-chain>
        <javaee:protocol-bindings>##SOAP11_HTTP ##SOAP11_HTTP_MTOM</javaee:protocol-bindings>
        <javaee:handler>
          <javaee:handler-name>SAML WSSecurity Handler</javaee:handler-name>
          <javaee:handler-class>org.picketlink.trust.jbossws.handler.SAML2Handler</javaee:handler-class>
        </javaee:handler>
        <javaee:handler>
          <javaee:handler-name>Recording Handler</javaee:handler-name>
          <javaee:handler-class>org.jboss.wsf.framework.invocation.RecordingServerHandler</javaee:handler-class>
        </javaee:handler>
      </javaee:handler-chain>
    </post-handler-chains>
  </endpoint-config>

 

For this endpoint to work, the library picketlink-trust-jbossws-1.0.0.CR3.jar must be included in .../deployers/jbossws.deployer/. This library is also attached here at the end of the article.

PicketLink STS

Our PicketLink STS application is a tweaked version of the picketink-sts.war  file that is available in the PicketLink project downloads page. More  specifically, we created a new security domain for the STS in jboss-web.xml, included an application policy for the new domain that uses the UsersRolesLoginModule to authenticate STS clients, included the users and roles properties files,  and changed the required role in web.xml to STSClient.

 

This is the content of the STS web.xml:

 

<?xml version="1.0"?>
<!DOCTYPE web-app PUBLIC
   -//Sun Microsystems, Inc.//DTD Web Application 2.3//EN
   http://java.sun.com/dtd/web-app_2_3.dtd>

<web-app>
   <servlet>
     <servlet-name>PicketLinkSTS</servlet-name>
     <servlet-class>org.picketlink.identity.federation.core.wstrust.PicketLinkSTS</servlet-class>
   </servlet>
   <servlet-mapping>
      <servlet-name>PicketLinkSTS</servlet-name>
      <url-pattern>/*</url-pattern>
   </servlet-mapping>

  <security-constraint>
     <web-resource-collection>
       <web-resource-name>TokenService</web-resource-name>
       <url-pattern>/*</url-pattern>
       <http-method>GET</http-method>
       <http-method>POST</http-method>
     </web-resource-collection>
     <auth-constraint>
       <role-name>STSClient</role-name>
     </auth-constraint>
   </security-constraint>

   <login-config>
      <auth-method>BASIC</auth-method>
      <realm-name>PicketLinkSTSRealm</realm-name>
   </login-config>

   <security-role>
      <role-name>STSClient</role-name>
   </security-role>

</web-app>

 

STS callers must all have the STSClient role in order to send a WS-Trust request to the STS.

 

The STS security domain is specified by the jboss-web.xml file:

 

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

<jboss-web>
  <security-domain>java:/jaas/sts-domain</security-domain>
</jboss-web>

 

The application policy for the sts-domain is defined in the sts-jboss-beans.xml file:

 

<?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-domain">
      <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>

 

The sts-users.properties specify the username/passwords of the STS callers:

 

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

 

The sts-roles.properties specify the roles of the STS callers:

 

JBoss=STSClient
UserA=STSClient
UserB=STSClient
UserC=STSClient

 

Notice that the JBoss user represents the JBoss server during the SAML validation process. All other users are the clients of the WS sample  application - they send a message to the STS to acquire a SAML assertion before calling the methods on the WS application.


Client Application

STSWSClient just acquires a SAML assertion from PicketLink STS and invokes the echo method of the WS:

 

package webservice.test;

import java.net.URL;

import org.w3c.dom.Element;

import javax.xml.namespace.QName;
import javax.xml.ws.BindingProvider;
import javax.xml.ws.Service;

import org.jboss.ws.core.StubExt;
import org.picketlink.identity.federation.api.wstrust.WSTrustClient.SecurityInfo;
import org.picketlink.identity.federation.api.wstrust.WSTrustClient;
import org.picketlink.identity.federation.core.wstrust.WSTrustException;
import org.picketlink.identity.federation.core.wstrust.plugins.saml.SAMLUtil;
import org.picketlink.trust.jbossws.SAML2Constants;

public class STSWSClient {

    private static String username = "UserA";
    private static String password = "PassA";
    
    public static void main(String[] args) throws Exception {
        WSTrustClient client = new WSTrustClient("PicketLinkSTS", "PicketLinkSTSPort",
                "http://localhost:8080/picketlink-sts-1.0.0/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);
        }

        URL wsdl = new URL("http://127.0.0.1:8080/wstest/WSTestBean?wsdl");
        QName serviceName = new QName("http://test.webservice/", "WSTestBeanService");
        Service service = Service.create(wsdl, serviceName);
        WSTest port = service.getPort(new QName("http://test.webservice/", "WSTestBeanPort"), WSTest.class);

        ((StubExt) port).setConfigName("SAML WSSecurity Client");
        ((BindingProvider) port).getRequestContext().put(SAML2Constants.SAML2_ASSERTION_PROPERTY, assertion);

        port.echo("Test");
    }

}

As we can see, the assertion is first obtained using the WSTrustClient API. Notice the client then adds the SAML assertion in the SAML2Constants.SAML2_ASSERTION_PROPERTY ("org.picketlink.trust.saml.assertion") property in the request context before invoking the echo method.

 

The client also uses a custom endpoint configuration (SAML WSSecurity Client) that must be included in the client's META-INF/standard-jaxws-client-config.xml:

 

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

<jaxws-config xmlns="urn:jboss:jaxws-config:2.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:javaee="http://java.sun.com/xml/ns/javaee"
  xsi:schemaLocation="urn:jboss:jaxws-config:2.0 schema/jaxws-config_2_0.xsd">

...

  <client-config>
    <config-name>SAML WSSecurity Client</config-name>
    <post-handler-chains>
      <javaee:handler-chain>
        <javaee:protocol-bindings>##SOAP11_HTTP ##SOAP11_HTTP_MTOM</javaee:protocol-bindings>
        <javaee:handler>
          <javaee:handler-name>SAML WSSecurity Client Handler</javaee:handler-name>
          <javaee:handler-class>org.picketlink.trust.jbossws.handler.SAML2Handler</javaee:handler-class>
        </javaee:handler>
      </javaee:handler-chain>
    </post-handler-chains>
  </client-config>

</jaxws-config>

Deploying and Running the EJB3 Sample Application on JBoss AS5

In  order to get the sample application running you must first install the  PicketLink jar files on JBoss. This is accomplished by copying picketlink-fed-2.0.0-SNAPSHOT.jar and picketlink-bindings-jboss-2.0.0-SNAPSHOT.jar (both attached in this document) files to the $JBOSS_HOME/server/<config>/lib folder. After installing the required PicketLink libs you must copy the wstest.jar and picketlink-sts-1.0.0.war to $JBOSS_HOME/server/<config>/deploy.

 

After  copying the required PicketLink jars and deploying the sample  application and the STS war, start your JBoss instance. If everything  is ok, you should see something like the following in the log:

 

16:36:30,062 INFO  [JBossASKernel] Created KernelDeployment for: wstest.jar
16:36:30,069 INFO  [JBossASKernel] installing bean: jboss.j2ee:jar=wstest.jar,name=WSTestBean,service=EJB3
16:36:30,069 INFO  [JBossASKernel]   with dependencies:
16:36:30,069 INFO  [JBossASKernel]   and demands:
16:36:30,069 INFO  [JBossASKernel]      jboss.ejb:service=EJBTimerService
16:36:30,070 INFO  [JBossASKernel]   and supplies:
16:36:30,070 INFO  [JBossASKernel]      Class:webservice.test.WSTest
16:36:30,070 INFO  [JBossASKernel]      jndi:WSTestBean/remote
16:36:30,070 INFO  [JBossASKernel]      jndi:WSTestBean/remote-webservice.test.WSTest
16:36:30,070 INFO  [JBossASKernel] Added bean(jboss.j2ee:jar=wstest.jar,name=WSTestBean,service=EJB3) to KernelDeployment of: wstest.jar
16:36:30,214 INFO  [SessionSpecContainer] Starting jboss.j2ee:jar=wstest.jar,name=WSTestBean,service=EJB3
16:36:30,227 INFO  [EJBContainer] STARTED EJB: webservice.test.WSTestBean ejbName: WSTestBean
16:36:30,354 INFO  [JndiSessionRegistrarBase] Binding the following Entries in Global JNDI:

        WSTestBean/remote - EJB3.x Default Remote Business Interface
        WSTestBean/remote-webservice.test.WSTest - EJB3.x Remote Business Interface

16:36:30,687 INFO  [DefaultEndpointRegistry] register: jboss.ws:context=wstest,endpoint=WSTestBean
16:36:31,924 INFO  [WSDLFilePublisher] WSDL published to: file:/opt/jboss-eap-5.0/jboss-as/server/wstest/data/wsdl/wstest.jar/WSTestBeanService7960307186338997650.wsdl
16:36:32,048 INFO  [TomcatDeployment] deploy, ctxPath=/wstest
16:36:32,102 WARNING [config] Unable to process deployment descriptor for context '/wstest'
16:36:32,136 INFO  [config] Initializing Mojarra (1.2_13-b01-FCS) for context '/wstest'
16:36:35,856 INFO  [DefaultEndpointRegistry] register: jboss.ws:context=picketlink-sts-1.0.0,endpoint=PicketLinkSTS
16:36:35,936 INFO  [TomcatDeployment] deploy, ctxPath=/picketlink-sts-1.0.0
16:36:36,120 INFO  [WSDLFilePublisher] WSDL published to: file:/opt/jboss-eap-5.0/jboss-as/server/wstest/data/wsdl/picketlink-sts-1.0.0.war/PicketLinkSTS.wsdl

To run the client application some JBoss' libraries must be included in the classpath:

 

java -Djava.endorsed.dirs=/opt/jboss-eap-5.0/jboss-as/lib/endorsed -classpath /opt/jboss-eap-5.0/jboss-as/client/jbossall-client.jar:/opt/jboss-eap-5.0/jboss-as/client/jbossws-native-core.jar:/opt/jboss-eap-5.0/jboss-as/client/jbossws-spi.jar:/opt/jboss-eap-5.0/jboss-as/client/jbossws-common.jar:/opt/jboss-eap-5.0/jboss-as/client/jbossws-native-jaxrpc.jar:/opt/jboss-eap-5.0/jboss-as/client/mail.jar:../lib/picketlink-bindings-jboss-2.0.0-SNAPSHOT.jar:../lib/picketlink-fed-2.0.0-SNAPSHOT.jar:../lib/picketlink-trust-jbossws-1.0.0.CR3.jar:/opt/jboss-eap-5.0/jboss-as/client/jboss-xml-binding.jar:/opt/jboss-eap-5.0/jboss-as/client/jaxb-impl.jar:/opt/jboss-eap-5.0/jboss-as/client/wsdl4j.jar:../lib/wstest.jar:. webservice.test.STSWSClient

If everything has been configured and deployed correctly, you should see this output in the client's side:

 

Invoking token service to get SAML assertion for UserA
SAML assertion for UserA successfully obtained!

And this in the server's side:

 

15:56:19,040 INFO  [PicketLinkSTS] Loading STS configuration
15:56:19,145 INFO  [PicketLinkSTS] picketlink-sts.xml configuration file loaded
15:56:21,648 INFO  [STDOUT] WSTest: Test
15:56:21,658 INFO  [STDOUT] Principal: org.picketlink.identity.federation.bindings.jboss.subject.PicketLinkPrincipal@4e39dd5
15:56:21,658 INFO  [STDOUT] Principal.getName(): UserA
15:56:21,669 INFO  [STDOUT] isUserInRole('testRole'): true

As we can see, the echo method was invoked and the Principal is an instance of PicketLinkPrincipal (meaning the authentication was successful). Invocation of the web service was successful and also WebServiceContext.isUserInRole("testRole") returns true meaning authorization was also successful.

 

Special thanks to David Boeren for helping out with the WS client part.