Monday, January 21, 2008

How to make Acegi work on Sun Application Server 8.x?

We are currently developing an application that employs Acegi and runs on Sun Application Server 8 (Sun AS 8.x).

Being a mature and widely adopted security solution, we did not have many problems until we encountered the following puzzling exception:
[#|2008-01-18T16:58:28.984+1100|SEVERE|sun-appserver-pe8.2|javax.enterprise.system.container.web|_ThreadID=22;|ApplicationDispatcher[/express_portal] Servlet.service() for servlet jsp threw exception
java.lang.ClassCastException: org.acegisecurity.providers.UsernamePasswordAuthenticationToken
at com.sun.web.server.J2EEInstanceListener.handleBeforeEvent(J2EEInstanceListener.java:130)
at com.sun.web.server.J2EEInstanceListener.instanceEvent(J2EEInstanceListener.java:68)
at org.apache.catalina.util.InstanceSupport.fireInstanceEvent(InstanceSupport.java:300)
at org.apache.catalina.core.ApplicationDispatcher.invoke(ApplicationDispatcher.java:712)
at org.apache.catalina.core.ApplicationDispatcher.processRequest(ApplicationDispatcher.java:482)
at org.apache.catalina.core.ApplicationDispatcher.doForward(ApplicationDispatcher.java:417)
at org.apache.catalina.core.ApplicationDispatcher.access$000(ApplicationDispatcher.java:80)
at org.apache.catalina.core.ApplicationDispatcher$PrivilegedForward.run(ApplicationDispatcher.java:95)
at java.security.AccessController.doPrivileged(Native Method)
at org.apache.catalina.core.ApplicationDispatcher.forward(ApplicationDispatcher.java:313)
at org.acegisecurity.ui.AccessDeniedHandlerImpl.handle(AccessDeniedHandlerImpl.java:65)
at org.acegisecurity.ui.ExceptionTranslationFilter.handleException(ExceptionTranslationFilter.java:166)
at org.acegisecurity.ui.ExceptionTranslationFilter.doFilter(ExceptionTranslationFilter.java:118)
at org.acegisecurity.util.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:274)
at org.acegisecurity.providers.anonymous.AnonymousProcessingFilter.doFilter(AnonymousProcessingFilter.java:125)
at org.acegisecurity.util.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:274)
at org.acegisecurity.wrapper.SecurityContextHolderAwareRequestFilter.doFilter(SecurityContextHolderAwareRequestFilter.java:81)
at org.acegisecurity.util.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:274)
at org.acegisecurity.ui.AbstractProcessingFilter.doFilter(AbstractProcessingFilter.java:217)
at org.acegisecurity.util.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:274)
at org.acegisecurity.ui.logout.LogoutFilter.doFilter(LogoutFilter.java:106)
at org.acegisecurity.util.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:274)
at org.acegisecurity.context.HttpSessionContextIntegrationFilter.doFilter(HttpSessionContextIntegrationFilter.java:229)
at org.acegisecurity.util.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:274)
at org.acegisecurity.concurrent.ConcurrentSessionFilter.doFilter(ConcurrentSessionFilter.java:95)
at org.acegisecurity.util.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:274)
at org.acegisecurity.util.FilterChainProxy.doFilter(FilterChainProxy.java:148)
at org.acegisecurity.util.FilterToBeanProxy.doFilter(FilterToBeanProxy.java:98)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:210)
at org.apache.catalina.core.ApplicationFilterChain.access$000(ApplicationFilterChain.java:55)
at org.apache.catalina.core.ApplicationFilterChain$1.run(ApplicationFilterChain.java:161)
at java.security.AccessController.doPrivileged(Native Method)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:157)
at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:263)
at org.apache.catalina.core.StandardPipeline.invoke(StandardPipeline.java:551)
at org.apache.catalina.core.StandardContextValve.invokeInternal(StandardContextValve.java:225)
at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:173)
at org.apache.catalina.core.StandardPipeline.invoke(StandardPipeline.java:551)
at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:170)
at org.apache.catalina.core.StandardPipeline.invoke(StandardPipeline.java:551)
at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:132)
at org.apache.catalina.core.StandardPipeline.invoke(StandardPipeline.java:551)
at org.apache.catalina.core.ContainerBase.invoke(ContainerBase.java:933)
at org.apache.coyote.tomcat5.CoyoteAdapter.service(CoyoteAdapter.java:189)
at com.sun.enterprise.web.connector.grizzly.ProcessorTask.doProcess(ProcessorTask.java:604)
at com.sun.enterprise.web.connector.grizzly.ProcessorTask.process(ProcessorTask.java:475)
at com.sun.enterprise.web.connector.grizzly.ReadTask.executeProcessorTask(ReadTask.java:371)
at com.sun.enterprise.web.connector.grizzly.ReadTask.doTask(ReadTask.java:264)
at com.sun.enterprise.web.connector.grizzly.TaskBase.run(TaskBase.java:281)
at com.sun.enterprise.web.connector.grizzly.WorkerThread.run(WorkerThread.java:83)
|#]


