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