Skip to main content

Securing a multitenant SaaS application

Authentication and authorization with Spring Security and Apache Directory Server

Massimiliano Parlione (mparlion@ie.ibm.com), Solutions Architect, IBM
Massimiliano Parlione
Massimiliano (Max) Parlione is a solutions architect in IBM's Dublin Innovation Center in Ireland, working on projects in the micro-finance domain. Massimiliano received his honours bachelor degree (Laurea) in computer science from the University of L'Aquila in 1995 and a doctorate in computer engineering from the University La Sapienza of Rome in 2000. He co-authored the IBM Redbooks, "Introducing IBM Tivoli Monitoring for Web Infrastructure" and "IBM Tivoli Monitoring Version 5.1.1 Creating Resource Models and Providers."
Chico Charlesworth, Senior Java Developer, IBM
Chico Charlesworth
Chico Charlesworth is a senior Java software developer with more than eight years of development expertise. Since graduating in 2000 with a honours bachelor degree in computing science from Staffordshire University in England, he has been working mainly with enterprise Java technology, focusing on industries such as telecommunications, e-billing, green technology and micro-finance. His primary interests include Java EE, open source, and software architecture.

Summary:  The multitenant nature of Software as a Service (SaaS) applications makes security an essential concern. This article introduces a viable and practical approach to securing a multitenant Java™ application with the open source Spring Security framework combined with Apache Directory Server. The authors present a multitenant example Web application to demonstrate this approach.

Date:  30 Sep 2008
Level:  Intermediate PDF:  A4 and Letter (99KB | 22 pages)Get Adobe® Reader®
Activity:  12659 views
Comments:  

SaaS in the enterprise

SaaS has flourished in recent years. More and more companies are turning to such on-demand solutions to respond to business needs faster and more cheaply. A 2007 survey by Forrester Research found that 16 percent of large enterprises and 15 percent of small to medium-sized businesses are using SaaS. Respectively, that's a 33 percent and 50 percent increase from the previous year (see Resources). In fact, according to Saugatuck Technology, May 2008, "by 2012, 70 percent or more of businesses with greater than 100 employees will have deployed at least one Software as a Service (SaaS) application."

SaaS lets service providers achieve economies of scale by offering hosted software applications, possibly providing services to previously unexploited market segments. The main advantage of multitenancy in SaaS solutions is that it allows the service provider to serve multiple client organizations (see Resources). The best way to secure SaaS applications, where many users share the same resources, is through logical partitioning of data and configuration (based on tenant IDs) so that safe multitenancy is guaranteed.

This article shows how to implement an effective primary line of defense to secure a Java-based multitenant SaaS application. The solution combines the use of Spring Security, a proven open source security framework, with Apache Directory Server, a popular Java-based open source Lightweight Directory Access Protocol (LDAP) v3 compliant server. The solution proposed is available as an example Java Web application that can be deployed on either Apache Tomcat or Apache Geronimo.

This article focuses on the mechanisms of authentication and authorization within a SaaS model. Other SaaS security concepts and techniques — such as data privacy and isolation, regulatory laws, auditing, and cryptography — are outside of this article's scope.

Authentication and authorization in a multitenant SaaS application

Authentication and authorization are two of the main security concerns for real-world applications:

  • Authentication is the process that allows an application to verify that a person (or another application, smart card, and so on) is the one that it claimed to be when connecting with it.

  • Authorization defines a user's rights and permissions on a system. After a user is authenticated, authorization determines what that user has the authority to do on the system. Authorization, therefore, is typically assumed to follow authentication.

Authentication and authorization entail a higher degree of complexity in SaaS applications. In a secure SaaS solution, the underlying authentication and authorization infrastructure can be designed in two ways: centralized or federated. The solution this article proposes uses a centralized authentication system (an LDAP server). A centralized authentication system does not preclude the possibility of supporting a distributed directory that stores information that can be partitioned or replicated. The article doesn't consider the alternative decentralized approach, termed federated identity management. Federated identity management introduces many new challenges for security in the SaaS space. (Typical use-cases involve things such as cross-domain, Web-based single sign-on; cross-domain user account provisioning; cross-domain entitlement management; and cross-domain user attribute exchange. See the link to "Meeting the SaaS Security Challenge" in Resources for further details.)


Introduction to Spring Security

Spring Security: More powerful than Acegi

Spring Security began in 2003 as the Acegi Security System for Spring. It became an official Spring project, rebranded as Spring Security, toward the end of 2007. We recommend you use Spring Security instead of Acegi because:
  • It offers all the same benefits as Acegi and more.
  • It exploits custom Spring 2.0 configuration namespaces.
  • Complex security details are now hidden behind simpler XML configuration.
  • It includes autoconfiguration capabilities.

