October 25, 2014
Hot Topics:
RSS RSS feed Download our iPhone app

Extend Spring Security to Protect Multi-tenant SaaS Applications

  • November 16, 2010
  • By Arun Viswanathan, Chetan Kothari
  • Send Email »
  • More Articles »

Spring Security, the open source security framework maintained by SpringSource, is a widely used framework that enterprises use to address the security requirements of enterprise Applications. It provides a comprehensive security solution that supports a wide range of authentication mechanisms, including HTTP authentications and authentication based on forms, LDAP, JAAS, OpenID, etc.

However, Spring Security currently does not provide out-of-box features to address the security requirements of SaaS applications. In this article, we present a solution to extend the JDBC- and LDAP-based implementations of Spring Security to address the multi-tenant security requirements of SaaS applications.

Security Requirements of SaaS Applications

A multi-tenant SaaS application should be designed to address the following security requirements:

  • The system must provide security and access control based on user permissions.
  • The system should provide mechanisms to authenticate users with user data residing in an on-premise environment, and authorize them with access control data residing in an on-demand environment.
  • The system should provide a mechanism to authenticate and authorize users for the following multi-tenant database strategies:
    • User data residing in a separate database for each of the tenant
    • User data residing in a shared database but a separate schema for each of the tenants
    • User data residing in a shared database and a shared schema
    • Access control data residing in an on-demand environment
  • The system should provide capabilities to enable each tenant's administrator to create, manage and delete user accounts for that tenant in the user account directory.

Currently, the J2EE container-based security framework cannot be used to address the security requirements of multi-tenant SaaS applications because it supports multiple security realms that are active at a given time. Although all the leading J2EE containers provide a mechanism to configure multiple realms, only one can be active at a time. So we need to build a custom solution to address SaaS security requirements.

In this article, we demonstrate how to provide a custom solution leveraging Spring Security to authenticate and authorize the user against multi-tenant database/LDAP strategies.

Extending Spring Security to Address SaaS Security Requirements

Spring Security provides comprehensive security services for J2EE-based enterprise software applications. The framework provides a server-agnostic approach to security and comes with many customizable features. For the two major security operations -- authentication and authorization -- Spring supports a wide variety of authentication models (e.g. HTTP BASIC, HTTP Digest, HTTP X.509, LDAP, Form-based, OpenID, Siteminder, JAAS, etc.) and a deep set of authorization capabilities (authorizing Web requests, methods, and access to individual domain object instances).

Technical Overview of Spring Security Framework

Let's first look at some of the building blocks of the Spring Security framework.

  • SecurityContextHolder -- This is the fundamental object of the framework, which stores the current security context of the application. It uses a ThreadLocal to store these details, and the authentication object stored in the context is accessible from anywhere in the application to obtain the name of the authenticated user.
  • UserDetailsService -- The UserDetailsService interface is implemented normally to create the authentication object that is stored in the security context. The implementation for this interface is configurable through the Spring Security configuration file. A number of implementations are provided by default. These implementations however do not take care of the SaaS security requirements. We hence will be extending some of these implementations as part of this article.
  • Authentication -- The authentication object provides two important methods: getPrincipal(), which provides the Principal object, and getAuthorities(), which provides an array of GrantedAuthority objects.
  • Filters -- Spring Security uses a chain of filters to perform its operations. This filter chain is applied to any Web application by adding the DelegatingFilterProxy or FilterChainProxy in the web.xml file.

Adding SaaS Capabilities to Spring Security Framework

To implement multi-tenancy in Web applications, we use an intercepting filter as the starting point to identify the tenant of the user. The process of identifying the tenant of a logged-in user can be as simple as the user passing the tenant information directly through an input field or a complex process involving the application to identify it based on the IP address or login ID. For this article, we assume the tenant ID is available or has been deduced based on the login information.

The SaaSSecurityFilter retrieves the context information from a data source such as a database or LDAP server based on the login information and stores it in the thread local object. The filter needs to be configured before the SpringSecurityFilterChain in the web.xml to identify the context before Spring Security performs the authentication and authorization activities. The code snippet of the SaaSSecurityFilter class below shows how the tenant information is retrieved and stored in thread local for future references.

