Spring Security - Build Multi-Tenant Application


The problem
We are evolving our application from single-purpose to multi-tenant application.

The solution
We use LDAP to authenticate user and define different LDAP Group for different roles in different environment for different sub-application.

In login page, user selects what sub-applications to login. The application will call LDAP to do authentication, which will return what what groups user belongs to. Then the application will check the group-mapping to decide whether user can access this application and what roles user should have.

We also store the sub-application name in the session, so it can be used later.


We store supported Applications - the application name and the mapping of application's ldap groups in database.

Check Spring Security: Integrate In-Memory Authentication for Test Automation for why we add test users in dev lines and how to do it.


Talk is cheap. Show me the code.
@Component
public class MyUsernamePasswordAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
 @Autowired
 private Environment environment;
 @Autowired
 private IConfigService configService;

 @Autowired
 private ApplicationProfile applicationProfile;
  // these test users are cross all applications in dev lines
 private final Set$lt;String$gt; testUsers = new HashSet$lt;$gt;();

 @PostConstruct
 public void postConstruct() {
  if (applicationProfile.isDev()) {
   addTestUser("spring.security.test.user.adminOnly.name");
   addTestUser("spring.security.test.user.provisionerOnly.name");
   addTestUser("spring.security.test.user.adminProvisioner.name");
  }
 }

 protected void addTestUser(final String testUserProperty) {
  final String testUser = environment.getProperty(testUserProperty);
  if (StringUtils.isNotBlank(testUser)) {
   testUsers.add(testUser);
  }
 }

 @Autowired
 @Override
 public void setAuthenticationManager(final AuthenticationManager authenticationManager) {
  super.setAuthenticationManager(authenticationManager);
 }

 @Override
 public Authentication attemptAuthentication(final HttpServletRequest request, final HttpServletResponse response)
   throws AuthenticationException {
  final String applicationName = request.getParameter(Util.APPLICATION_NAME);

  if (StringUtils.isEmpty(applicationName)) {
   throw new AuthenticationServiceException(
     MessageFormat.format("Not supported application: {0}", applicationName));
  }

  final Map$lt;String, SupportedAppSecurityConfig$gt; supportedApps = configService.getMySimpleConfig()
    .extractSupportedApplications();
  if (!supportedApps.containsKey(applicationName)) {
   throw new AuthenticationServiceException(
     MessageFormat.format("Not supported application: {0}", applicationName));
  }

  final Authentication auth = super.attemptAuthentication(request, response);

  if (auth.isAuthenticated()) {
   request.getSession(true).setAttribute(Util.APPLICATION_NAME, applicationName);
   if (testUsers.contains(auth.getName())) {
    return auth;
   }
   return checkAuthorizationAndMappingGroup(supportedApps, applicationName, auth);
  }
  return auth;
 }

 protected Authentication checkAuthorizationAndMappingGroup(
   final Map$lt;String, SupportedAppSecurityConfig$gt; supportedApps, final String applicationName,
   final Authentication auth) {
  // mapping group
  final SupportedAppSecurityConfig application = supportedApps.get(applicationName);

  final List$lt;GrantedAuthority$gt; newAuthorities = new ArrayList$lt;$gt;();

  boolean isAdmin = false, isProvisioner = false;
  for (final GrantedAuthority authority : auth.getAuthorities()) {
   if (authority.getAuthority().equals(application.getAdminLadpGroup())) {
    isAdmin = true;
   }
   if (authority.getAuthority().equals(application.getProvisionLdapGroup())) {
    isProvisioner = true;
   }
  }

  if (!isAdmin && !isProvisioner) {
   throw new AuthenticationServiceException(MessageFormat
     .format("User {0} does not have expected authority, having: {1}", auth.getName(), newAuthorities));
  }

  if (isAdmin) {
   newAuthorities.add(new SimpleGrantedAuthority(Util.ADMIN_GROUP));
  }
  if (isProvisioner) {
   newAuthorities.add(new SimpleGrantedAuthority(Util.PROVISION_GROUP));
  }

  final Authentication newAuth = new UsernamePasswordAuthenticationToken(auth.getPrincipal(),
    auth.getCredentials(), newAuthorities);
  return newAuth;
 }
}

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class MyWebSecurityConfiguration extends WebSecurityConfigurerAdapter {
      @Autowired
      private MyUsernamePasswordAuthenticationFilter usernamePasswordAuthenticationFilter;
      @Override
      protected void configure(final HttpSecurity http) throws Exception {
          http.authorizeRequests()
          .antMatchers("/* ignored*/").permitAll()
          .antMatchers("/* ignored*/").access(Util.ROLE_PROVISIONER_OR_ADMIN)
          .antMatchers("/* ignored*/").access(Util.ROLE_ADMIN)
          .and().formLogin().loginPage("/login").failureUrl("/loginerror")
          .loginProcessingUrl("/j_spring_security_check").passwordParameter("j_password")
          .usernameParameter("j_username").defaultSuccessUrl("/index.html").and().logout()
          .logoutUrl("/j_spring_security_logout").logoutSuccessUrl("/loggedout")
          .deleteCookies("JSESSIONID", "SESSION")
          .and().sessionManagement().sessionFixation().migrateSession().maximumSessions(1)
          .and().and().addFilter(usernamePasswordAuthenticationFilter);
      }
      // check http://lifelongprogrammer.blogspot.com/2016/04/spring-security-integrate-in-memory.html
      // for implementation
      @Bean @Override
      public AuthenticationManager authenticationManagerBean() throws Exception {}
}

public class SupportedAppSecurityConfig implements Serializable {
    private static final long serialVersionUID = 1L;
    private String name;
    private String adminLadpGroup;
    private String provisionLdapGroup;
}

Labels

adsense (5) Algorithm (69) Algorithm Series (35) Android (7) ANT (6) bat (8) Big Data (7) Blogger (14) Bugs (6) Cache (5) Chrome (19) Code Example (29) Code Quality (7) Coding Skills (5) Database (7) Debug (16) Design (5) Dev Tips (63) Eclipse (32) Git (5) Google (33) Guava (7) How to (9) Http Client (8) IDE (7) Interview (88) J2EE (13) J2SE (49) Java (186) JavaScript (27) JSON (7) Learning code (9) Lesson Learned (6) Linux (26) Lucene-Solr (112) Mac (10) Maven (8) Network (9) Nutch2 (18) Performance (9) PowerShell (11) Problem Solving (11) Programmer Skills (6) regex (5) Scala (6) Security (9) Soft Skills (38) Spring (22) System Design (11) Testing (7) Text Mining (14) Tips (17) Tools (24) Troubleshooting (29) UIMA (9) Web Development (19) Windows (21) xml (5)