By default, the Java Enterprise Edition (Java EE) 5 security mechanism doesn't support custom attributes, such as a tenant ID, in any type of authenticator (basic, form, digest, or client certificate). A custom solution must be implemented to provide multitenancy support. In this article, we show how to build such a solution by using the Spring Security framework.

Spring Security provides a comprehensive security solution that greatly simplifies the development of security measures in a Java EE application. It offers a higher level of abstraction that lets you plug in different authentication models, while supporting rich authorization capabilities. Furthermore, it offers high portability across different application servers. Some of Spring Security's features include:

  • Declarative security
  • Support for various authentication and authorization mechanisms such as basic, form, digest, JDBC, and LDAP
  • Support for method-level security and JSR-250 security annotations
  • Single sign-on support
  • Support for container integration
  • Support for anonymous sessions, concurrent sessions, remember-me, channel-enforcement, and much more

The focus of this article is on integrating Spring Security directly with LDAP. Other deployment scenarios might consider alternatives such as Java Authentication and Authorization Service (JAAS) or container-managed authentication through container adapters made available by the Spring Security framework.


LDAP and Apache Directory overview

In enterprises, a common approach to managing users and roles is to use an LDAP server. Several open source and commercial LDAP solutions are available (see Resources). For readers unfamiliar with LDAP or Apache Directory Server, a short overview follows.

LDAP essentials

LDAP is essentially a database. However, it tends to contain more descriptive, attribute-based information. The information in an LDAP directory is often read more than it is written, so LDAP has been designed to be read-optimized. The most common example is a telephone directory, with each person having an address and phone number attached.

LDAP as an authentication and authorization source has these advantages over relational database management systems:

  • Wired protocol; no need for drivers
  • Flexible schema
  • Identity focused
    • Authentication, authorization, and auditing
    • Groups
    • Security (passwords, certificates)

Apache Directory Server essentials

Apache Directory Server is an embeddable, extendable, standards-compliant, open source LDAP server written entirely in the Java language. For this article's solution, we chose Apache Directory Server because of its simplicity and because it's a pure Java implementation. For a real-world application, you should make a proper assessment to ascertain which LDAP solution best fits your business and technical requirements.

The Apache Directory project provides Apache Directory Server, which has been certified as LDAP v3 compliant, and Apache Directory Studio, which is a set of Eclipse-based directory tools (see Resources).


Integrating Spring Security with Apache Directory Server in a multitenant environment

Under normal circumstances, configuring Spring Security to work alongside an LDAP server is a straightforward task. Although it's still relatively easy to integrate the two in a multitenant environment, a little more effort is required. We'll first discuss how to create a multitenant user registry within Apache Directory Server, then show how a dynamic LDAP routing solution facilitates an effective multitenant security solution.

Multitenant Apache Directory Server user registry

This section describes an example multitenant user registry that comprises two security realms, named tenant1 and tenant2, each having two different groups of users: administrator and guest. Each group has various users linked to it who share a given role and belong to the group's corresponding security realm.

User credentials for different realms are stored in a Apache Directory Server user registry under different subtrees, as shown in Figure 1. Different LDAP suffixes are assigned to the different security realms. For example, a base distinguished name (DN) of [dc=tenant1, dc=com] for tenant1 and a base DN of [dc=tenant2, dc=com] for tenant2.


Figure 1. Example multitenant LDAP user registry
Example multitenant LDAP user registry

Different groups, which in reality translate to user roles, are then assigned to each security realm. For example, the two groups [cn=adm, ou=groups] and [cn=gst, ou=groups], which respectively translate to the administrator and guest user roles, belong to the tenant1 security realm with a base DN of [dc=tenant1, dc=com].

In turn, different user entries are associated with a particular group under a given security realm. For example, the user [uid=tenant1admin, ou=people] is assigned with the administrator role [cn=adm, ou=groups] and belongs to the security realm [dc=tenant1, dc=com].

For each security realm in Figure 1, a new partition and security context entry must be created in the server.xml configuration file in Apache Directory Server (Apache Directory Server Install Directory/instances/default/conf/server.xml) as shown in Listing 1:


Listing 1. Apache Directory Server server.xml file

<?xml version="1.0" encoding="UTF-8"?>
<spring:beans xmlns:spring="http://xbean.apache.org/schemas/spring/1.0" 
  xmlns:s="http://www.springframework.org/schema/beans"
  xmlns="http://apacheds.org/config/1.0">
