Tuesday, October 19, 2010

JSF Ajax redirect after session timeout

JSF 2 has a facility to be able to do Ajax redirects. The specification says - an element <redirect url="redirect url"/> found in the response causes a redirect to the URL "redirect url". JSF component libraries often write other "redirect" elements into response, but the end result is the same - a redirect happens. It works great in case of JSF Ajax redirects.
A web application normally has a security filter - an own solution or from Spring framework or something else. Such security filter does a redirect if user is not authenticated to use requested web resources. This is not an JSF redirect. The filter doesn't pass current request to the FacesServlet at all. Here is an example:
public class SecurityFilter implements Filter {

    private Authenticator authenticator;
    private String loginPage;
    
    public void init(FilterConfig config) throws ServletException {
        ...
        // create an Authenticator  
        authenticator = AuthenticatorFactory.createAuthenticator(config);
        loginPage = getLoginPage(config);
        ...
    }

    public void doFilter(ServletRequest request, ServletResponse response,
        FilterChain chain) throws IOException, ServletException {
        HttpServletRequest hReq = (HttpServletRequest) request;
        HttpServletResponse hRes = (HttpServletResponse) response;
        ...
        // get user roles
        Collection roles = ... 
        // get principal
        Principal principal = hReq.getUserPrincipal();
        
        if (!roles.isEmpty() && principal == null) {
            // user needs to be authenticated
            boolean bRet = authenticator.showLogin(hReq, hRes);
            if (!bRet) {
         // redirect the a login page or error sending
                ...
            }
        }
        ...
    }
}
Problem: indeed an Ajax request is redirected to a specified page after session timeout, but the response can not be proper treated on the client-side. We have a quite regular redirect in this case and the specified page in the response. Client-side Ajax code doesn't expect the entire page coming back.
A possible solution would be to pass "not authenticated" request through FacesServlet. We need to write an Authenticator class which stores the page URL we want to be redirected to in the request scope.
public class FormAuthenticator {
    ...

    public boolean showLogin(HttpServletRequest request, 
        HttpServletResponse response, String loginPage) throws IOException {
        ...
        request.setAttribute("web.secfilter.authenticator.showLogin", loginPage);
 
        return true;
    }
}
The method showLogin returns true in order to avoid a redirect by filter. Let us execute the redirect by JSF! The call gets now a third parameter "loginPage".
authenticator.showLogin(hReq, hRes, loginPage);
If you use Spring security you should have an entry point in your XML configuration file like this one
<beans:bean id="authenticationProcessingFilterEntryPoint"
       class="com.xyz.webapp.security.AuthenticationProcessingFilterEntryPoint">
    <beans:property name="loginFormUrl" value="/login.html"/>
    <beans:property name="serverSideRedirect" value="true"/>
</beans:bean>
The class AuthenticationProcessingFilterEntryPoint is derived from Spring's one
/**
 * Extended Spring AuthenticationProcessingFilterEntryPoint for playing together with JSF Ajax redirects.
 */
public class AuthenticationProcessingFilterEntryPoint extends
    org.springframework.security.ui.webapp.AuthenticationProcessingFilterEntryPoint
{
    public static final String ATTRIBUTE_LOGIN_PAGE = "web.secfilter.authenticator.showLogin";

    public void commence(ServletRequest request, ServletResponse response, AuthenticationException authException)
        throws IOException, ServletException {

        request.setAttribute(ATTRIBUTE_LOGIN_PAGE, getLoginFormUrl());
        super.commence(request, response, authException);
    }
}
On the JSF side we need a special phase listener to do the redirect by ExternalContext. We check at first the buffered page URL in the beforePhase. If it was set - an JSF redirect is needed and will be done.
/**
 * Phase listener for the Restore View Phase which manages display of login page.
 */
public class SecurityPhaseListener implements PhaseListener
{
    private static final Log LOG = LogFactory.getLog(SecurityPhaseListener.class);

    public void afterPhase(PhaseEvent event) { ; }

    public void beforePhase(PhaseEvent event) {
        FacesContext fc = event.getFacesContext();

        String loginPage = (String) fc.getExternalContext().getRequestMap().
            get("web.secfilter.authenticator.showLogin");
        if (StringUtils.isNotBlank(loginPage)) {
            doRedirect(fc, loginPage);
        }
    }

    public PhaseId getPhaseId() {
        return PhaseId.RESTORE_VIEW;
    }

    /**
     * Does a regular or ajax redirect.
     */
    public void doRedirect(FacesContext fc, String redirectPage) 
        throws FacesException {
        ExternalContext ec = fc.getExternalContext();

        try {
            if (ec.isResponseCommitted()) {
                // redirect is not possible
                return;
            }

            // fix for renderer kit (Mojarra's and PrimeFaces's ajax redirect)
            if ((RequestContext.getCurrentInstance().isAjaxRequest()
                || fc.getPartialViewContext().isPartialRequest())
                && fc.getResponseWriter() == null
                && fc.getRenderKit() == null) {
                    ServletResponse response = (ServletResponse) ec.getResponse();
                    ServletRequest request = (ServletRequest) ec.getRequest();
                    response.setCharacterEncoding(request.getCharacterEncoding());

                    RenderKitFactory factory = (RenderKitFactory)  
                     FactoryFinder.getFactory(FactoryFinder.RENDER_KIT_FACTORY);

                    RenderKit renderKit = factory.getRenderKit(fc,
                     fc.getApplication().getViewHandler().calculateRenderKitId(fc));

                    ResponseWriter responseWriter =
                        renderKit.createResponseWriter(
                        response.getWriter(), null, request.getCharacterEncoding());
                        fc.setResponseWriter(responseWriter);
            }

            ec.redirect(ec.getRequestContextPath() + 
                (redirectPage != null ? redirectPage : ""));
        } catch (IOException e) {
            LOG.error("Redirect to the specified page '" + 
                redirectPage + "' failed");
            throw new FacesException(e);
        }
    }
}
It works in both cases - for Ajax and regular request.

33 comments:

  1. Hello,
    Sorry but this does not work for me. I tried this with an autoComplete component : when a timeout occurs, the ajax redirect response is sent (with the right xml including the tag), but on the client side nothing happens, I still see the animated gif. Any help would be appreciated ...
    (Thanks anyway for the solution, it works on the server side)

    ReplyDelete
  2. Which JSF library do you use to send an Ajax request with the autoComplete component? How does the response look like for the Ajax redirect? Are you sure that your filter doesn't catch the response and redirect to the login page?

    ReplyDelete
  3. Which framework are you using? thanks.

    ReplyDelete
  4. Hello Anonymous,

    I'm using Mojarra 2.x for standard JSF 2 components and PrimeFaces 2.x as component library.

    ReplyDelete
  5. Hello Oleg,
    Excuse me for the delay !
    I am using PrimeFaces 2.2-RC2 and MyFaces 2.0.3.
    I have also tried it with Mojarra, but the code does not work : line 58 ec.redirect(...) throws a NPE because at some point the ViewRoot is null.
    Anyway, I would prefer to stay with MyFaces.
    So my problem is that even when the browser (in my case firefox 3.6) receives the ajax redirect response (xml fragment with redirect tag, I verified that it is correct with firebug), nothing more happens, I still see the "waiting" animated gif.
    I also have another question : I feel quite "uncomfortable" with the fact that we let an unauthenticated request go through spring security filters. Couldn't this be a kind of security hole ? And would it be long/difficult to reproduce the behavior of the externalContext.redirect method in another component (for example a Spring Security RedirectStrategy), so that the request does not go past the security filter ?
    Thanks for your help ...

    ReplyDelete
  6. Hello,

    I have faced a NPE with Mojarra 2.0.3 as well. But it's resolved since 2.0.4. I didn't change yet the code to be fit with the last JSF impl. I will update this post after changing to the new JSF impl.

    You can try to reproduce the behavior of the externalContext.redirect method outside of FacesServlet. I never tried that but I found this post how you can create FacesContext manually:
    http://ocpsoft.com/java/jsf-java/jsf-20-extension-development-accessing-facescontext-in-a-filter/

    ReplyDelete
  7. Hi,
    Thanks for the link.
    Do you have any clue for my ajax/javascript problem ? (ajax redirect requests not being properly recognize on the client side ?)
    Do you know some way to add a listener or a kind of hook to PrimeFaces javascript, in order to do the proper redirection ?

    ReplyDelete
  8. I use p:autoComplete, session timed out and when starting to type animated gif appears and does not go away. Response has redirect url set but redirect is not happening. I have some openfaces components on page is this because of that or bug on primefaces?

    2{"validationFailed":false}

    ReplyDelete
  9. Response from previous post looks like
    <?xml version=\"1.0\" encoding=\"utf-8\"?>
    2<partial-response><redirect url=\"/loginfailed.jsf\"></redirect><extension ln=\"openfaces\" ajaxResult=\"null\" type=\"ajaxResult\"></extension><extension primefacesCallbackParam=\"validationFailed\">{\"validationFailed\":false}</extension></partial-response>

    ReplyDelete
  10. Thanks for your post, but it doesn't seem to work for me.
    The problem is that my authenticationEntryPoint is not called on every request and the PhaseListener always finds a null value for the loginPage.

    Could you post a working security config xml for this case?
    Could it be a problem that I'm using the element in my configuration (which adds a LoginUrlAuthenticationEntryPoint). I tried replacing the Element but i didn't really succeed and I depend on it's functionality.

    Thank you and best regards,

    Robert

    ReplyDelete
  11. I forgot to write that the use of "AuthenticationProcessingFilterEntryPoint" is deprecated and should be replaced by "LoginUrlAuthenticationEntryPoint"

    ReplyDelete
  12. Also doesn't work for me. The request is already sent back with AuthenticationProcessingFilterEntryPoint, and then it reaches PhaseListener which now doesn't have request attributes from the original ajax request.

    ReplyDelete
  13. Request should not be sent back with AuthenticationProcessingFilterEntryPoint. Don't make a redirect. You should pass it through.

    ReplyDelete
  14. I managed to redirect to the login.jsf page using custom response... But now when I login again it redirects me to the page where I should go, but with additional xml in the body:


    <partial-response>
      <change
        <update id="javax.faces.ViewState">
           Some text
         </update>
       </change>
    </partial-response>

    Oleg, do you know how can I fix this?

    ReplyDelete
  15. Sasha, what do you use? If you use a security filter, it seems to buffer the last page location. We had a similar issue in our security filter. I think, you should look deeper into your filter and reset the last page location if it was stored in session.

    ReplyDelete
  16. Yes I use security filter. I added my custom filter so I can manage ajax request from jsf. And now it works. Thanx.

    ReplyDelete
  17. My xml-tags have been stripped out of my comment, so i put this onto pastebin:

    http://pastebin.com/wemY8EFW

    ReplyDelete
  18. As it seems that my comment has been removed, there it is on pastebin:
    http://pastebin.com/MuVexQHj

    Thank you in advance for any advice!

    Best Regards,
    Robert

    ReplyDelete
  19. FWIW I found a simpler solution using container security:

    http://community.jboss.org/thread/166752?tstart=0

    Regards.

    ReplyDelete
  20. That's almost the same. XML you shown in the example is automatically generated by ExternalContext.redirect() Many component libraries and JSF implementations have their own syntax and it's better to let generate partial response than try to write it self.

    ReplyDelete
  21. Hi Oleg

    Were you able to update the code to use the newer Mojarra implementation to resolve the NPE on line 58?

    ReplyDelete
  22. Sure, it's here http://paste.kde.org/101389/

    ReplyDelete
  23. Sorry, but I couldn't get your solution working. The attribute_login_page is always null in the phase listener.

    Seems that spring-security calls the entry-point multiple times and when the request reaches the phase listener, the attribute is gone.

    I have to agree with ssamayoa. The solution in http://community.jboss.org/thread/166752?tstart=0
    is so much simpler.

    ReplyDelete
  24. I got a problem with implementing your solution on mojarra 2.1.0. Method createPartialResponseWriter in ParialViewContext is throwing NPE in line
    responseWriter = ctx.getRenderKit().createResponseWriter(out,
    "text/xml", encoding);

    (it comes from redirect method in jsf phase listener you proposed)

    Any idea how to get over that exception?

    ReplyDelete
  25. To get rid of the NPE problem in redirect method just change lines:

    ResponseWriter responseWriter =
    renderKit.createResponseWriter(
    response.getWriter(), null, request.getCharacterEncoding());
    fc.setResponseWriter(responseWriter);

    to

    ResponseWriter responseWriter = renderKit.createResponseWriter(response.getWriter(), null, request.getCharacterEncoding());
    responseWriter = new PartialResponseWriter(responseWriter);
    fc.setResponseWriter(responseWriter);

    ReplyDelete
  26. Hello,
    I have a Problem when getting a org.springframework.security.authentication.InsufficientAuthenticationException: Full authentication is required to access this resource
    In this case the listener is not fired und no redirect will be done.

    Does anyone have an idea?

    ReplyDelete
  27. I do not know if the solution can recognize Spring security configuration?

    In my project, some resources are public, some are private and protected only for authenticated user.

    ReplyDelete
  28. And I only want to process ajax session time out...normal page session time out worked correctly.

    ReplyDelete
  29. Try this session time out warning using primefaces

    http://primefaces-tips.blogspot.in/2012/03/prime-faces-session-timeout-countdown.html

    ReplyDelete
  30. I have a simpler solution, by using a custom RedirectStrategy implementation.

    http://wowpleasedoitagain.blogspot.com/2012/08/web-dev-spring-security-jsf-ajax.html

    ReplyDelete
  31. I have a solution using java script, it is easy to implement.

    Take a look at this adress http://javacodetips.blogspot.com.tr/2014/03/how-to-implement-session-expired-alert.html

    ReplyDelete