There was very little online documentation on how to setup a Grails application with Shibboleth , so I thought I would document the steps I took to get it working, given that it wasn't straightforward.
Previously we relied on the combination of of the springsecurity-core plugin and the springsecurity-ldap plugin to authenticate users in our various number of web applications, which worked fine when running in a stand-alone environment, and was relative easy to setup. As these web applications became more integrated, users wanted a single-sign on solution. We opted to use an authentication service provided by the Australian Access Federation which uses shibboleth as the underlying technology.
I was hopeful when I had googled 'grails shibboleth plugin' to find somebody had already done some work:
https://grails.org/plugin/spring-security-shibboleth-native-sp
But when I realized it was last updated in eary 2012 for Grails version 1.3.x, I became less hopeful. It seemed quite dated. Nonetheless, I gave it go, and not surprisingly, it didn't work due to incompatibilities with my newer version of Grails 2.x. So I'll show you how I rolled up my own solution.
Here I will assume that you already have Shibboleth successfully working in your environment. If you would like to know more about how to set this up, you can read my other blog entry here:
http://pwu-developer.blogspot.com.au/2012/09/installing-shibboleth-on-linux.html
I'll focus only on what is needed to modify your springsecurity configuration to get your grails application authenticated in an existing shibboleth environment.
Wiring it together
In the resources.groovy file we wire up the various bean classes to create a preauthAuthenticationProvider, which is used in Config.groovy as one of the authentication providers as shown below:Grails 2.x resources.groovy
// Shibboleth integration
userDetailsServiceWrapper(org.springframework.security.core.userdetails.UserDetailsByNameServiceWrapper) {
userDetailsService = ref('userDetailsService')
}
preauthAuthenticationProvider(org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationProvider) {
preAuthenticatedUserDetailsService = ref('userDetailsServiceWrapper')
}
shibAuthFilter(apf.security.ShibbolethRequestHeaderAuthenticationFilter) {
principalRequestHeader = 'mail' //this is the shib header that contains the user ID
checkForPrincipalChanges = true
invalidateSessionOnPrincipalChange = true
continueFilterChainOnUnsuccessfulAuthentication = false
authenticationManager = ref('authenticationManager')
userDetailsService = ref('userDetailsService')
enable = true
}
For Grails 3.x, there was a slight adjustment to be made in the resources.groovy as shown below:
// Shibboleth integration
userDetailsServiceWrapper(org.springframework.security.core.userdetails.UserDetailsByNameServiceWrapper) {
userDetailsService = ref('userDetailsService')
}
preauthAuthenticationProvider(org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationProvider) {
preAuthenticatedUserDetailsService = ref('userDetailsServiceWrapper')
}
shibAuthFilter(apf.security.ShibbolethRequestHeaderAuthenticationFilter) {
principalRequestHeader = 'mail' //this is the shib header that contains the user ID
checkForPrincipalChanges = true
invalidateSessionOnPrincipalChange = true
continueFilterChainOnUnsuccessfulAuthentication = false
authenticationManager = ref('authenticationManager')
userDetailsService = ref('userDetailsService')
exceptionIfHeaderMissing = false
checkForPrincipalChanges = false
enable = true
}
The fields for 'exceptionIfHeaderMissing' and 'checkForPrincipalChanges' must be set to false for Grails 3.x projects.
Config.groovy
environments {
development {
grails.plugin.springsecurity.providerNames = ['ldapAuthProvider','rememberMeAuthenticationProvider']
}
test {
grails.plugin.springsecurity.providerNames = ['ldapAuthProvider','rememberMeAuthenticationProvider']
}
production {
grails.plugin.springsecurity.providerNames = ['preauthAuthenticationProvider','rememberMeAuthenticationProvider']
}
}
As you can see, for my production environment, which is shibboleth protected, I use the preauthAuthenticationProvider. However, for my development environment (which is not shibboleth protected), I continue to use the ldapAuthProvider which is sufficient for development purposes.
More information about the PreAuthenticatedAuthentcationProvider can be found here:
https://docs.spring.io/spring-security/site/docs/2.0.8.RELEASE/apidocs/org/springframework/security/providers/preauth/PreAuthenticatedAuthenticationProvider.html
The PreAuthenticatedAuthenticationProvider essentially tells springsecurity that the authentication is handled external to the web application. The trouble is that springsecurity doesn't know how to map the request headers populated by shibboleth to your User domain class. So we create a new class for this purpose.
ShibbolethRequestHeaderAuthenticationFilter
Next we need to create a class that will be responsible for handling the request headers passed in by shibboleth. Since each instance of shibboleth may use different attribute mappings and names, we can customize our own solution that specifically fits our environment using this filter. Furthermore, this filter will automatically create the user accounts if one doesn't already exist in our local database.ShibbolethRequestHeaderAuthenticationFilter.groovy
package apf.security
import javax.servlet.http.HttpServletRequest
import org.apache.log4j.Logger
import org.springframework.security.core.userdetails.UserDetails
import org.springframework.security.core.userdetails.UserDetailsService
import org.springframework.security.core.userdetails.UsernameNotFoundException
import org.springframework.security.web.authentication.preauth.PreAuthenticatedCredentialsNotFoundException
import org.springframework.security.web.authentication.preauth.RequestHeaderAuthenticationFilter
import apf.taskrequest.User
/**
* Handles for Shibboleth request headers to create Authorization ids.
* Map the request headers provided by shibboleth to properties of the User domain class
* Automatically create user accounts if none already exists.
* Use the email address of the user as the username
*
* @author Philip Wu
*/
public class ShibbolethRequestHeaderAuthenticationFilter extends RequestHeaderAuthenticationFilter {
Logger logger = Logger.getLogger(ShibbolethRequestHeaderAuthenticationFilter.class)
UserDetailsService userDetailsService
/**
* Required. Used to check if users exist.
* @param userDetailsManager
*/
boolean enable = true;
/**
* Since the superclass definition for princiaplRequestHeader is private, we store the value
* in 2 places by overriding the setPrincipalRequestHeader() method, so that we can access this value
*/
String accessiblePrincipalRequestHeader
/**
* Override to store an accessible version of the principalRequestHeader
*/
@Override
public void setPrincipalRequestHeader(String principalRequestHeader) {
super.setPrincipalRequestHeader(principalRequestHeader)
this.accessiblePrincipalRequestHeader = principalRequestHeader
}
/**
* This is called when a request is made, the returned object identifies the
* user and will either be Null or a String. This method will throw an exception if
* exceptionIfHeaderMissing is set to true (default) and the required header is missing.
* @param request
*/
protected Object getPreAuthenticatedPrincipal(HttpServletRequest request) {
if (!enable) return null;
// Extract username
// ShibUseHeaders On
String username = (String)(super.getPreAuthenticatedPrincipal(request));
// Or if AJP is used instead of ShibUseHeaders, then pull from attributes instead of headers
if (! username) {
username = request.getAttribute(accessiblePrincipalRequestHeader)
}
// Extract displayName
String displayName = request.getHeader("displayName")
if (! displayName) {
displayName = request.getAttribute("displayName")
}
logger.debug("authenticatedPrincipal: "+username)
if (username ) {
try {
UserDetails userDetails = userDetailsService.loadUserByUsername(username, false)
logger.debug("userDetails="+userDetails)
} catch (UsernameNotFoundException ex) {
logger.info("User does not exist. Creating new user")
User.withTransaction {
User u = new User();
u.username = username
u.email = username
u.enabled = true
u.displayName = displayName
boolean saved = u.save()
logger.info("New user created: "+saved)
if (! saved) {
logger.error("Errors saving user: "+u.errors)
}
}
}
}
return username;
}
}
Here we use the email address of the user as the username provided by shibboleth in the request headers as 'mail'. Looking back the resources.groovy you can see i've set the following:
principalRequestHeader = 'mail'
One workaround I had to deal with was getting access to the principalRequestHeader of the superclass which was marked as 'private'. In order to use this field, I had to create a duplicate field storing the same value as 'accessiblePrincipcalRequestHeader' and override the setPrincipalRequestHeader() method.
In your shibboleth configuration you may have either "ShibUseHeaders On" or use AJP to pass in your shibboleth attributes. It is recommended to avoid using ShibUseHeaders as it is a potential security issue and that AJP should be used instead. Nonetheless, the above code handles both scenarios.
For Grails 3.x, the 'email' and 'displayName' fields were not automatically included in the User domain class, so I had to manually recreate these fields so that the ShibbolethRequestHeaderAuthenticationFilter did not crash
Reference: Avoid ShibUseHeaders
Register the AuthenticationFilter in Bootstrap.groovy
Now that we've created our ShibbolethRequestHeaderAuthenticationFilter, we need to register it with SpringSecurity by modifying our Bootstrap.groovy as follows:Bootstrapy.groovy
def init = { servletContext ->
// Uncomment when ready to be deployed
securityDefaultAdmin()
injectAttachmentMethods()
// Shibboleth configuration
environments {
development {
}
test {
}
production {
SpringSecurityUtils.clientRegisterFilter('shibAuthFilter', SecurityFilterPosition.PRE_AUTH_FILTER.order + 10)
}
shibboleth {
SpringSecurityUtils.clientRegisterFilter('shibAuthFilter', SecurityFilterPosition.PRE_AUTH_FILTER.order + 10)
}
}
}
And that's it, you're ready to roll.
If you found this helpful, please 'like' this article.
References:
http://edcode.blogspot.com.au/2013/06/using-shibboleth-security-with-grails.html
http://wiki.aaf.edu.au/aaf-mini-grants/tpac/shibboleth-integration-with-spring-security
http://docs.spring.io/autorepo/docs/spring-security/3.2.0.RELEASE/apidocs/org/springframework/security/web/authentication/preauth/RequestHeaderAuthenticationFilter.html