...
  <jdbmPartition id="tenant1" cacheSize="100" suffix="dc=tenant1,dc=com"
                          optimizerEnabled="true" syncOnWrite="true">
    <indexedAttributes>
      <jdbmIndex attributeId="1.3.6.1.4.1.18060.0.4.1.2.1" cacheSize="100"/>
      <jdbmIndex attributeId="1.3.6.1.4.1.18060.0.4.1.2.2" cacheSize="100"/>
      <jdbmIndex attributeId="1.3.6.1.4.1.18060.0.4.1.2.3" cacheSize="100"/>
      <jdbmIndex attributeId="1.3.6.1.4.1.18060.0.4.1.2.4" cacheSize="100"/>
      <jdbmIndex attributeId="1.3.6.1.4.1.18060.0.4.1.2.5" cacheSize="10"/>
      <jdbmIndex attributeId="1.3.6.1.4.1.18060.0.4.1.2.6" cacheSize="10"/>
      <jdbmIndex attributeId="1.3.6.1.4.1.18060.0.4.1.2.7" cacheSize="10"/>
      <jdbmIndex attributeId="dc" cacheSize="100"/>
      <jdbmIndex attributeId="ou" cacheSize="100"/>
      <jdbmIndex attributeId="krb5PrincipalName" cacheSize="100"/>
      <jdbmIndex attributeId="uid" cacheSize="100"/>
      <jdbmIndex attributeId="objectClass" cacheSize="100"/>
    </indexedAttributes>
    <contextEntry>#tenant1ContextEntry</contextEntry>
  </jdbmPartition>
...
  <spring:bean id="tenant1ContextEntry" 
class="org.springframework.beans.factory.config.MethodInvokingFactoryBean">
    <spring:property name="targetObject">
      <spring:ref local='directoryService'/>
    </spring:property>
    <spring:property name="targetMethod">
      <spring:value>newEntry</spring:value>
    </spring:property>
    <spring:property name="arguments">
      <spring:list>
        <spring:value xmlns="http://www.springframework.org/schema/beans">
          objectClass: top
          objectClass: domain
          objectClass: extensibleObject
          dc: tenant1
        </spring:value>
        <spring:value>dc=tenant1,dc=com</spring:value>
      </spring:list>
    </spring:property>
  </spring:bean>
...
</spring:beans>

Similarly, as with tenant1, a new tenant2 partition and security context entry should also be added. A sample server.xml has been made available in the example Web application.

Once the security realms have been created, something similar in content to the LDAP Date Interchange Format (LDIF) file in Listing 2 can be imported into its corresponding security realm by using the LDIF Import Wizard, which is featured in the Apache Directory Studio Eclipse plug-in (see Resources). Sample LDIF files have been made available in the example Web application.


Listing 2. tenant1_users.ldif

dn: ou=groups,dc=tenant1,dc=com
objectclass: top
objectclass: organizationalUnit
ou: groups

dn: ou=people,dc=tenant1,dc=com
objectclass: top
objectclass: organizationalUnit
ou: people

dn: uid=tenant1admin,ou=people,dc=tenant1,dc=com
objectclass: top
objectclass: person
objectclass: organizationalPerson
objectclass: inetOrgPerson
cn: tenant1admin
sn: tenant1admin
uid: tenant1admin
userPassword: tenant1admin

dn: uid=tenant1guest,ou=people,dc=tenant1,dc=com
objectclass: top
objectclass: person
objectclass: organizationalPerson
objectclass: inetOrgPerson
cn: tenant1guest
sn: tenant1guest
uid: tenant1guest
userPassword: tenant1guest

dn: cn=gst,ou=groups,dc=tenant1,dc=com
objectclass: top
objectclass: groupOfNames
cn: gst
member: uid=tenant1guest,ou=people,dc=tenant1,dc=com

dn: cn=adm,ou=groups,dc=tenant1,dc=com
objectclass: top
objectclass: groupOfNames
cn: adm
member: uid=tenant1admin,ou=people,dc=tenant1,dc=com

Dynamic LDAP routing with Spring Security

The basis of dynamic LDAP routing hinges on the possibility of dynamically selecting the LDAP security context at run time based upon a lookup key (see Figure 2). In a multitenant environment, this translates to authenticating and authorizing against an LDAP source that is derived on the fly based on the tenant's ID.


Figure 2. Multitenant dynamic LDAP routing
Multitenant dynamic LDAP routing

Dynamic LDAP routing doesn't come out of the box with Spring, so we rolled out our own. Our ideas stemmed from the inspiration of a similar solution — Spring's AbstractRoutingDataSource — which was tailored for data sources (see Resources).

