jsfjakarta-eeomnifacesjava-ee-8

Login not forwarding correctly with CustomFormAuth and o:form


I'm trying to implement Jakarta Security on my web project, to do so I'm following the amazing "The Definitive Guide to JSF in Java EE 8" by Bauke Scholtz and Arjan Tijms but it seems that I'm hitting a wall.

When I type in a restricted url in the address bar the forward happens properly and I see the login page but after submitting the form the page is not redirected and the login page seems to be reloaded (the fields are emptied, but the url in the address bar is still the one from the restricted page).

With the help of the logs I know that the AuthenticationStatus returns as SUCCESS but even then if I manually change the address bar to another restricted url it gets forwarded to the login page.

If I type /login in the address bar and submit the authentication and redirect happens properly.

It seems that if I make useForwardToLogin = false it works properly but then getForwardURL always returns null and I can not redirect the user to the page they wanted to get to.

Thanks for your help.

ApplicationConfig.java

@CustomFormAuthenticationMechanismDefinition(
        loginToContinue = @LoginToContinue(
                loginPage = "/login",
                errorPage = ""
        )
)
@FacesConfig
@ApplicationScoped
public class ApplicationConfig {

}

login.xhtml

<ui:composition xmlns="http://www.w3.org/1999/xhtml"
                xmlns:c="http://java.sun.com/jsp/jstl/core"
                xmlns:ui="http://java.sun.com/jsf/facelets"
                xmlns:fn="http://xmlns.jcp.org/jsp/jstl/functions"
                xmlns:p="http://primefaces.org/ui"
                xmlns:o="http://omnifaces.org/ui"
                template="/WEB-INF/templates/login.xhtml">
    <ui:define name="content">
        <c:choose>
            <c:when test="#{not empty request.userPrincipal}">
                <div>Already logged</div>
            </c:when>
            <c:otherwise>
                <o:form>
                    <p:inputText id="login" value="#{loginBacking.userName}" required="true"/>
                    <p:password id="pwd" value="#{loginBacking.password}" required="true"/>
                    <p:inputNumber id="otp" value="#{loginBacking.otp}" />
                    
                    <o:messages id="messages" var="message">
                        <div class="#{fn:toLowerCase(message.severity)}">#{message.summary}</div>
                    </o:messages>

                    <p:commandButton value="Submit" action="#{loginBacking.login}" update="@form"/>
                </o:form>
            </c:otherwise>
        </c:choose>
    </ui:define>
</ui:composition>

LoginBacking.java

@Named
@RequestScoped
public class LoginBacking {
    @Inject private SecurityContext securityContext;

    @Getter @Setter private String userName;
    @Getter @Setter private String password;
    @Getter @Setter private Integer otp;

    public void login() {
        switch (continueAuthentication()) {
            case SEND_CONTINUE:
                Faces.responseComplete();
                break;
            case SEND_FAILURE:
                Messages.addGlobalFatal("Login failed.");
                break;
            case SUCCESS:
                redirect();
                break;
        }
    }

    private AuthenticationStatus continueAuthentication() {
        return this.securityContext.authenticate(
                Faces.getRequest(),
                Faces.getResponse(),
                AuthenticationParameters.withParams()
                        .newAuthentication(getForwardURL() == null)
                        .credential(new CustomCredential(userName, password, otp))
        );
    }

    private void redirect() {
        CustomUser user = securityContext.getPrincipalsByType(CustomPrincipal.class).stream()
                .map(CustomPrincipal::getCustomUser)
                .findAny()
                .orElse(null);

        if (user == null) Messages.addGlobalFatal("Something went wrong.");

        String forwardURL = getForwardURL();
        if (forwardURL != null) {
            Faces.redirect(forwardURL);
            return;
        }
        
        Faces.redirect(Faces.getRequestContextPath() + "/restricted/index");
    }

    public void logout() throws ServletException {
        Faces.logout();
        Faces.invalidateSession();
        Faces.redirect("login");
    }

    public String getForwardURL() {
        String requestURI = Faces.getRequestAttribute(RequestDispatcher.FORWARD_REQUEST_URI);
        String queryString = Faces.getRequestAttribute(RequestDispatcher.FORWARD_QUERY_STRING);

        if (requestURI == null) return null;
        return (queryString == null) ? requestURI : (requestURI + "?" + queryString);
    }
}

FIY, as you can see, I'm using Omnifaces, Primefaces and Lombok.


Solution

  • Following Kukeltje advice I made a minimal working example that you can find here https://github.com/Pilpin/mwe-jakartasecurity. It is a modified fork of the security part of the repo for the book "The Definitive Guide to JSF in Java EE 8" by Bauke Scholtz and Arjan Tijms => https://github.com/Apress/definitive-guide-to-jsf-javaee8.

    It's working perfectly fine so I decided to add omnifaces and primefaces to it and realized :

    1. As said here How can I redirect to the original request url with Java EE Security @CustomFormAuthenticationMechanismDefinition if you're using primefaces' p:commandButton you should add the ajax=false property to it.
    2. If you're using omnifaces' o:form you should add the useRequestURI="false" property to it. It makes sense because my fork uses forward to get to the login page so the URL and URI are the ones of whatever page you wanted to get to and when useRequestURI is set to true (default) on o:form it submits the form to the exact request URI so not to the login page.

    Thanks a lot to Kukeltje for nudging me in the right direction.