Jakarta Security

Jakarta Security is the overarching security API in Jakarta EE. Overarching here means that it strives to address the security needs of all other APIs in Jakarta EE in a holistic way.

Due to historical and political reasons, a number of security features are still distributed among several other APIs in Jakarta EE. Sometimes they overlap, and sometimes such features are only accessible from these other APIs. In this chapter, we’ll focus primarily on explaining Jakarta Security, but we’ll mention when other APIs are needed to accomplish a certain task.

Overview

Before we look at some practical examples, let’s quickly go through some basics.

Some of the guiding principles in Jakarta Security are:

  1. It should work directly out of the box, without requiring vendor-specific configuration.

  2. It leverages Jakarta CDI as much as possible. Most artifacts are CDI beans, and many features are done via CDI interceptors.

  3. The difference between framework-provided artifacts and custom (user provided) artifacts is minimal or non-existent.

  4. It fully integrates with security features from other Jakarta EE APIs and proprietary (vendor-specific) artifacts.

Jakarta Security defines several distinct artifacts that play an important role in the security process:

The first two of these are used in the authentication process:

An authentication mechanism is somewhat like a controller in the well-known MVC pattern; it is the entity that interacts with the caller (typically a human), via some kind of view to collect credentials, and with the model (business logic) to validate these credentials. An authentication mechanism knows about the environment this caller uses to communicate with the server. An authentication mechanism for HTTP knows about URLs to redirect or forward to, or about response headers to send to the client. It also knows about the data coming back, such as cookies, request headers, and post data. Examples of authentication mechanisms are Form authentication and Basic authentication.

An identity store is more like the model in the MVC pattern. This entity strictly performs a business / data operation where credentials go in, and an identity comes out. The identity contains logic to validate said credentials, and embeds or contacts a database. This "database" contains usernames, along with their credentials and (typically) roles. An identity store therefore knows nothing about the environment that this caller uses to communicate with the server; for example, it doesn’t know about HTTP or headers and more. Some examples of identity stores are services that contact SQL or NoSQL databases, LDAP servers, files on the file-system, and more.

Figure 1, “Mechanism Store in MVC” shows the authentication mechanism and identity store in an MVC-like structure.

Diagram illustrating the role of the authentication mechanism and identity store in an MVC like structure
Figure 1. Mechanism Store in MVC

The third one is used for the authorization process:

A permission store is another kind of model that stores permissions, typically either globally, or per role (role-based permissions). This entity then performs a business / data operation where a query and an identity go in, and a yes/no answer goes out. For instance, a query such as "can access /foo/bar?" along with the identity for user "John" with roles "bar" and "kaz" would return "yes" if that identity is authorized to access "/foo/bar", and "no" if not authorized. Examples of permission stores are the Jakarta Authorization usage of the Policy class, or the internal data structure where a Servlet Container such as Tomcat or Jetty stores the security constraints an application defined.

Provided authentication mechanisms and identity stores

Jakarta Security provides a number of built-in authentication mechanisms and identity stores. We’ll enumerate them here first, and will look at them in more detail below.

Authentication mechanisms:

Identity stores:

Custom authentication mechanisms and identity stores

When the provided authentication mechanisms and identity stores aren’t sufficient, we can easily define custom ones. Both provided and custom ones use the same interfaces, and the system doesn’t distinguish between them.

Authentication mechanisms and identity stores from other APIs

The Servlet specification defines the exact same Form and Basic authentication mechanisms. Authenticating with them will have the same result as authenticating with a Jakarta Security authentication mechanism. (Role checks will work the same independent on which API was used to authenticate.)

A Servlet authentication mechanism, however, will not necessarily consult a Jakarta Security identity store. This is server dependent. The identity store that is called is server dependent as well. Calling this server-dependent identity store is possible from Jakarta Security, but as an advanced feature.

Likewise, programmatic role checks can be done from various APIs, including Jakarta Security, Jakarta REST, and Jakarta Servlet. These all return the same outcome, independent of whether authentication took place with a Jakarta Security Authentication Mechanism or a Servlet Authentication Mechanism. Within a Jakarta EE environment the usage of Jakarta Security for this is encouraged, and the usage of those other APIs is discouraged.

Programmatic role checks in Jakarta REST, Jakarta Servlet and various other APIs are not being deprecated for the time being, as those APIs are also used stand-alone (outside Jakarta EE). Future versions of those APIs may contain warnings about their usage within Jakarta EE.

Securing an endpoint with Basic authentication

In the following example, we’ll be securing a REST endpoint using Basic authentication.

You’ll learn how to:

  1. Define security constraints

  2. Set a provided authentication mechanism

  3. Define (and implicitly set) a custom identity store

  4. Use the Jakarta Security SecurityContext

Write the application

Let’s start with defining a simple REST resource class for a /rest/resource endpoint:

@Path("/resource")
@RequestScoped
public class Resource {

    @Inject
    private SecurityContext securityContext;

    @GET
    @Produces(TEXT_PLAIN)
    public String getCallerAndRole() {
        return
            securityContext.getCallerPrincipal().getName() + " : " +
            securityContext.isCallerInRole("user");
    }

}

This resource uses the injected Jakarta EE SecurityContext to obtain access to the current authenticated caller, which is represented by a Principal instance.

If this resource were available to unauthenticated callers, getCallerPrincipal() would return null for unauthenticated requests, so we’d have to check for null. Our example, however, requires authentication for this resource, so we can skip that check.

There is a Jakarta REST-specific type that is also named SecurityContext and has similar methods as the ones we used here. From the Jakarta EE perspective, that is a discouraged type and the Jakarta Security version is to be preferred.

Declare the security constraints

Next we’ll define the security constraints in web.xml, which tell the security system that access to a given URL or URL pattern is protected, and hence authentication is required:

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="https://jakarta.ee/xml/ns/jakartaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="https://jakarta.ee/xml/ns/jakartaee https://jakarta.ee/xml/ns/jakartaee/web-app_6_0.xsd"
         version="6.0">

    <security-constraint>
        <web-resource-collection>
            <web-resource-name>protected</web-resource-name>
            <url-pattern>/rest/*</url-pattern>
        </web-resource-collection>
        <auth-constraint>
            <role-name>user</role-name>
        </auth-constraint>
    </security-constraint>

</web-app>

This XML essentially says that to access any URL that starts with "/rest" requires the caller to have the role "user". Roles are opaque strings; merely identifiers. It’s fully up to the application how broad or fine-grained they are.

In Jakarta EE, internally these XML constraints are transformed into Permission instances and made available via a specific type of permission store. Knowledge about this transformation is only needed for very advanced use cases.
The observant reader may wonder if XML is really the only option here, given the strong feelings that exist in parts of the community around XML. The answer is yes and no. Jakarta EE does define the @RolesAllowed annotation that could be used to replace the XML shown above, but only the legacy Enterprise Beans has specified a behaviour for this when put on an Enterprise Bean. Jakarta REST has done no such thing, although the JWT API in MicroProfile has defined this for REST resources. In Jakarta EE, however, this remains a vendor-specific extension. There are also a number of annotations and APIs in Jakarta EE to set these kinds of constraints for individual Servlets, but those won’t help us much either here.

Declare the authentication mechanism

@ApplicationScoped
@BasicAuthenticationMechanismDefinition(realmName = "basicAuth")
@DeclareRoles({ "user", "caller" })
@ApplicationPath("/rest")
public class ApplicationConfig extends Application {

}

To declare the usage of a specific authentication mechanism, Jakarta EE provides [XYZ]MechanismDefinition annotations. Such an annotation is picked up by the security system, and in response to it a CDI bean that implements the HttpAuthenticationMechanism is enabled for it.

The annotation can be put on any bean, but in a REST application it fits particularly well on the Application subclass because it also declares the path for REST resources.

Define the identity store

Finally, let’s define a simple identity store that the security system can use to validate provided credentials for Basic authentication:

@ApplicationScoped
public class TestIdentityStore implements IdentityStore {

    public CredentialValidationResult validate(UsernamePasswordCredential usernamePasswordCredential) {
        if (usernamePasswordCredential.compareTo("john", "secret1")) {
            return new CredentialValidationResult("john", Set.of("user", "caller"));
        }

        return INVALID_RESULT;
    }

}

This identity store only validates the single identity (user) "john", with password "secret1" and roles "user" and "caller". Defining this kind of identity store is often the simplest way to get started.

Jakarta Security doesn’t provide a simple identity store out of the box. The reason is that everything in Jakarta Security promotes best practices, and it’s not clear if a simple identity store fits in with those best practices.

The identity store is installed and used by the security system just by the virtue of being there; it picks up all enabled CDI beans that implement IdentityStore. Such beans can be enabled by the security system itself (following some configuration annotation), or can be programmatically added using the appropriate CDI APIs. Where the bean comes from doesn’t matter for Jakarta Security, only the fact that it’s there.

Test the application

It’s now time to test our application. A ready-to-test version is available from the Jakarta EE Examples project at https://github.com/eclipse-ee4j/jakartaee-examples.

Download or clone this repo, then cd into the focused folder and execute:

mvn clean install -pl :restBasicAuthCustomStore

This will run a test associated with the project, printing something like the following:

john : true
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 6.414 s - in jakartaee.examples.focused.security.restbasicauthcustomstore.RestBasicAuthCustomStoreIT

Let’s take a quick look at the actual test:

@RunWith(Arquillian.class)
@RunAsClient
public class RestBasicAuthCustomStoreIT extends ITBase {

    /**
     * Stores the base URL.
     */
    @ArquillianResource
    private URL baseUrl;

    /**
     * Test the call to a protected REST service
     *
     * @throws Exception when a serious error occurs.
     */
    @RunAsClient
    @Test
    public void testRestCall() throws Exception {
        DefaultCredentialsProvider credentialsProvider = new DefaultCredentialsProvider();
        credentialsProvider.addCredentials("john", "secret1");

        webClient.setCredentialsProvider(credentialsProvider);

        TextPage page = webClient.getPage(baseUrl + "/rest/resource");
        String content = page.getContent();

        System.out.println(content);
    }
}

The test starts a server and deploys the output of the build process (a .war file) to it. The test runs in the integration test phase, rather than the unit test phase, to make sure this build output is available when it runs. The test then sends a request to the server using the provided HtmlUnit webClient. Note that the webClient can be used for any other HTTP requests your test requires.

If you want to inspect the app yourself, you can manually deploy the WAR file (security/restBasicAuthCustomStore/target/restBasicAuthCustomStore.war) to the server of your choice (e.g. GlassFish 7), and request the URL via a browser or a commandline util such as curl.

The DefaultCredentialsProvider used here makes sure that the headers for Basic authentication are added to the request. The Basic authentication mechanism that we defined for our applications reads those headers, extracts the username and password from them, and consults our identity store with them.

Securing an endpoint with Basic authentication and a database identity store

In the following example, we’ll secure a REST endpoint using Basic authentication and the database identity store that is provided by Jakarta Security.

You’ll learn how to:

  1. Define security constraints

  2. Use the provided BasicAuthenticationMechanismDefinition

  3. Use the provided DatabaseIdentityStoreDefinition

  4. Populate and configure the identity store

  5. Use the Jakarta Security SecurityContext

Write the application

Let’s start with defining a simple REST resource class for a /rest/resource endpoint:

@Path("/resource")
@RequestScoped
public class Resource {

    @Inject
    private SecurityContext securityContext;

    @GET
    @Produces(TEXT_PLAIN)
    public String getCallerAndRole() {
        return
            securityContext.getCallerPrincipal().getName() + " : " +
            securityContext.isCallerInRole("user");
    }

}

This resource uses the injected Jakarta EE SecurityContext to obtain access to the current authenticated caller, which is represented by a Principal instance.

If this resource were available to unauthenticated callers, getCallerPrincipal() would return null for unauthenticated requests, so we’d have to check for null. Our example, however, requires authentication for this resource, so we can skip that check.

There is a Jakarta REST-specific type that is also named SecurityContext and has similar methods as the ones we used here. From the Jakarta EE perspective, that is a discouraged type and the Jakarta Security version is to be preferred.

Declare the authentication mechanism and identity store

