Wednesday, February 29, 2012

Grails security with CAS and LDAP

Problem:

I have multiple web applications that share the same users. The problem is my users should not have to login or authenticate multiple times to access each of the web applications. The solution was to use single sign on (SSO) using a combination of Central Authentication Service (CAS) and LDAP,  from the spring security plugins for Grails.

Solution:

Unfortunately, I wasn't able to use the CAS plugin alone, because my requirements needed to access the user's email address which was stored in LDAP.
Here, I've used CAS for authentication purposes with single-sign-on support and I've used LDAP to retrieve user details such as email address and display names, and my local database to determine the roles for each user.


The process of authentcation occurs with the following steps:
  1. CAS Authentcation
  2. Retrieve user details from LDAP
  3. Save user details to the database if it doesn't already exist 
Step 3 is required, because of the way the CAS plugin works. In the default behaviour, the CAS plugin expects that the User domain objects to already exist in the local database. Hence, even if authentication is successful at the CAS server, it will fail at your Grails application when it attempts to load the user details from the database. To workaround this, I've created a customized UserDetailsService implementation as outlined by the offical docs here. In this implementation, we are saving users to the database on demand. This avoids us from having to prepopulate users in our local database and is more dynamic.
The code is shown below:

package apf.bioinformatics.security

import org.apache.log4j.Logger
import org.codehaus.groovy.grails.plugins.springsecurity.GormUserDetailsService
import org.springframework.dao.DataAccessException;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.ldap.userdetails.InetOrgPerson
import org.springframework.security.ldap.userdetails.LdapUserDetailsService;

import apf.bioinformatics.Role
import apf.bioinformatics.UserRole
import apf.bioinformatics.enums.RoleType


/**
 * Prepopulates the database with the user details from LDAP directory and assigns a default Role to the user
 * @author Philip Wu
 *
 */
class PrepopulateUserDetailsService extends GormUserDetailsService {

    Logger logger = Logger.getLogger(getClass())
    
    LdapUserDetailsService ldapUserDetailsService
        
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException, DataAccessException {                
        return loadUserByUsername(username, true)
    }

    @Override
    public UserDetails loadUserByUsername(String username, boolean loadRoles)
            throws UsernameNotFoundException, DataAccessException {
                
                
        UserDetails userDetails = ldapUserDetailsService.loadUserByUsername(username)
        
        if (userDetails instanceof InetOrgPerson) {
            
            InetOrgPerson inetOrgPerson = (InetOrgPerson) userDetails
            logger.info("mail="+inetOrgPerson.getMail())
            
            apf.bioinformatics.User user = apf.bioinformatics.User.findByUsername(username)
            if (user == null) {
                
                apf.bioinformatics.User.withTransaction {
                
                    // Create new user and save to the database
                    user = new apf.bioinformatics.User()
                    user.username = username
                    user.email = inetOrgPerson.getMail()
                    user.displayName = inetOrgPerson.getDisplayName()
                    user.enabled = true
                    user.save()
        
                    Role clientRole = Role.findByAuthority(RoleType.INVESTIGATOR.toString())
                                
                    // Assign the default role of client
                    UserRole userRole = new UserRole()
                    userRole.user = user
                    userRole.role = clientRole
                    userRole.save()
                    logger.info("user saved to database")
                }
            }
                                
        }
        
        logger.info("ldap user details: "+userDetails)
        
        // Load user details from database
        return super.loadUserByUsername(username, loadRoles)                                
    }

}


The important part of the above code is in creating the user if it doesn't already exist prior to attempting to load from the database. This means the user is always available as long as the CAS server successfully passed authentication.

You may notice that there is an instance of LdapUserDetailsService. This is used to retrieve the email address and display name of the user directly from LDAP, which is pretty straight forward as shown in the above code. The tricky part is loading the configuration and wiring up the classes required for the LdapUserDetailsService in the resources.groovy file and Config.groovy as shown below:

