1

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.

Kukeltje
  • 12,223
  • 4
  • 24
  • 47
Pilpin
  • 150
  • 9
  • I have a deja-vu... – Kukeltje Jun 22 '20 at 21:38
  • @Kukeltje so am I: https://stackoverflow.com/questions/62354490/custom-form-authentication-in-jakarta-ee-8-cant-leave-login-page-after-authent . Anyway, I found a workaround. – Marcos Jun 22 '20 at 22:44
  • I guess I didn't do a proper job looking for similar questions, I'm sorry. @Kukeltje I saw this post https://stackoverflow.com/questions/62231511/how-can-i-redirect-to-the-original-request-url-with-java-ee-security-customform but adding ajax=false to the commandButton does not change anything. @Marcos, I'm supposed to implement an OTP as well so I can't use `@FormAuthenticationMechanismDefinition` – Pilpin Jun 23 '20 at 07:34
  • SylvainAutran: I could not find the recent similar post either... Weird... So thanks to @Marcos for linking it.. We'll see if we can find a cause for this and a solution as well. Can you make a [mcve] without template, no jstl, no lombok, with plain jsf components, no omnifaces etc...? – Kukeltje Jun 23 '20 at 08:43
  • Yes I'll try and do this. – Pilpin Jun 23 '20 at 16:29
  • At least 3 threads just here in _stackoverflow_ showing similar problems with this `@CustomFormAuthenticationMechanismDefinition`. This thing really doesn't work, no matter how well you follow the books or docs available. I think that someone in these _Jakarta EE_ implementations should really have a look at it. – Marcos Jun 24 '20 at 03:06

1 Answers1

2

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.

Pilpin
  • 150
  • 9
  • What application server are you using? – Marcos Jun 24 '20 at 17:28
  • Payara 5, versions 201 and 2020.2 – Pilpin Jun 24 '20 at 17:30
  • I cloned and executed your example in _Wildfly 20_. When the method `Login#continueAuthentication` is executed, I'm redirected to the _500.xhtml_ page with a `NullPointerException` error in the server JSF implementation: `UT005023: Exception handling request to /jakartasecurity/login.xhtml: javax.servlet.ServletException: java.lang.NullPointerException` `at javax.faces.api@3.0.0.SP03//javax.faces.webapp.FacesServlet.executeLifecyle(FacesServlet.java:725)` So it seems the bug is in Wildfly, not in my application or your example. – Marcos Jun 24 '20 at 18:14
  • @Marcos: comments on your problem should be in your question! – Kukeltje Jun 24 '20 at 18:46