@ApplicationScoped
@BasicAuthenticationMechanismDefinition(
    realmName = "basicAuth"
)
@DatabaseIdentityStoreDefinition(
    callerQuery = "select password from basic_auth_user where username = ?",
    groupsQuery = "select name from basic_auth_group where username = ?",
    hashAlgorithmParameters = {
        "Pbkdf2PasswordHash.Iterations=3072",
        "Pbkdf2PasswordHash.Algorithm=PBKDF2WithHmacSHA512",
        "Pbkdf2PasswordHash.SaltSizeBytes=64"
    }
)
@DeclareRoles("user")
@ApplicationPath("/rest")
public class ApplicationConfig extends Application {

To declare the usage of a specific authentication mechanism, Jakarta EE provides [XYZ]MechanismDefinition annotations. Such an annotation is picked up by the security system, and in response to it a CDI bean that implements the HttpAuthenticationMechanism is enabled for it.

The annotation can be put on any bean, but in a REST application it fits particularly well on the Application subclass because it also declares the path for REST resources.

Likewise, to declare the usage of a specific identity store, Jakarta EE provides [XYZ]StoreDefinition annotations.

The annotations can be put on any bean, but in a REST application it fits particularly well on the Application subclass that also declares the path for REST resources.

You can use the provided DatabaseIdentityStoreDefinition with any authentication mechanism that validates username/password credentials. It requires at least two SQL queries:

  1. A query that returns a password for the username part of credentials. The returned password is compared with the password part of those credentials. If they match (of more typically, their hashes match) the credential is considered valid.

  2. A query that returns a number of roles given that same username part of the credentials

Although not required, it’s a good practice to provide some parameters for the hash algorithm. Passwords should never be stored in plain-text in a database.

Populating the identity store

In order to use the identity store, we need to put some data in a database. The following code shows one way how to do that:

@ApplicationScoped
@BasicAuthenticationMechanismDefinition(
    realmName = "basicAuth"
)
@DatabaseIdentityStoreDefinition(
    callerQuery = "select password from basic_auth_user where username = ?",
    groupsQuery = "select name from basic_auth_group where username = ?",
    hashAlgorithmParameters = {
        "Pbkdf2PasswordHash.Iterations=3072",
        "Pbkdf2PasswordHash.Algorithm=PBKDF2WithHmacSHA512",
        "Pbkdf2PasswordHash.SaltSizeBytes=64"
    }
)
@DeclareRoles("user")
@ApplicationPath("/rest")
public class ApplicationConfig extends Application {

    /**
     * Id of the one and only user we populate in out DB.
     */
    private static final BigInteger USER_ID = ONE;

    /**
     * Id of the one and only group we populate in out DB.
     */
    private static final BigInteger GROUP_ID = ONE;

    @PersistenceContext
    private EntityManager entityManager;

    @Inject
    private Pbkdf2PasswordHash passwordHash;

    @Transactional
    public void onStart(@Observes @Initialized(ApplicationScoped.class) Object applicationContext) {
        passwordHash.initialize(Map.of(
            "Pbkdf2PasswordHash.Iterations", "3072",
            "Pbkdf2PasswordHash.Algorithm", "PBKDF2WithHmacSHA512",
            "Pbkdf2PasswordHash.SaltSizeBytes", "64"));

        if (entityManager.find(User.class, USER_ID) == null) {
            var user = new User();
            user.id  = USER_ID;
            user.username = "john";
            user.password = passwordHash.generate("secret1".toCharArray());
            entityManager.persist(user);
        }

        if (entityManager.find(Group.class, GROUP_ID) == null) {
            var group = new Group();
            group.id = GROUP_ID;
            group.name = "user";
            group.username = "john";
            entityManager.persist(group);
        }
    }

}

@Entity
@Table(name = "basic_auth_user")
class User {
    @Id
    BigInteger id;

    @Column(name = "password")
    String password;

    @Column(name = "username", unique = true)
    String username;
}

@Entity
@Table(name = "basic_auth_group")
class Group {
    @Column(name = "id")
    @Id
    BigInteger id;

    @Column(name = "name")
    String name;

    @Column(name = "username")
    String username;
}

The code above uses Jakarta Persistence, which generates SQL from Java types. Jakarta Persistence is discussed in detail in its own chapter. Since we haven’t specified a datasource, the @DatabaseIdentityStoreDefinition annotation will use the default datasource defined in Jakarta EE, so you don’t have to explicitly install and configure an external database such as Postgres or MySQL. However, if necessary, you can configure a different one using the dataSourceLookup attribute.

Test the application

It’s now time to test our application. A ready-to-test version is available from the Jakarta EE Examples project at https://github.com/eclipse-ee4j/jakartaee-examples.

Download or clone this repo, then cd into the focused folder and execute:

mvn clean install -pl :restBasicAuthDBStore

This will run a test associated with the project, printing something like the following:

john : true
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 8.307 s - in jakartaee.examples.focused.security.restbasicauthdbstore.RestBasicAuthDBStoreIT

The test itself is basically the same as that for the Securing an endpoint with Basic authentication example.

Securing an endpoint with Basic authentication and multiple identity stores

In the following example, we’ll be securing a REST endpoint using Basic authentication and two identity stores: the database identity store that is provided by Jakarta Security and a custom identity store.

You’ll learn how to:

Write the application

Let’s start with defining a simple REST resource class for a /rest/resource endpoint:

@Path("/resource")
@RequestScoped
public class Resource {

    @Inject
    private SecurityContext securityContext;

    @GET
    @Produces(TEXT_PLAIN)
    public String getCallerAndRole() {
        return
            securityContext.getCallerPrincipal().getName() + " : " +
            securityContext.isCallerInRole("user");
    }

}

This resource uses the injected Jakarta EE SecurityContext to obtain access to the current authenticated caller, which is represented by a Principal instance.

If this resource were available to unauthenticated callers, getCallerPrincipal() would return null for unauthenticated requests, so we’d have to check for null. Our example, however, requires authentication for this resource, so we can skip that check.

There is a Jakarta REST-specific type that is also named SecurityContext and has similar methods as the ones we used here. From the Jakarta EE perspective, that is a discouraged type and the Jakarta Security version is to be preferred.

Declare the authentication mechanism and identity store

@ApplicationScoped
@BasicAuthenticationMechanismDefinition(
    realmName = "basicAuth"
)
@DatabaseIdentityStoreDefinition(
    callerQuery = "select password from basic_auth_user where username = ?",
    groupsQuery = "select name from basic_auth_group where username = ?",
    hashAlgorithmParameters = {
        "Pbkdf2PasswordHash.Iterations=3072",
        "Pbkdf2PasswordHash.Algorithm=PBKDF2WithHmacSHA512",
        "Pbkdf2PasswordHash.SaltSizeBytes=64"
    }
)
@DeclareRoles("user")
@ApplicationPath("/rest")
public class ApplicationConfig extends Application {
@ApplicationScoped
public class CustomIdentityStore implements IdentityStore {

    public CredentialValidationResult validate(UsernamePasswordCredential usernamePasswordCredential) {
        if (usernamePasswordCredential.compareTo("pete", "secret2")) {
            return new CredentialValidationResult("pete", Set.of("user", "caller"));
        }

        return INVALID_RESULT;
    }

}

In this example we have two enabled CDI beans implementing the IdentityStore interface. One of them will be implicitly enabled via the @DatabaseIdentityStoreDefinition annotation, while the other one is defined explicitly via the CustomIdentityStore class. As with a single identity store, it doesn’t matter how or where the CDI beans are defined, only that multiple enabled ones exist.

When multiple identity stores are present, the security system will try them in order of their priority. We didn’t set a priority here, so the order will be undefined. If the default validation algorithm is used, a successful validation wins over a failed validation. For example, let’s say we have multiple identity stores that know about the user "pete". If "pete" fails validation in one store, but passes validation in another store, the end result is still that validation passed.

In the two stores above, however only one store knows about "pete" and that’s the CustomIdentityStore. The store created from @DatabaseIdentityStoreDefinition doesn’t know about "pete" at all, and will simply not validate it.

Populating the identity store

In order to use the identity store, we need to put some data in a database. This is done in the same as in Securing an endpoint with Basic authentication and a database identity store.

Test the application

It’s now time to test our application. A ready-to-test version is available from the Jakarta EE Examples project at https://github.com/eclipse-ee4j/jakartaee-examples.

Download or clone this repo, then cd into the focused folder and execute:

mvn clean install -pl :restBasicAuthDBStoreAndCustomStore

This will run a test associated with the project, printing something like the following:

john : true
pete : true
[INFO] Tests run: 2, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 9.239 s - in jakartaee.examples.focused.security.restbasicauthdbstoreandcustomstore.RestBasicAuthDBStoreAndCustomStoreIT

Let’s take a quick look at the actual test again:

@RunWith(Arquillian.class)
@RunAsClient
public class RestBasicAuthDBStoreAndCustomStoreIT extends ITBase {

    @ArquillianResource
    private URL baseUrl;

    /**
     * Test the call to a protected REST service
     *
     * <p>
     * This will use the "john" credentials, which should be validated by the DB store
     *
     * @throws Exception when a serious error occurs.
     */
    @RunAsClient
    @Test
    public void testRestCall1() throws Exception {
        DefaultCredentialsProvider credentialsProvider = new DefaultCredentialsProvider();
        credentialsProvider.addCredentials("john", "secret1");

        webClient.setCredentialsProvider(credentialsProvider);

        TextPage page = webClient.getPage(baseUrl + "/rest/resource");
        String content = page.getContent();

        System.out.println(content);
    }

    /**
     * Test the call to a protected REST service
     *
     * <p>
     * This will use the "pete" credentials, which should be validated by the custom store
     *
     * @throws Exception when a serious error occurs.
     */
    @RunAsClient
    @Test
    public void testRestCall2() throws Exception {
        DefaultCredentialsProvider credentialsProvider = new DefaultCredentialsProvider();
        credentialsProvider.addCredentials("pete", "secret2");

        webClient.setCredentialsProvider(credentialsProvider);

        TextPage page = webClient.getPage(baseUrl + "/rest/resource");
        String content = page.getContent();

        System.out.println(content);
    }
}

The test starts a server and deploys the output of the build process (a .war file) to it. The test runs in the integration test phase, rather than the unit test phase, to make sure this build output is available when it runs. The test then sends a request to the server using the provided HtmlUnit webClient. Note that the webClient can be used for any other HTTP requests your test requires.

If you want to inspect the app yourself, you can manually deploy the WAR file (security/restBasicAuthDBStoreAndCustomStore/target/restBasicAuthDBStoreAndCustomStore.war) to the server of your choice (e.g. GlassFish 7), and request the URL via a browser or a commandline util such as curl.

We have two tests here: in one test we try to authenticate as "john", in the other test as "pete". As we’ve seen, each identity store only validates one of them. The fact that both tests pass demonstrates that each store will validate the right user, and that not recognizing a username by any of them will not fail the overall validation.

Securing an endpoint with Form authentication

In the following example, we’ll secure a REST endpoint using Form authentication.

You’ll learn how to:

  1. Define security constraints

  2. Use the Form authentication mechanism

  3. How to define (and implicitly set) a custom identity store

  4. Use the Jakarta Security SecurityContext

Write the application

Let’s start with defining a simple REST resource class for a /rest/resource endpoint:

@Path("/resource")
@RequestScoped
public class Resource {

    @Inject
    private SecurityContext securityContext;

    @GET
    @Produces(TEXT_PLAIN)
    public String getCallerAndRole() {
        return
            securityContext.getCallerPrincipal().getName() + " : " +
            securityContext.isCallerInRole("user");
    }

}

This resource uses the injected Jakarta EE SecurityContext to obtain access to the current authenticated caller, which is represented by a Principal instance.

If this resource were available to unauthenticated callers, getCallerPrincipal() would return null for unauthenticated requests, so we’d have to check for null. Our example, however, requires authentication for this resource, so we can skip that check.

There is a Jakarta REST-specific type that is also named SecurityContext and has similar methods as the ones we used here. From the Jakarta EE perspective, that is a discouraged type and the Jakarta Security version is to be preferred.

Declare the security constraints

Next we’ll define the security constraints in web.xml, which tell the security system that access to a given URL or URL pattern is protected, and hence authentication is required:

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="https://jakarta.ee/xml/ns/jakartaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="https://jakarta.ee/xml/ns/jakartaee https://jakarta.ee/xml/ns/jakartaee/web-app_6_0.xsd"
         version="6.0">

