http://www.developer.com/http://www.developer.com/java/ent/extend-spring-security-to-protect-multi-tenant-saas-applications.html
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. A multi-tenant SaaS application should be designed to address the following security requirements: 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. 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). Let's first look at some of the building blocks of the 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. 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. 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. 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. 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. This class extends the JdbcDaoImpl class and overrides the 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 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. 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. This class extends the AbstractRoutingDataSource class provided by Spring and overrides the 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: Provided below are strategies for extending the Spring Security framework's LDAP implementation to enable multi-tenancy. 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. 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. 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. 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 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. applicationContext.xml This implementation is covered in detail in the article Securing a Multitenant SaaS Application. 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. 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. 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.
Extend Spring Security to Protect Multi-tenant SaaS Applications
November 16, 2010
Security Requirements of SaaS Applications
Extending Spring Security to Address SaaS Security Requirements
Technical Overview of Spring Security Framework
getPrincipal(), which provides the Principal object, and getAuthorities(), which provides an array of GrantedAuthority objects.Adding SaaS Capabilities to Spring Security Framework
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
1. Shared Database, Shared Schema
applicationContext-security.xml
<!-- Rest of configurations as specified by the framework -->
...
<authentication-provider user-service-ref='customUserDetailsService'/>
...
<!-- Rest of configurations as specified by the framework -->applicationContext.xml
<!-- 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
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 class2. Separate Database, Separate Schema / Shared Database, Separate Schema
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
<!-- 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
<!-- 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
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
1. Single LDAP Server, Shared Schema
applicationContext-security.xml
<!-- 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
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 ClassCustomLdapAuthoritiesPopulator.java
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 Class2. Separate LDAP Server, Separate Schema / Single LDAP Server, Separate Schema
<!-- 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 -->Adopting SaaS-enabled Spring Security
Conclusion
About the Authors