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

2

7

I have a JSF page which is not protected by j_security_check. I perform the following steps:

  1. Open the JSF page in a browser.
  2. Restart the server.
  3. Click a command button on the JSF page to initiate an ajax call.

Firebug shows that a ViewExpiredException is raised, as expected.

Post:

javax.faces.ViewState=8887124636062606698:-1513851009188353364

Response:

<partial-response>
<error>
<error-name>class javax.faces.application.ViewExpiredException</error-name>
<error-message>viewId:/viewer.xhtml - View /viewer.xhtml could not be restored.</error-message>
</error>
</partial-response>

However, once I configure the page to be protected by j_security_check and perform the same steps listed above, strangely (to me) the ViewExpiredException is no longer raised. Instead, the reponse is just a new view state.

Post:

javax.faces.ViewState=-4873187770744721574:8069938124611303615

Response:

<partial-response>
<changes>
<update id="javax.faces.ViewState">234065619769382809:-4498953143834600826</update>
</changes>
</partial-response>

Can someone help me figure this out? I expect it to raise an exception so I can process that exception and show an error page. Now it just responds with a new ViewState, my page just got stuck without any visual feedback.

Ca answered 19/9, 2012 at 23:10 Comment(0)
L
13

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.

Lane answered 20/9, 2012 at 3:29 Comment(6)
Oh an additional question, I am wondering why the following happens: If I use context.getExternalContext().redirect(loginURL), it indeed redirects me to the login page. But after logging in, an xml file, with ViewState as its content, is displayed by the browser. The xml file is the exactly the same one as the second xml file I posted in my question. If I use context.getExternalContext().redirect(homepageURL), all works fine. It will take me the login page. Once logging in, the home page will be displayed.Ca
You're right, that's another nasty issue: the container managed authentication remembers all request parameters (including the invalid javax.faces.ViewState and another request parameter indicating that it's an ajax request) and re-passes it after successful login which would result in a ViewExpiredException error page as ajax response. This can be avoided by redirecting to the forward URI instead which in turn should re-trigger the security check again by a plain GET request instead of an ajax POST request. I've updated the answer accordingly.Lane
@Lane OmniPartialViewContext does nothing for me. Ajax posting to a secured page causes a 403 error (rather than viewExpiredException), which is ignored by jsf ajax handler and thus nothing happens. It seems impossible to do ANYTHING server side when dealing with expired views in JSF. No filters, error handlers or anything is called. Any tips?Horseflesh
thx for the solution. is the check originalURL!=null needed? request.getAttribute(RequestDispatcher.FORWARD_REQUEST_URI) is always null for my web applicationKremenchug
@Zardo: It's null when server doesn't use RequestDispatcher#forward() for j_security_check (which is strange by the way, probably you're using a 3rd party authentication framework or even homegrowing your own, in that case you should ignore this Q&A because it's targeted at Java EE built-in authentication).Lane
Nice explanation. Any idea that OmniPartialViewContext suddenly not working. We're using omnifaces 1.14. It was working before.Clino
P
0

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?

Protonema answered 29/9, 2021 at 14:39 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.