    <security-constraint>
        <web-resource-collection>
            <web-resource-name>protected</web-resource-name>
            <url-pattern>/rest/*</url-pattern>
        </web-resource-collection>
        <auth-constraint>
            <role-name>user</role-name>
        </auth-constraint>
    </security-constraint>

</web-app>

This XML essentially says that to access any URL that starts with "/rest" requires the caller to have the role "user". Roles are opaque strings; merely identifiers. It’s fully up to the application how broad or fine-grained they are.

In Jakarta EE, internally these XML constraints are transformed into Permission instances and made available via a specific type of permission store. Knowledge about this transformation is only needed for very advanced use cases.
The observant reader may wonder if XML is really the only option here, given the strong feelings that exist in parts of the community around XML. The answer is yes and no. Jakarta EE does define the @RolesAllowed annotation that could be used to replace the XML shown above, but only the legacy Enterprise Beans has specified a behaviour for this when put on an Enterprise Bean. Jakarta REST has done no such thing, although the JWT API in MicroProfile has defined this for REST resources. In Jakarta EE, however, this remains a vendor-specific extension. There are also a number of annotations and APIs in Jakarta EE to set these kinds of constraints for individual Servlets, but those won’t help us much either here.

Declare the authentication mechanism

@ApplicationScoped
@FormAuthenticationMechanismDefinition(
    loginToContinue = @LoginToContinue(
        loginPage="/login.html",
        errorPage="/login-error.html"
    )
)
@DeclareRoles({ "user", "caller" })
@ApplicationPath("/rest")
public class ApplicationConfig extends Application {

}

To declare the usage of a specific authentication mechanism, Jakarta EE provides [XYZ]MechanismDefinition annotations. Such an annotation is picked up by the security system, and in response to it a CDI bean that implements the HttpAuthenticationMechanism is enabled for it.

The annotation can be put on any bean, but in a REST application it fits particularly well on the Application subclass because it also declares the path for REST resources.

Contrary to the Basic HTTP authentication mechanism, the Form authentication mechanism allows us to customize the login dialog (the process between the caller and the authentication mechanism) and to keep track of the authenticated session on the server (using a cookie). This also allows us to logout, something that for unknown reasons has never been specified for Basic HTTP authentication.

To use this authentication method, we need to designate two paths to resources that are relative to our application. One path is for the login page, which the user will be directed to when attempting to access a protected resource. The other path is for when login fails, such as when the user enters incorrect login credentials. If the paths are the same, a request parameter can be used to distinguish between them. Paths can point to anything our server can respond to; a static HTML file, a REST resource, or anything else. For simplicity, we’ll use two static HTML files here:

<!DOCTYPE html>
<html lang="en">
    <head><title>Login to continue</title></head>
    <body>
        <h1>Login to continue</h1>
        <form method="post" action="j_security_check">
            <div>
                <label>Username: <input type="text" name="j_username"></label>
            </div>
            <div>
                <label>Password: <input type="password" name="j_password"></label>
            </div>
            <div>
                <input type="submit" value="Submit">
            </div>
        </form>
    </body>
</html>
<!DOCTYPE html>
<html lang="en">
    <head><title>Login failed!</title></head>
    <body>
        <h1>Login failed!</h1>
        <div>
            <a href="login.html">Try again</a>
        </div>
    </body>
</html>

Define the identity store

Finally, let’s define a basic identity store that the security system can use to validate provided credentials for Form authentication:

@ApplicationScoped
public class CustomIdentityStore implements IdentityStore {

    public CredentialValidationResult validate(UsernamePasswordCredential usernamePasswordCredential) {
        if (usernamePasswordCredential.compareTo("john", "secret1")) {
            return new CredentialValidationResult("john", Set.of("user", "caller"));
        }

        return INVALID_RESULT;
    }

}

This identity store only validates the single identity (user) "john", with password "secret1" and roles "user" and "caller". Defining this kind of identity store is often the simplest way to get started. Note that Jakarta Security doesn’t define a simple identity store out of the box, because there are questions about whether that would promote security best practices.

The identity store is installed and used by the security system just by the virtue of being there; it picks up all enabled CDI beans that implement IdentityStore. Such beans can be enabled by the security system itself (following some configuration annotation), or can be programmatically added using the appropriate CDI APIs. Where the bean comes from doesn’t matter for Jakarta Security, only the fact that it’s there.

Test the application

It’s now time to test our application. A ready-to-test version is available from the Jakarta EE Examples project at https://github.com/eclipse-ee4j/jakartaee-examples.

Download or clone this repo, then cd into the focused folder and execute:

mvn clean install -pl :restBasicAuthCustomStore

This will run a test associated with the project, printing something like the following:

john : true
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 5.24 s - in jakartaee.examples.focused.security.restformauthcustomstore.RestFormAuthCustomStoreIT

Let’s take a quick look at the actual test:

@RunWith(Arquillian.class)
@RunAsClient
public class RestFormAuthCustomStoreIT extends ITBase {

    @ArquillianResource
    private URL baseUrl;

    /**
     * Test the call to a protected REST service
     *
     * @throws Exception when a serious error occurs.
     */
    @RunAsClient
    @Test
    public void testRestCall() throws Exception {
        HtmlPage loginPage = webClient.getPage(baseUrl + "/rest/resource");
        System.out.println(loginPage.asXml());

        HtmlForm form = loginPage.getForms()
                                 .get(0);

        form.getInputByName("j_username")
            .setValueAttribute("john");

        form.getInputByName("j_password")
            .setValueAttribute("secret1");

        TextPage page = form.getInputByValue("Submit")
                   .click();

        System.out.println(page.getContent());
    }
}

The test starts a server and deploys the output of the build process (a .war file) to it. The test runs in the integration test phase, rather than the unit test phase, to make sure this build output is available when it runs. The test then sends a request to the server using the provided HtmlUnit webClient. Note that the webClient can be used for any other HTTP requests your test requires.

If you want to inspect the app yourself, you can manually deploy the WAR file (security/restBasicAuthCustomStore/target/restBasicAuthCustomStore.war) to the server of your choice (e.g. GlassFish 7), and request the URL via a browser or a commandline util such as curl.

The test first sends a request here to the protected resource, and the server responds with the HTML form we defined above. Using the HtmlUnit API, it’s easy to navigate the HTML DOM, fill out the username and password in the form, and programmatically click the Submit button. The form posts back to a special "j_security_check" URL, where the authentication mechanism receives the request and retrieves the username and password from the POST data, much like the Basic authentication mechanism retrieves them from the HTTP headers.

Securing an endpoint with Basic authentication and a custom algorithm for handling multiple identity stores

In the following example, we’ll be securing a REST endpoint using Basic authentication and two identity stores: the database identity store that is provided by Jakarta Security and a custom identity store. Instead of relying on the default algorithm provided by Jakarta Security to handle multiple identity stores we’ll be using a custom algoritm.

You’ll learn how to:

  1. Define security constraints

  2. Use the provided BasicAuthenticationMechanismDefinition

  3. Use the provided DatabaseIdentityStoreDefinition

  4. Create a custom identity store

  5. Create a custom identity store handler

  6. Use the Jakarta Security SecurityContext

Write the application

We’ll use a slightly modified resource and security constraints compared to the ones we used for the Securing an endpoint with Basic authentication example.

The REST resource is now as follows:

@Path("/resource")
@RequestScoped
public class Resource {

    @Inject
    private SecurityContext securityContext;

    @GET
    @Produces(TEXT_PLAIN)
    public String getCallerAndRole() {
        return
            securityContext.getCallerPrincipal().getName() + " : " +
            securityContext.isCallerInRole("user") + "," +
            securityContext.isCallerInRole("caller1") + "," +
            securityContext.isCallerInRole("caller2");
    }

}

As can be seen, the difference is quite small; we’re now printing out the results of two extra role checks.

web.xml on its turn looks as follows now:

<web-app xmlns="https://jakarta.ee/xml/ns/jakartaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="https://jakarta.ee/xml/ns/jakartaee https://jakarta.ee/xml/ns/jakartaee/web-app_6_0.xsd"
         version="6.0">

