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:
- CAS Authentcation
- Retrieve user details from LDAP
- Save user details to the database if it doesn't already exist
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:
- http://swordsystems.com/2011/12/21/spring-security-cas-ldap/
- http://git.springsource.org/~ianbrandt/spring-security/ianbrandts-spring-security/blobs/9e751e22c89c55fb99dc09215fad1151c8654ccd/ldap/src/main/java/org/springframework/security/ldap/userdetails/LdapUserDetailsService.java
- http://www.jarvana.com/jarvana/view/org/springframework/security/spring-security-cas-client/3.0.5.RELEASE/spring-security-cas-client-3.0.5.RELEASE-sources.jar!/org/springframework/security/cas/web/CasAuthenticationFilter.java?format=ok
- http://www.jarvana.com/jarvana/view/org/springframework/security/spring-security-cas-client/3.0.4.RELEASE/spring-security-cas-client-3.0.4.RELEASE-sources.jar!/org/springframework/security/cas/authentication/CasAuthenticationProvider.java?format=ok
- http://static.springsource.org/spring-security/site/docs/3.0.x/apidocs/org/springframework/security/ldap/search/FilterBasedLdapUserSearch.html
- http://burtbeckwith.github.com/grails-spring-security-ldap/docs/manual/index.html
- https://github.com/grails-plugins/grails-spring-security-core/blob/master/src/groovy/org/codehaus/groovy/grails/plugins/springsecurity/GormUserDetailsService.groovy