Search code examples
authenticationseam3

How does Seam 3 handles the "redirect to capture view" feature after login?


Here is my use cases.

I have a login page which is /public/login.xhtml. All my other pages are required to log-in before reaching them. They are in /pages/ directory.

I want that :

  1. If my user access to http://host/myapp/pages/* it redirects him first to the login page, and then, to the URL he has firstly entered.
  2. If my user access to http://host/myapp/, it redirects him first to the login page, and then, to /pages/home.xhtml.
  3. If my user access to http://host/myapp/public/login.xhtml, it redirects him first to the login page, and then, to /pages/home.xhtml.
  4. If my user access to http://host/myapp/public/login.xhtml and is already logged in, it redirects to /pages/home.xhtml.

What is working currently?

With Seam 3 (v3.1.0.Final) and the Security + Faces module, my use case n°1 is automagically working with :

@ViewConfig
public interface PagesConfig {
    static enum Pages {
        @ViewPattern("/pages/*")
        @LoginView("/public/login.xhtml")
        @LoggedIn
        LOGGED_IN_PAGES,
    }
}

My problem is that I don't understand how Seam's working to do that redirection to the "capture view".

With Seam 2, it was easy to understand, in components.xml we had

<event type="org.jboss.seam.security.notLoggedIn">
    <action execute="#{redirect.captureCurrentView}" />
</event>
<event type="org.jboss.seam.security.loginSuccessful">
    <action execute="#{redirect.returnToCapturedView}" />
</event>

So we captured the events notLoggedIn and loginSuccessful to handle that with a redirect component.

In Seam 3, I didn't found that configuration : nothing seems to @Observes LoggedInEvent, and there is no Redirect class...

The point n°2 is achieved with that /index.htm file :

<html><head>
    <meta http-equiv="Refresh" content="0; URL=pages/home.xhtml">
</head></html>

But for my point n°3, I've tried solutions which don't fully work.

First I tried that in login.xhtml :

<f:metadata>
    <s:viewAction action="#{loginAction.redirectToHome}" if="#{identity.loggedIn}" immediate="true" />
</f:metadata>

And with or without onPostback="true", after I login, I'm still in the login page with that error message (twice) : "Unable to find matching navigation case with from-view-id «/public/login.xhtml» for action «#{identity.login}» with outcome «success».". It's only if I now re-access to http://host/myapp/public/login.xhtml that my viewAction redirects me to the home.

I also tried that navigation-rule in faces-config.xml :

<navigation-rule>
    <from-view-id>/public/login.xhtml</from-view-id>

    <navigation-case>
        <if>#{identity.loggedIn}</if>
        <to-view-id>/pages/home.xhtml</to-view-id>
        <redirect />
    </navigation-case>
</navigation-rule>

But then, my use case n°1 was disabled : every time I logged-in, I was redirected to the home.

Finally, for my point n°4, the s:viewAction does the job.


So does somebody knows the best practices in order to correctly handle those 4 use cases (which I think are common use cases), especially the point n°3?


Solution

  • Finally here is what I did.

    <f:metadata>
        <s:viewAction action="#{loginAction.redirectToHome}" immediate="true" />
    </f:metadata>
    

    So I removed the if="#{identity.loggedIn}" in order to call my redirectToHome method which redirects to the /pages/home.xhtml.

    • If the user is already authenticated, then he is redirected to the home page.
    • If he's not, then it is redirected to the home page, which redirects him to the login page thanks to my @ViewConfig

    Here is the loginAction :

    public void redirectToHome() throws IOException {
        externalContext.redirect(externalContext.encodeActionURL(externalContext.getRequestContextPath()+"/pages/home.xhtml"));
    }
    

    The problem I faced then was when I logged out.

    Here is my logout action :

    <h:commandLink  action="/public/login" actionListener="#{securityAction.logout()}" value="Disconnect" immediate="true" />
    

    And the securityAction.logout() method :

    public void logout() {
        identity.logout();
        if (!conversation.isTransient()) {
            conversation.end();
        }
    }
    

    The problem is that I was redirected to the login page (thanks to the @ViewConfig I think), but no PreLoginEvent were thrown, so the Seam LoginListener.observePreLoginEvent wasn't called, and so my previous URL wasn't put in session. So when I logged in (immediatly after logout), I was stuck on the login page, but was logged in.

    Thanks to Brian Leathem and he's previous answer, here is what I did : in my authenticate method of my BaseAuthenticator, I called that method after authentication :

    private void overrideRedirectToLogin() {
        final String PRE_LOGIN_URL = LoginListener.class.getName() + "_PRE_LOGIN_URL";
        final ExternalContext externalContext = FacesContext.getCurrentInstance().getExternalContext();
        final Map<String, Object> sessionMap = externalContext.getSessionMap();
        String redirectURL = (String) sessionMap.get(PRE_LOGIN_URL);
    
        if (redirectURL == null) {
            final HttpServletRequest request = (HttpServletRequest) externalContext.getRequest();
            redirectURL = request.getRequestURL().toString();
        }
    
        sessionMap.put(PRE_LOGIN_URL, redirectURL.replace("/public/login.xhtml", "/pages/home.xhtml"));
    }
    

    With that solution, my previous URL wasn't set in session, but at least, my user is redirected to the home page.