Introduction
I am converting an existing set of Java servlets to Grails. These servlets act as a RESTful API which take in addition to the API’s specific parameters two parameters to authenticate the request before handing over the information. In playing with Grails I very much enjoyed setting up my project to use the Grails Security Plugin. With minimal effort I was able to get the plugin to authenticate user’s using their existing credentials from the legacy system.
The Problem
Out of the box things worked well in the sense that if I visited one of the API’s URL if I had not authenticated myself it would direct me to a login page where I could supply the required credentials and then be sent back to the page I was requesting. Not bad when doing this interactively, but I desire to maintain the current functionality that I have where the credentials can be part of the URL. l also did not wish to change the name of the current parameters as I wish to make this change as transparent as possible.
My Solution
I found a number of articles that spoke of creating a custom filter (Hacking Custom Authentication Providers with Grails Spring Security this was for the previous ACEGI plugin and How do I implement a custom FilterSecurityInterceptor using grails 1.3.2 and the plugin spring-security-core 1?) however none of them worked perfectly for me.
In the end I took information from both of these pages and some poking around in the spring security core and web sources to put the following together. No unlike the two articles above I opted not to create my own Authentication Token as I decided to use the UsernamePasswordAuthenticationToken as it represented what I wanted (plus I had it working with the credentials when entered into a form). I also did not create my own Authentication Provider using the out of the box providers for much the same reason as not creating my own Authentication Token.
First up is my custom Filter:
package ca.umanitoba.ist.es.portal.api
import org.springframework.beans.factory.InitializingBean
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.ApplicationEventPublisherAware;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken
import org.springframework.security.core.Authentication
import org.springframework.security.core.AuthenticationException
import org.springframework.security.web.authentication.*
import org.springframework.security.core.context.SecurityContextHolder
import org.springframework.web.filter.GenericFilterBean
import javax.servlet.FilterChain
import javax.servlet.ServletException
import javax.servlet.ServletRequest
import javax.servlet.ServletResponse
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* @date created on 2011/03/25 William Moore
*
* @version $Revision: $ $Date: $ $Author: $
*
* @class APIAuthenticationFilter
* @brief This class adds a filter to handle URLs where the API credentials
* are supplied with the REST request
*
*/
class APIAuthenticationFilter extends GenericFilterBean implements ApplicationEventPublisherAware {
def authenticationManager
def eventPublisher
def rememberMeServices
def springSecurityService
AuthenticationSuccessHandler successHandler = new SavedRequestAwareAuthenticationSuccessHandler()
AuthenticationFailureHandler failureHandler = new SimpleUrlAuthenticationFailureHandler()
void afterPropertiesSet() {
assert authenticationManager != null, 'authenticationManager must be specified'
assert rememberMeServices != null, 'rememberMeServices must be specified'
}
void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req
HttpServletResponse response = (HttpServletResponse) res
if (SecurityContextHolder.getContext().getAuthentication() == null) {
def username = request.getParameter("apikey")
def password = request.getParameter("apisec")
Authentication auth
UsernamePasswordAuthenticationToken upat
if ( username && password ) {
try {
upat = new UsernamePasswordAuthenticationToken(username, password)
if (upat != null) {
auth = authenticationManager.authenticate(upat)
logger.debug("Authentication success: " + auth);
onSuccessfulAuthentication(request, response, auth)
}
} catch (AuthenticationException authenticationException) {
onUnsuccessfulAuthentication(request, response, authenticationException)
} catch(e) {
onUnsuccessfulAuthentication(request, response, e)
}
}
}
chain.doFilter(req, res)
}
protected void onSuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, Authentication authResult) {
SecurityContextHolder.getContext().setAuthentication(authResult)
rememberMeServices.onLoginSuccess(request, response, authResult)
}
protected void onUnsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) {
SecurityContextHolder.clearContext();
rememberMeServices.loginFail(request, response)
}
public void setApplicationEventPublisher(ApplicationEventPublisher eventPublisher) {
this.eventPublisher = eventPublisher
}
}
Now after creating the Filter I need to add a bean to my resources file
import ca.umanitoba.ist.es.portal.api.APIAuthenticationFilter
beans = {
[ .. ]
apiAuthFilter(APIAuthenticationFilter) {
authenticationManager = ref("authenticationManager")
rememberMeServices = ref("rememberMeServices")
springSecurityService = ref("springSecurityService")
}
}
Once this was done I needed to add my Filter to the existing chain of filters, I did this in the bootstrap file
import org.codehaus.groovy.grails.plugins.springsecurity.SecurityFilterPosition
import org.codehaus.groovy.grails.plugins.springsecurity.SpringSecurityUtils
class BootStrap {
[..]
def init = { servletContext ->
[..]
SpringSecurityUtils.clientRegisterFilter('apiAuthFilter', SecurityFilterPosition.SECURITY_CONTEXT_FILTER.order + 10)
[..]
}
[..]
}
Conclusion
I can still access the API interactively which will be nice for testing them out, but can also pass the credentials they way we are used to so that existing applications will continue to work with no hassle.