    <security-constraint>
        <web-resource-collection>
            <web-resource-name>protected</web-resource-name>
            <url-pattern>/rest/*</url-pattern>
        </web-resource-collection>
        <auth-constraint>
            <role-name>user</role-name>
            <role-name>caller2</role-name>
        </auth-constraint>
    </security-constraint>
    <security-role>
        <role-name>caller1</role-name>
    </security-role>

</web-app>

Compared to the example in Securing an endpoint with Basic authentication we have now added an extra role to the <auth-constraint> section. The semantics of that are that a caller needs to have both of these roles in order to be authorised to access the resource under /rest/*.

Although it’s customary to explicitly declare all roles in the application using <security-role>, it’s technically not needed. As long as the role name appears in some XML fragment or annotation attribute the Jakarta EE requirement to declare all roles upfront is satisfied. As we can see in the fragment above, the role names "user" and "caller2" already appear in the <auth-constraint> section, so they don’t have to be repeated.

The reason it’s deemed good practice to list all roles in the <security-role> element in web.xml (or alternatively in an @DeclareRoles annotation) even when not really needed is to have a single place where all roles are listed, instead of them being scattered throughout the application.

Declare the authentication mechanism and identity store

@ApplicationScoped
@BasicAuthenticationMechanismDefinition(
    realmName = "basicAuth"
)
@DatabaseIdentityStoreDefinition(
    callerQuery = "select password from basic_auth_user where username = ?",
    groupsQuery = "select name from basic_auth_group where username = ?",
    hashAlgorithmParameters = {
        "Pbkdf2PasswordHash.Iterations=3072",
        "Pbkdf2PasswordHash.Algorithm=PBKDF2WithHmacSHA512",
        "Pbkdf2PasswordHash.SaltSizeBytes=64"
    }
)
@DeclareRoles("user")
@ApplicationPath("/rest")
public class ApplicationConfig extends Application {
@ApplicationScoped
public class CustomIdentityStore implements IdentityStore {

    public CredentialValidationResult validate(UsernamePasswordCredential usernamePasswordCredential) {
        if (usernamePasswordCredential.compareTo("john", "secret1")) {
            return new CredentialValidationResult("john", Set.of("caller1", "caller2"));
        }

        return INVALID_RESULT;
    }

}

In this example we have two enabled CDI beans implementing the IdentityStore interface. One of them will be implicitly enabled via the @DatabaseIdentityStoreDefinition annotation, while the other one is defined explicitly via the CustomIdentityStore class. As with a single identity store, it doesn’t matter how or where the CDI beans are defined, only that multiple enabled ones exist.

When multiple identity stores are present, an identity store handler (of type IdentityStoreHandler) is consulted. Jakara Security provides a default one as explained in Securing an endpoint with Basic authentication and multiple identity stores. This default handler can be overridden however to provide custom semantics. We’ll use a custom handler to enforce a caller authenticates with both identity stores, and we’ll combine the roles returned by both in the final result.

Populating the identity store

In order to use the identity store, we need to put some data in a database. This is done in the same as in [Securing an endpoint with Basic authentication and a Database identity store].

In the custom identity store defined above and in the database identity store here we both use name "john' and password "secret1".

Writing the identity store handler

We’ll now write the identity store handler:

@Alternative (1)
@Priority(APPLICATION) (2)
@ApplicationScoped
public class CustomIdentityStoreHandler implements IdentityStoreHandler {

    @Inject
    Instance<IdentityStore> identityStores; (3)

    @Override
    public CredentialValidationResult validate(Credential credential) {
        CredentialValidationResult result = null;
        Set<String> groups = new HashSet<>();

        for (IdentityStore identityStore : identityStores) {
            result = identityStore.validate(credential);
            if (result.getStatus() == NOT_VALIDATED) {
                // Identity store probably doesn't handle our credential type
                continue;
            }

            if (result.getStatus() == INVALID) {
                // Identity store handled our credential type and determined its
                // invalid. End the loop.
                return INVALID_RESULT;
            }

            groups.addAll(result.getCallerGroups());
        }

        return new CredentialValidationResult(
            result.getCallerPrincipal(), groups);
    }
}
1 Since we’re overriding an existing CDI bean (the default IdentityStoreHandler provided by Jakarta Security), we have to annotate our custom IdentityStoreHandler with @Alternative.
2 To make @Alternative actually work, we additionally have to annotate with @Priority(APPLICATION)
3 With @Inject Instance<IdentityStore> identityStores CDI will give us a collection of all identity stores in the application. In the case of this example that will be the store behind @DatabaseIdentityStoreDefinition and our CustomIdentityStore. We can the iterate over those stores in our code, and offer the credentials (the username and password in this example) to each of them.

There are various result outcomes possible.

NOT_VALIDATED means the store did not try to validate the credentials at all. In most situations that status is set when the store in question doesnt’t handle a given credential. I.e. it only handles say JWTCredentials and not UsernamePasswordCredential.

INVALID means the store tried to validate the credentials, and validation failed. For example the username and password were wrong.

In our custom handler code here we return an INVALID_RESULT for the first store that fails, as we want all stores to validate successfully here. If validation does succeed (the outcome is VALID then) we grab the groups it returned and store in a set.

Identity stores also have a capability to query it for roles directly, without validating credentials. We haven’t used that feature here.

Eventually we return a result based on the CallerPrincipal from the last successful validation result, and all the collected groups.

In our example it doesn’t matter from which validation result we grab the CallerPrincipal as it’s all the one with name "pete" here. In general identity stores may transform the name from the input credential (for example "pete") to something else (for example "Pete Anderson").

Test the application

It’s now time to test our application. A ready-to-test version is available from the Jakarta EE Examples project at https://github.com/eclipse-ee4j/jakartaee-examples.

Download or clone this repo, then cd into the focused folder and execute:

mvn clean install -pl :restBasicAuthCustomStoreHandler

This will run a test associated with the project, printing something like the following:

john : true,true,true
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 7.634 s - in jakartaee.examples.focused.security.restbasicauthcustomstorehandler.RestBasicAuthCustomStoreHandlerIT

The resource that we defined above required only two roles to access it (user and caller2), but our custom identity store also returned caller1. The resource we created tests for this, and as it appears, we indeed had this role.

If we hadn’t declared caller1 in web.xml (or via an annotation), the test for caller1 might have returned false. This is however server dependent.

Securing an endpoint with a custom authentication mechanism and a custom identity store

In the following example, we’ll be securing a REST endpoint using a custom authentication mechanism. A custom authentication mechanism is one we provide ourselves, instead of using one provided by Jakarta Security (such as the Basic HTTP authentication mechanism).

You’ll learn how to:

  1. Define security constraints

  2. Define (and implicitly set) a custom authentication mechanism

  3. Define (and implicitly set) a custom identity store

  4. Use the Jakarta Security SecurityContext

Write the application

Let’s start with defining a simple REST resource class for a /rest/resource endpoint:

@Path("/resource")
@RequestScoped
public class Resource {

    @Inject
    private SecurityContext securityContext;

    @GET
    @Produces(TEXT_PLAIN)
    public String getCallerAndRole() {
        return
            securityContext.getCallerPrincipal().getName() + " : " +
            securityContext.isCallerInRole("user");
    }

}

This resource uses the injected Jakarta EE SecurityContext to obtain access to the current authenticated caller, which is represented by a Principal instance.

If this resource were available to unauthenticated callers, getCallerPrincipal() would return null for unauthenticated requests, so we’d have to check for null. Our example, however, requires authentication for this resource, so we can skip that check.

There is a Jakarta REST-specific type that is also named SecurityContext and has similar methods as the ones we used here. From the Jakarta EE perspective, that is a discouraged type and the Jakarta Security version is to be preferred.

Define the authentication mechanism

Let’s now define a simple authentication mechanism that the security system can use to interact with the caller who tries to access a resource:

@ApplicationScoped
public class CustomAuthenticationMechanism implements HttpAuthenticationMechanism {

    @Inject
    private IdentityStoreHandler identityStoreHandler;

    @Override
    public AuthenticationStatus validateRequest(
        HttpServletRequest request,
        HttpServletResponse response,
        HttpMessageContext httpMessageContext) throws AuthenticationException {

        var callerName = request.getHeader("callername"); (1)
        var password = request.getHeader("callerpassword");

        if (callerName == null || password == null) { (2)
            return httpMessageContext.doNothing();
        }

        var result = identityStoreHandler.validate( (4)
            new UsernamePasswordCredential(callerName, password)); (3)

        if (result.getStatus() != VALID) {
            return httpMessageContext.responseUnauthorized();
        }

        return httpMessageContext.notifyContainerAboutLogin( (5)
            result.getCallerPrincipal(),
            result.getCallerGroups());
    }

}

This custom authentication mechanism interacts with the caller by grabbing two headers from the request: callername and callerpassword. (1) In case any of them are null, we return a special status; the "do nothing" status. (2) This means there has been no request or attempt to do authentication. If the resource the caller is trying to access is not protected, the caller can access it anonymously. If it is proteced, the caller will not be able to access it.

When the two required headers are provided by the caller, we create a UsernamePasswordCredential out of their values (3) and pass that into the injected IdentityStoreHandler. (4) We saw how this type of handler worked in the example Securing an endpoint with Basic authentication and a custom algorithm for handling multiple identity stores.

An authentication mechanism in Jakarta Security is not strictly required to delegate the credential validation to the identity store handler. However not doing so is considered bad practice, as it would restrict developers from things like inserting extra identity stores into the chain that can do things like adding extra groups.

If the credentials validated correctly, we use the HttpMessageContext to communicate the details of the authenticated caller to the container. (5)

In Jakarta Security the two basic items that make up an "authenticated identity" are just a caller principal (of type Principal) and a set of groups (of type String). Via a Service Provider Interface a specific Jakarta EE product (such as WildFly or GlassFish) is able to receive these two items and then stores it internally in some way.

The authentication mechanism is installed and used by the security system just by the virtue of being there; it picks up all enabled CDI beans that implement HttpAuthenticationMechanism. Such beans can be enabled by the security system itself (following some configuration annotation), or can be programmatically added using the appropriate CDI APIs. Where the bean comes from doesn’t matter for Jakarta Security, only the fact that it’s there.

Define the identity store

Finally, let’s define a simple identity store that the security system can use to validate provided credentials for Basic authentication:

@ApplicationScoped
public class TestIdentityStore implements IdentityStore {

    public CredentialValidationResult validate(UsernamePasswordCredential usernamePasswordCredential) {
        if (usernamePasswordCredential.compareTo("john", "secret1")) {
            return new CredentialValidationResult("john", Set.of("user", "caller"));
        }

        return INVALID_RESULT;
    }

}

This identity store only validates the single identity (user) "john", with password "secret1" and roles "user" and "caller". Defining this kind of identity store is often the simplest way to get started.

Jakarta Security doesn’t provide a simple identity store out of the box. The reason is that everything in Jakarta Security promotes best practices, and it’s not clear if a simple identity store fits in with those best practices.

The identity store is installed and used by the security system just by the virtue of being there; it picks up all enabled CDI beans that implement IdentityStore. Such beans can be enabled by the security system itself (following some configuration annotation), or can be programmatically added using the appropriate CDI APIs. Where the bean comes from doesn’t matter for Jakarta Security, only the fact that it’s there.

Test the application

It’s now time to test our application. A ready-to-test version is available from the Jakarta EE Examples project at https://github.com/eclipse-ee4j/jakartaee-examples.

Download or clone this repo, then cd into the focused folder and execute:

mvn clean install -pl :restCustomAuthCustomStore

This will run a test associated with the project, printing something like the following:

john : true
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 4.591 s - in jakartaee.examples.focused.security.restcustomauthcustomstore.RestCustomAuthCustomStoreIT

Let’s take a quick look at the actual test:

@RunWith(Arquillian.class)
@RunAsClient
public class RestCustomAuthCustomStoreIT extends ITBase {

    @ArquillianResource
    private URL baseUrl;

    /**
     * Test the call to a protected REST service
     *
     * @throws Exception when a serious error occurs.
     */
    @RunAsClient
    @Test
    public void testRestCall() throws Exception {
        webClient.addRequestHeader("callername", "john");
        webClient.addRequestHeader("callerpassword", "secret1");

        TextPage page = webClient.getPage(baseUrl + "rest/resource");
        String content = page.getContent();

        System.out.println(content);
    }
}

The test starts a server and deploys the output of the build process (a .war file) to it. The test runs in the integration test phase, rather than the unit test phase, to make sure this build output is available when it runs. The test then sends a request to the server using the provided HtmlUnit webClient. Note that the webClient can be used for any other HTTP requests your test requires.

If you want to inspect the app yourself, you can manually deploy the WAR file (security/restCustomAuthCustomStore/target/restCustomAuthCustomStore.war) to the server of your choice (e.g. GlassFish 7), and request the URL via a browser or a commandline util such as curl.

The webClient.addRequestHeader() calls used here make sure that the headers for our custom authentication mechanism are added to the request. The authentication mechanism that we defined for our applications reads those headers, extracts the username and password from them, and consults our identity store with them.

Securing an endpoint with Form authentication and remember-me

In the following example, we’ll secure a REST endpoint using Form authentication and remember-me.

Remember-me is a facility where an authenticated identity can be remembered beyond the scope of an HTTP session. This happens via a separate cookie that has a longer life-time than the cookie used for the HTTP session (and the session itself on the server).

You’ll learn how to:

  1. Define security constraints

  2. Use the Form authentication mechanism

  3. Enable the remember-me feature

  4. How to define (and implicitly set) a custom remember-me identity store

  5. How to define (and implicitly set) a custom identity store

  6. Use the Jakarta Security SecurityContext

Write the application

Let’s start with defining a simple REST resource class for a /rest/resource endpoint:

@Path("/resource")
@RequestScoped
public class Resource {

    @Inject
    private SecurityContext securityContext;

    @GET
    @Produces(TEXT_PLAIN)
    public String getCallerAndRole() {
        return
            securityContext.getCallerPrincipal().getName() + " : " +
            securityContext.isCallerInRole("user");
    }

}

This resource uses the injected Jakarta EE SecurityContext to obtain access to the current authenticated caller, which is represented by a Principal instance.

If this resource were available to unauthenticated callers, getCallerPrincipal() would return null for unauthenticated requests, so we’d have to check for null. Our example, however, requires authentication for this resource, so we can skip that check.

There is a Jakarta REST-specific type that is also named SecurityContext and has similar methods as the ones we used here. From the Jakarta EE perspective, that is a discouraged type and the Jakarta Security version is to be preferred.

Declare the security constraints

