jakarta-eewildflyundertowjakarta-ee-security-api

Jakarta EE 8 Security > Wildfly 26 Elytron - Role not being set


I am trying to setup a simple JSF login using Jakarta EE 8 Security, I have implemented the login page as a custom form as follows:

@ApplicationScoped
@CustomFormAuthenticationMechanismDefinition(
    loginToContinue = @LoginToContinue(
        loginPage = "/user-login.xhtml",
        useForwardToLogin = false,
        errorPage = ""
    )
)
@FacesConfig(version = FacesConfig.Version.JSF_2_3)
public class UserLoginConfig{
}

The user area is secured with following servlet and defines a single role 'user':

@WebServlet("/user/*")
@DeclareRoles({"user"})
@ServletSecurity(@HttpConstraint(rolesAllowed = "user"))
public class UserLoginServlet extends HttpServlet {

    /**
     * 
     */
    private static final long serialVersionUID = 1L;

}

The simple JSF form submits to the LoginBacking bean

@Named
@ViewScoped
public class LoginBean implements Serializable {

    /**
     * 
     */
    private static final long serialVersionUID = 1L;

    private String password;
 
    private String email;
 
    @Inject private SecurityContext securityContext;
    @Inject private Logger log;
    
    public void login() throws IOException {

        ExternalContext externalContext = Faces.getExternalContext();
        AuthenticationStatus status = securityContext.authenticate(
            (HttpServletRequest) externalContext.getRequest(),
            (HttpServletResponse) externalContext.getResponse(),
            AuthenticationParameters.withParams()
              .credential(new UsernamePasswordCredential(email, password))
        );
        switch (status) {
            case SEND_CONTINUE:
                Faces.getContext().responseComplete();
                break;
            case SEND_FAILURE:
                Faces.getContext().addMessage(null,
                        new FacesMessage(FacesMessage.SEVERITY_ERROR, "Login failed", null));
                break;
            case SUCCESS:
                Faces.getContext().addMessage(null,
                        new FacesMessage(FacesMessage.SEVERITY_INFO, "Login succeed", null));
                externalContext.redirect(externalContext.getRequestContextPath() + "/user/home.xhtml");
                break;
            case NOT_DONE:
        }
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }
    
    
}

And the backing bean triggers the authenticate method that is implemented within a custom ItentityStore

@ApplicationScoped
public class MyIdentityStore implements IdentityStore {
    
    @Inject private UserDAOQueries userDAOQueries;
    @Inject private PasswordEncryptorEntities passwordEncryptor;

    @Override
    public int priority() {
        return 90;
    }

    @Override
    public Set<ValidationType> validationTypes() {
        return EnumSet.of(ValidationType.VALIDATE);
    }
    
    @Override
    public CredentialValidationResult validate(Credential credential) {
 
        UsernamePasswordCredential login = (UsernamePasswordCredential) credential;
 
        User user = userDAOQueries.findNonDeletedByEmail(login.getCaller());
        if (user!=null) {
            if(passwordEncryptor.isPasswordCorrect(login.getPasswordAsString(), user.getPassword())){
                Set<String> roles = new HashSet<String>();
                roles.add("user");
                
                return new CredentialValidationResult(login.getCaller(), roles);
            }else {
                return CredentialValidationResult.INVALID_RESULT;
            }
            
        } else {
            return CredentialValidationResult.NOT_VALIDATED_RESULT;
        }
    }

    @Override
    public Set<String> getCallerGroups(CredentialValidationResult validationResult) {
        Set<String> roles = new HashSet<String>();
        if(validationResult.getStatus().equals(Status.VALID)) {
            roles.add("user");
        }
        return roles;
    }
}

