Differentiating between an initial login, a session expired, and a logout when displaying messages to the user on the login screen seems like a simple task. The implementation is unfortunately not quite as simple as it sounds and requires a little legwork. The following tutorial will guide you through the requirements.

The first component is a PhaseListener that allows you to determine whether the user’s session expired or whether this is the first time the user accessed the application. You have to make some assumptions here, but you can basically notify the user when the server session has ended by adding the following to a PhaseListener:

...

@Observer("org.jboss.seam.beforePhase")
public void beforePhase(PhaseEvent event)
{
  if(event.getPhaseId() == PhaseId.RESTORE_VIEW)
  {
    HttpServletRequest request =
      (HttpServletRequest) FacesContext.getCurrentInstance()
        .getExternalContext().getRequest();

    if(request.getRequestedSessionId() != null
           && request.getSession().isNew())
       Events.instance().raiseEvent("security.sessionExpired");

       ...

Based on general cookie settings this will raise the event when the user still has the browser window open, the http session expired, and the user tries to access the application. If the user closes and reopens the browser to start the application, the event will not be raised. This of course makes the assumption that cookies expire when the browser session is ended (which is generally the case).

This works well, but unfortunately the FacesMessages component is not available until after the RENDER_RESPONSE phase has executed. This is because the FacesMessages component is conversation scoped and the conversation is not started until after the RENDER_RESPONSE phase. Thus, you have to have a component available to store your message until the FacesMessages component becomes available. This can be accomplished by using an appropriately scoped component. For example:

@Name("customAuthenticator")
@AutoCreate
@Scope(SESSION)
public class CustomAuthenticator implements Authenticator {
    @Logger
    private Log log;
    ...
    private List<string> messages;
    ...
    @Observer("security.sessionExpired")
    public void sessionExpired(String message)
    {
        log.info("Adding session expired message...");

        this.messages.add("User session expired.");
    }

    public void flushMessages()
    {
        for(String message : this.messages)
            FacesMessages.instance().add(message);

        this.messages.clear();
    }
}

The flushMessages method should be invoked either by an action on your pages.xml or by flushing after the INVOKE_APPLICATION phase (this is up to your preference). Simply ensure that the flushing occurs only once the conversation has been initialized so that FacesMessages is available in the context.

This takes care of differentiating between a session timeout and a new session. So what about the logout? If you flush the security messages on every request, the user will end up seeing a message saying that the “User session ended” on logout.

To resolve this we can place a page in between and perform a meta redirect. First define a Seam page that does not require a login. Then in this page, add the following meta tags:

<meta http-equiv="Refresh" content="3; URL=/myApp/myPage.seam" />

This page can display a simple message to the user along the lines of: “You have successfully logged out.” The user will then be redirected to myPage after 3 seconds. As long as the messages are flushed on each request, the “User session ended” message will be long gone once the meta redirect executes.

Next add an entry in your pages.xml to redirect to this page when a logout occurs:

<page view-id="*">
  <navigation from-action="#{identity.logout}">
    <redirect view-id="/logoutConfirmation.xhtml" />
  </navigation>
</page>

A feature request has been placed to include the security.sessionExpired event as well as a security.newSession event. If you are interested in Seam providing this behavior out-of-the-box, please vote!

Update: Are you looking for an advanced polling solution for session expiration? Christian Bauer blogged about an approach to polling for session expiration here.