Our answer to dynamic LDAP routing is encapsulated in three main classes:

  • AbstractRoutingSpringSecurityContextSource, shown in Listing 3, is an abstract implementation based on Spring's LdapContextSource class. It holds a reference to a collection of "real" security context sources (see targetSpringSecurityContextSources), and its purpose is to route calls to one of various target security context sources based on the determined lookup key (see getResolvedContextSource()).

    Listing 3. AbstractRoutingSpringSecurityContextSource.java
    
    public abstract class AbstractRoutingSpringSecurityContextSource<T 
        extends Serializable> extends LdapContextSource 
        implements SpringSecurityContextSource, InitializingBean {
    
      private Map<T, DefaultSpringSecurityContextSource>
        targetSpringSecurityContextSources;
    
      /** Determine the current lookup key. This will typically be
          implemented to check a thread-bound context. */
      protected abstract T determineCurrentLookupKey();
    
      /** Determine the 'real' security context source dynamically
          at runtime based upon a lookup key. */
      protected DefaultSpringSecurityContextSource getResolvedContextSource() {
        T lookupKey = determineCurrentLookupKey();
        DefaultSpringSecurityContextSource springSecurityContextSource =
          this.targetSpringSecurityContextSources.get(lookupKey);
        if (springSecurityContextSource == null) {
            throw new IllegalStateException(
              "Cannot determine target SpringSecurityContextSource for lookup key [" +
               lookupKey + "]");
        }
        return springSecurityContextSource;
      }
    
      public void setTargetSpringSecurityContextSources(
        Map<T, DefaultSpringSecurityContextSource> targetSpringSecurityContextSources) {
        this.targetSpringSecurityContextSources = targetSpringSecurityContextSources;
      }
    
      public void afterPropertiesSet() throws Exception {
        if (this.targetSpringSecurityContextSources == null) {
          throw new IllegalArgumentException(
            "targetSpringSecurityContextSources is required");
        }
      }
    
      public DirContext getReadWriteContext(String userDn, Object credentials) {
        return this.getResolvedContextSource().getReadWriteContext(userDn, credentials);
      }
    
      @Override
      public DirContext getReadOnlyContext() {
        return this.getResolvedContextSource().getReadOnlyContext();
      }
    
      @Override
      public DirContext getReadWriteContext() {
        return this.getResolvedContextSource().getReadWriteContext();
      }
    
      @Override
      public DistinguishedName getBaseLdapPath() {
        return this.getResolvedContextSource().getBaseLdapPath();
      }
    
      @Override
      public String getBaseLdapPathAsString() {
        return this.getResolvedContextSource().getBaseLdapPathAsString();
      }
    
      @Override
      public Class getContextFactory() {
       return this.getResolvedContextSource().getContextFactory();
      }
    
      @Override
      public Class getDirObjectFactory() {
        return this.getResolvedContextSource().getDirObjectFactory();
      }
    
      @Override
      public boolean isPooled() {
        return this.getResolvedContextSource().isPooled();
      }
    
      @Override
      public AuthenticationSource getAuthenticationSource() {
        return this.getResolvedContextSource().getAuthenticationSource();
      }
    
      @Override
      public boolean isAnonymousReadOnly() {
        return this.getResolvedContextSource().isAnonymousReadOnly();
      }
    
      @Override
      public String[] getUrls() {
        return this.getResolvedContextSource().getUrls();
      }
      
    }

  • The second class is TenantRoutingSpringSecurityContextSource, shown in Listing 4. Notice that it implements the abstract method determineCurrentLookupKey(), thereby enforcing logical boundaries in a clear separation of concerns.

    Listing 4. TenantRoutingSpringSecurityContextSource.java
    
    public class TenantRoutingSpringSecurityContextSource<T extends Serializable>
        extends AbstractRoutingSpringSecurityContextSource<String> {
       
      @Override
      protected String determineCurrentLookupKey() {
        String lookupKey = TenantSecurityContextHolder.getTenantID();
        return lookupKey;
      }
    
    }

  • The last piece of the puzzle is the TenantSecurityContextHolder class, shown in Listing 5, which keeps a thread-bound context that holds a reference to the tenant ID so that the TenantRoutingSpringSecurityContextSource class has access to it at run time.

    Listing 5. TenantSecurityContextHolder.java
    
    public class TenantSecurityContextHolder {
    
      private static final ThreadLocal<String> contextHolder = new ThreadLocal<String>();
    
      public static void setTenantID(String tenantID) {
        contextHolder.set(tenantID);
      }
    
      public static String getTenantID() {
        return contextHolder.get();
      }
    
      public static void clearTenantID() {
        contextHolder.remove();
      }
    
    }

Dynamic LDAP routing Web integration

We've laid the groundwork for dynamic LDAP routing but still must do a little integration work to enable it in a Web application. There are a myriad of ways to achieve this, as long as the thread-bound context that holds the reference to the tenant ID is set. One such approach is to include a servlet filter, which takes on the responsibility for that task.

The security servlet filter shown in Listing 6 works on the assumption that the tenant ID is passed in as a request parameter when the user logs in, which it then stores in the Web session for later retrieval when authenticated requests come in.


Listing 6. TenantSecurityContextFilter.java

