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
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 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
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
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'sLdapContextSourceclass. It holds a reference to a collection of "real" security context sources (seetargetSpringSecurityContextSources), and its purpose is to route calls to one of various target security context sources based on the determined lookup key (seegetResolvedContextSource()).
Listing 3. AbstractRoutingSpringSecurityContextSource.javapublic 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 methoddetermineCurrentLookupKey(), thereby enforcing logical boundaries in a clear separation of concerns.
Listing 4. TenantRoutingSpringSecurityContextSource.javapublic 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 theTenantRoutingSpringSecurityContextSourceclass has access to it at run time.
Listing 5. TenantSecurityContextHolder.javapublic 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:
- A set of mock-up objects are set up in the
onSetUp()method, along with ourTenantSecurityContextFilter. - 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 callingassertGivenUserCredentialsAreValid()andassertGivenUserCredentialsAreInvalid(), respectively. - When invoked, the
assertGivenUserCredentialsAreValid()method tries to authenticate the given user and confirms that the user has been authenticated as expected. - 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. - When called upon by either
assertGivenUserCredentialsAreValid()orassertGivenUserCredentialsAreInvalid(), theperformUserAuthentication()method sets off the actual authentication process:- A mock HTTP login request is made, using the given tenant ID, username, and password as request parameters, to our previously created
TenantSecurityContextFilterinstance. - 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 thepassThroughFilterChain. You may have noticed that withinonSetUp(), thepassThroughFilterChainis 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 useTenantRoutingSpringSecurityContextSourceas the LDAP context source. Hence, when Spring Security tries to get the resolved context source to authenticate against,TenantRoutingSpringSecurityContextSource(which extendsAbstractRoutingSpringSecurityContextSource) dynamically returns the correct LDAP source according to the tenant ID held inTenantSecurityContextHolder.
- A mock HTTP login request is made, using the given tenant ID, username, and password as request parameters, to our previously created
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] |
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.
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.
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.
| Description | Name | Size | Download method |
|---|---|---|---|
| SaaS Security PoC - Example Application | j-saas.zip | 47KB | HTTP |
Information about download methods
Learn
-
"SaaS bright spot in waning economy" (Michael Ybarra, SearchCIO-Midmarket, July 2008): Get details on the increase in SaaS uptake.
-
Spring Security: Visit the Spring Security project site.
-
Apache Directory Project: The
Apache Directory Project provides the LDAP v3 compliant Apache Directory Server and Apache Directory Studio.
-
OpenLDAP, Fedora Directory Server and Tivoli® Directory Server: Some of the many other available open source and commercial LDAP solutions.
-
LDAP Data Interchange Format: Wikipedia explains LDIF.
-
LDIF Import Wizard: This wizard for importing LDIF files is featured in the Apache Directory Studio Eclipse plug-in.
-
Spring's
AbstractRoutingDataSourceclass: Spring's dynamic routing solution tailored for data sources inspired the solution for dynamic LDAP routing presented in this article; also see "Dynamic DataSource Routing" (Mark Fisher, SpringSource Team Blog, January 2007). - The IOC Container: Read about Spring's implementation of the IOC principle.
-
Open Web Application Security Project (OWASP): OWASP is an open community focused on improving the security of application software.
-
JUnit: Learn more about the JUnit testing framework.
- Testing: Read about Spring's support for integration testing.
- IBM SaaS Demonstration Series: Learn technologies, techniques, and best practices for delivering your software as a hosted service.
-
"Securing a composite business service delivered as a software-as-a-service: Part I, secure multitenancy with WebSphere Portal Server" (Indrajit Poddar et al., developerWorks, September 2007): This article examines security scenarios in a proof-of-concept SaaS application for banking.
-
"Develop and deploy multitenant Web-delivered solutions using IBM middleware, Part 1: Challenges and architectural patterns" (Germán Goldszmidt and
Indrajit Poddar, developerWorks, April 2008): This article describes patterns that address the challenges of multitenancy in SaaS applications.
-
"Software as a Service: Building Web delivered SaaS applications on open-source and entry-level IBM middleware" (Mary Taylor, developerWorks, December 2007): This demo shows a set of architectural patterns exploiting features in open source and entry-level IBM middleware to build cost-effective SaaS solutions.
-
"Meeting security requirements of Software as a Service (SaaS) applications" (Chetan J. Kothari, developerWorks, September 2007): Explore mechanisms for addressing requirements to achieve secure authentication and authorization of users in SaaS applications.
-
"Multi-Tenant Data Architecture" (Frederick Chong, Gianpaolo Carraro, and Roger Wolter, MSDN, June 2006): This article in a series about designing multitenant applications identifies three distinct approaches for creating data architectures.
-
"Architecture Strategies for Catching the Long Tail" Frederick Chong and Gianpaolo Carraro, MSDN, April 2006): This article provides an overview of the SaaS model and a high-level description of the architecture of a SaaS application.
-
"LdapTemplate: LDAP Programming in Java Made Simple" (Mattias Arthursson and Ulrik Sandberg, java.net, April 2006): This article introduces a framework for simpler LDAP programming in the Java language.
-
"Securing Java applications with Acegi, Part 2: Working with an LDAP directory server" (Bilal Siddiqui, developerWorks, May 2007): See how to combine Acegi with an LDAP directory server for Java application security.
-
"Storing Java objects in Apache Directory Server, Part 1" (Bilal Siddiqui, developerWorks, May 2006): Get an overview of Apache Directory Server and its core structure.
-
Browse the
technology bookstore for books on these and other technical topics.
-
developerWorks Java technology zone: Find hundreds of articles about every aspect of Java programming.
Get products and technologies
-
Apache Directory Server: Download Apache Directory Server.
-
Apache Directory Studio: Apache Directory Studio is available from the Eclipse update site.
-
Spring LDAP: Download Spring LDAP, a Java library for simplifying LDAP operations.
-
Download
IBM product evaluation versions and get your hands on application development tools and middleware products from DB2®, Lotus®, Rational®, Tivoli®, and WebSphere®.
Discuss
-
Check out developerWorks blogs and get involved in the developerWorks community.

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





