2
  • Jakarta EE 8
  • Wildfly 21
  • Java 11

Using Java EE Security, I'm trying custom form authentication in a simple application.

These are the relevant files (the description of the problem is below the files):

CustomFormAuthenticationConfig.java

package br.dev.authentication.view;

import javax.enterprise.context.ApplicationScoped;
import javax.faces.annotation.FacesConfig;
import javax.security.enterprise.authentication.mechanism.http.CustomFormAuthenticationMechanismDefinition;
import javax.security.enterprise.authentication.mechanism.http.LoginToContinue;

@CustomFormAuthenticationMechanismDefinition(
    loginToContinue = @LoginToContinue(
        loginPage = "/login.xhtml",
        useForwardToLogin = false,
        errorPage = ""
    )
)
@FacesConfig
@ApplicationScoped
public class CustomFormAuthenticationConfig
{
}

UserAuthenticator.java

package br.dev.authentication.view;

import java.util.Set;

import javax.enterprise.context.ApplicationScoped;
import javax.security.enterprise.credential.Credential;
import javax.security.enterprise.credential.UsernamePasswordCredential;
import javax.security.enterprise.identitystore.CredentialValidationResult;
import javax.security.enterprise.identitystore.IdentityStore;

@ApplicationScoped
public class UserAuthenticator implements IdentityStore
{
    @Override
    public CredentialValidationResult validate(Credential credencial)
    {
        var userCredentials = (UsernamePasswordCredential) credencial;
        var userName = userCredentials.getCaller();
        var password = userCredentials.getPasswordAsString();

        if (userName.equals("1") && password.equals("1"))
        {
            return new CredentialValidationResult(userName, Set.of("USER"));
        }
        else
        {
            return CredentialValidationResult.INVALID_RESULT;
        }
    }
}

login.xhtml

<ui:composition template="/templates/layout.xhtml"
    xmlns="http://www.w3.org/1999/xhtml"
    xmlns:ui="http://xmlns.jcp.org/jsf/facelets"
    xmlns:f="http://xmlns.jcp.org/jsf/core"
    xmlns:h="http://xmlns.jcp.org/jsf/html"
    xmlns:au="http://dev.br/authentication">

    <ui:define name="title">
        Login
    </ui:define>

    <ui:define name="content">
        <au:errors />
        <div id="fields">
            <h:outputLabel value="User name:" for="userName" />
            <h:inputText id="userName" value="#{login.userName}" />

            <h:outputLabel value="Password:" for="password" />
            <h:inputSecret id="password" value="#{login.password}" />

            <h:commandButton value="Enter" action="#{login.authenticateUser}" />
        </div>
    </ui:define>
</ui:composition>

Login.java

package br.dev.authentication.view;

import java.io.IOException;

import javax.enterprise.context.RequestScoped;
import javax.faces.application.FacesMessage;
import javax.faces.context.ExternalContext;
import javax.faces.context.FacesContext;
import javax.inject.Inject;
import javax.inject.Named;
import javax.security.enterprise.AuthenticationStatus;
import javax.security.enterprise.SecurityContext;
import javax.security.enterprise.authentication.mechanism.http.AuthenticationParameters;
import javax.security.enterprise.credential.UsernamePasswordCredential;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.validation.constraints.NotBlank;

@RequestScoped
@Named
public class Login
{
    private String _userName;
    private String _password;

    @Inject
    private FacesContext _facesContext;
    
    @Inject
    private ExternalContext _externalContext;

    @Inject
    private SecurityContext _securityContext;
    
    @NotBlank(message = "User name is required.")
    public String getUserName()
    {
        return _userName;
    }
    
    public void setUserName(String userName)
    {
        _userName = userName;
    }
    
    @NotBlank(message = "Password is required.")
    public String getPassword()
    {
        return _password;
    }
    
    public void setPassword(String password)
    {
        _password = password;
    }
    
