DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Zones

Culture and Methodologies Agile Career Development Methodologies Team Management
Data Engineering AI/ML Big Data Data Databases IoT
Software Design and Architecture Cloud Architecture Containers Integration Microservices Performance Security
Coding Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Culture and Methodologies
Agile Career Development Methodologies Team Management
Data Engineering
AI/ML Big Data Data Databases IoT
Software Design and Architecture
Cloud Architecture Containers Integration Microservices Performance Security
Coding
Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance
Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks

Enterprise AI Trend Report: Gain insights on ethical AI, MLOps, generative AI, large language models, and much more.

2024 Cloud survey: Share your insights on microservices, containers, K8s, CI/CD, and DevOps (+ enter a $750 raffle!) for our Trend Reports.

PostgreSQL: Learn about the open-source RDBMS' advanced capabilities, core components, common commands and functions, and general DBA tasks.

AI Automation Essentials. Check out the latest Refcard on all things AI automation, including model training, data security, and more.

Related

  • Secure Your Web Applications With Facial Authentication
  • How to Automatically Detect Multiple Cybersecurity Threats from an Input Text String in Java
  • Daily 10 Tech Q&A With Bala
  • Designing High Performant Responsive Web Application With AWS Services and Finetuning for Performance

Trending

  • Top Secrets Management Tools for 2024
  • The Future of Kubernetes: Potential Improvements Through Generative AI
  • Deploying Heroku Apps To Staging and Production Environments With GitLab CI/CD
  • The Data Streaming Landscape 2024
  1. DZone
  2. Coding
  3. Java
  4. Jakarta EE Security: Using Identity Stores

Jakarta EE Security: Using Identity Stores

Jakarta EE Security (formerly JSR 375) introduces the notion of identity stores. Here, learn how they are implemented by Jakarta EE platforms like Payara.

By 
Nicolas Duminil user avatar
Nicolas Duminil
DZone Core CORE ·
Jan. 11, 24 · Tutorial
Like (1)
Save
Tweet
Share
4.9K Views

Join the DZone community and get the full member experience.

Join For Free

As one of the most important aspects of modern business applications and services, the security of the Java enterprise-grade applications didn't wait for the Jakarta EE 10 outbreak. Starting from the first releases of J2EE in early Y2K, security was the crux of enterprise software architecture. It evolved little by little with the gradual development of specifications, but the JSR-375 as we know it today appeared a couple of years ago with Jakarta EE 8, under the name of Java EE Security API 1.0. The current release of the Jakarta EE 10 comes with a major update of Java EE Security API under its new name: Jakarta Security 3.0.

The Jakarta Security specifications are organized around a new terminology defined by the following new concepts:

  • Authentication mechanisms: Invoked by callers to obtain their credentials and to validate them against the existing ones in identity stores
  • Caller: Principal (user or service) originating a call to the API
  • Identity store: Software component that controls access to the API through credentials, roles groups, and permissions
The Jakarta Security interacts with other 2 important specifications, as follows:
  • Jakarta Authorization (formerly JSR-115: JACC - Java Authorization Contracts for Containers)
  • Jakarta Authentication (formerly JASPIC - Java Authentication SPI for Containers)

The concept of authorization mechanism, as defined by the Jakarta Security specifications, designates controllers that interact with a caller and a container environment to obtain credentials, validate them, and pass an authenticated identity (such as users of group names) to the container. In order to validate the credentials, the authorization mechanisms use identity stores. The specifications define built-in identity stores for files, RDBMS (Relational Data Base Management System) and LDAP (Lightweight Directory Access Protocol) servers, in addition to fully customized ones.

In this blog, we'll look at how to secure Java web applications using Jakarta Security built-in RDBMS and LDAP-based identity stores. We chose Payara as the Jakarta EE platform to illustrate this, but the process should be the same, whatever the Jakarta EE-compliant implementation might be.

A Common Use Case

The project that serves to exemplify our storyline can be found here. It is structured as a maven project having a separate module for each of the demonstrated built-in identity stores, as follows:

  • An aggregator POM called jsr-375
  • A WAR artifact called servlet-with-ldap-identity-store, demonstrating the LDAP built-in identity store
  • A WAR artifact called servlet-with-jdbc-identity-store, demonstrating the LDAP built-in identity store
  • An infrastructure project called platform, which relies on testcontainers in order to run two instances of the Payara Platform, a Server and a Micro, each one having deployed to it the two WARs referenced above

