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...
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