resources.groovy
    def config = SpringSecurityUtils.securityConfig
    SpringSecurityUtils.loadSecondaryConfig 'DefaultLdapSecurityConfig'
    config = SpringSecurityUtils.securityConfig

    initialDirContextFactory(org.springframework.security.ldap.DefaultSpringSecurityContextSource,
       config.ldap.context.server){
        userDn = config.ldap.context.managerDn
        password = config.ldap.context.managerPassword        
        anonymousReadOnly = config.ldap.context.anonymousReadOnly
    }

    ldapUserSearch(org.springframework.security.ldap.search.FilterBasedLdapUserSearch, 
       config.ldap.search.base,
       config.ldap.search.filter,       
        initialDirContextFactory){
    }

    ldapAuthoritiesPopulator(org.springframework.security.ldap.userdetails.DefaultLdapAuthoritiesPopulator,
        initialDirContextFactory,
       config.ldap.authorities.groupSearchBase){
          groupRoleAttribute = config.ldap.authorities.groupRoleAttribute
          groupSearchFilter = config.ldap.authorities.groupSearchFilter
          searchSubtree = config.ldap.authorities.searchSubtree
          convertToUpperCase = config.ldap.mapper.convertToUpperCase
          ignorePartialResultException = config.ldap.authorities.ignorePartialResultException
    }

    ldapUserDetailsMapper(InetOrgPersonContextMapper)
       
    ldapUserDetailsService(org.springframework.security.ldap.userdetails.LdapUserDetailsService,
        ldapUserSearch,
        ldapAuthoritiesPopulator){        
        userDetailsMapper = ref('ldapUserDetailsMapper')        
    }    
    
    userDetailsService(PrepopulateUserDetailsService) {
        ldapUserDetailsService=ref('ldapUserDetailsService')
        grailsApplication = ref('grailsApplication')
    }



Config.groovy
// Added by the Spring Security Core plugin:
grails.plugins.springsecurity.userLookup.userDomainClassName = 'apf.bioinformatics.User'
grails.plugins.springsecurity.userLookup.authorityJoinClassName = 'apf.bioinformatics.UserRole'
grails.plugins.springsecurity.authority.className = 'apf.bioinformatics.Role'


// Define the authentication providers
grails.plugins.springsecurity.providerNames = ['casAuthenticationProvider']


// Define the CAS configuration
grails.plugins.springsecurity.cas.loginUri = '/login'
grails.plugins.springsecurity.cas.serviceUrl =   '${grails.serverURL}/j_spring_cas_security_check'
grails.plugins.springsecurity.cas.serverUrlPrefix = 'https://login-test.anu.edu.au' //'https://your-cas-server/cas'
grails.plugins.springsecurity.cas.proxyCallbackUrl = '${grails.serverURL}/secure/receptor' //'${grails.serverURL}'
grails.plugins.springsecurity.cas.proxyReceptorUrl = '/secure/receptor'
grails.plugins.springsecurity.cas.key = 'grailsCasTest'


// Define the LDAP configuration
grails.plugins.springsecurity.ldap.context.server = 'ldap://ldap.anu.edu.au:389'
grails.plugins.springsecurity.ldap.authorities.groupSearchBase ='ou=People,o=anu.edu.au'
grails.plugins.springsecurity.ldap.search.base ='ou=People,o=anu.edu.au'
grails.plugins.springsecurity.ldap.search.attributesToReturn = ['uid','mail', 'displayName']
grails.plugins.springsecurity.ldap.mapper.userDetailsClass= 'inetOrgPerson'// 'org.springframework.security.ldap.userdetails.InetOrgPerson'
grails.plugins.springsecurity.ldap.mapper.usePassword= false
grails.plugins.springsecurity.ldap.authenticator.dnPatterns='uid={0},ou=People,o=anu.edu.au'
grails.plugins.springsecurity.ldap.context.anonymousReadOnly=true
grails.plugins.springsecurity.ldap.authorities.ignorePartialResultException = true
grails.plugins.springsecurity.ldap.authorities.retrieveDatabaseRoles = true



It's important to note that the configuration for the LdapUserDetailsService is configured from Config.groovy

I hope this helps another Grails developer out there as it took me a day and half to figure this out, with some help provided by the online references listed below.

References:

No comments:

Post a Comment