Next we’ll define the security constraints in web.xml, which tell the security system that access to a given URL or URL pattern is protected, and hence authentication is required:

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="https://jakarta.ee/xml/ns/jakartaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="https://jakarta.ee/xml/ns/jakartaee https://jakarta.ee/xml/ns/jakartaee/web-app_6_0.xsd"
         version="6.0">

    <security-constraint>
        <web-resource-collection>
            <web-resource-name>protected</web-resource-name>
            <url-pattern>/rest/*</url-pattern>
        </web-resource-collection>
        <auth-constraint>
            <role-name>user</role-name>
        </auth-constraint>
    </security-constraint>

</web-app>

This XML essentially says that to access any URL that starts with "/rest" requires the caller to have the role "user". Roles are opaque strings; merely identifiers. It’s fully up to the application how broad or fine-grained they are.

In Jakarta EE, internally these XML constraints are transformed into Permission instances and made available via a specific type of permission store. Knowledge about this transformation is only needed for very advanced use cases.
The observant reader may wonder if XML is really the only option here, given the strong feelings that exist in parts of the community around XML. The answer is yes and no. Jakarta EE does define the @RolesAllowed annotation that could be used to replace the XML shown above, but only the legacy Enterprise Beans has specified a behaviour for this when put on an Enterprise Bean. Jakarta REST has done no such thing, although the JWT API in MicroProfile has defined this for REST resources. In Jakarta EE, however, this remains a vendor-specific extension. There are also a number of annotations and APIs in Jakarta EE to set these kinds of constraints for individual Servlets, but those won’t help us much either here.

Declare the authentication mechanism

We’ll use the same authentication mechanism declaration as we used for Securing an endpoint with Form authentication

Enable remember-me

In Jakarta Security, there are several services available through CDI Interceptors [1], one of which is the remember-me service. Remember-me can be transparently applied to basically every authentication mechanism. In CDI, it’s trivial to add Interceptors to beans that we define ourselves, but a little less trivial to add to provided beans. In this section we explain how to do this via a CDI extension.

For this example, we’ll add the CDI extension interface (1) to our application config class:

@ApplicationScoped
@FormAuthenticationMechanismDefinition(
    loginToContinue = @LoginToContinue(
        loginPage="/login.html",
        errorPage="/login-error.html"
    )
)
@ApplicationPath("/rest")
public class ApplicationConfig extends Application
    implements BuildCompatibleExtension { (1)

    @Enhancement(
        types = HttpAuthenticationMechanism.class,
        withSubtypes = true) (2)
    public void addRememberMe(ClassConfig httpAuthenticationMechanism) {
        httpAuthenticationMechanism.addAnnotation(
            RememberMe.Literal.INSTANCE); (3)
    }
}

CDI allows us to enhance classes using a method annotated with the @Enhancement annotation and as attribute the class we’re seeking to enhance. For our example that will be a sub-type of the HttpAuthenticationMechanism interface (we know the bean enabled by FormAuthenticationMechanismDefinition will implement the HttpAuthenticationMechanism interface), hence we set the withSubtypes attribute to true. (2)

Within the method we can then programmatically add the @RememberMe annotation used to bind the remember-me interceptor to a class. In the example here we use the default instance (which has all attributes set to their defaults). There are attributes for setting various aspects of the cookie, such as its name, whether it should be secure and http only, and perhaps most importantly the max age of the cookie (default is one day).

Define the remember-me identity store

For remember-me to work a token has to be created that is used as a credential to authenticate right away instead of invoking the authentication mechanism that is being intercepted. Jakarta Security uses a special identity store for this; the RememberMeIdentityStore. This type of identity store is exclusively used by the remember-me feature, hence it’s a different type from IdentityStore.

Jakarta Security does not ship with any provided remember-me identity store, but for demonstration purposes we can easily create one ourselves.

The following shows an example:

@ApplicationScoped
public class CustomRememberMeIdentityStore implements RememberMeIdentityStore {

    private final Map<String, CredentialValidationResult> tokenToIdentityMap =
        new ConcurrentHashMap<>();

    @Override
    public String generateLoginToken(
        CallerPrincipal callerPrincipal, Set<String> groups) { (1)
        var token = UUID.randomUUID().toString();

        tokenToIdentityMap.put(
            token,
            new CredentialValidationResult(callerPrincipal, groups));

        return token;
    }

    @Override
    public CredentialValidationResult validate(
        RememberMeCredential credential) { (2)
        if (tokenToIdentityMap.containsKey(credential.getToken())) {
            return tokenToIdentityMap.get(credential.getToken());
        }

        return INVALID_RESULT;
    }

    @Override
    public void removeLoginToken(String token) { (3)
        tokenToIdentityMap.remove(token);
    }

}

The RememberMeIdentityStore needs to perform 3 tasks.

It first needs to generate a token representing a caller principal and a set of groups. The caller principal and the set of groups are the ones set by the authentication mechanism right after the caller successfully authenticated. In our example (1) here we’re generating a random UUID that’s used as a key in an application scoped map.

Storing the authenticated identity (principal and groups) in an application scoped map is just an example. Other options could be storing it in a database or key-value store, encrypting the principal and groups, or generating some kind of JSON Web Token (JWT).
When storing the Principal, care must be taken that the Principal could be an elaborate custom Principal containing many more fields than just name.

The next thing that must be done is essentially similar to what a normal identity store does: validating a Credential. For a RememberMeIdentityStore this will always be of type RememberMeCredential with getToken() returning a token of the kind that was generated in generateLoginToken(). In our example (2) we’re just using the token as key in our map.

Finally we can provide behaviour to remove the login token (and essentially invalidate it) via the removeLoginToken method. This method is called when a caller explicitly logs out. In our example (3) we just remove the token from our map.

When storing the principal and groups in a token that we send to the client we can’t always easily invalidate it when the caller logs out; the caller can always keep the token and send it again.

Define the identity store

Finally, let’s define a simple identity store that the security system can use to validate provided credentials for Basic authentication:

@ApplicationScoped
public class TestIdentityStore implements IdentityStore {

    public CredentialValidationResult validate(UsernamePasswordCredential usernamePasswordCredential) {
        if (usernamePasswordCredential.compareTo("john", "secret1")) {
            return new CredentialValidationResult("john", Set.of("user", "caller"));
        }

        return INVALID_RESULT;
    }

}

This identity store only validates the single identity (user) "john", with password "secret1" and roles "user" and "caller". Defining this kind of identity store is often the simplest way to get started.

Jakarta Security doesn’t provide a simple identity store out of the box. The reason is that everything in Jakarta Security promotes best practices, and it’s not clear if a simple identity store fits in with those best practices.

The identity store is installed and used by the security system just by the virtue of being there; it picks up all enabled CDI beans that implement IdentityStore. Such beans can be enabled by the security system itself (following some configuration annotation), or can be programmatically added using the appropriate CDI APIs. Where the bean comes from doesn’t matter for Jakarta Security, only the fact that it’s there.

Test the application

It’s now time to test our application. A ready-to-test version is available from the Jakarta EE Examples project at https://github.com/eclipse-ee4j/jakartaee-examples.

Download or clone this repo, then cd into the focused folder and execute:

mvn clean install -pl :restFormAuthCustomStoreRememberMe

This will run a test associated with the project, printing something like the following:

john : true
john : true
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 5.702 s - in jakartaee.examples.focused.security.restformauthcustomatorerememberme.RestFormAuthCustomStoreIT

Let’s take a quick look at the actual test:

@RunWith(Arquillian.class)
@RunAsClient
public class RestFormAuthCustomStoreRememberMeIT extends ITBase {

    @ArquillianResource
    private URL baseUrl;

    /**
     * Test the call to a protected REST service
     *
     * @throws Exception when a serious error occurs.
     */
    @RunAsClient
    @Test
    public void testRestCall() throws Exception {
        // Initial request
        HtmlPage loginPage = webClient.getPage(baseUrl + "/rest/resource");
        System.out.println(loginPage.asXml());

        // Response is login form, so we can authenticate
        HtmlForm form = loginPage.getForms()
                                 .get(0);

        form.getInputByName("j_username")
            .setValueAttribute("john");

        form.getInputByName("j_password")
            .setValueAttribute("secret1");

        // After logging in, we should get the actual resource response
        TextPage page = form.getInputByValue("Submit")
                   .click();

        System.out.println(page.getContent());

        // Remove all cookies (specially the JSESSONID), except for the
        // JREMEMBERMEID cookie which carries the token to login again
        for (Cookie cookie : webClient.getCookieManager().getCookies()) {
            if (!"JREMEMBERMEID".equals(cookie.getName())) {
                webClient.getCookieManager().removeCookie(cookie);
            }
        }

        // Should get the resource response, and not the login form
        TextPage pageAgain = webClient.getPage(baseUrl + "/rest/resource");

        System.out.println(pageAgain.getContent());
    }
}

The test starts a server and deploys the output of the build process (a .war file) to it. The test runs in the integration test phase, rather than the unit test phase, to make sure this build output is available when it runs. The test then sends a request to the server using the provided HtmlUnit webClient. Note that the webClient can be used for any other HTTP requests your test requires.

If you want to inspect the app yourself, you can manually deploy the WAR file (security/restFormAuthCustomStoreRememberMe/target/restFormAuthCustomStoreRememberMe.war) to the server of your choice (e.g. GlassFish 7), and request the URL via a browser or a commandline util such as curl.

The test first sends a request here to the protected resource, and the server responds with the HTML form we defined above. Using the HtmlUnit API, it’s easy to navigate the HTML DOM, fill out the username and password in the form, and programmatically click the Submit button. The form posts back to a special "j_security_check" URL, where the authentication mechanism receives the request and retrieves the username and password from the POST data, much like the Basic authentication mechanism retrieves them from the HTTP headers.

Then we delete all cookies, specifically the JSESSIONID cookie that keeps the session that the form authentication mechanism uses to remember the authenticated identity. The test then does another request, and this time the value from the JREMEMBERMEID cookie is used to login.

Securing an endpoint with a custom authentication mechanism, a custom identity store and remember-me

In the following example, we’ll secure a REST endpoint using custom authentication and remember-me.

Remember-me is a facility where an authenticated identity can be remembered beyond the scope of an HTTP session. This happens via a separate cookie that has a longer life-time than the cookie used for the HTTP session (and the session itself on the server).

You’ll learn how to:

  1. Define security constraints

  2. Define (and implicitly set) a custom authentication mechanism with remember-me

  3. How to define (and implicitly set) a custom remember-me identity store

  4. Define (and implicitly set) a custom identity store

  5. Use the Jakarta Security SecurityContext

Write the application

Let’s start with defining a simple REST resource class for a /rest/resource endpoint:

@Path("/resource")
@RequestScoped
public class Resource {

    @Inject
    private SecurityContext securityContext;

    @GET
    @Produces(TEXT_PLAIN)
    public String getCallerAndRole() {
        return
            securityContext.getCallerPrincipal().getName() + " : " +
            securityContext.isCallerInRole("user");
    }

}

This resource uses the injected Jakarta EE SecurityContext to obtain access to the current authenticated caller, which is represented by a Principal instance.

If this resource were available to unauthenticated callers, getCallerPrincipal() would return null for unauthenticated requests, so we’d have to check for null. Our example, however, requires authentication for this resource, so we can skip that check.

There is a Jakarta REST-specific type that is also named SecurityContext and has similar methods as the ones we used here. From the Jakarta EE perspective, that is a discouraged type and the Jakarta Security version is to be preferred.

Define the authentication mechanism

Let’s now define a simple authentication mechanism that the security system can use to interact with the caller who tries to access a resource and specifically make sure the RememberMe feature is used:

@RememberMe (6)
@ApplicationScoped
public class CustomAuthenticationMechanism implements HttpAuthenticationMechanism {

    @Inject
    private IdentityStoreHandler identityStoreHandler;

    @Override
    public AuthenticationStatus validateRequest(
        HttpServletRequest request,
        HttpServletResponse response,
        HttpMessageContext httpMessageContext) throws AuthenticationException {

        var callerName = request.getHeader("callername"); (1)
        var password = request.getHeader("callerpassword");

        if (callerName == null || password == null) { (2)
            return httpMessageContext.doNothing();
        }

        var result = identityStoreHandler.validate( (4)
            new UsernamePasswordCredential(callerName, password)); (3)

        if (result.getStatus() != VALID) {
            return httpMessageContext.responseUnauthorized();
        }

        return httpMessageContext.notifyContainerAboutLogin( (5)
            result.getCallerPrincipal(),
            result.getCallerGroups());
    }

}
This is the same custom authentication mechanism that was used in Securing an endpoint with a custom authentication mechanism and a custom identity store, but with the @RememberMe annotation added.

This custom authentication mechanism interacts with the caller by grabbing two headers from the request: callername and callerpassword. (1) In case any of them are null, we return a special status; the "do nothing" status. (2) This means there has been no request or attempt to do authentication. If the resource the caller is trying to access is not protected, the caller can access it anonymously. If it is proteced, the caller will not be able to access it.

When the two required headers are provided by the caller, we create a UsernamePasswordCredential out of their values (3) and pass that into the injected IdentityStoreHandler. (4) We’ve seen how such handler worked in the example Securing an endpoint with Basic authentication and a custom algorithm for handling multiple identity stores.

An authentication mechanism in Jakarta Security is not strictly required to delegate the credential validation to the identity store handler. However not doing so is considered bad practice, as it would restrict developers from things like inserting extra identity stores into the chain that can do things like adding extra groups.

If the credentials validated correctly, we use the HttpMessageContext to communicate the details of the authenticated caller to the container. (5)

In Jakarta Security the two basic items that make up an "authenticated identity" are just a caller principal (of type Principal) and a set of groups (of type String). Via a Service Provider Interface a specific Jakarta EE product (such as WildFly or GlassFish) is able to receive these two items and then stores it internally in some way.

We annotate our custom authentication mechanism with the @RememberMe annotation to enable the remember-me feature for use with this authentication mechanism. In the example here we don’t set any attributes (all of them have default values). There are attributes for setting various aspects of the cookie used for remember-me, such as its name, whether it should be secure and http only, and perhaps most importantly the max age of the cookie (default is one day).