public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) {
// Retrieve user id, password and tenant Id from the login page

// Set the tenant Id into a ThreadLocal object
ThreadLocalContextUtil.setObject("tenant", tenantId);

// Retrieve the context information from the database and store it in ThreadLocal
// The context object is a simple POJO storing the context related information like user id, tenant id
Context contextVO = new Context();
contextVO.setUserId(userId.trim());
contextVO.setTenantId(tenanted.trim());

if (createContext(contextVO, userId, password, httpRequest, httpResponse) && chain != null) {
chain.doFilter(request, response);
} else {
// handle error
handleErrorCondition("error.user.accessdenied", message, null, httpRequest, httpResponse);
}
}

JDBC-based Security Implementation

As mentioned earlier, an enterprise application can either use a database or LDAP server for storing tenant-specific user and roles information. Moreover, there are three approaches to managing multi-tenant data. Below are the SaaS implementation strategies for two of the approaches we use.

1. Shared Database, Shared Schema

In a shared database, shared schema approach, multiple tenants share the same database and schema. Figure 1 shows a sample database schema in a shared database, shared schema environment. A tenant ID column in each of the tables identifies the tenant to which the user information belongs.

 

 

Spring Security provides a UserDetailsService that can obtain authentication information from a JDBC data store. Spring further allows developers to extend this implementation and configure it in the Spring configuration files. We extended the JdbcDaoImpl class provided by Spring Security by providing a custom SQL query to achieve multi-tenancy in a shared database, shared schema environment. The custom query should now authenticate the user based on the tenant ID as well as the user ID. The following are the code snippets for the changes required in the configuration files and Java classes.

applicationContext-security.xml

Spring Security allows developers to configure a custom user details service based on application requirements in the applicationContext-security.xml file. This configuration is done as shown below.

<!-- Rest of configurations as specified by the framework -->
...
<authentication-provider user-service-ref='customUserDetailsService'/>
...
<!-- Rest of configurations as specified by the framework -->

applicationContext.xml

The custom user details service is defined in the applicationContext.xml file required by Spring. This bean will take the data source as well as the SQL query to be executed for authentication.

<!-- Rest of configurations as specified by the framework -->
...
<bean id="customUserDetailsService" class="com.test.security.spring.dao.CustomDaoImpl">

<property name="dataSource" ref="dataSource"/>

<property name="usersByUsernameQuery">

<value>SELECT username, password, enabled, tenant_id FROM users WHERE username = ? AND tenant_id = ?</value>

</property>

</bean>

<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource" destroy-method="close">

<property name="driverClassName" value="org.hsqldb.jdbcDriver" />

<property name="url" value="jdbc:hsqldb:hsql://localhost" />

<property name="username" value="sa" />

<property name="password" value="" />

</bean>

...

<!-- Rest of configurations as specified by the framework -->

CustomDaoImpl.java

This class extends the JdbcDaoImpl class and overrides the loadUserByUsername(String username) method to customize the authentication process. In order to enable multi-tenancy in a shared database, shared schema approach, the authentication process involves querying the database table for the tenant ID along with the user ID. The current user tenant ID would be retrieved from the ThreadLocal object.

public class CustomDaoImpl extends JdbcDaoImpl {
...
public UserDetails loadUserByUsername(String username)
throws UsernameNotFoundException, DataAccessException {
// Retrieve tenant id from the ThreadLocal
String tenantId = ThreadLocalContextUtil.getObject("tenant");

// Initialize the user and authority related SQL queries
if(usersByUsernameMapping == null || authoritiesByUsernameMapping == null)
initMappingSqlQueries();

String[] params = new String[2];
params[0] = username;
params[1] = tenantId;

// Get the list of users by executing the inner class CustomUsersByUsernameMapping
List users = usersByUsernameMapping.execute(params);

if (users.size() == 0) {
throw new UsernameNotFoundException(
messages.getMessage("JdbcDaoImpl.notFound",
new Object[]{username}, "Username {0} not found"), username);
}

UserInfo user = (UserInfo) users.get(0);

// Get the list of authorities for the user by executing the inner class
// AuthoritiesByUsernameMapping. A tenant specific custom query for authorities has not
// been implemented. This would only require adding a custom query in the config file as
// well as customizing the AuthoritiesByUsernameMapping class,

List dbAuths = authoritiesByUsernameMapping.execute(user.getUsername());
if (dbAuths.size()==0) {
throw new UsernameNotFoundException("User has no GrantedAuthority");
}

GrantedAuthority[] arrayAuths = {};
addCustomAuthorities(user.getUsername(), dbAuths);
arrayAuths = (GrantedAuthority[]) dbAuths.toArray(arrayAuths);

// This method will return a custom User object which extends the Spring User object
// This will store the tenant specific user information
return new UserInfo(user.getUsername(),
user.getPassword(), user.isEnabled(), user.getTenantId(), arrayAuths);
}

// Inner Classes
/**
* Query object to look up a user.
*/
protected class CustomUsersByUsernameMapping extends MappingSqlQuery {
protected CustomUsersByUsernameMapping(DataSource ds) {
super(ds, getUsersByUsernameQuery());
declareParameter(new SqlParameter(Types.VARCHAR));
declareParameter(new SqlParameter(Types.VARCHAR));
compile();
}

protected Object mapRow(ResultSet rs, int rownum) throws SQLException {
String username = rs.getString(1);
String password = rs.getString(2);
String enabled = rs.getString(3);
String tenant = rs.getString(4);
UserDetails user = new UserInfo(username, password, Boolean.parseBoolean(enabled),
tenant, new GrantedAuthority[] {new GrantedAuthorityImpl("HOLDER")});
return user;
}
}
...
} // End of class

