SAML EJB Integration with PicketLink STS on Weblogic

Version 1

    In this document we show how to use PicketLink STS to validate SAML assertions and authenticate EJB3 clients on Weblogic. It's a slightly modified version of this article.

    Software used for this exercise: JDK6, PicketLink 1.0.4-SNAPSHOT, Weblogic Server 10.3.2.0.

    PicketLink STS was deployed on JBoss EAP 5.

     

    Process overview

    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 as a String in the  security context of the EJB request before invoking an operation on the  bean. This is required because Weblogic Authentication Providers only accept a String credential (the default CallbackHandler doen't handle Objects as credentials).

    Upon receiving the invocation, the EJB container 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 Weblogic, the SAML assertion validation process is handled by the SimpleSAML2STSAuthenticator. This is a custom Weblogic Authentication Provider that reads properties from a configurable file specified in the provider's configuration options and establishes communication with STS based on these properties. If the assertion is valid, a Principal is created using the assertion subject name and associated with the caller's Subject. The authenticator then proceeds to associate any user's groups with the Subject. The user's groups are also configured in a properties file specified in the provider's configuration options.

     

    EJB3 Integration Example

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

    EJB3  Sample App

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

     

    /*
     * JBoss, Home of Professional Open Source.
     * Copyright 2008, Red Hat Middleware LLC, and individual contributors
     * as indicated by the @author tags. See the copyright.txt file in the
     * distribution for a full listing of individual contributors. 
     *
     * This is free software; you can redistribute it and/or modify it
     * under the terms of the GNU Lesser General Public License as
     * published by the Free Software Foundation; either version 2.1 of
     * the License, or (at your option) any later version.
     *
     * This software is distributed in the hope that it will be useful,
     * but WITHOUT ANY WARRANTY; without even the implied warranty of
     * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
     * Lesser General Public License for more details.
     *
     * You should have received a copy of the GNU Lesser General Public
     * License along with this software; if not, write to the Free
     * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
     * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
     */
    package org.jboss.test.security.ejb3;
     
     
    import java.security.Principal;
     
     
    /**
     * <p>
     * This is the remote interface of session beans used in the EJB3 security tests.
     * </p>
     * 
     * @author <a href="mailto:sguilhen@redhat.com">Stefan Guilhen</a>
     */
    public interface SimpleSession
    {
       /**
        * <p>
        * This is a method available for regular users and administrators. Implementations must annotate either the class or
        * this method with {@code @RolesAllowed({"RegularUser", "Administrator"})} to enforce that only these roles should
        * be granted access to this method.
        * </p>
        * 
        * @return the caller's {@code Principal}.
        */
       public Principal invokeRegularMethod();
     
     
       /**
        * <p>
        * This is a method available for administrators only. Implementations must annotate either the class or this method
        * with {@code @RolesAllowed({"Administrator"})} to enforce that only administrators should be granted access to
        * this method.
        * </p>
        * 
        * @return the caller's {@code Principal}.
        */
       public Principal invokeAdministrativeMethod();
     
     
       /**
        * <p>
        * This is a method available for all authenticated users, regardless or role. Implementations must annotate this
        * method with {@code @PermitAll} to specify that all security roles should be granted access.
        * </p>
        * 
        * @return the caller's {@code Principal}.
        */
       public Principal invokeUnprotectedMethod();
     
     
       /**
        * <p>
        * This is a method that is unavailable for everybody. Implementations must annotate this method with
        * {@code @DenyAll} to specify that access should be restricted for everybody.
        * </p>
        * 
        * @return the caller's {@code Principal}.
        */
       public Principal invokeUnavailableMethod();
     
    }
    
    

     

    And this is the implementation class:

     

    /*
     * JBoss, Home of Professional Open Source.
     * Copyright 2008, Red Hat Middleware LLC, and individual contributors
     * as indicated by the @author tags. See the copyright.txt file in the
     * distribution for a full listing of individual contributors. 
     *
     * This is free software; you can redistribute it and/or modify it
     * under the terms of the GNU Lesser General Public License as
     * published by the Free Software Foundation; either version 2.1 of
     * the License, or (at your option) any later version.
     *
     * This software is distributed in the hope that it will be useful,
     * but WITHOUT ANY WARRANTY; without even the implied warranty of
     * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
     * Lesser General Public License for more details.
     *
     * You should have received a copy of the GNU Lesser General Public
     * License along with this software; if not, write to the Free
     * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
     * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
     */
    package org.jboss.test.security.ejb3;
     
     
    import java.security.Principal;
     
     
    import javax.annotation.Resource;
    import javax.annotation.security.DenyAll;
    import javax.annotation.security.PermitAll;
    import javax.annotation.security.RolesAllowed;
    import javax.ejb.Remote;
    import javax.ejb.SessionContext;
    import javax.ejb.Stateless;
     
     
    /**
     * <p>
     * Stateless session bean implementation used in the EJB3 security tests.
     * </p>
     * 
     * @author <a href="mailto:sguilhen@redhat.com">Stefan Guilhen</a>
     */
    @Stateless
    @Remote(SimpleSession.class)
    @RolesAllowed({"RegularUser", "Administrator"})
    public class SimpleStatelessSessionBean implements SimpleSession
    {
     
     
       @Resource
       private SessionContext context;
     
     
       /*
        * (non-Javadoc)
        * 
        * @see org.jboss.test.security.ejb3.SimpleSession#invokeRegularMethod()
        */
       public Principal invokeRegularMethod()
       {
          // this method allows the same roles as the class.
          return this.context.getCallerPrincipal();
       }
     
     
       /*
        * (non-Javadoc)
        * 
        * @see org.jboss.test.security.ejb3.SimpleSession#invokerAdministrativeMethod()
        */
       @RolesAllowed({"Administrator"})
       public Principal invokeAdministrativeMethod()
       {
          // this method overrides the roles defined by the class to grant access to admnistrators only.
          return this.context.getCallerPrincipal();
       }
     
     
       /*
        * (non-Javadoc)
        * 
        * @see org.jboss.test.security.ejb3.SimpleSession#invokeUnprotectedMethod()
        */
       @PermitAll
       public Principal invokeUnprotectedMethod()
       {
          // this method overrides the roles defined by the class to grant access to all roles.
          return this.context.getCallerPrincipal();
       }
     
     
       /*
        * (non-Javadoc)
        * 
        * @see org.jboss.test.security.ejb3.SimpleSession#invokeUnavailableMethod()
        */
       @DenyAll
       public Principal invokeUnavailableMethod()
       {
          // this method should never be called - it overrides the class roles to deny access to all roles.
          return this.context.getCallerPrincipal();
       }
    }
    
    

     

    The session defines four methods: invokeRegularMethod (available to both  Administrators and RegularUsers), invokeAdministrativeMethod (available to  Administrators only), invokeUnprotectedMethod   (available to all authenticated clients), and invokeUnavailableMethod (annotated with @DenyAll and thus  unavailable to all roles).

     

    The ejb3.jar also includes a META-INF/weblogic-ejb-jar.xml deployment descriptor to correctly map the roles without having to rely on the security realm defined in the Weblogic domain:

     

    <?xml version="1.0" encoding="UTF-8"?>
    <weblogic-ejb-jar xmlns="http://www.bea.com/ns/weblogic/10.0"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.bea.com/ns/weblogic/10.0 http://www.bea.com/ns/weblogic/10.0/weblogic-ejb-jar.xsd">
        
        <security-role-assignment>
            <role-name>Administrator</role-name>
            <principal-name>Administrator</principal-name>
        </security-role-assignment>
        <security-role-assignment>
            <role-name>RegularUser</role-name>
            <principal-name>RegularUser</principal-name>
        </security-role-assignment>
        <security-role-assignment>
            <role-name>Guest</role-name>
            <principal-name>Guest</principal-name>
        </security-role-assignment>
    
    </weblogic-ejb-jar>
    

     

    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 EJB3 sample  application - they send a message to the STS to acquire a SAML assertion  before calling the methods on the EJB3 application.

     

    Client Application

    The SAML2EJB3WeblogicTest shows what happens when  each of the users (UserA, UserB, and UserC) acquire a SAML assertion  from PicketLinkSTS and invoke all methods on the sample EJB3. Let's take  a look at the code:

     

    /*
     * JBoss, Home of Professional Open Source.
     * Copyright 2008, Red Hat Middleware LLC, and individual contributors
     * as indicated by the @author tags. See the copyright.txt file in the
     * distribution for a full listing of individual contributors. 
     *
     * This is free software; you can redistribute it and/or modify it
     * under the terms of the GNU Lesser General Public License as
     * published by the Free Software Foundation; either version 2.1 of
     * the License, or (at your option) any later version.
     *
     * This software is distributed in the hope that it will be useful,
     * but WITHOUT ANY WARRANTY; without even the implied warranty of
     * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
     * Lesser General Public License for more details.
     *
     * You should have received a copy of the GNU Lesser General Public
     * License along with this software; if not, write to the Free
     * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
     * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
     */
    import java.security.Principal;
    import java.util.Properties;
    
    import javax.ejb.EJBAccessException;
    import javax.naming.Context;
    import javax.naming.InitialContext;
    
    import org.jboss.test.security.ejb3.SimpleSession;
    import org.picketlink.identity.federation.api.wstrust.WSTrustClient;
    import org.picketlink.identity.federation.api.wstrust.WSTrustClient.SecurityInfo;
    import org.picketlink.identity.federation.core.wstrust.SamlCredential;
    import org.picketlink.identity.federation.core.wstrust.WSTrustException;
    import org.picketlink.identity.federation.core.wstrust.plugins.saml.SAMLUtil;
    import org.w3c.dom.Element;
    
    import weblogic.jndi.WLInitialContextFactory;
    
    public class SAML2EJB3WeblogicTest
    {
       public static void main(String[] args) throws Exception
       {
          System.out.println("\nTesting UserA");
          testUserA();
          System.out.println("\nTesting UserB");
          testUserB();
          System.out.println("\nTesting UserC");
          testUserC();
       }
       
       private static SimpleSession test(String username, String password) throws Exception
       {
          Properties env = new Properties();
          env.put(Context.INITIAL_CONTEXT_FACTORY, WLInitialContextFactory.class.getName());
          env.put(Context.PROVIDER_URL, "t3://127.0.0.1:7001");
          env.put(Context.SECURITY_PRINCIPAL, username);
    
          WSTrustClient client = new WSTrustClient("PicketLinkSTS", "PicketLinkSTSPort", 
                "http://127.0.0.1:8080/picketlink-sts-1.0.0/PicketLinkSTS", 
                new SecurityInfo(username, password));
          Element assertion = null;
          try 
          {
             assertion = client.issueToken(SAMLUtil.SAML2_TOKEN_TYPE);
          }
          catch (WSTrustException wse)
          {
             System.out.println("Unable to issue assertion: " + wse.getMessage());
             wse.printStackTrace();
             System.exit(1);
          }
          SamlCredential credential = new SamlCredential(assertion);
          String pwd = credential.getAssertionAsString();
          
          env.put(Context.SECURITY_CREDENTIALS, pwd);
          InitialContext ctx = new InitialContext(env);
          SimpleSession session = (SimpleSession) ctx.lookup("SimpleSession#org.jboss.test.security.ejb3.SimpleSession");
          return session;
       }
       
       private static void testUserA() throws Exception
       {
          SimpleSession session = test("UserA", "PassA");
          Principal p = null;
          try
          {
             p = session.invokeRegularMethod();
             System.out.println(p);
          }
          catch (Exception e)
          {
             e.printStackTrace();
          }
          
          try
          {
             p = session.invokeAdministrativeMethod();
             System.out.println(p);
          }
          catch (Exception e)
          {
             e.printStackTrace();
          }
          
          try
          {
             p = session.invokeUnprotectedMethod();
             System.out.println(p);
          }
          catch (Exception e)
          {
             e.printStackTrace();
          }
          
          try
          {
             p = session.invokeUnavailableMethod();
          }
          catch (EJBAccessException e)
          {
             System.out.println("Correctly failed to invoke unavailable method");
          }
       }
       
       private static void testUserB() throws Exception
       {
          SimpleSession session = test("UserB", "PassB");
          Principal p = null;
          try
          {
             p = session.invokeRegularMethod();
             System.out.println(p);
          }
          catch (Exception e)
          {
             e.printStackTrace();
          }
          
          try
          {
             p = session.invokeAdministrativeMethod();
          }
          catch (Exception e)
          {
             System.out.println("Correctly failed to invoke administrative method");
          }
          
          try
          {
             p = session.invokeUnprotectedMethod();
             System.out.println(p);
          }
          catch (Exception e)
          {
             e.printStackTrace();
          }
          
          try
          {
             p = session.invokeUnavailableMethod();
          }
          catch (EJBAccessException e)
          {
             System.out.println("Correctly failed to invoke unavailable method");
          }
       }
       
       private static void testUserC() throws Exception
       {
          SimpleSession session = test("UserC", "PassC");
          Principal p = null;
          try
          {
             p = session.invokeRegularMethod();
          }
          catch (Exception e)
          {
             System.out.println("Correctly failed to invoke regular method");
          }
          
          try
          {
             p = session.invokeAdministrativeMethod();
          }
          catch (Exception e)
          {
             System.out.println("Correctly failed to invoke administrative method");
          }
          
          try
          {
             p = session.invokeUnprotectedMethod();
             System.out.println(p);
          }
          catch (Exception e)
          {
             e.printStackTrace();
          }
          
          try
          {
             p = session.invokeUnavailableMethod();
          }
          catch (EJBAccessException e)
          {
             System.out.println("Correctly failed to invoke unavailable method");
          }
       }
    }
    
    

     

    As we can see, the assertion is first obtained using the WSTrustClient API. Once the assertion has been acquired, it is included in the InitialContext env as a String so that WLInitialContextFactory pushes it to the client-side security context. Then we attempt to call all methods on the sample EJB3 session and print the results of these calls.

     

    Configuring the custom authentication provider on Weblogic

    To configure SimpleSAML2STSAuthenticator copy the attached simpleSAML2STSAuthenticator.jar to $DOMAIN_HOME/lib/mbeantypes/. This library was created using Weblogic's MDF and MJF utils. This authentication provider depends on PicketLink and Log4j so copy the attached picketlink-fed-1.0.4-SNAPSHOT.jar and log4j.jar to $DOMAIN_HOME/lib/.

    Start Weblogic and access the Administration Console application. Navigate to the active Security Realm. Go to the Providers tab and select to add a new provider. SimpleSAML2STSAuthenticator should be available as one of the options. Add one instance of this provider. Reorder the providers so that this new provider is the first one.

    Select this provider to access the configuration options. The control flag should be set to SUFFICIENT as this authentication provider only checks SAML2 assertions. If a password is sent as the credential, the provider's login module is ignored and the next provider in the list will handle authentication.

    In the provider's specific configuration, set up the roles properties file. This is the full path to a properties file containing the groups each user belongs to. Here is the example used:

     

    UserA=RegularUser,Administrator
    UserB=RegularUser
    UserC=Guest
    

    Set up also the STS configuration file. This is the full path to a properties file containing the information to set the STS connection. Here is the example used:

     

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

    Restart Weblogic after setting up the provider so the changes take effect.

     

    Deploying the application and running the client

    Access the Administration Console application and navigate to Deployments. Select ejb3.jar and deploy it as an application with the default values (including DDOnly security configuration).

    Run the client in a shell:

     

    java -cp wlclient.jar:picketlink-fed-1.0.4-SNAPSHOT.jar:/opt/jboss-eap-5.0/jboss-as/client/jbossall-client.jar:. SAML2EJB3WeblogicTest
    

    If everything is configured correctly, the output should look like this:

     

    Testing UserA
    UserA
    UserA
    UserA
    Correctly failed to invoke unavailable method
    
    Testing UserB
    UserB
    Correctly failed to invoke administrative method
    UserB
    Correctly failed to invoke unavailable method
    
    Testing UserC
    Correctly failed to invoke regular method
    Correctly failed to invoke administrative method
    UserC
    Correctly failed to invoke unavailable method