JavaEnterprise JavaExtend Spring Security to Protect Multi-tenant SaaS Applications

Extend Spring Security to Protect Multi-tenant SaaS Applications

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;
}
}

LDAP-based Security Implementation

Another option for enterprise applications is to use an LDAP-based security implementation. In case of SaaS-based applications, a number of approaches can be used to implement a multi-tenant data architecture:

  1. Single LDAP Server, Shared Schema
  2. Single LDAP Server, Separate Schemas
  3. Separate LDAP Server, Separate Schemas

Provided below are strategies for extending the Spring Security framework’s LDAP implementation to enable multi-tenancy.

1. Single LDAP Server, Shared Schema

In a single LDAP server, shared schema approach, multiple tenants will share the same LDAP schema in a single server. Spring Security provides an LDAP-based authentication provider implementation, which allows developers to use an LDAP server for authentication and authorization. This implementation, however, is for a single-tenant application. To achieve multi-tenancy in such an environment, we extend the AbstractLdapAuthenticator and LdapAuthoritiesPopulator classes of Spring Security by providing a custom user DN pattern to authenticate the user based on the tenant ID in addition to the user ID.

For the purpose of this implementation, we assume an LDAP schema as shown in Figure 2 to cater to multiple tenants within a single server.

 

 

The following are the code snippets for the changes required in the configuration files and Java classes.

applicationContext-security.xml

The first step is to configure the context source reference in the Spring Security configuration file. We use the default bean class DefaultSpringSecurityContextSource for connecting to the LDAP sever. We then configure custom implementations of the authentication and authorization classes as shown in the code snippet below.

<!-- Rest of configurations as specified by the framework -->
...
<bean id="contextSource" class="org.springframework.security.ldap.DefaultSpringSecurityContextSource">
<constructor-arg value="ldap://localhost:10389/dc=tenants,dc=com"/>
<property name="userDn" value="uid=admin,ou=system" />
<property name="password" value="secret" />
</bean>

<bean id="ldapAuthProvider" class="org.springframework.security.providers.ldap.LdapAuthenticationProvider">
<s:custom-authentication-provider />
<constructor-arg>
<bean class="com.test.security.spring.ldap.CustomBindAuthenticator">
<constructor-arg ref="contextSource" />
<property name="userDnPatterns">
<list><value>uid={0},ou=people,o={1}</value></list>
</property>
</bean>
</constructor-arg>
<constructor-arg>
<bean class="com.test.security.spring.ldap.CustomLdapAuthoritiesPopulator">
<constructor-arg ref="contextSource" />
<constructor-arg value="ou=groups,o={0}" />
<property name="groupSearchFilter" value="(member={0})"/>
<property name="rolePrefix" value="ROLE_"/>
<property name="searchSubtree" value="true"/>
<property name="convertToUpperCase" value="true"/>
</bean>
</constructor-arg>
</bean>
...
<!-- Rest of configurations as specified by the framework -->

CustomBindAuthenticator.java

This is the custom implementation of the authenticator class that binds as a user. This class extends the Spring AbstractLdapAuthenticator class and overrides the authenticate() method to provide custom authentication. The customization done here is mainly due to the user DN specified in the configuration above to provide the tenant ID along with the user ID. The code snippet below shows the method using the tenant ID from a ThreadLocal object.

public class CustomBindAuthenticator extends AbstractLdapAuthenticator {
...
public DirContextOperations authenticate(Authentication authentication) {
DirContextOperations user = null;
Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication,
"Can only process UsernamePasswordAuthenticationToken objects");

String username = authentication.getName();
String password = (String)authentication.getCredentials();

// Retrieve the tenant id for the user from the ThreadLocal
String tenantId = (String) ThreadLocalContextUtil.getObject("tenant");

// If DN patterns are configured, try authenticating with them directly
Iterator dns = getUserDns(username, tenantId).iterator();

while (dns.hasNext() && user == null) {
user = bindWithDn((String) dns.next(), username, password);
}

// Otherwise use the configured locator to find the user
// and authenticate with the returned DN.
if (user == null && getUserSearch() != null) {
DirContextOperations userFromSearch = getUserSearch().searchForUser(username);
user = bindWithDn(userFromSearch.getDn().toString(), username, password);
}

if (user == null) {
throw new BadCredentialsException(
messages.getMessage("BindAuthenticator.badCredentials", "Bad credentials"));
}

return user;
}

protected List getUserDns(String username, String tenantId) {
if (userDnFormat == null) {
return Collections.EMPTY_LIST;
}

List userDns = new ArrayList(userDnFormat.length);
String[] args = new String[] {username, tenantId};

synchronized (userDnFormat) {
for (int i = 0; i < userDnFormat.length; i++) {
userDns.add(userDnFormat[i].format(args));
}
}

return userDns;
}
...

} // End of Class

CustomLdapAuthoritiesPopulator.java

This is the custom implementation of the authorizer class, which obtains the user role information from the directory. This class extends the Spring LdapAuthoritiesPopulator class and overrides the getGrantedAuthorities() method to provide a custom authorization mechanism. The customization done here is to retrieve user roles based on the user ID and tenant ID. The code snippet below shows the methods requiring customizations for SaaS requirements.