2. Separate Database, Separate Schema / Shared Database, Separate Schema

In a separate database, separate schema or shared database, separate schema data architecture approach, each tenant has a separate database instance or database schema. The tenant-specific information is stored in its individual space without any dependency on the other tenant's information. In such a scenario, the application would need to deduce the data source required for the logged-in tenant and point to the right data source at runtime.

The Spring framework provides this functionality to developers by means of extending a class named AbstractRoutingDataSource. The extending class needs to override the determineCurrentLookupKey() method, which can be used to provide the logic for identifying the tenant-specific data source. The following code snippets shows how this process is done.

applicationContext-Security.xml

The first step is to provide the data source reference to the authentication provider in the Spring Security configuration file. In this case we use the default JDBC-based user service for authentication and authorization.

<!-- Rest of configurations as specified by the framework -->
...
<authentication-provider>

<jdbc-user-service data-source-ref="dataSource" />

</authentication-provider>

<!-- Rest of configurations as specified by the framework -->

applicationContext.xml

In the Spring framework configuration file, we need to provide the definition for the data source bean. Normally, the bean class DriverManagerDataSource is used along with the database connection details to be used by the application. However, in the case of a multi-tenant application, we need to use multiple data sources based on the tenants. Hence, we use a custom class called TenantRoutingDataSource, which extends the AbstractRoutingDataSource. This class takes as input a map of key to the corresponding tenant-specific data source bean reference values.

<!-- Rest of configurations as specified by the framework -->
...
<bean id="dataSource" class="com.test.security.spring.dao.TenantRoutingDataSource ">
<property name="targetDataSources">
<map>
<entry key="Tenant1" value-ref="tenant1DataSource"/>
<entry key="Tenant2" value-ref="tenant2DataSource"/>
</map>
</property>
</bean>

<bean id="tenant1DataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource" destroy-method="close">
<property name="driverClassName" value="org.hsqldb.jdbcDriver" />
<property name="url" value="jdbc:hsqldb:hsql://localhost" />
<property name="username" value="sa" />
<property name="password" value="" />
</bean>

<bean id="tenant2DataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource" destroy-method="close">
<property name="driverClassName" value="oracle.jdbc.driver.OracleDriver" />
<property name="url" value="jdbc:oracle:thin:@172.25.203.136:1521:TESTDB" />
<property name="username" value="testuser" />
<property name="password" value="testpassword" />
</bean>
...
<!-- Rest of configurations as specified by the framework -->

TenantRoutingDataSource.java

This class extends the AbstractRoutingDataSource class provided by Spring and overrides the determineCurrentLookupKey() method. This method will return the lookup key corresponding to the key entry defined in the applicationContext.xml file. The determineCurrentLookupKey() method will be called by the AbstractRoutingDataSource::determineTargetDataSource() method to retrieve the current target data source.

public class TenantRoutingDataSource extends AbstractRoutingDataSource {

protected Object determineCurrentLookupKey() {
// Retrieve the current tenant id from the thread local object
// where it was stored in the filter earlier
String lookupKey = (String) ThreadLocalContextUtil.getObject("tenant");
return lookupKey;
}
}

Tags: SaaS, Spring Security, J2EE

Originally published on http://www.developer.com.

Page 1 of 2



Comment and Contribute

 


(Maximum characters: 1200). You have characters left.

 

 


Sitemap | Contact Us

Rocket Fuel