1 Reply Latest reply on May 19, 2010 6:29 AM by Vaughn Butt

    Here's how to support log-in and log-out even when the view has expired

    Stephen Friedrich Novice

      Here's how to support log-in and log-out even when the view has expired


      Put mildly, I think it is very annoying to click on a log-out and be answered
      with an error message that tells me that I have to log-in first.
      Trying to log-in and being shown the log-in page again because the previous view
      state has already expired is an equally bad user experience.


      I have been struggling with these issues and finally found solutions.
      Here they are. They might be helpful for somebody else or there might be a
      better/easier way to achieve the same.


      Note that there is a facelets parameter, that should  automatically provide
      the desired behaviour, but on first attempt it didn't work for me and the bug
      mentioned near the end of this thread sounds daunting:
      Nabble: PARAM_BUILD_BEFORE_RESTORE



      Log-out with expired view



      This is easy to handle using Seam:
      Just make the log-out link a plain (GET) link (instead of a command link):


      <h:outputLink value="#{request.contextPath}/log-out" rendered="#{identity.loggedIn}">
         Log-Out
      </h:outputLink>
      


      and handle the actual log-out in a page action (pages.xml):


      <page view-id="/log-out.xhtml">
         <rewrite pattern="/log-out"/>
         <action execute="#{authenticator.logout}"/>
      </page>
      


      where authenticator.logout is simply


         public String logout() {
            Session.instance().invalidate();
            return null;
         }
      



      Instead of using an outputLink with a page action it would probably
      work just as well to use <s:link action="#{authenticator.logout}"...


      Log-in with expired view



      This one is a little tougher: You can't use a GET request, because naturally
      passwords should never be part of the URL. However a JSF postback always
      requires the view state to be restored.


      So I fell back to writing the log-in form in plain html. You can actually use
      JSF components for input fields and labels, but use a plain html form tag
      instead of h:form, so that the action can be customized and the POST request
      handled outside of JSF.


      This is in /log-in.xhtml:


      <form method="post" action="/acme/do-log-in" >
           <h:inputText id="nameInput"/>
           <h:inputText id="passwordInput"/>
           <h:inputHidden id="cid" value="#{conversation.id}"/>
           <h:commandButton id="loginButton" value="Login"/>
      </form>



      Labels, messages, and layout excluded for brevity.
      Be sure to pass the conversation id in a hidden field (the name of that parameter is configured in components.xml: <core:manager ... conversation-id-parameter="cid"/>)


      To handle the POST request my first attempt was a custom servlet (integrated
      in Seam by configuring the ContextFilter), but that did not work because the
      events and redirects that occur after a login attempt require an active
      FacesContext.


      But fortunately a Seam page action can handle POST requests, too:


         <page view-id="/do-log-in.xhtml" action="#{authenticator.doLogIn}">
            <rewrite pattern="/do-log-in"/>
            <navigation from-action="#{authenticator.doLogIn}">
               <rule if="#{not identity.loggedIn}">
                  <redirect view-id="/log-in.xhtml"/>
               </rule>
      
               <rule if="#{identity.loggedIn and empty redirect.viewId}">
                  <!-- Either the user hit the log-in page directly, or the session has expired while the log-in page sat idle -->
                  <redirect view-id="/contact-data.xhtml"/>
               </rule>
            </navigation>
          </page>
      


      and the Authenticator.doLogIn() just grabs the request values, validates them
      (we are bypassing JSF, so no help from JSF validators), stuffs them into the
      credentials and calls identity.login():


          @In private Credentials credentials;
          @In private FacesMessages facesMessages;
          @In private Identity identity;
      
          public void doLogIn() {
              boolean valid = true;
              String userName = FacesUtil.getRequestParameter("nameInput");
              if (Stringz.isEmpty(userName)) {
                  valid = false;
                  facesMessages.addToControl("nameInput", StatusMessage.Severity.ERROR, "Please enter a value");
              }
      
              String password = FacesUtil.getRequestParameter("passwordInput");
              if (Stringz.isEmpty(password)) {
                  valid = false;
                  facesMessages.addToControl("passwordInput", StatusMessage.Severity.ERROR, "Please enter a value");
              }
      
              if (!valid) {
                  return;
              }
              credentials.setUsername(userName);
              credentials.setPassword(password);
              String result = identity.login();
          }
      



      When testing the validation, you'll see that the faces messages are not added to
      the controls, but will end up as global messages. That is a known bug in Seam:
      JBSEAM-1855


      Tada!


      I just don't really want to think about the fact that these are problems that
      never would have happened if I used plain JSP pages. JSF comes at a cost.


      (Then again it was not so much harder than getting all the formatting instructions for this forum message right.)