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

LDAP-based Spring Security Implementation for SaaS, Page 2

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.


Tags: SaaS, Spring Security, J2EE

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

Page 2 of 2



Comment and Contribute

 


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

 

 


Sitemap | Contact Us

Rocket Fuel