Instead of using the @RememberMe annotation here, we could also have used the same extension that was used in Securing an endpoint with Form authentication and remember-me to enable the remember-me feature. The annotation however is a little bit easier to use.

The authentication mechanism is installed and used by the security system just by the virtue of being there; it picks up all enabled CDI beans that implement HttpAuthenticationMechanism. Such beans can be enabled by the security system itself (following some configuration annotation), or can be programmatically added using the appropriate CDI APIs. Where the bean comes from doesn’t matter for Jakarta Security, only the fact that it’s there.

Define the identity store

Finally, let’s define a simple identity store that the security system can use to validate provided credentials for Basic authentication:

@ApplicationScoped
public class TestIdentityStore implements IdentityStore {

    public CredentialValidationResult validate(UsernamePasswordCredential usernamePasswordCredential) {
        if (usernamePasswordCredential.compareTo("john", "secret1")) {
            return new CredentialValidationResult("john", Set.of("user", "caller"));
        }

        return INVALID_RESULT;
    }

}

This identity store only validates the single identity (user) "john", with password "secret1" and roles "user" and "caller". Defining this kind of identity store is often the simplest way to get started.

Jakarta Security doesn’t provide a simple identity store out of the box. The reason is that everything in Jakarta Security promotes best practices, and it’s not clear if a simple identity store fits in with those best practices.

The identity store is installed and used by the security system just by the virtue of being there; it picks up all enabled CDI beans that implement IdentityStore. Such beans can be enabled by the security system itself (following some configuration annotation), or can be programmatically added using the appropriate CDI APIs. Where the bean comes from doesn’t matter for Jakarta Security, only the fact that it’s there.

Define the remember-me identity store

We’ll use the same remember-me identity store as we used for the Securing an endpoint with Form authentication and remember-me example.

Test the application

It’s now time to test our application. A ready-to-test version is available from the Jakarta EE Examples project at https://github.com/eclipse-ee4j/jakartaee-examples.

Download or clone this repo, then cd into the focused folder and execute:

mvn clean install -pl :restCustomAuthCustomStoreRememberMe

This will run a test associated with the project, printing something like the following:

john : true
john : true
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 5.287 s - in jakartaee.examples.focused.security.restcustomauthcustomstorerememberme.RestCustomAuthCustomStoreRememberMeIT

Let’s take a quick look at the actual test:

@RunWith(Arquillian.class)
@RunAsClient
public class RestFormAuthCustomStoreRememberMeIT extends ITBase {

    @ArquillianResource
    private URL baseUrl;

    /**
     * Test the call to a protected REST service
     *
     * @throws Exception when a serious error occurs.
     */
    @RunAsClient
    @Test
    public void testRestCall() throws Exception {
        // Initial request
        HtmlPage loginPage = webClient.getPage(baseUrl + "/rest/resource");
        System.out.println(loginPage.asXml());

        // Response is login form, so we can authenticate
        HtmlForm form = loginPage.getForms()
                                 .get(0);

        form.getInputByName("j_username")
            .setValueAttribute("john");

        form.getInputByName("j_password")
            .setValueAttribute("secret1");

        // After logging in, we should get the actual resource response
        TextPage page = form.getInputByValue("Submit")
                   .click();

        System.out.println(page.getContent());

        // Remove all cookies (specially the JSESSONID), except for the
        // JREMEMBERMEID cookie which carries the token to login again
        for (Cookie cookie : webClient.getCookieManager().getCookies()) {
            if (!"JREMEMBERMEID".equals(cookie.getName())) {
                webClient.getCookieManager().removeCookie(cookie);
            }
        }

        // Should get the resource response, and not the login form
        TextPage pageAgain = webClient.getPage(baseUrl + "/rest/resource");

        System.out.println(pageAgain.getContent());
    }
}

The test starts a server and deploys the output of the build process (a .war file) to it. The test runs in the integration test phase, rather than the unit test phase, to make sure this build output is available when it runs. The test then sends a request to the server using the provided HtmlUnit webClient. Note that the webClient can be used for any other HTTP requests your test requires.

If you want to inspect the app yourself, you can manually deploy the WAR file (security/restCustomAuthCustomStoreRememberMe/target/restCustomAuthCustomStoreRememberMe.war) to the server of your choice (e.g. GlassFish 7), and request the URL via a browser or a commandline util such as curl.

The webClient.addRequestHeader() calls used here make sure that the headers for our custom authentication mechanism are added to the request. The authentication mechanism that we defined for our applications reads those headers, extracts the username and password from them, and consults our identity store with them.

The test sends a request here to the protected resource along with the headers we mentioned above, and the server responds with the right content.

Then we delete all cookies, except for the JREMEMBERMEID cookie, and we unset all headers that we used before. The test then does another request, and this time the value from the JREMEMBERMEID cookie is used to login.

Securing an endpoint with OpenID Connect authentication

In the following example, we’ll be securing a REST endpoint using OpenID Connect authentication.

With OpenID Connect authentication a caller is redirected to a third party server, typically a public one such as Google, Facebook, Linkedin, Apple, and more, but it can be a private one as well. The caller authenticates with that third party server, and is then redirected back along with a token. Our server than validates that token, and if it’s valid the caller is considered authenticated.

You’ll learn how to:

  1. Define security constraints

  2. Use the OpenID Connect authentication mechanism

  3. Define (and implicitly set) a custom identity store used for authorization only

  4. Use the Jakarta Security SecurityContext

Write the application

Let’s start with defining a simple REST resource class for a /rest/resource endpoint:

@Path("/resource")
@RequestScoped
public class Resource {

    @Inject
    private SecurityContext securityContext;

    @GET
    @Produces(TEXT_PLAIN)
    public String getCallerAndRole() {
        return
            securityContext.getCallerPrincipal().getName() + " : " +
            securityContext.isCallerInRole("user");
    }

}

This resource uses the injected Jakarta EE SecurityContext to obtain access to the current authenticated caller, which is represented by a Principal instance.

If this resource were available to unauthenticated callers, getCallerPrincipal() would return null for unauthenticated requests, so we’d have to check for null. Our example, however, requires authentication for this resource, so we can skip that check.

There is a Jakarta REST-specific type that is also named SecurityContext and has similar methods as the ones we used here. From the Jakarta EE perspective, that is a discouraged type and the Jakarta Security version is to be preferred.

Declare the security constraints

