Version 2

    In this article we present a sample JEE application that uses PicketLink STS to issue and propagate SAML assertions within JBoss AS.

    Software used: JDK 6, JBoss AS 6.0.0.M4, PicketLink 1.0.4 (to be released soon)

    Scenario Description

    The sample application illustrates a very common scenario: a Web application that interacts with other applications (in this case a simple EJB3 app) in order to process the client request. But, instead of propagating the incoming username/password pair to the EJB container, we are going to exchange the username for a SAML assertion using PicketLink STS and send the assertion to the EJB container which will in turn validate the incoming assertion with the STS.

     

    saml-sts-modules.png

    What are the advantages of using SAML in this scenario? First, the client's private password is not propagated to any of the services that are used by the Web application. The fact that the password is not exposed beyond the point where it is really needed adds to the overall application security. Second, assertions can be short-lived and can be revoked independently of the private password. In other words, you can limit the amount of time within witch the assertion can be used and if any service behaves badly you can simply revoke the assertion without compromising the private password. Third, the assertion can carry a lot more data (attributes) than the username/password pair, which means that more complex authorization scenarios can be implemented more easily.

    EJB3 Application

    In this example we are using the same EJB3 sample application from the SAMLEJBIntegrationwithPicketLinkSTS article. In short, the EJB application requires users to authenticate using SAML assertions, so it relies on the SAML2STSLoginModule to validate the assertions with the STS. The EJB source code and related configuration files such as the login modules configuration can be found in the aforementioned article and in the attached ejb3-sampleapp.jar.

    Web Application

    The sample Web application consists of a Servlet that calls a few methods on the remote EJB3 bean to show which methods are accessible to the client and a Filter that is responsible for setting the SAML assertion in the security context so it can be propagated to the EJB container.

     

    Here's how things work: the client attempts to call the servlet and is directed to the login screen where he enters his username and password. The Web container uses the SAML2STSIssuingLoginModule to send a WS-Trust request to the STS in order to authenticate the client and exchange the client credentials for a SAML assertion. If the authentication process succeeds, the resulting assertion is inserted in a SAMLPrincipal instance by the login module and this instance is set into the CallerPrincipal group. This group makes the SAMLPrincipal available to JEE applications via the getCallerPrincipal or getUserPrincipal methods.

     

    Now, before the client's request reaches the TestServlet, it passes through the Filter. At this point, the Web container security context has the client's username and password as these are the credentials the client supplied to the Web container. So, in order to propagate the SAML assertion, we need to replace the username/password for the SAML assertion in the security context. This is what our TestFilter does.

     

    Let's take a look at the TestServlet and TestFilter 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.
     */
    package test;
    
    import java.io.IOException;
    import java.io.PrintWriter;
    import java.security.Principal;
    
    import javax.ejb.EJBAccessException;
    import javax.naming.Context;
    import javax.naming.InitialContext;
    import javax.rmi.PortableRemoteObject;
    import javax.servlet.ServletException;
    import javax.servlet.http.HttpServlet;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    
    import org.jboss.test.security.ejb3.SimpleSession;
    
    /**
     * <p>
     * Simple test {@code Servlet} that calls methods on a remote EJB3 beans and prints whether the client has access
     * to each method or not in the response. 
     * </p>
     * 
     * @author <a href="mailto:sguilhen@redhat.com">Stefan Guilhen</a>
     */
    public class TestServlet extends HttpServlet
    {
       private static final long serialVersionUID = 2195802688711027241L;
    
       /*
        * (non-Javadoc)
        * @see javax.servlet.http.HttpServlet#doGet(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse)
        */
       @Override
       protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException
       {
          PrintWriter out = resp.getWriter();
    
          // if logout was passed as a get parameter, perform the logout by invalidating the session.
          if (req.getParameter("logout") != null)
          {
             req.getSession().invalidate();
             resp.sendRedirect("test");
             return;
          }
          
          String username = req.getRemoteUser();
          out.println("Hello " + username);
          out.println("Calling EJB that requires a SAML assertion credential...");
    
          try
          {
             // look up the remote EJB and call all its methods.
             Context context = new InitialContext();
             Object object = context.lookup("SimpleStatelessSessionBean/remote");
             SimpleSession session = (SimpleSession) PortableRemoteObject.narrow(object, SimpleSession.class);
             
             try
             {
                Principal principal = session.invokeAdministrativeMethod();
                out.println(principal.getName() + " successfully called administrative method!");
             }
             catch (EJBAccessException eae)
             {
                out.println(username + " is not authorized to call administrative method!");
             }
             
             // invoke method that requires the RegularUser role.
             try
             {
                Principal principal = session.invokeRegularMethod();
                out.println(principal.getName() + " successfully called regular method!");
             }
             catch (EJBAccessException eae)
             {
                out.println(username + " is not authorized to call regular method!");
             }
    
             // invoke method that allows all roles.
             try
             {
                Principal principal = session.invokeUnprotectedMethod();
                out.println(principal.getName() + " successfully called unprotected method!");
             }
             catch (EJBAccessException eae)
             {
                // this should never happen as long as the user has successfully authenticated.
                out.println(username + " is not authorized to call unprotected method!");
             }
    
             // invoke method that denies access to all roles.
             try
             {
                Principal principal = session.invokeUnavailableMethod();
                // this should never happen because the method should deny access to all roles.
                out.println(principal.getName() + " successfully called unavailable method!");
             }
             catch (EJBAccessException eae)
             {
                out.println(username + " is not authorized to call unavailable method!");
             }
          }
          catch (Exception e)
          {
             throw new RuntimeException("Error processing request:  " + e.getMessage(), e);
          }
          out.close();
       }
    
       /*
        * (non-Javadoc)
        * @see javax.servlet.http.HttpServlet#doPost(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse)
        */
       @Override
       protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException
       {
          this.doGet(req, resp);
       }
    }
    

    As we can see, the Servlet code is fairly simple: all it does is call the remote EJB and print the method access results to the response. It can also perform a logout if a logout parameter is found in the request.

     

    /*
     * 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 test;
    
    import java.io.IOException;
    import java.security.Principal;
    
    import javax.servlet.Filter;
    import javax.servlet.FilterChain;
    import javax.servlet.FilterConfig;
    import javax.servlet.ServletException;
    import javax.servlet.ServletRequest;
    import javax.servlet.ServletResponse;
    import javax.servlet.http.HttpServletRequest;
    
    import org.jboss.security.client.SecurityClient;
    import org.jboss.security.client.SecurityClientFactory;
    import org.picketlink.identity.federation.core.wstrust.SAMLPrincipal;
    
    /**
     * <p>
     * Simple filter that verifies if the caller principal is an instance of {@code SAMLPrincipal} and replaces the username
     * and password in the security context with the {@code SAMLPrincipal} information.
     * </p>
     * 
     * @author <a href="mailto:sguilhen@redhat.com">Stefan Guilhen</a>
     */
    public class TestFilter implements Filter
    {
    
       /*
        * (non-Javadoc)
        * @see javax.servlet.Filter#doFilter(javax.servlet.ServletRequest, javax.servlet.ServletResponse, javax.servlet.FilterChain)
        */
       @Override
       public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException,
             ServletException
       {
          if (request instanceof HttpServletRequest)
          {
             HttpServletRequest httpRequest = (HttpServletRequest) request;
             Principal principal = httpRequest.getUserPrincipal();
             // if we have a SAMLPrincipal, perform a simple login to replace the security context.
             if (principal != null && principal instanceof SAMLPrincipal)
             {
                try
                {
                   SAMLPrincipal samlPrincipal = (SAMLPrincipal) principal;
                   System.out.println("SAMLPrincipal found, updating security context");
                   SecurityClient client = SecurityClientFactory.getSecurityClient();
                   // simple login just updates the security context.
                   client.setSimple(samlPrincipal, samlPrincipal.getSAMLCredential());
                   client.login();
                }
                catch (Exception e)
                {
                   throw new RuntimeException("Error updating security context: " + e.getMessage(), e);
                }
             }
             else
                System.out.println("Request principal is not a SAMLPrincipal, security context not updated");
          }
          chain.doFilter(request, response);
       }
    
       /*
        * (non-Javadoc)
        * @see javax.servlet.Filter#init(javax.servlet.FilterConfig)
        */
       @Override
       public void init(FilterConfig config) throws ServletException
       {
       }
    
       /*
        * (non-Javadoc)
        * @see javax.servlet.Filter#destroy()
        */
       @Override
       public void destroy()
       {
       }
    }
    

    The Filter retrieves the SAMLPrincipal from the request via getUserPrincipal and then uses the SecurityClient API to update the security context. Any calls made by the Servlet will use the updated security context, propagating the SAML assertion.

     

    As we mentioned before, the Web application uses the SAML2STSIssuingLoginModule to authenticate the clients and exchange their credentials for a SAML assertion. Let's take a look at the login modules configuration, defined in the WEB-INF/testapp-jboss-beans.xml file:

    <?xml version="1.0" encoding="UTF-8"?>
    
    <deployment xmlns="urn:jboss:bean-deployer:2.0">
    
       <!-- web test application-policy definition -->
       <application-policy xmlns="urn:jboss:security-beans:1.0" name="testapp">
          <authentication>
             <login-module code="org.picketlink.identity.federation.bindings.jboss.auth.SAML2STSIssuingLoginModule" flag="required">
                <module-option name="password-stacking">useFirstPass</module-option>
                <module-option name="endpointAddress">http://localhost:8080/picketlink-sts-1.0.0/PicketLinkSTS</module-option>
             </login-module>
             <login-module code="org.jboss.security.auth.spi.UsersRolesLoginModule" flag="required">
                <module-option name="password-stacking">useFirstPass</module-option>
                <module-option name="usersProperties">testapp-users.properties</module-option>
                <module-option name="rolesProperties">testapp-roles.properties</module-option>
             </login-module>
          </authentication>
       </application-policy>
    
    </deployment>
    

    The second UsersRoleLoginModule is being used here just to retrieve the client roles (the Web app requires the role of WebAdmin to run, so the testapp-roles.properties file just assigns this role to all users)

     

    For the sake of brevity we're not showing all the files that are part of our Web application. They include the web.xml and jboss-web.xml configuration files, as well as the login.jsp and error.jsp pages. All these files can be found in the attached testapp.war.

    PicketLink STS

    The STS we're using in this scenario has exactly the same configuration of the STS used in the SAMLEJBIntegrationwithPicketLinkSTS article, so please refer to that document for more details.

    Deploying and Running the Test Application

    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-1.0.4.jar and picketlink-bindings-jboss-1.0.4.jar (both attached to this document) files to the JBOSS_HOME/server/partition/lib folder. After installing the required PicketLink libs you must copy the STS war (picketlink-sts-1.0.0.war), EJB3 application jar (ejb3-sampleapp.jar), and the Web application war (testapp.war) to JBOSS_HOME/server/partition/deploy. All required deployments can be found attached to this article.

     

    After installing everything, start you JBoss server partition. Now point your browser to http://localhost:8080/testapp/test. You will be redirected to the login screen. We are working with three different sample clients:

    • UserA (password = PassA) - can invoke all methods on the EJB except for the one annotated with @DenyAll;
    • UserB (password = PassB) - can only invoke the regular and the unprotected methods;
    • UserC (password = PassC) - can only invoke the unprotected method.

     

    Let's start with UserA - after logging in with UserA/PassA you should see something like the this in the browser window:

    Hello UserA
    Calling EJB that requires a SAML assertion credential...
    UserA successfully called administrative method!
    UserA successfully called regular method!
    UserA successfully called unprotected method!
    UserA is not authorized to call unavailable method!
    
    

    Now logout by adding the ?logout parameter to the URL (http://localhost:8080/testapp/test?logout). You will be redirected to the login screen again.

     

    Now enter UserB/PassB. You will notice that UserB cannot access the administrative method:

    Hello UserB
    Calling EJB that requires a SAML assertion credential...
    UserB is not authorized to call administrative method!
    UserB successfully called regular method!
    UserB successfully called unprotected method!
    UserB is not authorized to call unavailable method!
    
    

     

    Logout again (just hit http://localhost:8080/testapp/test?logout). Now enter UserC/PassC and check the result: only the unprotected method was successfully called, as expected.

    Hello UserC
    Calling EJB that requires a SAML assertion credential...
    UserC is not authorized to call administrative method!
    UserC is not authorized to call regular method!
    UserC successfully called unprotected method!
    UserC is not authorized to call unavailable method!