The Infrastructure

As explained above, our sample application is deployed on the Payara Server as well as on the Payara Micro. In order to do this, we're running two Docker containers: one for the Payara Server instance and one for the Payara Micro one. We need to orchestrate these containers; hence, we'll be using the docker-compose utility. Here is an excerpt of the associated YAML file:

YAML
 
version: '3.6'
services:
  payara-micro:
    container_name: payara-micro
    image: payara/micro:latest
    ports:
      - 28080:8080
      - 26900:6900
    expose:
      - 8080
      - 6900
    volumes:
      - ../../../../servlet-with-ldap-identity-store/target/servlet-with-ldap-identity-store.war:/opt/payara/deployments/servlet-with-ldap-identity-store.war
      - ../../../../servlet-with-jdbc-identity-store/target/servlet-with-jdbc-identity-store.war:/opt/payara/deployments/servlet-with-jdbc-identity-store.war
  payara-full:
    container_name: payara-full
    image: payara/server-full:latest
    ports:
      - 18080:8080
      - 18081:8081
      - 14848:4848
      - 19009:9009
    expose:
      - 8080
      - 8081
      - 4848
      - 9009
    volumes:
      - ../../../../servlet-with-ldap-identity-store/target/servlet-with-ldap-identity-store.war:/opt/payara/deployments/servlet-with-ldap-identity-store.war
      - ../../../../servlet-with-jdbc-identity-store/target/servlet-with-jdbc-identity-store.war:/opt/payara/deployments/servlet-with-jdbc-identity-store.war
      - ./scripts/init.sql:/opt/payara/init.sql


As we can see in the docker-compose.yaml file above, the following services are started as Docker containers:

  • A service named payara-micro listening for HTTP connexions on the TCP port 28080
  • A service named payara-full listening for HTTP connexions on the TCP port 18080

Note that the two Payara services are mounting WARs to the container's deployment directory. This has the effect of deploying the given WARs.

Note also that the service payara-full - which runs the Payara Server and, consequently, hosts the H2 database instance - also mounts the SQL script init.sql, which will be run in order to create and initialize the H2 schema required for the use of our identity store. Accordingly, it is the H2 database instance hosted by the Payara Server that will be used by both payara-full and payara-micro services.

In order to run the docker-compose commands to start these services we're using the docker-compose-maven-plugin. Here is an excerpt of the associated POM:

XML
 
...
      <plugin>
        <groupId>com.dkanejs.maven.plugins</groupId>
        <artifactId>docker-compose-maven-plugin</artifactId>
        <inherited>false</inherited>
        <executions>
          <execution>
            <id>up</id>
            <phase>install</phase>
            <goals>
              <goal>up</goal>
            </goals>
            <configuration>
              <composeFile>${project.basedir}/src/main/resources/docker-compose.yml</composeFile>
              <detachedMode>true</detachedMode>
              <removeOrphans>true</removeOrphans>
            </configuration>
          </execution>
          <execution>
            <id>down</id>
            <phase>clean</phase>
            <goals>
              <goal>down</goal>
            </goals>
            <configuration>
              <composeFile>${project.basedir}/src/main/resources/docker-compose.yml</composeFile>
              <removeVolumes>true</removeVolumes>
              <removeOrphans>true</removeOrphans>
            </configuration>
          </execution>
        </executions>
      </plugin>
...


Here we bind the up operation to the install phase and the down one to the clean phase. This way we'll get the containers running by executing mvn install and we'll stop and remove them with mvn clean.

The RDBMS Identity Store

The module servlet-with-jdbc-identity-store, 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

This class defines the configuration of our RDBMS identity store. The idea behind the RDBMS identity store is that the principal-related information is stored in a relational database. In our example, this database is the H2 instance that comes with the Payara Platform. This is an in-memory database, used here for the sake of simplicity. Of course, such a design shouldn't be reproduced in production where more production-ready databases, like Oracle, PostgreSQL, or MySQL should be used. In any case, the H2 schema is created and initialized by the JdbcSetup class, as it will be explained in a moment. 

The listing below shows an excerpt of the code:

Java
 