public class TenantSecurityContextFilter implements Filter {

  private static final String
    SPRING_SECURITY_CHECK_MAPPING = "/j_spring_security_check";

  private static final String
    SPRING_SECURITY_LOGOUT_MAPPING = "/j_spring_security_logout";

  private static final String TENANT_HTTP_KEY = "tenant";

  protected final Log logger = LogFactory.getLog(this.getClass());

  private FilterConfig filterConfig;

  public void init(FilterConfig filterConfig) throws ServletException {
    this.filterConfig = filterConfig;
  }

  public void destroy() {
    this.filterConfig = null;
  }

  public void doFilter(ServletRequest request, ServletResponse response,
    FilterChain chain) throws IOException, ServletException {
    if (null == filterConfig) {
      return;
    }
    HttpServletRequest httpRequest = (HttpServletRequest) request;

    // Clear tenant security context holder, and if it's a logout
    // request then clear tenant attribute from the session
    TenantSecurityContextHolder.clearTenantID();
    if (httpRequest.getRequestURI().endsWith(SPRING_SECURITY_LOGOUT_MAPPING)) {
      httpRequest.getSession().removeAttribute(TENANT_HTTP_KEY);
    }

    // Resolve Tenant ID
    String tenantID = null;
    if (httpRequest.getRequestURI().endsWith(SPRING_SECURITY_CHECK_MAPPING)) {
      tenantID = request.getParameter(TENANT_HTTP_KEY);
      httpRequest.getSession().setAttribute(TENANT_HTTP_KEY, tenantID);
    } else {
      tenantID = (String) httpRequest.getSession().getAttribute(TENANT_HTTP_KEY);
    }

    // If found, set the Tenant ID in the security context
    if (null != tenantID) {
      TenantSecurityContextHolder.setTenantID(tenantID);
      if (logger.isInfoEnabled()) logger.info(
        "Tenant context set with Tenant ID: " + tenantID);
    }

    chain.doFilter(request, response);
  }

}

Spring Security XML configuration

One of Spring's main strengths is its implementation of the Inversion of Control (IoC) principle, which cleanly separates an application's configuration and dependency specification from the actual application code (see Resources). Yet one complaint often made about Spring is that its configuration files, which are typically in XML format, can become verbose and unwieldy. Luckily, the Spring Security XML configuration has been cut down quite substantially since the introduction of the namespace configuration feature in Spring 2.0.

By using Spring Security XML configuration, you can define most details of authentication and authorization in a Spring application context file. Such details can include the configuration of the LDAP authentication provider and the URL-level authorization based on the user's role. (Although not shown here, fine-grained authorization is also possible in Spring Security through its support of method-level security.)

The application security context XML configuration file shown in Listing 7 only serves as an example but gives a good idea of the basic XML components needed to configure Spring Security declaratively. Notice, though, how our custom multitenant LDAP routing security context source has been autowired in to provide seamless multitenancy integration.


Listing 7. application-context-security.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:s="http://www.springframework.org/schema/security"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
    http://www.springframework.org/schema/beans/spring-beans-2.0.xsd
    http://www.springframework.org/schema/security
    http://www.springframework.org/schema/security/spring-security-2.0.1.xsd">
...
 <!-- HTTP security configuration -->
 <s:http>        
        <s:intercept-url pattern="/poc/login" access="IS_AUTHENTICATED_ANONYMOUSLY"/>
        <s:intercept-url pattern="/poc/admin/**" access="ROLE_ADM" />
        <s:intercept-url pattern="/poc/**" access="ROLE_GST, ROLE_ADM" />
        
        <s:form-login login-page="/poc/login" default-target-url="/poc/home"/>
        <s:anonymous />
        <s:logout />
 </s:http>
    
 <!-- LDAP Authentication Provider -->
 <s:ldap-authentication-provider server-ref="contextSource"
        group-search-filter="member={0}" group-search-base="ou=groups"
        user-search-base="ou=people" user-search-filter="uid={0}"/>
    
 <!-- Custom Multitenant Routing Spring Security Context Source -->
 <bean id="contextSource" class=
 "poc.saas.security.core.multitenancy.context.TenantRoutingSpringSecurityContextSource">
<property name="targetSpringSecurityContextSources">
      <map>
         <entry key="Tenant1" value-ref="tenant1ContextSource"/>
         <entry key="Tenant2" value-ref="tenant2ContextSource"/>
      </map>
        </property>
 </bean>
    
 <!-- This bean points at the at the Tenant1 LDAP Server  -->
 <bean id="tenant1ContextSource"
    class="org.springframework.security.ldap.DefaultSpringSecurityContextSource">
        <constructor-arg value="ldap://localhost:10389/dc=tenant1,dc=com"/>
        <property name="userDn"><value>uid=admin,ou=system</value></property>
      <property name="password"><value>secret</value></property>
 </bean>
    
 <!-- This bean points at the at the Tenant2 LDAP Server  -->
 <bean id="tenant2ContextSource"
    class="org.springframework.security.ldap.DefaultSpringSecurityContextSource">
        <constructor-arg value="ldap://localhost:10389/dc=tenant2,dc=com"/>
        <property name="userDn"><value>uid=admin,ou=system</value></property>
      <property name="password"><value>secret</value></property>
 </bean>
