Spring security: CAS + LDAP
After getting straight LDAP authentication to work with the spring-security-ldap plugin, I moved on to the next requirement which was integrating with CAS. Like many projects before us, we need to do authentication through CAS and then follow up authorization (i.e. role checking) through LDAP. The authentication part was easy thanks to the spring-security-cas plugin. However, there are two mildly annoying issues with the plugin as a whole:
First, once it is installed, you can’t turn it off (at least, not in development mode). The value of the cas.active setting is ignored unless you deploy as a war. There is already a bug filed for this and someone has submitted a simple patch. You can either build the patched plugin, or just tweak the few lines directly in your project’s copy of the plugin.
The second issue relates to auto-creating user accounts. I posted about using an AuthenticationEvent listener to do this last week. Unfortunately, this will not work with the default configuration of the CAS plugin. The plugin does not override the userDetailsService so you get the default GormUserDetailsService. That class will throw a “user not found” exception if there is no local user domain object for the given user name. If you have no need for role information (authorities in spring-security speak), then you can simply plug in a simplistic userDetailsService like this one:
import org.codehaus.groovy.grails.plugins.springsecurity.GrailsUserDetailsService
import org.springframework.security.core.userdetails.UserDetails
import org.springframework.security.core.userdetails.User
import org.codehaus.groovy.grails.plugins.springsecurity.SpringSecurityUtils
import org.springframework.security.core.authority.GrantedAuthorityImpl
/**
* Dumb service which just returns a UserDetails object with the username set.
* @author esword
*/
class EmptyUserDetailsService implements GrailsUserDetailsService {
/**
* Taken from GormUserDetailsService: Some Spring Security classes expect at least one
* role, so we give a user with no granted roles this one which gets past that restriction
* but doesn't grant anything.
*/
static final List NO_ROLES = [new GrantedAuthorityImpl(SpringSecurityUtils.NO_ROLE)]
UserDetails loadUserByUsername(String username, boolean loadRoles) {
return loadUserByUsername(username)
}
UserDetails loadUserByUsername(String username) {
new User(username, '', true, true, true, true, NO_ROLES)
}
}
You could extend InMemoryUserDetailsManager if you didn’t want to re-create the UserDetails all the time, or wrap a GormUserDetailsService to first check if you have a local account and return info from that if so. I just threw together this class so that I could verify that the rest of the authentication process with CAS worked.
LDAP Integration
If you do need role information from LDAP, you will need to add a few more beans to your resources.groovy file. Someone posted a thread on the grails-dev mailing list about a year ago with the core information for this configuration. However, the example they give hard-codes the LDAP connection settings in the bean definitions themselves. Since our app is deployed with the LDAP plugin (it is turned off if CAS is turned on), I wanted to use the same property settings so that we could easily toggle back and forth between plain LDAP and CAS. Here is the revised bean configuration within resources.groovy:
// If CAS is active and if ldap is configured, do UserDetails lookup from ldap to get the roles.
// All of these setting names and how they are used come from reading the SpringSecurityLdapGrailsPlugin.groovy
if (application.config.grails.plugins.springsecurity.cas.active) {
def config = SpringSecurityUtils.securityConfig
if (config.ldap.context.server) {
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
}
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
rolePrefix = "ROLE_"
convertToUpperCase = config.ldap.mapper.convertToUpperCase
ignorePartialResultException = config.ldap.authorities.ignorePartialResultException
}
userDetailsService(org.springframework.security.ldap.userdetails.LdapUserDetailsService,
ldapUserSearch,
ldapAuthoritiesPopulator){
}
}
else {
//Dummy service if LDAP isn't set up
userDetailsService(EmptyUserDetailsService)
}
}
Ideally, I would like to be able to keep the LDAP plugin turned on in an “authorization only” mode so that I could use the userDetailsService configuration directly from it. That is not yet possible with the plugin, so this is the next best thing. You still avoid having to write any new code in your application and at least get the benefit of being able to fall back on the default property settings for the LDAP plugin.