Wednesday, April 14, 2010

Bypassing Acegi Security

Acegi Security is rather easy to include in your application, but because our application needs to integrate with a single-sign on system, some method of bypassing Acegi or automatically logging into our application is required.

Since Acegi is built upon a list of filters that intercept HttpServletRequests, we can inject our own filter into the chain that will automatically log a user into our application. For simplicity, we assume the SSO is providing user information in a request header parameter.

The first step is include our filter in the list of filters to be invoked by Acegi. Edit conf\SecurityConfig.groovy, which should have been created during installation of the Acegi plugin, to include the following:
filterNames = ['httpSessionContextIntegrationFilter',
'logoutFilter',
'authenticationProcessingFilter',
'securityContextHolderAwareRequestFilter',
'rememberMeProcessingFilter',
'mySsoFilter',
'anonymousProcessingFilter',
'exceptionTranslationFilter',
'filterInvocationInterceptor']

The filters listed are the common filters invoked by Acegi in the listed order. We've included our filter mySsoFilter.


Since the filters in Acegi are actually Spring beans, the next step is to define the bean representing our filter in conf\spring\resources.groovy
mySsoFilter(MySsoFilter) {
userDetailsService = ref("userDetailsService")
}

The userDetailsService bean passed to our filter is pre-defined by Acegi and Grails. It's purpose is described later.


Finally we create our filter file, MySsoFilter.groovy, in the utils directory. The skeleton is listed below. Note
-- we defined an instance field userDetailsService, the Spring bean injected into our filter bean.
-- the method getOrder() defines the order and priority for the filter during invocation
-- the method doFilterHttp(...) is where the functionality of our filter will be contained.
import org.apache.commons.logging.LogFactory

import org.springframework.beans.factory.InitializingBean
import org.springframework.security.GrantedAuthority
import org.springframework.security.GrantedAuthorityImpl
import org.springframework.security.context.SecurityContextHolder
import org.springframework.security.providers.UsernamePasswordAuthenticationToken
import org.springframework.security.ui.FilterChainOrder
import org.springframework.security.ui.SpringSecurityFilter

import javax.servlet.FilterChain
import javax.servlet.http.HttpServletRequest
import javax.servlet.http.HttpServletResponse


class MySsoFilter extends SpringSecurityFilter implements InitializingBean
{
static final HEADER_ATTRIBUTE_LOGIN = 'x-login'
static final HEADER_ATTRIBUTE_FIRSTNAME = 'x-firstname'
static final HEADER_ATTRIBUTE_LASTNAME = 'x-lastname'
static final HEADER_ATTRIBUTE_EMAIL = 'x-email'

static final log = LogFactory.getLog(MySsoFilter.class)

def userDetailsService

void afterPropertiesSet() {
log.debug '************************ inside afterPropertiesSet'
}


void doFilterHttp(HttpServletRequest request, HttpServletResponse response, FilterChain chain) {
chain.doFilter(request, response)
}

int getOrder() {
return FilterChainOrder.REMEMBER_ME_FILTER
}
}



Now that we have a filter being invoked in the proper order, containing information about the user who has been logged in via SSO, the difficulty now lies in simulating a login in Acegi.

First, we get information about the user we're trying to log in. This is provided by the userDetailsService bean, an instance of the Acegi implementation of the UserDetailsService. The interface's only method returns an instance of UserDetails, which is a wrapper around the loginUserDomainClass and authorityDomainClass defined during Acegi installation (and also defined in SecurityConfig.groovy).
def details = userDetailsService.loadUserByUsername(username) 


Next, we create a security token, by using the concrete class UsernamePasswordAuthenticationToken. The password is obtained during lookup of the user.
def token = new UsernamePasswordAuthenticationToken(
details, passwd, details.authorities)


Finally, we give the token to the SecurityContextHolder, an Acegi object which manages the security token per executing thread.
SecurityContextHolder.context.authentication = token 



Our final implementation for doFilterHttp(...) is as follows
    void doFilterHttp(HttpServletRequest request, HttpServletResponse response, FilterChain chain) {
log.debug '************************ inside doFilterHttp'
log.debug 'req value ' + request.getHeader(HEADER_ATTRIBUTE_LOGIN)

// no SSO header, do not continue
def login = request.getHeader(HEADER_ATTRIBUTE_LOGIN)
if (!login) {
log.debug('no sso header found, continuing')
chain.doFilter(request, response)
return
}

// user not in our system, do not continue
def user = User.findByUsername(login)
if (!user) {
log.info('username ' + login + ' not found')
chain.doFilter(request, response)
return
}

// user does not have admin role, do not continue
def adminRole = Role.findByAuthority('ROLE_ADMINISTRATOR')
if (!user.authorities.contains(adminRole)) {
log.info('user ' + login + ' does not have admin permission')
chain.doFilter(request, response)
return
}

log.info('user ' + login + ' given SSO access')
def details = userDetailsService.loadUserByUsername(user.username)
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(
details, user.passwd, details.authorities)
SecurityContextHolder.context.authentication = token
chain.doFilter(request, response)
}

No comments:

Post a Comment