...
</beans>

For Spring Security to be enabled in a standard Java Web application, you also need to include the application security context and the security filters in the web.xml deployment descriptor file, as shown in Listing 8:


Listing 8. web.xml

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://java.sun.com/xml/ns/j2ee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="2.4"
xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee
        http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd">
...
 <!-- Spring context configuration files -->
 <context-param>
<param-name>contextConfigLocation</param-name>
<param-value>
...
classpath*:application-context-security.xml
</param-value>
 </context-param>
...
 <!-- Tenant Security Context Filter -->
 <filter>
<filter-name>Tenant Security Context Filter</filter-name>
<filter-class>
  poc.saas.security.core.multitenancy.web.filter.TenantSecurityContextFilter
</filter-class>
 </filter>
...
 <!-- Spring Security Filter -->
 <filter>
        <filter-name>springSecurityFilterChain</filter-name>
        <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
 </filter>
...
 <!-- Tenant Security Context Filter Mapping -->
 <filter-mapping>
<filter-name>Tenant Security Context Filter</filter-name>
<url-pattern>/*</url-pattern>
 </filter-mapping>
...
 <!-- Spring Security Filter Mapping -->
 <filter-mapping>
      <filter-name>springSecurityFilterChain</filter-name>
      <url-pattern>/*</url-pattern>
 </filter-mapping>
...
 <!-- Spring MVC web context listener -->
 <listener>
<listener-class>
  org.springframework.web.context.ContextLoaderListener
</listener-class>
 </listener>
...
</web-app>

Verifying multitenant authentication

A JUnit test, facilitated by Spring's support for integration testing, is shown in Listing 9. This integration test allows us to verify that the multitenant authentication process works as intended without requiring deployment of our code to an application server. However, the test does require the Apache Directory Server to be running to act as the authentication provider. The main purpose of this test is to try to authenticate various users, some of which are valid and some are not, against the two security realms: tenant1 and tenant2. A detailed walk-through follows the code listing.


Listing 9. MultiTenantAuthenticationTest.java
public class MultiTenantAuthenticationTest
    extends AbstractDependencyInjectionSpringContextTests {

  private static final String
    APPLICATION_CONTEXT_SECURITY = "classpath:application-context-security.xml";

  private FilterChain passThroughFilterChain;

  private Filter formLoginFilter;

  private Filter tenantSecurityContextFilter;

  @Override
  protected String[] getConfigLocations() {
    return new String[] {APPLICATION_CONTEXT_SECURITY};
  }

  @Override
  protected void onSetUp() throws Exception {
    // Setup mock instances
    MockServletContext servletContext = new MockServletContext("");
    servletContext.addInitParameter(ContextLoader.CONFIG_LOCATION_PARAM,
                                APPLICATION_CONTEXT_SECURITY);
    ServletContextListener contextListener = new ContextLoaderListener();
    ServletContextEvent event = new ServletContextEvent(servletContext);
    contextListener.contextInitialized(event);
    MockFilterConfig mockConfig = new MockFilterConfig(servletContext);        
        
    // Setup tenant security context filter
    tenantSecurityContextFilter = new TenantSecurityContextFilter();
    tenantSecurityContextFilter.init(mockConfig);

    // Setup Spring Security's form login filter
    formLoginFilter = 
      (Filter) this.getApplicationContext().getBean("_formLoginFilter");
    formLoginFilter.init(mockConfig);

    // Setup a pass through filter chain
    passThroughFilterChain =
      new PassThroughFilterChain(formLoginFilter, new MockFilterChain());
  }

  public void testMultiTenantAuthenticationWorksAsExpected() throws Exception {
    this.assertGivenUserCredentialsAreValid("Tenant1", "tenant1admin", "tenant1admin");
    this.assertGivenUserCredentialsAreValid("Tenant2", "tenant2admin", "tenant2admin");
    this.assertGivenUserCredentialsAreInvalid("Tenant1", "invalidUser", "wrongPassword");
    this.assertGivenUserCredentialsAreInvalid("Tenant1", "tenant1admin", "wrongPassword");
    this.assertGivenUserCredentialsAreInvalid("Tenant2", "tenant1admin", "tenant1admin");
    this.assertGivenUserCredentialsAreValid("Tenant2", "tenant2guest", "tenant2guest");
  }

  private void assertGivenUserCredentialsAreValid
    (String tenantId, String username, String password) throws Exception {
    // Authenticate valid user using the given tenant id
    Object[] result = this.performUserAuthentication(tenantId, username, password);

    // Ensure user is now authenticated and has been redirected to the home page
    assertNotNull(SecurityContextHolder.getContext().getAuthentication());
    assertEquals(username,
      SecurityContextHolder.getContext().getAuthentication().getName());                
    assertEquals("/poc/home", result[0]);
        
    System.out.println("Authentication success for user " + username + " [" +
      SecurityContextHolder.getContext().getAuthentication().getPrincipal().getClass()
      + "]");
  }

  private void assertGivenUserCredentialsAreInvalid(
    String tenantId, String username, String password) throws Exception {
    // Attempt to authenticate invalid user using the given tenant id
    Object[] result = this.performUserAuthentication(tenantId, username, password);

    // Ensure user was denied authentication and has
    // been redirected back to the login page
    assertNull(SecurityContextHolder.getContext().getAuthentication());
    assertEquals("/poc/login", result[0]);

    System.out.println("Authentication failed for user "
      + username + " [" + result[1].getClass() + "]");
  }

  private Object[] performUserAuthentication(
    String tenantId, String username, String password) throws Exception {
    // Build mock request
    MockHttpServletRequest request =
      new MockHttpServletRequest("POST", "/poc/j_spring_security_check");
    request.setParameter("tenant", tenantId);
    request.setParameter("j_username", username);
    request.setParameter("j_password", password);
        
    // Run security filter and return response URL
    MockHttpServletResponse response = new MockHttpServletResponse();
    tenantSecurityContextFilter.doFilter(request, response, passThroughFilterChain);
        
    Object[] result = 
      {response.getRedirectedUrl(), 
       request.getSession().getAttribute("SPRING_SECURITY_LAST_EXCEPTION")};
    return result;
  }

}

The test simulates this logical flow:

  1. A set of mock-up objects are set up in the onSetUp() method, along with our TenantSecurityContextFilter.

  2. The testMultiTenantAuthenticationWorksAsExpected() method, which is invoked on each test run, verifies that valid users are granted access and that invalid users are denied access, by calling assertGivenUserCredentialsAreValid() and assertGivenUserCredentialsAreInvalid(), respectively.

  3. When invoked, the assertGivenUserCredentialsAreValid() method tries to authenticate the given user and confirms that the user has been authenticated as expected.

  4. When invoked, the assertGivenUserCredentialsAreInvalid() method tries to authenticate the given user (whose credentials are invalid) and confirms that the user has been denied access and therefore redirected back to the login page.

  5. When called upon by either assertGivenUserCredentialsAreValid() or assertGivenUserCredentialsAreInvalid(), the performUserAuthentication() method sets off the actual authentication process:
    1. A mock HTTP login request is made, using the given tenant ID, username, and password as request parameters, to our previously created TenantSecurityContextFilter instance.

    2. Our security filter intercepts the login request, extracts the tenant ID, and stores it as a thread-bound context in TenantSecurityContextHolder, before passing on the request to the passThroughFilterChain. You may have noticed that within onSetUp(), the passThroughFilterChain is set up to act as a wrapper for the standard Spring Security form login filter, which is created behind the scenes by the LDAP authentication provider defined in application-context-security.xml and configured to use TenantRoutingSpringSecurityContextSource as the LDAP context source. Hence, when Spring Security tries to get the resolved context source to authenticate against, TenantRoutingSpringSecurityContextSource (which extends AbstractRoutingSpringSecurityContextSource) dynamically returns the correct LDAP source according to the tenant ID held in TenantSecurityContextHolder.

Displaying a shiny green success bar won't convey much, so Listing 10 shows the console output generated by running MultiTenantAuthenticationTest within Eclipse as a JUnit test:


Listing 10. Test output
Authentication success for user tenant1admin
  [org.springframework.security.userdetails.ldap.LdapUserDetailsImpl@676437]
  
Authentication success for user tenant2admin
  [org.springframework.security.userdetails.ldap.LdapUserDetailsImpl@992bae]
  
Authentication failed for user invalidUser
  [org.springframework.security.userdetails.UsernameNotFoundException]
  
Authentication failed for user tenant1admin
  [org.springframework.security.BadCredentialsException]
  
Authentication failed for user tenant1admin
  [org.springframework.security.userdetails.UsernameNotFoundException]
  
Authentication success for user tenant2guest
  [org.springframework.security.userdetails.ldap.LdapUserDetailsImpl@1f64158]


Example application

This article provides an example Web application that demonstrates all the concepts you have learned so far (see Download). As a bonus, it also includes some additional user and password management functionality, which was implemented with Spring LDAP, a framework designed to relieve Java developers from the common infrastructure details of using LDAP in a Spring-based Java application (see Resources).

Although the example Web application has been designed with security in mind, you should still be aware of the many potential vulnerabilities — including cross-site scripting, request-forgery, and session-hijacking — that should be taken into account when you secure real enterprise Web applications. The Open Web Application Security Project (OWASP) (see Resources) maintains a lot of useful reference material concerning these types of security risks.

The example Web application is based on Servlet API 2.5, JavaServer Pages 2.1, Spring 2.5, Spring Web MVC 2.5, and Spring Security 2.0.1. With the help of Eclipse and Apache Maven 2, the application has been successfully deployed and tested on:

  • Apache Tomcat 6, running on Java SE 6
  • Apache Geronimo 2.1 with Tomcat 6 or Jetty 6 embedded, running on Java SE 5

Download the example application to begin exploring it now. The download includes a readme.html file that guides you step-by-step through getting the application up and running. You can also view the Flash demo of the running application.


Conclusion

This article examined some important security considerations for a multitenant SaaS application. We addressed them by showing how to set up a multitenant Apache Directory Server user registry and authenticate and authorize against a multitenant LDAP source by leveraging the Spring Security framework. You also learned how to integrate Spring Security with Apache Directory Server in a multitenant ecosystem with the help of a dynamic LDAP routing solution.

Even though we advocate the technology choices proposed in this article, we recommend that a proper assessment be made to determine which technical solution best fits your SaaS requirements. You can explore this article's Resources to discover other possibilities for building SaaS solutions.

Acknowledgments

We would like to thank David Jencks and Paul Browne for their suggestions during the review of this article, and Kevan Miller and the Geronimo and WebSphere Community Edition team for facilitating it.



Download

DescriptionNameSizeDownload method
SaaS Security PoC - Example Applicationj-saas.zip47KB HTTP

Information about download methods


Resources

Learn

Get products and technologies

Discuss

About the authors

Massimiliano Parlione

Massimiliano (Max) Parlione is a solutions architect in IBM's Dublin Innovation Center in Ireland, working on projects in the micro-finance domain. Massimiliano received his honours bachelor degree (Laurea) in computer science from the University of L'Aquila in 1995 and a doctorate in computer engineering from the University La Sapienza of Rome in 2000. He co-authored the IBM Redbooks, "Introducing IBM Tivoli Monitoring for Web Infrastructure" and "IBM Tivoli Monitoring Version 5.1.1 Creating Resource Models and Providers."

Chico Charlesworth

Chico Charlesworth is a senior Java software developer with more than eight years of development expertise. Since graduating in 2000 with a honours bachelor degree in computing science from Staffordshire University in England, he has been working mainly with enterprise Java technology, focusing on industries such as telecommunications, e-billing, green technology and micro-finance. His primary interests include Java EE, open source, and software architecture.

Comments



Trademarks  |  My developerWorks terms and conditions

Help: Update or add to My dW interests

What's this?

This little timesaver lets you update your My developerWorks profile with just one click! The general subject of this content (AIX and UNIX, Information Management, Lotus, Rational, Tivoli, WebSphere, Java, Linux, Open source, SOA and Web services, Web development, or XML) will be added to the interests section of your profile, if it's not there already. You only need to be logged in to My developerWorks.

And what's the point of adding your interests to your profile? That's how you find other users with the same interests as yours, and see what they're reading and contributing to the community. Your interests also help us recommend relevant developerWorks content to you.

View your My developerWorks profile

Return from help

Help: Remove from My dW interests

What's this?

Removing this interest does not alter your profile, but rather removes this piece of content from a list of all content for which you've indicated interest. In a future enhancement to My developerWorks, you'll be able to see a record of that content.

View your My developerWorks profile

Return from help

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=1
Zone=Java technology
ArticleID=342613
ArticleTitle=Securing a multitenant SaaS application
publish-date=09302008
author1-email=mparlion@ie.ibm.com
author1-email-cc=jaloi@us.ibm.com
author2-email=chico.charlesworth@googlemail.com
author2-email-cc=jaloi@us.ibm.com

My developerWorks community

Tags

Help
Use the search field to find all types of content in My developerWorks with that tag.

Use the slider bar to see more or fewer tags.

Popular tags shows the top tags for this particular content zone (for example, Java technology, Linux, WebSphere).

My tags shows your tags for this particular content zone (for example, Java technology, Linux, WebSphere).

Use the search field to find all types of content in My developerWorks with that tag. Popular tags shows the top tags for this particular content zone (for example, Java technology, Linux, WebSphere). My tags shows your tags for this particular content zone (for example, Java technology, Linux, WebSphere).

Special offers