We traced the application server and found that it was likely caused by the Sun AS 8.x trying to cast the Acegi-implemented Principal to an internal Sun AS implementation.

We did some googling and found that Andrey Grebnev blogged about this two years ago, and he suggested a workaround by overriding the getUserPrincipal() method (of SecurityContextHolderAwareRequestWrapper) by always returning a null.

Because SecurityContextHolderAwareRequestWrapper and its subclasses are used internally by Acegi, his workaround implied changing the source code of Acegi, which we are reluctant to do.

Realizing that the it was only unsafe for the HttpServletRequest to return the Acegi-implemented Principal when the servlet filter chain has been executed and the control is handed over to the AS and the running application, we came up with a NullPrincipalFilter that wraps the incoming HttpServletRequest with a NullPrincipalHttpServletRequestWrapper, which returns null for getUserPrincipal(), and hands the control over to the AS. This filter should always be placed at the end of the filter proxy chain in Acegi. And of course, the application must not use HttpServletRequest's getUserPrincipal() method to retrieve the user principal, which is very easy to do, as it can invoke SecurityContextHolder.getContext().getAuthentication() to achieve the same goal, without coupling to the Servlet API.

The following is the source code of NullPrincipalFilter:

package au.net.ozgwei.util.httpservlet;

import java.io.IOException;

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

/**
* A filter designed specific to get around the bug in Sun Application Server
* 8.x where a custom security framework's implementation of Principal is
* casted to Sun Application Server's own implementation.
*
* @author Alex
* @version 1.0
*/
public class NullPrincipalFilter implements Filter {

@SuppressWarnings("unused")
private static final Log log = LogFactory.getLog(NullPrincipalFilter.class);

/* (non-Javadoc)
* @see javax.servlet.Filter#destroy()
*/
public void destroy() {
}

/* (non-Javadoc)
* @see javax.servlet.Filter#doFilter(javax.servlet.ServletRequest, javax.servlet.ServletResponse, javax.servlet.FilterChain)
*/
public void doFilter(ServletRequest aRequest, ServletResponse aResponse, FilterChain aFileterChain) throws IOException, ServletException {
aFileterChain.doFilter(
new NullPrincipalHttpServletRequestWrapper(
(HttpServletRequest) aRequest), aResponse);
}

/* (non-Javadoc)
* @see javax.servlet.Filter#init(javax.servlet.FilterConfig)
*/
public void init(FilterConfig aArg0) throws ServletException {
}

}


And the source code of NullPrincipalHttpServletRequestWrapper:

package au.net.ozgwei.util.httpservlet;

import java.security.Principal;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

/**
* A HttpServletRequestWrapper that always return null for Principal.
*
* @author Alex
* @version 1.0
*/
public class NullPrincipalHttpServletRequestWrapper extends HttpServletRequestWrapper {

@SuppressWarnings("unused")
private static final Log log = LogFactory.getLog(
NullPrincipalHttpServletRequestWrapper.class);

public NullPrincipalHttpServletRequestWrapper(HttpServletRequest aReq) {
super(aReq);
}

@Override
public Principal getUserPrincipal() {
return null;
}

}


And a nullPrincipalFilter bean should be defined in the Spring application context and added to the end of the filterChainProxy, as following:
<bean class="org.acegisecurity.util.FilterChainProxy" id="filterChainProxy">
<property name="filterInvocationDefinitionSource">
<value>
CONVERT_URL_TO_LOWERCASE_BEFORE_COMPARISON
PATTERN_TYPE_APACHE_ANT
/**=concurrentSessionFilter,httpSessionContextIntegrationFilter,logoutFilter,authenticationProcessingFilter,securityContextHolderAwareRequestFilter,anonymousProcessingFilter,exceptionTranslationFilter,filterInvocationInterceptor,nullPrincipalFilter
</value>
</property>
</bean>

<bean class="au.net.ozgwei.util.httpservlet.NullPrincipalFilter" id="nullPrincipalFilter">


Finally, this bug has been fixed in GlassFish eventually.

No comments: