ViewExpiredException not thrown on ajax request if JSF page is protected by j_security_check ViewExpiredException not thrown on ajax request if JSF page is protected by j_security_check ajax ajax

ViewExpiredException not thrown on ajax request if JSF page is protected by j_security_check


I was able to reproduce your problem. What is happening here is that the container invokes a RequestDispatcher#forward() to the login page as specified in security constraint. However, if the login page is by itself a JSF page as well, then the FacesServlet will be invoked as well on the forwarded request. As the request is a forward, this will simply create a new view on the forwarded resource (the login page). However, as it's an ajax request and there's no render information (the whole POST request is basically discarded during the security check forward), only the view state will be returned.

Note that if the login page were not a JSF page (e.g. JSP or plain HTML), then the ajax request would have returned the whole HTML output of the page as ajax response which is unparseable by JSF ajax and interpreted as "empty" response.

It is, unfortunately, working "as designed". I suspect that there's some oversight in the JSF spec as to security constraint checks on ajax requests. The cause is after all understandable and fortunately easy to solve. Only, you actually don't want to show an error page here, but instead just the login page in its entirety, exactly as would happen during a non-ajax request. You just have to check if the current request is an ajax request and is been forwarded to the login page, then you need to send a special "redirect" ajax response so that the whole view will be changed.

You can achieve this with a PhaseListener as follows:

public class AjaxLoginListener implements PhaseListener {    @Override    public PhaseId getPhaseId() {        return PhaseId.RESTORE_VIEW;    }    @Override    public void beforePhase(PhaseEvent event) {        // NOOP.    }    @Override    public void afterPhase(PhaseEvent event) {        FacesContext context = event.getFacesContext();        HttpServletRequest request = (HttpServletRequest) context.getExternalContext().getRequest();        String originalURL = (String) request.getAttribute(RequestDispatcher.FORWARD_REQUEST_URI);        String loginURL = request.getContextPath() + "/login.xhtml";        if (context.getPartialViewContext().isAjaxRequest()            && originalURL != null            && loginURL.equals(request.getRequestURI()))        {            try {                context.getExternalContext().invalidateSession();                context.getExternalContext().redirect(originalURL);            } catch (IOException e) {                throw new FacesException(e);            }        }    }}

Update this solution is since OmniFaces 1.2 been built into the OmniPartialViewContext. So if you happen to use OmniFaces already, then this problem is fully transparently solved and you don't need a custom PhaseListener for this.


The above AjaxLoginListener solution works for me. Interestingly we are using omnifaces 3.11.1 but the OmniPartialViewContext is not working in my scenario. This is because the check for the loginViewId does not match the current viewId since I have an error-page in my web.xml for org.jboss.weld.contexts.NonexistentConversationException. Note that when AjaxLoginListener is fired for me it throws an exception on the call to context.getExternalContext().invalidateSession(); so it never calls the redirect(). So I'm not sure if my scenario is exactly the same as the original one in this thread. Here are the steps I use to recreate my scenario:

  1. Visit an xhtml page with an ajax command button.
  2. Wait for the session to timeout.
  3. Click the ajax command button.
  4. User is redirected to the error-page mapped to the NonexistentConversationException in web.xml
  5. Click a link on that page which requests a secured url
  6. System shows login page - login.
  7. Click the link that takes you to the xhtml page that contained the ajax command button in step 1.
  8. System shows the partial response containing the NonexistentConversationException error-page contents.

Is it possible that the AjaxLoginListener is working because it is mapped to PhaseId.RESTORE_VIEW whereas the OmniPartialViewContext is mapped to PhaseId.RENDER_RESPONSE?