    public void authenticateUser() throws IOException
    {
        // After a successful login (username and password correct),
        // executeUserAuthentication() returns AuthenticationStatus.SEND_CONTINUE, 
        // and not AuthenticationStatus.SUCCESS.
        // Why?
        // As a result, the code in the AuthenticationStatus.SUCCESS branch above is not executed.
        AuthenticationStatus result = executeUserAuthentication();
        if (result == AuthenticationStatus.SUCCESS)
        {
            _externalContext.redirect(_externalContext.getRequestContextPath() + "/start.xhtml");
        }
        else if (result == AuthenticationStatus.SEND_CONTINUE)
        {
            _facesContext.responseComplete();
        }
        else if (result == AuthenticationStatus.SEND_FAILURE)
        {
            _facesContext.addMessage(null, new FacesMessage(
                FacesMessage.SEVERITY_ERROR, "Invalid user name and/or password.", null));
        }
    }
    
    private AuthenticationStatus executeUserAuthentication()
    {
        return _securityContext.authenticate(
            (HttpServletRequest) _externalContext.getRequest(),
            (HttpServletResponse) _externalContext.getResponse(),
            AuthenticationParameters.withParams().credential(
                new UsernamePasswordCredential(_userName, _password))
        );
    }   
}

The problem is that, after a successful login (user name and password correct), as you saw in the comments in Login class above, the method executeUserAuthentication() returns AuthenticationStatus.SEND_CONTINUE instead of AuthenticationStatus.SUCCESS. Below is an image of the application running in debug mode in the moment of execution to show this:

enter image description here

As a result, the browser address bar is not updated with the real url (start.xhtml) because the AuthenticationStatus.SUCCESS branch in the code above was not executed:

enter image description here

I thought I was doing something wrong, but then I decided to clone this simple application from @ArjanTijms that uses the same authentication logic:

https://github.com/rieckpil/blog-tutorials/tree/master/jsf-simple-login-with-java-ee-security-api

His application is explained in this post:

https://rieckpil.de/howto-simple-form-based-authentication-for-jsf-2-3-with-java-ee-8-security-api/

And the results were the same of my application: the AuthenticationStatus.SEND_CONTINUE branch was executed instead of the AuthenticationStatus.SUCCESS branch:

enter image description here

No updated address bar too:

enter image description here

So, what's going on here? Is this a problem in the applications, Wildfly, is this correct behaviour (if so, what's the use of the AuthenticationStatus.SUCCESS enum, when will it be used?)? I just want to be able to execute the redirect by myself.

UPDATE 1

For your information, after the executeUserAuthentication() the user is already authenticated: ((HttpServletRequest) FacesContext.getCurrentInstance().getExternalContext().getRequest()).getUserPrincipal() is not null.

Some other files in the application:

web.xml

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://xmlns.jcp.org/xml/ns/javaee" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd" version="4.0">
    <display-name>autenticacao-visao</display-name>

    <!-- ========== JSF ========== -->

    <servlet>
        <servlet-name>Faces Servlet</servlet-name>
        <servlet-class>javax.faces.webapp.FacesServlet</servlet-class>
        <load-on-startup>1</load-on-startup>
    </servlet>
    <servlet-mapping>
        <servlet-name>Faces Servlet</servlet-name>
        <url-pattern>*.xhtml</url-pattern>
    </servlet-mapping>
    
    <context-param>
        <param-name>javax.faces.INTERPRET_EMPTY_STRING_SUBMITTED_VALUES_AS_NULL</param-name>
        <param-value>true</param-value>
    </context-param>
    
    <!-- ========== Security ========== -->
    
    <security-constraint>
        <web-resource-collection>
            <web-resource-name>restrict</web-resource-name>
            <url-pattern>/app/*</url-pattern>
        </web-resource-collection>
        <auth-constraint>
            <role-name>USER</role-name>
        </auth-constraint>
    </security-constraint>
    <security-constraint>
        <web-resource-collection>
            <web-resource-name>allowed</web-resource-name>
            <url-pattern>/app/resources/*</url-pattern>
        </web-resource-collection>
    </security-constraint>

    <security-role>
        <role-name>USER</role-name>
    </security-role>

    <!-- ========== Resources ========== -->
    
    <context-param>
        <param-name>dev.br.RESOURCES</param-name>
        <param-value>resources</param-value>
    </context-param>

    <!-- ========== Start page ========== -->
  
    <welcome-file-list>
        <welcome-file>app/start.xhtml</welcome-file>
    </welcome-file-list>
</web-app>

jboss-app.xml

<?xml version="1.0" encoding="UTF-8"?>
<jboss-app>
    <security-domain>jaspitest</security-domain>
</jboss-app>

start.xhtml

<ui:composition template="/templates/layout.xhtml"
    xmlns="http://www.w3.org/1999/xhtml"
    xmlns:ui="http://xmlns.jcp.org/jsf/facelets"
    xmlns:f="http://xmlns.jcp.org/jsf/core"
    xmlns:h="http://xmlns.jcp.org/jsf/html">

    <ui:define name="title">
        Início
    </ui:define>
    
    <ui:define name="content">
        #{start.loggedInMessage}
        <br />
        <h:commandButton value="Execute" />
    </ui:define>
</ui:composition>

Start.java

package br.dev.authentication.view;

import java.security.Principal;

import javax.enterprise.context.RequestScoped;
import javax.faces.context.ExternalContext;
import javax.faces.context.FacesContext;
import javax.inject.Inject;
import javax.inject.Named;
import javax.servlet.http.HttpServletRequest;

@RequestScoped
@Named
public class Start
{
    @Inject
    private ExternalContext _externalContext;
    
    public String getLoggedInMessage()
    {
        Principal user = ((HttpServletRequest) FacesContext.getCurrentInstance().getExternalContext().getRequest()).getUserPrincipal();
        if (user != null)
        {
            return "Logged in: " + user.getName();
        }
        else
        {
            return "Not logged in";
        }
    }
}

UPDATE 2

After lots of tests, I noticed (at least in my application) that sometimes SecurityContext.authenticate() returns AuthenticationStatus.SEND_CONTINUE and sometimes returns AuthenticationStatus.SUCCESS. Until now, I haven't figured out the reason why.

So, for the time being, I decided to solve the problem with a hack for the cases when AuthenticationStatus.SEND_CONTINUE would be returned. So, basically, what I'm doing is redirecting to the start page manually. The hack is below:

web.xml (changed the welcome page to redirectortostart.xhtml)

<welcome-file-list>
    <welcome-file>app/redirectortostart.xhtml</welcome-file>
</welcome-file-list>

redirectortostart.xhtml

<ui:composition template="/templates/layout.xhtml"
    xmlns="http://www.w3.org/1999/xhtml"
    xmlns:ui="http://xmlns.jcp.org/jsf/facelets"
    xmlns:f="http://xmlns.jcp.org/jsf/core">

    <ui:define name="metadados">
        <f:event type="preRenderView" listener="#{redirectorToStart.goToStart}" />
    </ui:define>
</ui:composition>

RedirectorToStart.java

package br.dev.authentication.view;

import java.io.IOException;

import javax.enterprise.context.RequestScoped;
import javax.faces.context.ExternalContext;
import javax.inject.Inject;
import javax.inject.Named;

@RequestScoped
@Named
public class RedirectorToStart
{
    @Inject
    private ExternalContext _externalContext;

    public void goToStart() throws IOException
    {
        _externalContext.redirect(_externalContext.getRequestContextPath() + "/app/start.xhtml");
    }
}

It's not elegant, but for the time being it solves my problem. Hope someday some of you have a clue about what really could be happening with my application and could give a hint to me.

Marcos
  • 1,237
  • 1
  • 15
  • 31
  • did you ever find a solution for this? We are seeing the same behaviour in Wildfly 26 – DaveB May 17 '22 at 15:45
  • I haven't tested if this problem was solved in newer Wildfly versions. As reported by you, it seems that it is still broken. – Marcos May 18 '22 at 17:54

1 Answers1

0

The AuthenticationStatus is used as a return value by primarily the HttpAuthenticationMechanism to indicate the result (status) of the authentication process.

SEND_CONTINUE : The authentication mechanism was called and a multi-step authentication dialog with the caller has been started (for instance, the caller has been redirected to a login page). Simply said authentication is "in progress". Calling application code (if any) should not write to the response when this status is received. Javadocs

How does your web.xml looks like?

have you added the security constraints ?

Example :

  <security-constraint>
        <web-resource-collection>
            <web-resource-name>Application pages</web-resource-name>
            <url-pattern>/app/*</url-pattern>
        </web-resource-collection>
        <auth-constraint>
            <role-name>ADMIN</role-name>
            <role-name>USER</role-name>
        </auth-constraint>
    </security-constraint>

    <security-role>
        <role-name>USER</role-name>
    </security-role>
    <security-role>
        <role-name>ADMIN</role-name>
    </security-role>

have you the jboss-web.xml? in the src/main/webapp/WEB-INF

jaspitest is the security domain to use HTTP Authentication Mechanism Interface in Wildfly/Jboss

<?xml version="1.0" encoding="UTF-8"?>
<jboss-web version="8.0" xmlns="http://www.jboss.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.jboss.com/xml/ns/javaee http://www.jboss.org/schema/jbossas/jboss-web_8_0.xsd">
  <context-root/>
  <security-domain>jaspitest</security-domain>
</jboss-web>

I just tested the application that you linked and work as Expected

enter image description here

you can configure the session timeout (in minutes) on the web.xml

<session-config>
    <session-timeout>
        1
    </session-timeout>
    <cookie-config>
        <http-only>true</http-only>
        <secure>false</secure>
    </cookie-config>
</session-config>
Luis Galaz
  • 390
  • 4
  • 9
  • I updated the my question to answer your questions. Look there. Because my application is an EAR application, I use a _jboss-app.xml_ instead. – Marcos Nov 09 '20 at 14:24
  • When the line `_facesContext.responseComplete();` in the method `authenticateUser()` is reached in my application, the user is already authenticated. There is no `AuthenticationStatus.SUCCESS` after that anymore for me. – Marcos Nov 09 '20 at 14:33
  • Nice, its seem that you have answered your question. – Luis Galaz Nov 09 '20 at 15:28
  • No. I answered your questions. I still don't know why `AuthenticationStatus.SUCCESS` is not returned. After all, when will `AuthenticationStatus.SUCCESS` ever be returned? – Marcos Nov 09 '20 at 15:44
  • The web container is doing a forward to the login page under the covers, not a redirection. If `AuthenticationStatus.SUCCESS` is not returned, there's no way for me to do the redirection to the login page that I want. If `AuthenticationStatus.SEND_CONTINUE` is returned, the only thing I can do is call `_facesContext.responseComplete()`. – Marcos Nov 09 '20 at 15:52
  • I usually work only with WAR files. The code seem right. Have you test put even the web container configuration (jboss-web.xml) in your src/main/wabapp/WEB-INF in the war file in your EAR file ? – Luis Galaz Nov 09 '20 at 16:06
  • As I said in my question, I also tested @ArjanTijms application (it's just a web application -> WAR file) available at https://github.com/rieckpil/blog-tutorials/tree/master/jsf-simple-login-with-java-ee-security-api . It exhibits the same problem of my application, returning `AuthenticationStatus.SEND_CONTINUE` instead of `AuthenticationStatus.SUCCESS`. – Marcos Nov 09 '20 at 16:24
  • I just tested that application and work as expected (see the added picture). Are you using the standalone-full.xml? – Luis Galaz Nov 09 '20 at 18:13
  • No. _standalone.xml_. Did you run it in Wildfly 21? – Marcos Nov 09 '20 at 18:16
  • Yes, you can see that in the screenshot. Try the full configuration. – Luis Galaz Nov 09 '20 at 18:18
  • I've just tried the full configuration and got the same result, but I also noticed (before trying the ful configuration) that when I logout from the application (it has a logout button) and login again, the `AuthenticationStatus.SUCCESS` is returned. So it seems that it just returns `AuthenticationStatus.SEND_CONTINUE` when the application is first started. After logout, it returns the correct value everytime I login again. Very strange. – Marcos Nov 09 '20 at 18:40
  • Ok, that's it because the Session is still alive, you can configure the session time out in the web.xml or use private surfing when testing. 1 – Luis Galaz Nov 09 '20 at 18:47
  • In the logout proccess I also invalidate the session: `request.logout(); request.getSession().invalidate()`. – Marcos Nov 09 '20 at 18:50
  • Yes, it does work. And as I said, after logout, when I login again the correct `AuthenticationStatus.SUCCESS` is returned. – Marcos Nov 09 '20 at 18:56
  • I'll do other checks here and report any findings later. Thank you for your help. – Marcos Nov 09 '20 at 19:01