@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 scope. 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 use of 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. This means that at the application startup, we'll be presented with a login screen and challenged to authenticate with a username 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 with 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  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 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 Jakarta Security 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. 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 run automatically at the deployment time. Additionally, 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 that 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 tests that should succeed.
  2. The integration test already tested the service in a Docker container started with testcontainers. 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 the below ones to test on Payara Server and, respectively, on Payara Micro:
Shell
 
curl http://localhost:18080/servlet-with-jdbc-identity-store/secured -u "admin:passadmin"


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


The LDAP Identity Store

Using relational databases to store security principal-related information is a quite common practice; however, these databases aren't exactly the right tool for such use cases. More often than not, organizations use Microsoft ActiveDirectory to store users, groups, and roles-related information together with their associated credentials and other information. While we could have used in our example ActiveDirectory or any other similar LDAP implementation (for example, Apache DS), such an infrastructure would have been too heavy and complex. Hence, in order to avoid that, we preferred to use an in-memory LDAP server.

There are several open-source LDAP in-memory implementations, among which one of the most suitable is UnboundID LDAP SDK for Java. In order to use it, all we need is a dedicated Maven plugin, as shown below:

XML
 
    <dependency>
      <groupId>com.unboundid</groupId>
      <artifactId>unboundid-ldapsdk</artifactId>
    </dependency>


We also need to define our schema in an LDIF (LDAP Data Interchange Format) file that will be loaded into the in-memory directory. For example, we define two principals named admin and, respectively, user. The admin principal has the roles admin-role and user-role, while the user principal has only the user-role one. Here is the required LDIF notation:

Plain Text
 
...
dn: uid=admin,ou=caller,dc=payara,dc=fish
objectclass: top
objectclass: uidObject
objectclass: person
uid: admin
cn: Administrator
sn: Admin
userPassword: passadmin

dn: uid=user,ou=caller,dc=payara,dc=fish
objectclass: top
objectclass: uidObject
objectclass: person
uid: user
cn: User
sn: User
userPassword: passuser
...
dn: cn=admin-role,ou=group,dc=payara,dc=fish
objectclass: top
objectclass: groupOfNames
cn: admin-role
member: uid=admin,ou=caller,dc=payara,dc=fish

dn: cn=user-role,ou=group,dc=payara,dc=fish
objectclass: top
objectclass: groupOfNames
cn: user-role
member: uid=admin,ou=caller,dc=payara,dc=fish
member: uid=user,ou=caller,dc=payara,dc=fish
...


The module servlet-with-ldap-identity-store, which is the one that interests us here, is organized around the following classes:

  • LdapIdentityStoreConfig: This is the configuration class.
  • LdapIdentitySoreServlet: This is a servlet demonstrating the database identity store-based authentication.
  • LdapSetup: This class is setting up the identity store required schema.

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

The Class LdapIdentityStoreConfig

This class defines the configuration of our LDAP-based identity store. Here is a code excerpt:

Java
 
@ApplicationScoped
@BasicAuthenticationMechanismDefinition(realmName="admin-realm")
@LdapIdentityStoreDefinition(
  url = "ldap://localhost:33389",
  callerBaseDn = "ou=caller,dc=payara,dc=fish",
  groupSearchBase = "ou=group,dc=payara,dc=fish")
public class LdapIdentityStoreConfig{}


As already mentioned, we're using the HTTP basic authentication. This is quite convenient as the browser will display a login screen allowing you to type in the user name and the associated password. Furthermore, these credentials will be used in order to authenticate against the ones stored in our LDAP service, listening for connections on the container's 33389 TCP port. The callerBaseDN argument defines, as its name implies the distinguished name of the caller, while the groupSearchBase one defines the LDAP query required in order to find the groups to which a user belongs.

The Class LdapIdentityStoreServlet

Our servlet is a protected one, authorized solely for principals having the role admin-role.

Java
 
@WebServlet("/secured")
@DeclareRoles({ "admin-role", "user-role" })
@ServletSecurity(@HttpConstraint(rolesAllowed = "admin-role"))
public class LdapIdentityStoreServlet extends HttpServlet
{
  @Override
  protected void doGet(HttpServletRequest request, HttpServletResponse response)
    throws IOException
  {
    response.getWriter().write("This is a secured servlet \n");
    Principal principal = request.getUserPrincipal();
    String user = principal == null ? null : principal.getName();
    response.getWriter().write("User name: " + user + "\n");
    response.getWriter().write("\thas role \"admin-role\": " + request.isUserInRole("admin-role") + "\n");
    response.getWriter().write("\thas role \"user-role\": " + request.isUserInRole("user-role") + "\n");
  }
}