public class CustomLdapAuthoritiesPopulator implements LdapAuthoritiesPopulator {
...

private String tenantId = "";

public final GrantedAuthority[] getGrantedAuthorities(DirContextOperations user, String username) {
String userDn = user.getNameInNamespace();

// Retrieve the tenant id for the user using the context service
tenantId = (String) ThreadLocalContextUtil.getObject("tenant");

Set roles = getGroupMembershipRoles(userDn, username);
Set extraRoles = getAdditionalRoles(user, username);

if (extraRoles != null) { roles.addAll(extraRoles); }

if (defaultRole != null) { roles.add(defaultRole); }

return (GrantedAuthority[]) roles.toArray(new GrantedAuthority[roles.size()]);
}

public Set getGroupMembershipRoles(String userDn, String username) {
Set authorities = new HashSet();

if (getGroupSearchBase() == null) { return authorities; }

Set userRoles = ldapTemplate.searchForSingleAttributeValues(getGroupSearchBase(),
groupSearchFilter, new String[]{userDn, username}, groupRoleAttribute);

Iterator it = userRoles.iterator();

while (it.hasNext()) {
String role = (String) it.next();
if (convertToUpperCase) { role = role.toUpperCase(); }
authorities.add(new GrantedAuthorityImpl(rolePrefix + role));
}

return authorities;
}

protected String getGroupSearchBase() {
String[] dnPattern = new String[] {groupSearchBase};
MessageFormat[] userDnFormat = new MessageFormat[dnPattern.length];

for (int i = 0; i < dnPattern.length; i++) {
userDnFormat[i] = new MessageFormat(dnPattern[i]);
}

List userDns = new ArrayList(userDnFormat.length);
String[] args = new String[] {tenantId};

synchronized (userDnFormat) {
for (int i = 0; i < userDnFormat.length; i++) {
userDns.add(userDnFormat[i].format(args));
}
}

this.customSearchBase = (String) userDns.get(0);

return customSearchBase;
}
...

} // End of Class

2. Separate LDAP Server, Separate Schema / Single LDAP Server, Separate Schema

In a separate LDAP server instance or single LDAP server, separate context schemas, each tenant defines its own custom schema in an individual server instance or different schema within the same server. Both these approaches can be grouped since the idea behind them is similar — i.e. separate the schema for individual tenants. Figure 3 shows the different instances for two tenants in a diagram.

 

 

The following process describes the strategy for implementing multi-tenancy in the security requirements for a SaaS application.

  1. Spring by default allows developers to define only one context source or data source for a given bean name. Hence, in the case of multiple context sources or data sources we need to provide a default bean that should redirect to the target source based on the some lookup key. In case of data sources, Spring provides such a capability through the AbstractRoutingDataSource class. There is however no equivalent to this class for LDAP context source redirection.
  2. One approach is to define a custom class TenantRoutingSpringSecurityContextSource, which will extend the LdapContextSource class, and implement the SpringSecurityContextSource interface. This class will determine the real security context dynamically at runtime based on a lookup key.
  3. This class is then configured in the Spring configuration file, applicationContext.xml, as follows:

    applicationContext.xml

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

    ...

    <bean id="contextSource" class="com.test.security.spring.ldap.TenantRoutingSpringSecurityContextSource">

    <property name="targetSpringSecurityContextSources">

    <map>

    <entry key="tenant1" value-ref="tenant1ContextSource"/>

    <entry key="tenant2" value-ref="tenant2ContextSource"/>

    </map>

    </property>

    </bean>

    ...

    <!-- Rest of configurations as specified by the framework -->
  4. As shown in the code snippet above, we can provide a map of tenant-specific context sources through which the target context source is determined at runtime based on the logged-in user’s tenant information.
  5. The referenced context sources and LdapAuthenticationProvider bean can then be configured in the applicationContext.xml as per Spring’s specified format.

This implementation is covered in detail in the article Securing a Multitenant SaaS Application.

Adopting SaaS-enabled Spring Security

Adopting Spring Security in SaaS application development is straightforward. The Spring Security framework can be extended as mentioned in the previous sections to enable multi-tenancy. The important decision of whether to use a database or LDAP strategy has to be based on business requirements.

Migrating an existing application to the SaaS model would require careful analysis of the existing application and database setup. The solutions provided in the previous sections do not require any changes to the existing application source code from an authentication and authorization perspective. It can be implemented independently and plugged in through the configuration files easily.

Based on the data schema that is used in different enterprise applications, the solution provided here can be updated and reused. Other security requirements such as UI-level access control, fine-grained levels of security, encryption and so on might require minor to moderate code changes, however.

Conclusion

This article can help architects and developers leverage the Spring Security framework in an effective way while building and designing truly multi-tenant, J2EE-based SaaS applications. It explained how to extend the Spring Security framework to address SaaS-specific security requirements for authentication and authorization, presenting both JDBC and LDAP implementations.

About the Authors

Arun Viswanathan works as a Technology Lead at the Java Enterprise Centre of Excellence at SETLabs, Infosys Technologies. He has over 8 years experience in Java and Java EE technologies. He is currently involved in design and consulting for SaaS based application development. He can be reached at Arun_Viswanathan01@infosys.com.

Chetan Kothari works as a Principal Architect at the Java Enterprise Center of Excellence at Infosys Technologies, a Global leader in IT & Business Consulting Services. Chetan has over 12 years experience with expertise in J2EE application framework development, defining, architecting and implementing large-scale, mission-critical, IT Solutions across a range of industries. He can be reached at chetan_kothari@infosys.com.

Get the Free Newsletter!

Subscribe to Developer Insider for top news, trends & analysis

Latest Posts

Related Stories