This all works ok and the user is forwarded to the /user/home.xhtml page successfully, however I am then getting a 403 Forbidden response from the server. Looking at the org.wildfly.security TRACE I can see that the 'user' role is not being passed to the container following authentication (or they aren't being mapped correctly).

11:47:16,055 TRACE [org.wildfly.security] (default task-4) Handling CallerPrincipalCallback
11:47:16,055 TRACE [org.wildfly.security] (default task-4) Original Principal = 'javax.security.enterprise.CallerPrincipal@317242d5', Caller Name = 'null', Resulting Principal = 'javax.security.enterprise.CallerPrincipal@317242d5'
11:47:16,056 TRACE [org.wildfly.security] (default task-4) Role mapping: principal [javax.security.enterprise.CallerPrincipal@317242d5] -> decoded roles [] -> domain decoded roles [] -> realm mapped roles [] -> domain mapped roles []
11:47:16,057 INFO  [io.undertow.accesslog] (default task-4) [14/Apr/2022:11:47:16 +0100] "POST /user-login.xhtml HTTP/1.1" 302 - - https HTTP/1.1
11:47:16,057 TRACE [org.wildfly.security.http.servlet] (default task-4) ServerAuthContext.validateRequest returned AuthStatus=AuthStatus.SEND_CONTINUE
11:47:16,065 TRACE [org.wildfly.security.http.servlet] (default task-4) Created ServletSecurityContextImpl enableJapi=true, integratedJaspi=false, applicationContext=my-webapp 
11:47:16,065 TRACE [org.wildfly.security] (default task-4) Handling CallerPrincipalCallback
11:47:16,065 TRACE [org.wildfly.security] (default task-4) Original Principal = 'javax.security.enterprise.CallerPrincipal@317242d5', Caller Name = 'null', Resulting Principal = 'javax.security.enterprise.CallerPrincipal@317242d5'
11:47:16,066 TRACE [org.wildfly.security] (default task-4) Role mapping: principal [javax.security.enterprise.CallerPrincipal@317242d5] -> decoded roles [] -> domain decoded roles [] -> realm mapped roles [] -> domain mapped roles []
11:47:16,066 TRACE [org.wildfly.security.http.servlet] (default task-4) ServerAuthContext.validateRequest returned AuthStatus=AuthStatus.SUCCESS
11:47:16,066 TRACE [org.wildfly.security] (default task-4) No roles request of CallbackHandler.
11:47:16,066 TRACE [org.wildfly.security.http.servlet] (default task-4) Storing SecurityIdentity in HttpSession
11:47:16,066 TRACE [org.wildfly.security] (default task-4) Role mapping: principal [javax.security.enterprise.CallerPrincipal@317242d5] -> decoded roles [] -> domain decoded roles [] -> realm mapped roles [] -> domain mapped roles []
11:47:16,068 TRACE [org.wildfly.security] (default task-4) Role mapping: principal [javax.security.enterprise.CallerPrincipal@317242d5] -> decoded roles [] -> domain decoded roles [] -> realm mapped roles [] -> domain mapped roles []
11:47:16,068 TRACE [org.wildfly.security] (default task-4) Permission mapping: identity [javax.security.enterprise.CallerPrincipal@317242d5] with roles [] implies ("javax.security.jacc.WebResourcePermission" "/user/home.xhtml" "GET") = false
11:47:16,069 INFO  [io.undertow.accesslog] (default task-4) [14/Apr/2022:11:47:16 +0100] "GET /user/home.xhtml HTTP/1.1" 403 68 - https HTTP/1.1

I suspect I am missing some config in the elytron subsystem, I have a custom security domain <security-domain>other</security-domain> defined in jboss-web.xml

In undertow config I have an application-security-domain defined as such:

<application-security-domain name="other" security-domain="ApplicationDomain" enable-jaspi="true" integrated-jaspi="false"/>

In Elytron I have the ApplicationDomain configured as follows:

<security-domain name="ApplicationDomain" default-realm="ApplicationRealm" permission-mapper="default-permission-mapper">
                    <realm name="ApplicationRealm" role-decoder="groups-to-roles"/>
                    <realm name="local"/>
                </security-domain>

With the default-permission-mapper as follows:

<simple-permission-mapper name="default-permission-mapper">
                    <permission-mapping>
                        <role name="user"/>
                        <permission class-name="org.wildfly.security.auth.permission.LoginPermission"/>
                    </permission-mapping>
                </simple-permission-mapper>

I have also tried using the from-roles-attribute role decoder but that doesn't seem to work

Essentially I am just trying to tell Wildfly that the user has logged in and has 'x' roles assigned...


Solution

  • Ok in case anyone is looking for the answer to this, if you are using @CustomFormAuthenticationMechanismDefinition with a Custom IdentityStore you need to implement two IdentityStore's one that overrides the validate method (like the one I have shown above) and another that assigns the roles/groups by overriding the getCallerGroups method and setting ValidationType.PROVIDE_GROUPS....then the roles are assigned to the principal and all is good

    UPDATE: 2 identity stores not required, just don't override the ValidationType at all and it can do both