Securing your web application can be cumbersome. I recently tried to secure a JSF 2.3 application with the latest Java EE 8 Security API (JSR-375) and it was quite simple. In this blog post, I'll show you the required configuration steps for securing your JSF application with a form-based authentication mechanism. In the example, I'll use an in-memory user store of two users, add PrimeFaces for enhancing the UI and deploy it to Payara 5.182.
JSF project setup
The pom.xml
is straightforward for this application:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 | <?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>de.rieckpil.blog</groupId> <artifactId>jsf-simple-login-with-java-ee-security-api</artifactId> <version>1.0-SNAPSHOT</version> <packaging>war</packaging> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <maven.compiler.source>1.8</maven.compiler.source> <maven.compiler.target>1.8</maven.compiler.target> <failOnMissingWebXml>false</failOnMissingWebXml> </properties> <repositories> <repository> <id>prime-repo</id> <name>Prime Repo</name> <url>http://repository.primefaces.org</url> </repository> </repositories> <dependencies> <dependency> <groupId>javax</groupId> <artifactId>javaee-api</artifactId> <version>8.0</version> </dependency> <dependency> <groupId>org.primefaces</groupId> <artifactId>primefaces</artifactId> <version>6.2</version> </dependency> <dependency> <groupId>org.primefaces.themes</groupId> <artifactId>all-themes</artifactId> <version>1.0.10</version> </dependency> </dependencies> <build> <finalName>jsf-simple-login</finalName> </build> </project> |
The new Security API is part of Java EE 8 and the definitions are mostly located in the javax.security
package. The default reference implementation is Soteria and is already part of the most recent application servers (Payara, Wildfly, OpenLiberty, TomEE …). With this new API, we now get a common security workflow definition and with the new interfaces and annotations, we can configure the authentication and authorization of our Java EE applications.
Using the Java EE Security API with an in-memory IndentityStore
One of the central interfaces is the IdentityStore
which is responsible for validating the incoming credentials. In this example, I'm using a simple in-memory IdentityStore which has only two valid users:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | @ApplicationScoped public class CustomInMemoryIdentityStore implements IdentityStore { @Override public CredentialValidationResult validate(Credential credential) { UsernamePasswordCredential login = (UsernamePasswordCredential) credential; && login.getPasswordAsString().equals("ADMIN1234")) { return new CredentialValidationResult("admin", new HashSet<>(Arrays.asList("ADMIN"))); && login.getPasswordAsString().equals("USER1234")) { return new CredentialValidationResult("user", new HashSet<>(Arrays.asList("USER"))); } else { return CredentialValidationResult.NOT_VALIDATED_RESULT; } } } |
For a more realistic example, you can use the new @DatabaseIdentityStoreDefinition
or @LdapIdentityStoreDefinition
annotation to configure a database or a LDAP system for user validation. Our IdentityStore will just check if the incoming mail and password match with one of the two dummy users and return a CredentialValidationResult which contains the principal's name and the associated roles.
The web application has two pages: the login.xhtml
file at the root level which is visible for everybody and a secured page at /app/index.html
. To secure a path in your JSF application, you have to update your web.xml
. In our example, I am securing the path to /app/*
to be visible only for authenticated users with the ADMIN or USER role:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 | <?xml version="1.0" encoding="UTF-8"?> <web-app version="4.0" xmlns="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"> <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> <context-param> <param-name>primefaces.THEME</param-name> <param-value>bootstrap</param-value> </context-param> <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> <welcome-file-list> <welcome-file>app/index.xhtml</welcome-file> </welcome-file-list> </web-app> |
To register a custom login page, you have to inform the authentication mechanism about the name and location of your login page. This can be done programmatically with a simple configuration class:
1 2 3 4 5 6 7 8 9 10 | @CustomFormAuthenticationMechanismDefinition( loginToContinue = @LoginToContinue( loginPage = "/login.xhtml", useForwardToLogin = false ) ) @FacesConfig @ApplicationScoped public class ApplicationConfig { } |
The property useForwardToLogin
is set to false to use a redirect instead of a forward.
Creating the JSF view for form authentication
The login.xhtml
file is a simple JSF page containing a form with two input fields:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 | <!DOCTYPE html> <html xmlns="http://www.w3.org/1999/xhtml" xmlns:h="http://xmlns.jcp.org/jsf/html" xmlns:f="http://xmlns.jcp.org/jsf/core" xmlns:p="http://primefaces.org/ui"> <f:view> <h:head> <h:outputStylesheet> h3 { text-align: center; } </h:outputStylesheet> </h:head> <h:body> <p:outputPanel> <div class="ui-g"> <div class="ui-g-4"/> <div class="ui-g-4"> <h3>Login</h3> <h:form id="login"> <p:messages id="messages" globalOnly="true"/> <h:panelGrid columns="3" cellpadding="5" style="margin: 0 auto;"> <p:outputLabel for="email" value="E-Mail"/> <p:inputText id="email" value="#{loginBacking.email}"/> <p:message for="email"/> <p:outputLabel for="password" value="Password"/> <p:inputText type="password" id="password" value="#{loginBacking.password}"/> <p:message for="password"/> <p:commandButton id="submit" update="@form" value="Login" action="#{loginBacking.submit}"/> </h:panelGrid> </h:form> </div> </div> </p:outputPanel> </h:body> </f:view> </html> |
The backing bean for the login page is responsible for validating the incoming credentials and redirects to the secured area if the credentials are valid. To use our custom IdentityStore the backing beans injects the SecurityContext and passes the email and password to our in-memory authentication. With the bean validation annotations we get a pre-validation (e.g. check the password against custom password rules) of the user input out of the box:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 | @Named @RequestScoped public class LoginBacking { @NotEmpty @Size(min = 8, message = "Password must have at least 8 characters") private String password; @NotEmpty @Email(message = "Please provide a valid e-mail") private String email; @Inject private SecurityContext securityContext; @Inject private ExternalContext externalContext; @Inject private FacesContext facesContext; public void submit() throws IOException { switch (continueAuthentication()) { case SEND_CONTINUE: facesContext.responseComplete(); break; case SEND_FAILURE: facesContext.addMessage(null, new FacesMessage(FacesMessage.SEVERITY_ERROR, "Login failed", null)); break; case SUCCESS: facesContext.addMessage(null, new FacesMessage(FacesMessage.SEVERITY_INFO, "Login succeed", null)); externalContext.redirect(externalContext.getRequestContextPath() + "/app/index.xhtml"); break; case NOT_DONE: } } private AuthenticationStatus continueAuthentication() { return securityContext.authenticate( (HttpServletRequest) externalContext.getRequest(), (HttpServletResponse) externalContext.getResponse(), AuthenticationParameters.withParams() .credential(new UsernamePasswordCredential(email, password)) ); } // getters & setters } |
Entering valid credentials will redirect the user to the secured index.html
which will display the user's name and a button to logout:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | <!DOCTYPE html> <html xmlns="http://www.w3.org/1999/xhtml" xmlns:h="http://xmlns.jcp.org/jsf/html" xmlns:f="http://xmlns.jcp.org/jsf/core" xmlns:p="http://primefaces.org/ui"> <f:view> <h:head></h:head> <h:body> <p:outputLabel>Welcome to the application!</p:outputLabel> <p>Logged-in as #{request.userPrincipal.name}</p> <h:form> <p:commandButton value="Logout" action="#{logoutBacking.submit}"/> </h:form> </h:body> </f:view> </html> |
The backing bean for the logout is invalidating the session of the user and will redirect to the login page:
1 2 3 4 5 6 7 8 9 | @Named @RequestScoped public class LogoutBacking { public String submit() { FacesContext.getCurrentInstance().getExternalContext().invalidateSession(); return "/login.xhtml?faces-redirect=true"; } } |
For using this on WildFly 14+ (Java EE 8 compliant), you have to add the following jboss-web.xml
in src/main/webapp/WEB-INF
and reference to the jaspitest security Domain which is defined in the legacy security subsystem:
1 2 3 | <jboss-web> <security-domain>jaspitest</security-domain> </jboss-web> |
To try this on your machine, have a look at my GitHub repository for the full codebase and a step-by-step tutorial to get this running on your machine in the README.md of the project. There is quite more to discover with JSF 2.3 and Java EE in general, so stay tuned for the next blog posts. To start with JSF 2.3 I can recommend The Definitive Guide to JSF in Java EE 8 from Bauke Scholtz (@OmniFaces) & Arjan Tijms (@arjan_tijms).
Have fun securing your applications,
Phil
Hello,
I am using Wildfly 14 which is Java EE 8 compliant.
This project is not working, appears “Forbidden” instead of the login page.
Any clues, please ?
Hey Cassius,
could you provide me a link to a Github repository, so that I can have a look at it?
Kind regards,
Phil
Good evening!
I’ve had this issue as well, and I believe it’s failing to inject FacesContext and ExternalContext. As soon as I switched to:
FacesContext facesContext = FacesContext.getCurrentInstance();
ExternalContext externalContext = facesContext.getExternalContext();
it’s working perfectly.
Come to think of it, I’ve not been able to inject the FacesContext like the book suggests, I always revert to the variable declaration. I’ve had this with Payara and Wildfly as well, don’t know what’s causing it
Hey Vasilis,
are you referring to the code for the logout or during the login?
Kind regards,
Philip
For the login part. For some reason the @Inject FacesContext doesn’t work and I have to do the getCurrentInstance() way.
Has happened with a couple of Payara versions and with a couple of Wildfly ones as well. Very strange.
In fact, I can’t run your code. After I click submit, nothing happens. I’ll upload my version and if you have time maybe take a look at it over the weekend. It’s essentially the same as yours.
I’ll have a look at it
I don’t have any issues with injection of FacesContext or ExternalContent, but for some reason it seems the authentication status that is returned sometimes is SEND_CONTINUE and other times is SUCCESS. When SUCCESS is returned then the result is the welcome page… but when the status is SEND_CONTINUE then nothing happens and the result is the login page with no error message.
Add the following code [ .newAuthentication(true) ] will fix the problem with redirect on SEND_CONTINUE as explained in the book “The Definitive Guide to JSF in Java EE 8 Building Web Applications with JavaServer Faces” on page 428. Nor did I manage to figure out exactly what happened there but it works.
Full method code:
private AuthenticationStatus continueAuthentication() {
return securityContext.authenticate(
(HttpServletRequest) externalContext.getRequest(),
(HttpServletResponse) externalContext.getResponse(),
AuthenticationParameters.withParams()
.newAuthentication(true)
.credential(new UsernamePasswordCredential(username, password))
);
}
As promised although a bit late, here is the fix for WildFly. Add jaspitest as security domain and works like a charm: Wildfly Administration Book
Thank you very much Vasilis! I’ve updated the post for using this on WildFly
Thank you for that example, good work.
Can you explain briefly how to integrate this application with the postgres database as DatabaseIdentityStoreDefinition ?
Thank you! To integrate this with a database as the identity store, you can remove the shown CustomInMemoryIdentityStore and add
@DatabaseIdentityStoreDefinition(
dataSourceLookup = "${'java:global/yourDatasource'}",
callerQuery = "#{'select password from caller where name = ?'}",
groupsQuery = "select group_name from caller_groups where caller_name = ?",
priority = 10
)
If we do not add there hashAlgorithm passwords in database should be stored as planetext ?
If we add, for example “hashAlgorithm = PasswordHash.class” I suppose that passwords in databnase should be encrypted but in that case we have to implements methods of PasswordHash interface ?
@Retention(RUNTIME)
@Target(TYPE)
public @interface DatabaseIdentityStoreDefinition {
String dataSourceLookup() default "java:comp/DefaultDataSource";
Class<? extends PasswordHash> hashAlgorithm()
default Pbkdf2PasswordHash.class;
// ... and some more
}
The default hash algorithm, Pbkdf2PasswordHash, is an interface denoting a standard, built-in
PasswordHash. All implementations of this specification MUST provide an implementation of the
Pbkdf2PasswordHash interface, with configuration and behavior as described by the interface’s
javadoc.
This is from the Java EE Security API specification document you can find here (page 36).
Really thanks for this article! Very good written and easy to follow. I haven’t any problem with running it on my computer. I want to have a little contribution to it. I made a two user authentication where each one of them have access to some pages and I struggled on each page with redirection to the login page – if You have this problem use two asteriks instead of one in url-pattern e.g /adm/**
regards!
thanks for the feedback and your contribution!
I need to learn how to hatch passwords and how new users are registered ?
what do you mean with hatch? You can for example query a database within the
CustomInMemoryIdentityStore
to make it more dynamic and just add new customers to this database table.I am failing to creating and register users before they can login
Hey Almeda,
can you please create an issue on GitHub? This helps me to better communicate with you. In addition, please add information about your Java Version, the Java EE version and which application server you use.
Kind regards,
Phil
[…] Security with JSF: #HOWTO: Simple form-based authentication for JSF 2.3 with Java EE 8 security API […]
[…] 8. Simple form-based authentication for JSF 2.3 with Java EE 8 … […]
[…] 8. Simple form-based authentication for JSF 2.3 with Java EE 8 … […]