We're using the @WebServlet annotation in order to declare our class as a servlet. The @ServletSecurity annotation means here that only users having the role admin-role are allowed.

The Class LdapSetup

Last but not least, the class LdapSetup instantiates and initializes the in-memory LDAP service:

Java
 
@Startup
@Singleton
public class LdapSetup
{
  private InMemoryDirectoryServer directoryServer;

  @PostConstruct
  public void init()
  {
    try
    {
      InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig("dc=fish");
      config.setListenerConfigs(
        new InMemoryListenerConfig("myListener", null, 33389, null, null, null));
      directoryServer = new InMemoryDirectoryServer(config);
      directoryServer.importFromLDIF(true,
        new LDIFReader(this.getClass().getResourceAsStream("/users.ldif")));
      directoryServer.startListening();
    } catch (LDAPException e)
    {
      throw new IllegalStateException(e);
    }
  }

  @PreDestroy
  public void destroy()
  {
    directoryServer.shutDown(true);
  }
}


This is a Startup (runs automatically at the start-up) CDI bean, having the Singleton scope. It instantiates the in-memory directory server by loading the LDIF file shown above and starts listening for LDAP requests on the TCP port number 33389 of the localhost.

Testing

An integration test is provided to be executed with the failsafe Maven plugin. During Maven's integration test phase, this integration test uses testcontainers to create a Docker container running a Payara Micro image and deploy to it our WAR. Here is an excerpt from the integration test with testcontainers:

 
  @Container
  private static GenericContainer payara =
    new GenericContainer("payara/micro:latest")
      .withExposedPorts(8080)
      .withCopyFileToContainer(MountableFile.forHostPath(
        Paths.get("target/servlet-with-ldap-identity-store.war")
          .toAbsolutePath(), 0777), "/opt/payara/deployments/test.war")
      .waitingFor(Wait.forLogMessage(".* Payara Micro .* ready in .*\\s", 1))
      .withCommand(
        "--noCluster --deploy /opt/payara/deployments/test.war --contextRoot /test");


Here we create a Docker container running the image payara/micro:latest and exposing the TCP port 8080. We also copy to the image the WAR that we just built during Maven's package phase, and, finally, we start the container. Since Payara Micro might need a couple of seconds in order to start, we need to wait until it has fully booted. There are several ways to wait for the server boot to complete but here we use the one consisting of scanning the log file until a message containing "Payara Micro is ready" is displayed.

Last but not least, testing the deployed servlet is easy using the REST assured library, as shown below:

Java
 
@Test
public void testGetSecuredPageShouldSucceed() throws IOException
{
  given()
   .contentType(ContentType.TEXT)
   .auth().basic("admin", "passadmin")
   .when()
   .get(uri)
   .then()
   .assertThat().statusCode(200)
   .and()
   .body(containsString("admin-role"))
   .and()
   .body(containsString("admin-role"));
}


Running

In order to run the applications 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 with testcontainers. But you can now test it on more production-ready containers, like the one managed by the platform Maven module. To test on the Payara Server, you can run commands like this: 
Shell
 
curl http://localhost:18080/servlet-with-ldap-identity-store/secured -u "admin:passadmin"


Run the below commands to test on Payara Micro.

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


Enjoy!

API Java EE Application security Web application

Published at DZone with permission of Nicolas Duminil. See the original article here.

Opinions expressed by DZone contributors are their own.

Related

  • Secure Your Web Applications With Facial Authentication
  • How to Automatically Detect Multiple Cybersecurity Threats from an Input Text String in Java
  • Daily 10 Tech Q&A With Bala
  • Designing High Performant Responsive Web Application With AWS Services and Finetuning for Performance

Partner Resources


Comments

ABOUT US

  • About DZone
  • Send feedback
  • Community research
  • Sitemap

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Core Program
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 100
  • Nashville, TN 37211
  • support@dzone.com

Let's be friends: