Use Jakarta EE Identity Store With Payara and RDBMS

Photo of Nicolas DUMINIL by Nicolas DUMINIL

In my previous blog post,here,I examined the JSR-375 specifications and their implementation by Jakarta EE and, more specifically, by the Payara Platform (Server and Micro).

I presented a brief overview of the JSR-375 specifications and their new features, focusing on the notion of Identity Store and illustrating, with a simple example, one of the most common use cases: the LDAP (Lightweight Directory Access Protocol) based authentication and authorization process.

While using LDAP for this purpose is probably the most recommended way to manage Identity Stores, many organizations prefer using RDBMS (Relational Database Management Server) due, in most cases, to historical and cultural reasons. For these organizations, I'm presenting here a new use case which, despite its apparent simplicity, fully covers all the possible difficulties and traps that might be experienced by the software developer.

The Database Identity Store

The authentication and authorization process performed by any enterprise grade application wasn't discovered byJakarta EE, but has existed since the dawn of time! What Jakarta EE and its JSR-375 specs bring, as a new added value, is its standardization. There are dozens of Java security frameworks, each one coming with its own authentication and authorization mechanisms. Compared to Jakarta EE, these other security frameworks are neither better nor worse, they are just not standard.  And that is the tremendous benefit of Jakarta EE: identifying common patterns and providing standard solutions, achieving the consensus of the majority of the software editors and working across the majority of platforms.

The Project

In order to illustrate the use of the database Identity Store, I'll be using the same project as the one I presented previously, to illustrate the LDAP Identity Store. It may be foundhere.

For the readers who didn't have the chance to see ourprevious post, I'll recall that this is an aggregate project, having the following structure:

  • an aggregator POM called jsr-375

  • a WAR artifact, called servlet-with-ldap-identity-store, demonstrating the LDAP Identity Store

  • a WAR artifact, called servlet-with-jdbc-identity-store, demonstrating the database Identity Store

  • an infrastructure project, called platform, which relays on testcontainers in order to run two instances of Payara Platform, a Server and a Micro. Each one having deployed to it the two WARs referenced above

The Module servlet-with-jdbc-identity-store

This module, which is the one that interests us here, is organized around the following classes:

  • JdbcIdentityStoreConfig: this is the configuration class
  • JdbcIdentitySoreServlet: this is a servlet demonstrating the database identity stored based authentication
  • JdbcSetup: this class is setting up the Identity Store required schema.

Let's have a more detailed view of each of these classes.

The Class JdbcIdentityStoreConfig

The listing below shows an excerpt of the code:

@ApplicationScoped
@BasicAuthenticationMechanismDefinition(realmName="admin-realm")
@DatabaseIdentityStoreDefinition(
dataSourceLookup = "${'java:global/H2'}",
callerQuery = "select password from caller where name = ?",
groupsQuery = "select group_name from caller_groups where caller_name = ?"
)
public class JdbcIdentityStoreConfig{}

As we can see, our class is a CDI (Context and Dependency Injection) bean, having the application scoped. The annotation @DatabaseIdentityStoreDefinition is the new Jakarta EE one, defining the database Identity Store mechanism. The argument named dataSourceLookup declares a JNDI (Java Name and Directory Interface) lookup name which will bring the associated data source definition. Once this data source reference is found, we'll execute the two defined SQL queries, callerQuery and groupsQuery, in order to find the caller credentials, i.e. its identifier and password, as well as its group membership. The notion of caller here is somehow equivalent to the one of user, while being a less human connotation, as it could also be a service. Hence the pronoun "it".

But the most interesting thing to be noticed is the fact that we're using here the HTTP basic authentication mechanism, defined by the @BasicAuthenticationMechanismDefinition annotation. Which means that, at the application startup, we'll be presented with a login screen and challenged to authenticate with a user name and password. This information will be further transmitted to the database Identity Store mechanism which will compare them with the ones stored in the database. This way we're composing two JSR-375 security features: the HTTP basic authentication associated to the database Identity Store. I'm leaving to the sovereign appraisal of the reader this facility which saves several dozens of lines of code.

The Class JdbcIdentityStoreServlet

This class is the same as the one used to demonstrate the LDAP Identity Store. It's a servlet to which access is authorized to any caller having the role of admin-role. This is defined through the Jakarta EE annotation @ServletSecurity. Another specific Jakarta EE annotation is @DeclareRoles which allows for the enumeration of all the possible roles that the application should be aware of.

The Class JdbcSetup

This class is responsible for the creation and the initialization of the data-model required by the database Identity Store mechanism. In its @PostConstruct method, it creates two database tables named caller and, respectively, caller_groups. Then, these tables are initialized with caller names, passwords and group names. The caller named admin is then attached to the groups admin-role and user-role while the caller named user is only a member of the group user-roles.

It should be noted that the password is stored in the database as hashed. The JSR-375 specifications define the interface Pbkdf2PasswordHash having a default implementation based on the PBKDF2WithHmacSHA256 algorithm. This implementation can be simply injected, as you can see in the associated source code. Here we are using the default implementation which is largely satisfactory for our example. But other more secure hash algorithms may be used as well and, in this case, the Pbkdf2PasswordHash default implementation may be initialized by passing to it a map containing the algorithm name as well as parameters like the salt, the number of iterations, etc. The Jakarta EE documentation presents all these details in extenso.

Another thing to mention is the fact that, using Java JDBC (Java Data Base Connectivity) code to initialize the database in a runtime singleton @PostConstruct method, is probably not the most elegant way to deal with SQL. The in-memory H2 database used here accepts, on its JDBC connection string, the argument named "run script from" allowing us to define the script to be run in order to initialize the database. Accordingly, instead of doing that in Java JDBC code and having to provide a dedicated EJB (Enterprise JavaBeans) for this purpose, we could have had an initialization SQL script to be ran automatically at the deployment time. And in order to deal with the password hashing, we could have dealt with the HASH function that H2 provides in its more recent releases. However, the Payara Platform comes with an older release of the H2 database, which doesn't support this feature. Accordingly, to save ourselves the burden of having to upgrade the H2 database release which comes with the Payara Platform, we finally preferred this simpler alternative.

Running The Example

In order to run our example, proceed as follows:

  1. Execute the command mvn clean install. This command will stop the Docker containers, if they are running, and starts new instances. It also will run the integration test that should succeed.
  2. The integration test already tested the service in a Docker container started withTestcontainers. But you can now test it on more production-ready containers, like the one managed by the platform Maven module. You can run commands like:

    curl http://localhost:18080/servlet-with-jdbc-identity-store/secured -u "admin:passadmin"

    to test on Payara Server or

    curl http://localhost:28080/servlet-with-jdbc-identity-store/secured -u "admin:passadmin"

Enjoy !

Further reading:

Comments