Next we’ll define the security constraints in web.xml, which tell the security system that access to a given URL or URL pattern is protected, and hence authentication is required:

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="https://jakarta.ee/xml/ns/jakartaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="https://jakarta.ee/xml/ns/jakartaee https://jakarta.ee/xml/ns/jakartaee/web-app_6_0.xsd"
         version="6.0">

    <security-constraint>
        <web-resource-collection>
            <web-resource-name>protected</web-resource-name>
            <url-pattern>/rest/*</url-pattern>
        </web-resource-collection>
        <auth-constraint>
            <role-name>user</role-name>
        </auth-constraint>
    </security-constraint>

</web-app>

This XML essentially says that to access any URL that starts with "/rest" requires the caller to have the role "user". Roles are opaque strings; merely identifiers. It’s fully up to the application how broad or fine-grained they are.

In Jakarta EE, internally these XML constraints are transformed into Permission instances and made available via a specific type of permission store. Knowledge about this transformation is only needed for very advanced use cases.
The observant reader may wonder if XML is really the only option here, given the strong feelings that exist in parts of the community around XML. The answer is yes and no. Jakarta EE does define the @RolesAllowed annotation that could be used to replace the XML shown above, but only the legacy Enterprise Beans has specified a behaviour for this when put on an Enterprise Bean. Jakarta REST has done no such thing, although the JWT API in MicroProfile has defined this for REST resources. In Jakarta EE, however, this remains a vendor-specific extension. There are also a number of annotations and APIs in Jakarta EE to set these kinds of constraints for individual Servlets, but those won’t help us much either here.

Declare the authentication mechanism

@OpenIdAuthenticationMechanismDefinition(
    providerURI = "https://localhost:8443/openid-connect-server-webapp", (1)
    clientId = "client", (2)
    clientSecret = "secret", (3)
    redirectToOriginalResource = true (4)
)
@ApplicationScoped
@ApplicationPath("/rest")
public class ApplicationConfig extends Application {

}

To declare the usage of a specific authentication mechanism, Jakarta EE provides [XYZ]MechanismDefinition annotations. Such an annotation is picked up by the security system, and in response to it a CDI bean that implements the HttpAuthenticationMechanism is enabled for it.

The annotation can be put on any bean, but in a REST application it fits particularly well on the Application subclass because it also declares the path for REST resources.

Contrary to the Basic HTTP authentication mechanism and the Form authentication mechanism, the OpenID Connect authentication mechanism requires a third party server that performs the actual authentication. Such third party server is called the OpenID Connect Provider (OIDC provider or OpenID Provider are also used). After authentication this provider handles user consent and and issues a token. The client requesting a user’s authentication is called a Relying Party. In the case of Jakarta EE and Jakarta Security, the Jakarta EE server running the OpenID Connect authentication mechanism is a Relying Party.

To use this authentication mechanism, Jakarta Security provides the @OpenIdAuthenticationMechanismDefinition annotation, for which we typically need 3 mandatory configuration items as shown in the example code above.

The first is the providerURI (1), which points to the third party OpenID Connect Provider. In this example we use https://localhost:8443/openid-connect-server-webapp, which is the URL on which the example code has installed and started a local OpenID Connect provider called "Mitre". Whenever a caller accesses a protected resource, that caller is redirected to that OpenID Connect Provider.

The OpenId authentication mechanism needs to identify itself to the OpenID Connect Provider via a username/password (called clientId (2) and clientSecret (3)). We use "client" respectively "secret" here for those, which are the credentials for a default client that is available in Mitre.

After a caller successfully authenticates with the OpenID Connect Provider, that caller is redirected back to a URL on the Relying Party (our Jakarta EE server). This is called the "callback URL" and can be set via the redirectURI attribute. The default value is ${baseURL}/Callback, where ${baseURL} expands to the context-root of the application that uses Jakarta Security, for example https://localhost:8080/openid-client in our example. This exact URI must be known to Mitre. Mitre (and any OpenID Connect Provider in general) never redirects to unknown URIs.

By default, after the caller is redirected back to the Relying Party (our Jakarta EE server), the resource behind /Callback is invoked. When the attribute redirectToOriginalResource (4) is set to true however, the caller is once again redirected to the URL originally requested and which triggered the authentication process.

When redirectToOriginalResource is set to to true it’s not necessary to actually map anything to the callback URL (for example a Servlet or a REST resource). The authentication mechanism is invoked before the resource mapped to the callback URL is invoked, so if the authentication mechanism always redirects it never invokes this resource and the resource therefore doesn’t need to actually exist.

Define the identity store

In many cases the OpenID Connect Provider has no knowledge of the application for which it authenticates the caller, and therefore does not normally provide any logical groups for the authenticated user. Those groups are application specific after all. We therefore define an additional identity store that does provide those groups for a caller.

Despite not being typical, Jakarta Security supports getting the groups via the claimsDefinition attribute of the @OpenIdAuthenticationMechanismDefinition annotation. This can be used to set a claim name (default is "groups"). Jakarta Security then tries to find this name in the AccessToken, IdentityToken, or in the info returned by the /userinfo endpoint of the OpenId Connect Provider. Providers often need special configuration to return group claims.
@ApplicationScoped
public class AuthorizationIdentityStore implements IdentityStore {

    private Map<String, Set<String>> groupsPerCaller =
        Map.of("user", Set.of("user")); (2)

    @Override
    public Set<ValidationType> validationTypes() {
        return EnumSet.of(PROVIDE_GROUPS); (1)
    }

    @Override
    public Set<String> getCallerGroups(
        CredentialValidationResult validationResult) { (3)
        return groupsPerCaller.get(validationResult.getCallerPrincipal().getName());
    }

}

This identity store is set to PROVIDE_GROUPS (1) only, which means the default IdentityStoreHandler will consult this identity store for groups after another identity store has successfully validated the credentials. For our example here we create a Map (2) with as key the caller principal name, and as value the set of groups. When the IdentityStoreHandler comes asking for the groups (3) of caller "user", a set with just the group "user" is returned.

As validation of the IdentityToken that’s returned by the OpenID Connect Provider is integral to the OpenID Connect flow and not application specific, developers don’t have to provide or define an identity store explicitly for this. Such a store is provided by Jakarta Security as an implementation detail, and automatically activated when the OpenID Connect authentication mechanism is activated.

The identity store is installed and used by the security system just by the virtue of being there; it picks up all enabled CDI beans that implement IdentityStore. Such beans can be enabled by the security system itself (following some configuration annotation), or can be programmatically added using the appropriate CDI APIs. Where the bean comes from doesn’t matter for Jakarta Security, only the fact that it’s there.

Install and configure Mitre

Installing and configuring the OpenID Connect provider Mitre is outside the scope of Jakarta Security itself, but for completeness sake we’ll briefly discuss it by illustration of Maven pom fragments.

<plugin>

    <!--
        Unpack and install Tomcat + Mitre

        Mitre is a Spring based OpenID Connect Server that best runs on a javax based Tomcat.
    -->
    <artifactId>maven-dependency-plugin</artifactId>
    <executions>
        <execution>
            <goals>
                <goal>unpack</goal>
            </goals>
            <configuration>
                <artifactItems>
                    <artifactItem>
                        <groupId>org.apache.tomcat</groupId>
                        <artifactId>tomcat</artifactId>
                        <version>9.0.76</version>
                        <type>zip</type>
                        <outputDirectory>${tomcat.root}</outputDirectory>
                    </artifactItem>
                    <artifactItem>
                        <groupId>org.mitre</groupId>
                        <artifactId>openid-connect-server-webapp</artifactId>
                        <version>1.3.4</version>
                        <type>war</type>
                        <outputDirectory>${tomcat.dir}/webapps/openid-connect-server-webapp</outputDirectory>
                    </artifactItem>
                </artifactItems>
            </configuration>
        </execution>

Mitre is a Spring application that uses the javax.* namespace. We therefore need a Tomcat from the 9.x series, which is available as a zip file from the Maven coordinates org.apache.tomcat:tomcat:9.0.76. Likewise, Mitre is available from org.mitre:openid-connect-server-webapp:1.3.4. We simply need to unzip Tomcat, and unzip Mitre into its webapps folder. We also need to update Tomcat with the JAXB standalone libraries (see full example code).

<!--
    Configure Tomcat to use HTTPS, as Open ID Connect strictly speaking requires this.
    Some servers may refuse to use Open ID Connect if not running on a secure connection.

    Also configure Mitre to use the callback of our client.

    Then start Tomcat and with it Mitre.
-->
<plugin>
    <artifactId>maven-antrun-plugin</artifactId>
    <executions>
        <execution>
            <goals>
                <goal>run</goal>
            </goals>
            <configuration>
                <target>
                    <echo level="info">Replacing in ${tomcat.dir}</echo>

                    <!-- Configure Mitre to let it know its running on HTTPS  -->
                    <replace token="http://localhost:8080" value="https://localhost:8443" dir="${tomcat.dir}/webapps/openid-connect-server-webapp/WEB-INF" summary="yes">
                        <include name="server-config.xml" />
                    </replace>

                    <!-- Configure Mitre to let it know where the Open ID callback needs to go to -->
                    <replace token="http://localhost/" value="http://localhost:8080/openid-client/Callback" dir="${tomcat.dir}/webapps/openid-connect-server-webapp/WEB-INF/classes/db/hsql" summary="yes">
                        <include name="clients.sql" />
                    </replace>

                    <!-- Configure Tomcat using our pre-configured server.xml (which sets https) -->
                    <copy file="src/test/resources/server.xml" todir="${tomcat.dir}/conf"/>
                    <copy file="src/test/resources/localhost-rsa.jks" todir="${tomcat.dir}/conf"/>

                    <chmod dir="${tomcat.dir}/bin" perm="ugo+rx" includes="*" />

                    <!-- Start Tomcat and Mitre -->
                    <exec executable="${tomcat.dir}/bin/startup.sh" dir="${tomcat.dir}" >
                        <env key="CATALINA_PID" value="${tomcat.pidfile}" />
                    </exec>
                </target>
            </configuration>
        </execution>
    </executions>
</plugin>

Out of the box Mitre runs on HTTP, but since we’re using HTTPS instead we need to configure it to run on HTTPS using the server-config.xml file. We also need to tell it about the exact callback URL that we discussed above (http://localhost:8080/openid-client/Callback), which can be done in clients.sql. Tomcat has to be configured to run on HTTPS as well, which requires updating server.xml and providing it with a keystore.

Tomcat, and with it Mitre, can be started by executing [tomcat dir]/bin/startup.sh.

Test the application

It’s now time to test our application. A ready-to-test version is available from the Jakarta EE Examples project at https://github.com/eclipse-ee4j/jakartaee-examples.

Download or clone this repo, then cd into the focused folder and execute:

mvn clean install -pl :restOpenIdConnectAuth

This will run a test associated with the project, printing something like the following:

user : true
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 22.324 s - in jakartaee.examples.focused.security.restopenidconnectauth.RestOpenIdConnectAuthIT

Let’s take a quick look at the actual test:

@RunWith(Arquillian.class)
@RunAsClient
public class RestOpenIdConnectAuthIT extends ITBase {

    @ArquillianResource
    private URL baseUrl;

    /**
     * Test the call to a protected REST service
     *
     * @throws Exception when a serious error occurs.
     */
    @RunAsClient
    @Test
    public void testRestCall() throws Exception {
        HtmlPage page = webClient.getPage(baseUrl + "/rest/resource"); (1)

        // Authenticate with the OpenId Provider using the
        // username and password for a default user
        page.getElementById("j_username")
            .setAttribute("value", "user");

        page.getElementById("j_password")
            .setAttribute("value", "password"); (2)

        // Submit
        HtmlPage confirmationPage =
            page.getElementByName("submit")
                .click(); (3)

        // Confirm
        TextPage originalResource =
            confirmationPage.getElementByName("authorize")
                            .click(); (4)

        System.out.println(originalResource.getContent());
    }
}

The test starts a server and deploys the output of the build process (a .war file) to it. The test runs in the integration test phase, rather than the unit test phase, to make sure this build output is available when it runs. The test then sends a request to the server using the provided HtmlUnit webClient. Note that the webClient can be used for any other HTTP requests your test requires.

If you want to inspect the app yourself, you can manually deploy the WAR file (security/restOpenIdConnectAuth/target/restOpenIdConnectAuth.war) to the server of your choice (e.g. GlassFish 7), and request the URL via a browser or a commandline util such as curl.

After the test requests our protected resource at "/rest/resource" (1), the OpenID Connect authentication mechanism redirects to Mitre, which will respond with a login page. The test programmically sets the fields j_username and j_password, (2) and then clicks submits (3). After confirming (4), Mitre will redirect the test code back to the /Callback URL, which will redirect back to the original resource at "/rest/resource".

Securing an endpoint with Basic authentication and an LDAP identity store

In the following example, we’ll secure a REST endpoint using Basic authentication and the LDAP identity store that is provided by Jakarta Security.

You’ll learn how to:

  1. Define security constraints

  2. Use the provided BasicAuthenticationMechanismDefinition

  3. Use the provided LdapIdentityStoreDefinition

  4. Populate and configure the identity store

  5. Use the Jakarta Security SecurityContext

Write the application

Let’s start with defining a simple REST resource class for a /rest/resource endpoint:

@Path("/resource")
@RequestScoped
public class Resource {

    @Inject
    private SecurityContext securityContext;

    @GET
    @Produces(TEXT_PLAIN)
    public String getCallerAndRole() {
        return
            securityContext.getCallerPrincipal().getName() + " : " +
            securityContext.isCallerInRole("user");
    }

}

This resource uses the injected Jakarta EE SecurityContext to obtain access to the current authenticated caller, which is represented by a Principal instance.

If this resource were available to unauthenticated callers, getCallerPrincipal() would return null for unauthenticated requests, so we’d have to check for null. Our example, however, requires authentication for this resource, so we can skip that check.

There is a Jakarta REST-specific type that is also named SecurityContext and has similar methods as the ones we used here. From the Jakarta EE perspective, that is a discouraged type and the Jakarta Security version is to be preferred.

Declare the authentication mechanism and identity store

@ApplicationScoped
@BasicAuthenticationMechanismDefinition(
    realmName = "basicAuth"
)
@LdapIdentityStoreDefinition(
    url = "ldap://localhost:40000",
    callerBaseDn = "ou=caller,dc=jakartaee",
    groupSearchBase = "ou=group,dc=jakartaee",
    groupSearchFilter = "(&(member=%s)(objectclass=groupofnames))"
)
@ApplicationPath("/rest")
public class ApplicationConfig extends Application {

To declare the usage of a specific authentication mechanism, Jakarta EE provides [XYZ]MechanismDefinition annotations. Such an annotation is picked up by the security system, and in response to it a CDI bean that implements the HttpAuthenticationMechanism is enabled for it.

The annotation can be put on any bean, but in a REST application it fits particularly well on the Application subclass because it also declares the path for REST resources.

Likewise, to declare the usage of a specific identity store, Jakarta EE provides [XYZ]StoreDefinition annotations.

The annotations can be put on any bean, but in a REST application it fits particularly well on the Application subclass that also declares the path for REST resources.

You can use the provided @LdapIdentityStoreDefinition with any authentication mechanism that validates username/password credentials. LDAP structures are very open-ended, and there’s a lot of possible ways to model callers, their passwords, and their groups. We’ll present one way here, where we’ll define a caller.jakartaee object, that contains the caller name and password, and a group.jakartaee object that contains the group name and a list of all callers in that group.

For this structure we need 3 attributes to be defined:

  1. callerBaseDn - Used for credential validation using "direct binding", with the default caller name being "uid".

  2. groupSearchBase - The object root used to search for groups of the caller

  3. groupSearchFilter - The subtree to search for groups

Populating the identity store

In order to use the identity store, we need to put some data in an LDAP server. The following code shows one way how to do that:

@ApplicationScoped
@BasicAuthenticationMechanismDefinition(
    realmName = "basicAuth"
)
@LdapIdentityStoreDefinition(
    url = "ldap://localhost:40000",
    callerBaseDn = "ou=caller,dc=jakartaee",
    groupSearchBase = "ou=group,dc=jakartaee",
    groupSearchFilter = "(&(member=%s)(objectclass=groupofnames))"
)
@DeclareRoles("user")
@ApplicationPath("/rest")
public class ApplicationConfig extends Application {

    private InMemoryDirectoryServer directoryServer;

    public void onStart(@Observes @Initialized(ApplicationScoped.class) Object applicationContext) {
        try {
            InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig("dc=jakartaee");
            config.setListenerConfigs(
                new InMemoryListenerConfig("myListener", null, 40000, null, null, null));

            directoryServer = new InMemoryDirectoryServer(config);

            directoryServer.importFromLDIF(true,
                new LDIFReader(new ByteArrayInputStream("""

                        # Define caller.jakartaee and group.jakartaee structure

                        dn: dc=jakartaee
                        objectclass: top
                        objectclass: dcObject
                        objectclass: organization
                        dc: jakartaee
                        o: jakartaee

                        dn: ou=caller,dc=jakartaee
                        objectclass: top
                        objectclass: organizationalUnit
                        ou: caller

                        dn: ou=group,dc=jakartaee
                        objectclass: top
                        objectclass: organizationalUnit
                        ou: group


                        # Add caller john:secret1 and group user with member john

                        dn: uid=john,ou=caller,dc=jakartaee
                        objectclass: top
                        objectclass: uidObject
                        objectclass: person
                        uid: john
                        cn: John Smith
                        sn: John
                        userPassword: secret1

                        dn: cn=user,ou=group,dc=jakartaee
                        objectclass: top
                        objectclass: groupOfNames
                        cn: user
                        member: uid=john,ou=caller,dc=jakartaee

                        """.getBytes())));

            directoryServer.startListening();
        } catch (LDAPException e) {
            throw new IllegalStateException(e);
        }
    }

}

The code above uses an in-memory LDAP server called Unboundid that we start on port 40000. We populate it using embedded LDIF, which is a popular format to configure LDAP servers.

In-depth explanation of LDAP itself is beyond the scope of this tutorial, but we’ll briefly explain the process here. When a caller authenticates with username "john" and password "secret1", our LDAP identity store will construct the full name "uid=john,ou=caller,dc=jakartaee" using callerNameAttribute ("uid" as a default), and callerBaseDn ("ou=caller,dc=jakartaee" here). The store then uses the LDAP 'Bind' operation and directly attempts to "log in" as that user to the LDAP server. Unlike the Database identity store, the LDAP store doesn’t look up and compare the passwords. If the aforementioned login succeeds, the credentials are assumed to be correct.

The LDAP store will then search for groups using the javax.naming.directory.DirContext.search() method, with the groupSearchBase value ("ou=group,dc=jakartaee") and the formatted value from groupSearchFilter ("(&(member=uid=john,ou=caller,dc=jakartaee)(objectclass=groupofnames))") as parameters. The LDAP server will subsequently return "cn=user,ou=group,dc=jakartaee" for our example. From this the value of groupNameAttribute (defaults to "cn") is taken, which resolves to "user" here.

Test the application

It’s now time to test our application. A ready-to-test version is available from the Jakarta EE Examples project at https://github.com/eclipse-ee4j/jakartaee-examples.

Download or clone this repo, then cd into the focused folder and execute:

mvn clean install -pl :restBasicAuthLdapStore

This will run a test associated with the project, printing something like the following:

john : true
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 8.247 s - in jakartaee.examples.focused.security.restBasicAuthLdapStore.RestBasicAuthLdapStoreIT

The test itself is basically the same as that for the Securing an endpoint with Basic authentication example.

Securing an endpoint with Custom Form authentication

In the following example, we’ll secure a REST endpoint using Custom Form authentication.

You’ll learn how to:

  1. Define security constraints

  2. Use the Custom Form authentication mechanism

  3. Use Jakarta Faces and Jakarta Validation to customize the Form used for Form authentication

  4. How to define (and implicitly set) a custom identity store

  5. Use the Jakarta Security SecurityContext

Write the application

Let’s start with defining a simple REST resource class for a /rest/resource endpoint:

@Path("/resource")
@RequestScoped
public class Resource {

    @Inject
    private SecurityContext securityContext;

    @GET
    @Produces(TEXT_PLAIN)
    public String getCallerAndRole() {
        return
            securityContext.getCallerPrincipal().getName() + " : " +
            securityContext.isCallerInRole("user");
    }

}

This resource uses the injected Jakarta EE SecurityContext to obtain access to the current authenticated caller, which is represented by a Principal instance.

If this resource were available to unauthenticated callers, getCallerPrincipal() would return null for unauthenticated requests, so we’d have to check for null. Our example, however, requires authentication for this resource, so we can skip that check.

There is a Jakarta REST-specific type that is also named SecurityContext and has similar methods as the ones we used here. From the Jakarta EE perspective, that is a discouraged type and the Jakarta Security version is to be preferred.

Declare the security constraints

Next we’ll define the security constraints in web.xml, which tell the security system that access to a given URL or URL pattern is protected, and hence authentication is required:

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="https://jakarta.ee/xml/ns/jakartaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="https://jakarta.ee/xml/ns/jakartaee https://jakarta.ee/xml/ns/jakartaee/web-app_6_0.xsd"
         version="6.0">

    <security-constraint>
        <web-resource-collection>
            <web-resource-name>protected</web-resource-name>
            <url-pattern>/rest/*</url-pattern>
        </web-resource-collection>
        <auth-constraint>
            <role-name>user</role-name>
        </auth-constraint>
    </security-constraint>

</web-app>

This XML essentially says that to access any URL that starts with "/rest" requires the caller to have the role "user". Roles are opaque strings; merely identifiers. It’s fully up to the application how broad or fine-grained they are.

In Jakarta EE, internally these XML constraints are transformed into Permission instances and made available via a specific type of permission store. Knowledge about this transformation is only needed for very advanced use cases.
The observant reader may wonder if XML is really the only option here, given the strong feelings that exist in parts of the community around XML. The answer is yes and no. Jakarta EE does define the @RolesAllowed annotation that could be used to replace the XML shown above, but only the legacy Enterprise Beans has specified a behaviour for this when put on an Enterprise Bean. Jakarta REST has done no such thing, although the JWT API in MicroProfile has defined this for REST resources. In Jakarta EE, however, this remains a vendor-specific extension. There are also a number of annotations and APIs in Jakarta EE to set these kinds of constraints for individual Servlets, but those won’t help us much either here.

Declare the authentication mechanism

@ApplicationScoped
@CustomFormAuthenticationMechanismDefinition(
    loginToContinue = @LoginToContinue(
        loginPage="/login.xhtml",
        errorPage=""
    )
)
@DeclareRoles({ "user", "caller" })
@FacesConfig
@ApplicationPath("/rest")
public class ApplicationConfig extends Application {

}

To declare the usage of a specific authentication mechanism, Jakarta EE provides [XYZ]MechanismDefinition annotations. Such an annotation is picked up by the security system, and in response to it a CDI bean that implements the HttpAuthenticationMechanism is enabled for it.

The annotation can be put on any bean, but in a REST application it fits particularly well on the Application subclass because it also declares the path for REST resources.

Contrary to the Basic HTTP authentication mechanism, the Form authentication mechanism allows us to customize the login dialog (the process between the caller and the authentication mechanism) and to keep track of the authenticated session on the server (using a cookie). This also allows us to logout, something that for unknown reasons has never been specified for Basic HTTP authentication.

Contrary to the regular Form authentication mechanism, the Custom Form authentication mechanism lets us customize the login dialog even more by having the ability to execute custom code between the postback of a login form and the form authentication mechanism taking the provided credentials.

To use this authentication method, we need to designate a path to a resource that is relative to our application. The authentication mechanism will redirect the caller to this resource when authentication is required. The resource can be anything, but a postback should eventually lead to some code being executed that continues the authentication dialog. For example a plain .html file or .jsp file combined with a Filter, or a Faces view with a backing bean.

Define the authentication mechanism’s view and backing code

For this example we’ll use a Faces view with a backing bean:

<!DOCTYPE html>
<html lang="en" xmlns:h="jakarta.faces.html">
    <h:head>
        <title>Login to continue</title>
    </h:head>
    <h:body>
        <h1>Login to continue</h1>

        <h:messages />

        <h:form id="form">
            <div>
                <h:outputLabel for="username" value="Username" />
                <h:inputText id="username" value="#{loginBacking.username}"/>
            </div>
            <div>
                <h:outputLabel for="password" value="Password" />
                <h:inputSecret id="password" value="#{loginBacking.password}"/>
            </div>
            <div>
                <h:commandButton value="Login" type="submit" action="#{loginBacking.login}" />
            </div>
        </h:form>
    </h:body>
</html>
@Named
@RequestScoped
public class LoginBacking {

    @Inject
    private SecurityContext securityContext;

    @Inject
    private FacesContext facesContext;

    @NotNull
    @Size(min = 3, max = 15, message="Username must be between 3 and 15 characters")
    private String username;

    @NotNull
    @Size(min = 5, max = 50, message="Password must be between 5 and 50 characters")
    private String password;

    public void login() {
        switch (
            // Continue the authentication dialog manually by invoking the authenticate()
            // method. The form authentication picks this up, just like a post to j_security does.
            securityContext.authenticate(
                getRequest(),
                getResponse(),
                withParams()
                    .credential(new UsernamePasswordCredential(username, new Password(password))))) {

            case SEND_CONTINUE:

                // Authentication mechanism has send a redirect, should not
                // send anything to response from Faces now.
                facesContext.responseComplete();
                return;

            case SEND_FAILURE:

                addError("Login failed");
                return;

            default:
        }
    }

    // getters/setters + utility methods omitted
}

The view itself is quite similar to the HTML page we used for the form in Securing an endpoint with Form authentication. The main difference is bindings of the form fields to a (CDI) backing bean. The bean side of the binding has Jakarta Validation constraints applied to it; this allows for fine-grained validation of some general requirements of the credentials without actually attempting authentication.

If this initial validation passes, we arrive in the login() method. For our example the only thing we need to do here is signaling that we want to continue the authentication dialog (the process or interaction between the caller and the authentication mechanism), and when doing so provide the credentials that we earlier obtained in a custom way.

We have two important outcomes to handle. SEND_CONTINUE effectively means the credentials were validated successfully, and the caller is therefore directed to the resource that was originally requested. SEND_FAILURE means the opposite; the credentials were not validated successfully. By just returning from our callback method in the last case the form will be redisplayed, albeit with the error message added.

NOT_DONE and SUCCESS are two other outcomes, but we don’t have to handle them here. NOT_DONE only applies to pre-emptive authentication, but we’re doing explicit (forced, mandatory) authentication here. SUCCESS means we can go ahead and render the page, which is exactly what happens if we don’t do anything.

See Getting Started with Web Applications for more information on running .xhtml files and their backing beans.

Define the identity store

Finally, let’s define a basic identity store that the security system can use to validate provided credentials for Form authentication:

@ApplicationScoped
public class CustomIdentityStore implements IdentityStore {

    public CredentialValidationResult validate(UsernamePasswordCredential usernamePasswordCredential) {
        if (usernamePasswordCredential.compareTo("john", "secret1")) {
            return new CredentialValidationResult("john", Set.of("user", "caller"));
        }

        return INVALID_RESULT;
    }

}

This identity store only validates the single identity (user) "john", with password "secret1" and roles "user" and "caller". Defining this kind of identity store is often the simplest way to get started. Note that Jakarta Security doesn’t define a simple identity store out of the box, because there are questions about whether that would promote security best practices.

The identity store is installed and used by the security system just by the virtue of being there; it picks up all enabled CDI beans that implement IdentityStore. Such beans can be enabled by the security system itself (following some configuration annotation), or can be programmatically added using the appropriate CDI APIs. Where the bean comes from doesn’t matter for Jakarta Security, only the fact that it’s there.

Test the application

It’s now time to test our application. A ready-to-test version is available from the Jakarta EE Examples project at https://github.com/eclipse-ee4j/jakartaee-examples.

Download or clone this repo, then cd into the focused folder and execute:

mvn clean install -pl :restCustomFormAuthCustomStore

This will run a test associated with the project, printing something like the following:

john : true
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 9.272 s - in jakartaee.examples.focused.security.restCustomFormAuthCustomStore.RestCustomFormAuthCustomStoreIT

Let’s take a quick look at the actual test:

@RunWith(Arquillian.class)
@RunAsClient
public class RestCustomFormAuthCustomStoreIT extends ITBase {

    @ArquillianResource
    private URL baseUrl;

    /**
     * Test the call to a protected REST service
     *
     * @throws Exception when a serious error occurs.
     */
    @RunAsClient
    @Test
    public void testRestCall() throws Exception {
        HtmlPage loginPage = webClient.getPage(baseUrl + "/rest/resource");
        System.out.println(loginPage.asXml());

        HtmlForm form = loginPage.getForms()
                                 .get(0);

        form.getInputByName("form:username")
            .setValueAttribute("john");

        form.getInputByName("form:password")
            .setValueAttribute("secret1");

        TextPage page = form.getInputByValue("Login")
                            .click();

        System.out.println(page.getContent());
    }
}

The test starts a server and deploys the output of the build process (a .war file) to it. The test runs in the integration test phase, rather than the unit test phase, to make sure this build output is available when it runs. The test then sends a request to the server using the provided HtmlUnit webClient. Note that the webClient can be used for any other HTTP requests your test requires.

If you want to inspect the app yourself, you can manually deploy the WAR file (security/restCustomFormAuthCustomStore/target/restCustomFormAuthCustomStore.war) to the server of your choice (e.g. GlassFish 7), and request the URL via a browser or a commandline util such as curl.

The test first sends a request here to the protected resource, and the server responds with the rendered version of the Faces view form we defined above. Using the HtmlUnit API, it’s easy to navigate the HTML DOM, fill out the username and password in the form, and programmatically click the Login button. The form posts back to the same URL it was requested from. Faces will detect this postback and will orchestrate the validation using Jakarta Validation and invoking the CDI based backing bean.


1. Technically Jakarta Interceptors is an API separate from CDI, but in modern applications they are used exclusively with CDI, hence we use the term "CDI Interceptors" here.