Using Jakarta EE Identity Store With Payara

Photo of Nicolas DUMINIL by Nicolas DUMINIL

These days the world-wide open-source community celebrates the advent of Jakarta EE 10. It is then a good time to look at one of its most relevant and, at the same time, unknown parts: security!

In this blog, I'll give an introduction to Jakarta EE Security, and then explain how Payara Platform builds on Jakarta EE Security with built-in identity stores for RDBMS (Relational Database Management System) and LDAP (Lightweight Directory Access Protocol).

JSR-375: Java Security API

As one of the most important aspects of modern business applications and services, the Java Security API didn't wait forJakarta EE 10 to appear. 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. What's new inJakarta EE10 however, is the unified Jakarta packages namespace, together with different fixes for some reported issues. Which means that we can say, nevertheless, that the JSR-375 in its current form, including but not limited to the packages namespace, comes with Jakarta EE 10.

The 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 control the access to the API through credentials, roles groups and permissions
The JSR-375 interacts with other 2 specifications, as follows:
  • JSR-115 (JACC - Java Authorization Contracts for Containers)
  • JSR-196 (JASPIC - Java Authentication SPI for Containers)

The Payara Platform

Jakarta EE Securityspecifications define the notion of authorization mechanism which are controllers that interact with a caller and a container’s environment to obtain the caller’s credentials, validate these, and pass an authenticated identity (such as name and groups) to the container. In order to validate the credentials, the authorization mechanisms use identity stores. The Payara Server API provides built-in identity stores for RDBMS and LDAP.

The Project

There are lots of tutorials demonstrating how to use JSR-375 built-in identity stores with the Payara Platform. However, using RDBMS for storing credentials, as well as principals, users, groups and role related information, isn't a good practice. This kind of information, which doesn't have anything relational, is much better stored in LDAP directories, designed on purpose for that. But as surprising as it might seem, the different examples and tutorials publicly available, completely lacks LDAP based use cases (though we do have our own guide,Integrating LDAP with Payara Server). Hence our project availablehereand described below.

The Project Software Architecture

Our project is structured in several sub-projects, each one corresponding to a Maven module, the whole coordinated by an aggregator POM, named jsr-375.

Most of organizations useMicrosoft 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 (Directory Server), this infrastructure would have been too heavy and complex. Hence, in order to avoid that, we preferred to use an in-memory LDAP server.

In order to make our project as autonomous as possible, we're using Linux containers to run Docker images for our Payara Server andPayara Micro. We deploy our sample applications on these two platforms. This happens in theMavenmodule named platform.

The module platform

As explained above, our application is deployed on the Payara Server as well as on the Payara Micro Docker container. In order to orchestrate all these containers we'll be using docker-compose utility. Here is an excerpt of the associated YAML file:

version: '3.6'
services:
payara-micro:
container_name: payara-micro
image: payara/micro:5.2022.2-jdk11
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
payara-full:
container_name: payara-full
image: payara/server-full:5.2022.2-jdk11
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

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.

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:

<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 module servlet-with-ldap-identity-store

This module effectively demonstrates the use of the LDAP based identity store for authorization purposes. It consists of several classes, as follows:

  • LdapIdentityStoreServlet: a secured servlet allowing the access of users belonging to the role admin-role.
  • LdapIdentityStoreConfig: a CDI (Contexts Dependency Injection) class that configures the required authentication mechanism and identity store. We're using here the HTTP basic authentication mechanism together with the LDAP based identity store.
  • LdapSetup: this a singleton EJB ran at the application start-up and its role is to instantiate the in-memory LDAP service and to initialize it with a test domain described in the file users.ldif.

The following Maven dependency is required in order to be able to use the in-memory LDAP service:

<dependency>
<groupId>com.unboundid</groupId>
<artifactId>unboundid-ldapsdk</artifactId>
<version>6.0.6</version>
</dependency>

Here is an excerpt of the servlet code:

@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 belonging to the role admin-role are allowed. The listing below shows an excerpt from the CDI configuration class:

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

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

@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);
}
}

Testing

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

@Container
private static GenericContainer payara =
new GenericContainer("payara/micro:5.2022.3-jdk11")
.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:5.2022.3-jdk11 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 that it has fully booted. There are several ways to wait for the server boot to complete but here we use the one consisting in 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:

@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("user-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 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-ldap-identity-store/secured -u "admin:passadmin"
    to test on Payara Server or
    curl http://localhost:28080/servlet-with-ldap-identity-store/secured -u "admin:passadmin"
    to test on Payara Micro.

Enjoy !

We hope this has helped demonstate how Jakarta Security and its identity store function can be used with Payara and LDAP! 

Further reading:

Comments